Merge branch 'feature/kotlin' into dev

pull/316/head^2
Alinson S. Xavier 8 years ago
parent 2db4c06fe8
commit 180c18f6bf

1
.gitignore vendored

@ -23,3 +23,4 @@ gen/
local.properties local.properties
crowdin.yaml crowdin.yaml
local local
tmp/

@ -1,3 +0,0 @@
#!/bin/bash
find uhabits-android/build/outputs/failed/test-screenshots -name '*.expected*' -delete
rsync -av uhabits-android/build/outputs/failed/test-screenshots/ uhabits-android/src/androidTest/assets/

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com> * Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
* *
* This file is part of Loop Habit Tracker. * This file is part of Loop Habit Tracker.
* *
@ -17,25 +17,26 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.isoron.uhabits.activities; package org.isoron.androidbase.activities;
import org.isoron.uhabits.core.models.*; import android.content.*;
import dagger.*; import dagger.*;
@Module @Module
public class HabitModule public class ActivityContextModule
{ {
private final Habit habit; private Context context;
public HabitModule(Habit habit) public ActivityContextModule(Context context)
{ {
this.habit = habit; this.context = context;
} }
@Provides @Provides
public Habit getHabit() @ActivityContext
public Context getContext()
{ {
return habit; return context;
} }
} }

@ -19,29 +19,20 @@
package org.isoron.androidbase.activities; package org.isoron.androidbase.activities;
import android.content.*;
import dagger.*; import dagger.*;
@Module @Module
public class ActivityModule public class BaseActivityModule
{ {
private BaseActivity activity; private BaseActivity activity;
public ActivityModule(BaseActivity activity) public BaseActivityModule(BaseActivity activity)
{ {
this.activity = activity; this.activity = activity;
} }
@Provides @Provides
public BaseActivity getActivity() public BaseActivity getBaseActivity()
{
return activity;
}
@Provides
@ActivityContext
public Context getContext()
{ {
return activity; return activity;
} }

@ -51,6 +51,15 @@ public class StyledResources
return bool; return bool;
} }
public int getDimension(@AttrRes int attrId)
{
TypedArray ta = getTypedArray(attrId);
int dim = ta.getDimensionPixelSize(0, 0);
ta.recycle();
return dim;
}
public int getColor(@AttrRes int attrId) public int getColor(@AttrRes int attrId)
{ {
TypedArray ta = getTypedArray(attrId); TypedArray ta = getTypedArray(attrId);
@ -80,13 +89,13 @@ public class StyledResources
public int[] getPalette() public int[] getPalette()
{ {
int resourceId = getStyleResource(R.attr.palette); int resourceId = getResource(R.attr.palette);
if (resourceId < 0) throw new RuntimeException("resource not found"); if (resourceId < 0) throw new RuntimeException("resource not found");
return context.getResources().getIntArray(resourceId); return context.getResources().getIntArray(resourceId);
} }
int getStyleResource(@AttrRes int attrId) public int getResource(@AttrRes int attrId)
{ {
TypedArray ta = getTypedArray(attrId); TypedArray ta = getTypedArray(attrId);
int resourceId = ta.getResourceId(0, -1); int resourceId = ta.getResourceId(0, -1);

@ -1,4 +1,5 @@
buildscript { buildscript {
ext.kotlin_version = '1.1.2-4'
repositories { repositories {
jcenter() jcenter()
maven { url 'https://maven.google.com' } maven { url 'https://maven.google.com' }
@ -10,6 +11,7 @@ buildscript {
classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:0.6.4' classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:0.6.4'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
classpath 'org.jacoco:org.jacoco.core:+' classpath 'org.jacoco:org.jacoco.core:+'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
} }
} }

@ -198,6 +198,18 @@ uninstall_test_apk() {
$ADB uninstall ${PACKAGE_NAME}.test || fail $ADB uninstall ${PACKAGE_NAME}.test || fail
} }
fetch_images() {
rm -rf tmp/test-screenshots > /dev/null
mkdir -p tmp/
adb pull /sdcard/Android/data/org.isoron.uhabits/files/test-screenshots tmp/
adb shell rm -rf /sdcard/Android/data/org.isoron.uhabits/files/test-screenshots
}
accept_images() {
find tmp/test-screenshots -name '*.expected*' -delete
rsync -av tmp/test-screenshots/ uhabits-android/src/androidTest/assets/
}
run_local_tests() { run_local_tests() {
clean_output_dir clean_output_dir
run_adb_as_root run_adb_as_root
@ -263,6 +275,14 @@ case "$1" in
run_local_tests run_local_tests
;; ;;
fetch-images)
fetch_images
;;
accept-images)
accept_images
;;
install) install)
shift; parse_opts $* shift; parse_opts $*
build_apk build_apk
@ -278,6 +298,8 @@ case "$1" in
ci-tests Start emulator silently, run tests then kill emulator ci-tests Start emulator silently, run tests then kill emulator
local-tests Run all tests on connected device local-tests Run all tests on connected device
install Install app on connected device install Install app on connected device
fetch-images Fetches failed view test images from device
accept-images Copies fetched images to corresponding assets folder
Options: Options:
-u --uninstall-first Uninstall existing APK first -u --uninstall-first Uninstall existing APK first

@ -1,3 +1,3 @@
org.gradle.parallel=true org.gradle.parallel=false
org.gradle.daemon=true org.gradle.daemon=true
org.gradle.jvmargs=-Xms1024m -Xmx4096m -XX:MaxPermSize=2048m org.gradle.jvmargs=-Xms2048m -Xmx2048m -XX:MaxPermSize=2048m

@ -1,5 +1,7 @@
apply plugin: 'idea' apply plugin: 'idea'
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'jacoco' apply plugin: 'jacoco'
android { android {
@ -11,6 +13,13 @@ android {
minSdkVersion 19 minSdkVersion 19
targetSdkVersion 25 targetSdkVersion 25
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArgument "size", "medium"
javaCompileOptions {
annotationProcessorOptions {
includeCompileClasspath false
}
}
} }
buildTypes { buildTypes {
@ -64,18 +73,16 @@ dependencies {
implementation 'com.google.dagger:dagger:2.9' implementation 'com.google.dagger:dagger:2.9'
implementation 'com.jakewharton:butterknife:8.6.1-SNAPSHOT' implementation 'com.jakewharton:butterknife:8.6.1-SNAPSHOT'
implementation 'org.apmem.tools:layouts:1.10' implementation 'org.apmem.tools:layouts:1.10'
implementation 'org.jetbrains:annotations-java5:15.0'
implementation 'com.google.code.gson:gson:2.7' implementation 'com.google.code.gson:gson:2.7'
implementation 'com.google.code.findbugs:jsr305:3.0.2' implementation 'com.google.code.findbugs:jsr305:3.0.2'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
compileOnly 'javax.annotation:jsr250-api:1.0' compileOnly 'javax.annotation:jsr250-api:1.0'
compileOnly 'com.google.auto.factory:auto-factory:1.0-beta3' compileOnly 'com.google.auto.factory:auto-factory:1.0-beta3'
annotationProcessor 'com.google.dagger:dagger-compiler:2.9' kapt 'com.google.dagger:dagger-compiler:2.9'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.6.1-SNAPSHOT' kapt 'com.jakewharton:butterknife-compiler:8.6.1-SNAPSHOT'
annotationProcessor 'com.google.auto.factory:auto-factory:1.0-beta3' kapt 'com.google.auto.factory:auto-factory:1.0-beta3'
androidTestAnnotationProcessor 'com.google.auto.factory:auto-factory:1.0-beta3'
androidTestAnnotationProcessor 'com.google.dagger:dagger-compiler:2.9'
androidTestAnnotationProcessor 'com.jakewharton:butterknife-compiler:8.6.1-SNAPSHOT'
androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:2.2.2' androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:2.2.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:2.2.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:2.2.2'
androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.1' androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.1'
@ -87,6 +94,7 @@ dependencies {
androidTestImplementation 'com.android.support.test:runner:0.5' androidTestImplementation 'com.android.support.test:runner:0.5'
androidTestImplementation 'com.google.guava:guava:20.0' androidTestImplementation 'com.google.guava:guava:20.0'
androidTestImplementation project(":uhabits-core") androidTestImplementation project(":uhabits-core")
kaptAndroidTest 'com.google.dagger:dagger-compiler:2.9'
// mockito-android 2+ includes net.bytebuddy, which causes tests to fail. // mockito-android 2+ includes net.bytebuddy, which causes tests to fail.
// Excluding the package net.bytebuddy on AndroidManifest.xml breaks some // Excluding the package net.bytebuddy on AndroidManifest.xml breaks some
@ -94,9 +102,6 @@ dependencies {
androidTestImplementation "org.mockito:mockito-core:1+" androidTestImplementation "org.mockito:mockito-core:1+"
androidTestImplementation "com.google.dexmaker:dexmaker-mockito:+" androidTestImplementation "com.google.dexmaker:dexmaker-mockito:+"
testAnnotationProcessor 'com.google.auto.factory:auto-factory:1.0-beta3'
testAnnotationProcessor 'com.google.dagger:dagger-compiler:2.9'
testAnnotationProcessor 'com.jakewharton:butterknife-compiler:8.6.1-SNAPSHOT'
testImplementation 'com.google.dagger:dagger:2.9' testImplementation 'com.google.dagger:dagger:2.9'
testImplementation "org.mockito:mockito-core:2.8.9" testImplementation "org.mockito:mockito-core:2.8.9"
testImplementation "org.mockito:mockito-inline:2.8.9" testImplementation "org.mockito:mockito-inline:2.8.9"
@ -114,6 +119,10 @@ repositories {
mavenCentral() mavenCentral()
} }
kapt {
correctErrorTypes = true
}
task coverageReport(type: JacocoReport) { task coverageReport(type: JacocoReport) {
jacocoClasspath = configurations['androidJacocoAnt'] jacocoClasspath = configurations['androidJacocoAnt']

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 577 B

After

Width:  |  Height:  |  Size: 585 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 21 KiB

@ -31,6 +31,7 @@ import android.util.*;
import junit.framework.*; import junit.framework.*;
import org.isoron.androidbase.*; import org.isoron.androidbase.*;
import org.isoron.androidbase.activities.*;
import org.isoron.androidbase.utils.*; import org.isoron.androidbase.utils.*;
import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.preferences.*; import org.isoron.uhabits.core.preferences.*;
@ -67,12 +68,15 @@ public class BaseAndroidTest extends TestCase
protected CountDownLatch latch; protected CountDownLatch latch;
protected AndroidTestComponent component; protected HabitsApplicationTestComponent appComponent;
protected ModelFactory modelFactory; protected ModelFactory modelFactory;
protected HabitsActivityTestComponent component;
private boolean isDone = false; private boolean isDone = false;
@Override
@Before @Before
public void setUp() public void setUp()
{ {
@ -86,21 +90,31 @@ public class BaseAndroidTest extends TestCase
setTheme(R.style.AppBaseTheme); setTheme(R.style.AppBaseTheme);
setLocale("en", "US"); setLocale("en", "US");
component = DaggerAndroidTestComponent latch = new CountDownLatch(1);
appComponent = DaggerHabitsApplicationTestComponent
.builder() .builder()
.appContextModule(new AppContextModule(targetContext.getApplicationContext())) .appContextModule(new AppContextModule(targetContext.getApplicationContext()))
.build(); .build();
HabitsApplication.setComponent(component); HabitsApplication.setComponent(appComponent);
prefs = component.getPreferences(); prefs = appComponent.getPreferences();
habitList = component.getHabitList(); habitList = appComponent.getHabitList();
taskRunner = component.getTaskRunner(); taskRunner = appComponent.getTaskRunner();
logger = component.getHabitsLogger(); logger = appComponent.getHabitsLogger();
modelFactory = appComponent.getModelFactory();
prefs.reset();
modelFactory = component.getModelFactory();
fixtures = new HabitFixtures(modelFactory, habitList); fixtures = new HabitFixtures(modelFactory, habitList);
fixtures.purgeHabits(appComponent.getHabitList());
Habit habit = fixtures.createEmptyHabit();
latch = new CountDownLatch(1); component = DaggerHabitsActivityTestComponent
.builder()
.activityContextModule(new ActivityContextModule(targetContext))
.habitsApplicationComponent(appComponent)
.build();
} }
protected void assertWidgetProviderIsInstalled(Class componentClass) protected void assertWidgetProviderIsInstalled(Class componentClass)
@ -118,7 +132,7 @@ public class BaseAndroidTest extends TestCase
protected void awaitLatch() throws InterruptedException protected void awaitLatch() throws InterruptedException
{ {
assertTrue(latch.await(5, TimeUnit.SECONDS)); assertTrue(latch.await(1, TimeUnit.SECONDS));
} }
protected void setLocale(@NonNull String language, @NonNull String country) protected void setLocale(@NonNull String language, @NonNull String country)
@ -195,4 +209,10 @@ public class BaseAndroidTest extends TestCase
{ {
Debug.stopMethodTracing(); Debug.stopMethodTracing();
} }
protected Long day(int offset)
{
return DateUtils.getStartOfToday() -
offset * DateUtils.millisecondsInOneDay;
}
} }

@ -21,6 +21,7 @@ package org.isoron.uhabits;
import android.graphics.*; import android.graphics.*;
import android.support.annotation.*; import android.support.annotation.*;
import android.support.test.*;
import android.view.*; import android.view.*;
import android.widget.*; import android.widget.*;
@ -31,14 +32,14 @@ import org.isoron.uhabits.widgets.*;
import java.io.*; import java.io.*;
import java.util.*; import java.util.*;
import static android.os.Build.VERSION.*; import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.KITKAT; import static android.os.Build.VERSION_CODES.KITKAT;
import static android.os.Build.VERSION_CODES.LOLLIPOP; import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.view.View.MeasureSpec.*; import static android.view.View.MeasureSpec.makeMeasureSpec;
public class BaseViewTest extends BaseAndroidTest public class BaseViewTest extends BaseAndroidTest
{ {
double similarityCutoff = 0.00075; public double similarityCutoff = 0.00015;
@Override @Override
public void setUp() public void setUp()
@ -49,6 +50,7 @@ public class BaseViewTest extends BaseAndroidTest
protected void assertRenders(View view, String expectedImagePath) protected void assertRenders(View view, String expectedImagePath)
throws IOException throws IOException
{ {
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
expectedImagePath = getVersionedPath(expectedImagePath); expectedImagePath = getVersionedPath(expectedImagePath);
Bitmap actual = renderView(view); Bitmap actual = renderView(view);
if(actual == null) throw new IllegalStateException("actual is null"); if(actual == null) throw new IllegalStateException("actual is null");
@ -198,6 +200,7 @@ public class BaseViewTest extends BaseAndroidTest
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap); Canvas canvas = new Canvas(bitmap);
view.invalidate();
view.draw(canvas); view.draw(canvas);
return bitmap; return bitmap;
} }

@ -84,6 +84,9 @@ public class HabitFixtures
habit.setName("Take a walk"); habit.setName("Take a walk");
habit.setDescription("How many steps did you walk today?"); habit.setDescription("How many steps did you walk today?");
habit.setType(Habit.NUMBER_HABIT); habit.setType(Habit.NUMBER_HABIT);
habit.setTargetType(Habit.AT_LEAST);
habit.setTargetValue(200.0);
habit.setUnit("steps");
habitList.add(habit); habitList.add(habit);
long timestamp = DateUtils.getStartOfToday(); long timestamp = DateUtils.getStartOfToday();

@ -0,0 +1,53 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits
import dagger.*
import org.isoron.androidbase.activities.*
import org.isoron.uhabits.activities.*
import org.isoron.uhabits.activities.about.*
import org.isoron.uhabits.activities.habits.list.*
import org.isoron.uhabits.activities.habits.list.views.*
import org.isoron.uhabits.activities.habits.show.*
import org.isoron.uhabits.core.ui.screens.habits.list.*
import org.mockito.Mockito.*
@Module
class TestModule {
@Provides fun ListHabitsBehavior() = mock(ListHabitsBehavior::class.java)
}
@ActivityScope
@Component(modules = arrayOf(
ActivityContextModule::class,
AboutModule::class,
HabitsActivityModule::class,
ListHabitsModule::class,
ShowHabitModule::class,
HabitModule::class,
TestModule::class
), dependencies = arrayOf(HabitsApplicationComponent::class))
interface HabitsActivityTestComponent {
fun getCheckmarkPanelViewFactory(): CheckmarkPanelViewFactory
fun getHabitCardViewFactory(): HabitCardViewFactory
fun getCheckmarkButtonViewFactory(): CheckmarkButtonViewFactory
fun getNumberButtonViewFactory(): NumberButtonViewFactory
fun getNumberPanelViewFactory(): NumberPanelViewFactory
}

@ -19,7 +19,6 @@
package org.isoron.uhabits; package org.isoron.uhabits;
import org.isoron.androidbase.*; import org.isoron.androidbase.*;
import org.isoron.uhabits.core.*; import org.isoron.uhabits.core.*;
import org.isoron.uhabits.core.tasks.*; import org.isoron.uhabits.core.tasks.*;
@ -32,7 +31,8 @@ import dagger.*;
HabitsModule.class, HabitsModule.class,
SingleThreadModule.class, SingleThreadModule.class,
}) })
public interface AndroidTestComponent extends HabitsApplicationComponent public interface HabitsApplicationTestComponent
extends HabitsApplicationComponent
{ {
} }
@ -42,7 +42,7 @@ class SingleThreadModule
{ {
@Provides @Provides
@AppScope @AppScope
public static TaskRunner provideTaskRunner() static TaskRunner provideTaskRunner()
{ {
return new SingleThreadTaskRunner(); return new SingleThreadTaskRunner();
} }

@ -142,7 +142,8 @@ public class CommonSteps extends BaseUserInterfaceTest
switch(screen) switch(screen)
{ {
case LIST_HABITS: case LIST_HABITS:
onView(withId(R.id.header)).check(matches(isDisplayed())); onView(withClassName(endsWith("ListHabitsRootView")))
.check(matches(isDisplayed()));
break; break;
case SHOW_HABIT: case SHOW_HABIT:

@ -20,7 +20,6 @@
package org.isoron.uhabits.acceptance.steps; package org.isoron.uhabits.acceptance.steps;
import android.support.test.espresso.*; import android.support.test.espresso.*;
import android.support.test.uiautomator.*;
import android.view.*; import android.view.*;
import org.hamcrest.*; import org.hamcrest.*;
@ -29,13 +28,18 @@ import org.isoron.uhabits.activities.habits.list.views.*;
import java.util.*; import java.util.*;
import static android.support.test.InstrumentationRegistry.*; import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.Espresso.*; import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.action.ViewActions.*; import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant;
import static android.support.test.espresso.matcher.ViewMatchers.*; import static android.support.test.espresso.matcher.ViewMatchers.isEnabled;
import static android.support.test.espresso.matcher.ViewMatchers.withClassName;
import static android.support.test.espresso.matcher.ViewMatchers.withContentDescription;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withParent;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.CoreMatchers.*;
import static org.isoron.uhabits.BaseUserInterfaceTest.device; import static org.isoron.uhabits.BaseUserInterfaceTest.device;
import static org.isoron.uhabits.acceptance.steps.CommonSteps.*; import static org.isoron.uhabits.acceptance.steps.CommonSteps.clickText;
public abstract class ListHabitsSteps public abstract class ListHabitsSteps
{ {
@ -89,17 +93,8 @@ public abstract class ListHabitsSteps
private static void clickTextInsideOverflowMenu(int id) private static void clickTextInsideOverflowMenu(int id)
{ {
UiObject toolbar = device.findObject( onView(allOf(withContentDescription("More options"), withParent(
new UiSelector().resourceId("org.isoron.uhabits:id/toolbar")); withParent(withClassName(endsWith("Toolbar")))))).perform(click());
if (toolbar.exists())
{
onView(allOf(withContentDescription("More options"),
withParent(withParent(withId(R.id.toolbar))))).perform(click());
}
else
{
openActionBarOverflowOrOptionsMenu(getTargetContext());
}
onView(withText(id)).perform(click()); onView(withText(id)).perform(click());
} }

@ -1,87 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list.views;
import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.utils.*;
import org.junit.*;
import org.junit.runner.*;
import java.io.*;
@RunWith(AndroidJUnit4.class)
@MediumTest
public class CheckmarkButtonViewTest extends BaseViewTest
{
public static final String PATH = "habits/list/CheckmarkButtonView/";
private CheckmarkButtonView view;
@Override
@Before
public void setUp()
{
super.setUp();
view = new CheckmarkButtonView(targetContext);
view.setValue(Checkmark.UNCHECKED);
view.setColor(PaletteUtils.getAndroidTestColor(5));
measureView(view, dpToPixels(48), dpToPixels(48));
}
@Test
public void testRender_explicitCheck() throws Exception
{
view.setValue(Checkmark.CHECKED_EXPLICITLY);
assertRendersCheckedExplicitly();
}
@Test
public void testRender_implicitCheck() throws Exception
{
view.setValue(Checkmark.CHECKED_IMPLICITLY);
assertRendersCheckedImplicitly();
}
@Test
public void testRender_unchecked() throws Exception
{
view.setValue(Checkmark.UNCHECKED);
assertRendersUnchecked();
}
protected void assertRendersCheckedExplicitly() throws IOException
{
assertRenders(view, PATH + "render_explicit_check.png");
}
protected void assertRendersCheckedImplicitly() throws IOException
{
assertRenders(view, PATH + "render_implicit_check.png");
}
protected void assertRendersUnchecked() throws IOException
{
assertRenders(view, PATH + "render_unchecked.png");
}
}

@ -0,0 +1,99 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list.views
import android.support.test.filters.*
import android.support.test.runner.*
import org.isoron.uhabits.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.utils.*
import org.junit.*
import org.junit.runner.*
@RunWith(AndroidJUnit4::class)
@MediumTest
class CheckmarkButtonViewTest : BaseViewTest() {
private val PATH = "habits/list/CheckmarkButtonView"
lateinit var view: CheckmarkButtonView
var toggled = false
@Before
override fun setUp() {
super.setUp()
view = component.getCheckmarkButtonViewFactory().create().apply {
value = Checkmark.UNCHECKED
color = PaletteUtils.getAndroidTestColor(5)
onToggle = { toggled = true }
}
measureView(view, dpToPixels(48), dpToPixels(48))
}
@Test
fun testRender_explicitCheck() {
view.value = Checkmark.CHECKED_EXPLICITLY
assertRendersCheckedExplicitly()
}
@Test
fun testRender_implicitCheck() {
view.value = Checkmark.CHECKED_IMPLICITLY
assertRendersCheckedImplicitly()
}
@Test
fun testRender_unchecked() {
view.value = Checkmark.UNCHECKED
assertRendersUnchecked()
}
@Test
fun testClick_withShortToggleDisabled() {
prefs.isShortToggleEnabled = false
view.performClick()
assertFalse(toggled)
}
@Test
fun testClick_withShortToggleEnabled() {
prefs.isShortToggleEnabled = true
view.performClick()
assertTrue(toggled)
}
@Test
fun testLongClick() {
view.performLongClick()
assertTrue(toggled)
}
private fun assertRendersCheckedExplicitly() {
assertRenders(view, "$PATH/render_explicit_check.png")
}
private fun assertRendersCheckedImplicitly() {
assertRenders(view, "$PATH/render_implicit_check.png")
}
private fun assertRendersUnchecked() {
assertRenders(view, "$PATH/render_unchecked.png")
}
}

@ -1,96 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list.views;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.*;
import org.isoron.uhabits.core.models.Checkmark;
import org.isoron.uhabits.core.models.Habit;
import org.isoron.uhabits.BaseViewTest;
import org.isoron.uhabits.utils.*;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.concurrent.CountDownLatch;
@RunWith(AndroidJUnit4.class)
@MediumTest
public class CheckmarkPanelViewTest extends BaseViewTest
{
public static final String PATH = "habits/list/CheckmarkPanelView/";
private CountDownLatch latch;
private CheckmarkPanelView view;
private int checkmarks[];
@Override
@Before
public void setUp()
{
super.setUp();
prefs.setShouldReverseCheckmarks(false);
Habit habit = fixtures.createEmptyHabit();
latch = new CountDownLatch(1);
checkmarks = new int[]{
Checkmark.CHECKED_EXPLICITLY, Checkmark.UNCHECKED,
Checkmark.CHECKED_IMPLICITLY, Checkmark.CHECKED_EXPLICITLY};
view = new CheckmarkPanelView(targetContext);
view.setValues(checkmarks);
view.setButtonCount(4);
view.setColor(PaletteUtils.getAndroidTestColor(7));
measureView(view, dpToPixels(200), dpToPixels(200));
}
// protected void waitForLatch() throws InterruptedException
// {
// assertTrue("Latch timeout", latch.await(1, TimeUnit.SECONDS));
// }
@Test
public void testRender() throws Exception
{
assertRenders(view, PATH + "render.png");
}
// @Test
// public void testToggleCheckmark_withLeftToRight() throws Exception
// {
// setToggleListener();
// view.getButton(1).performToggle();
// waitForLatch();
// }
//
// @Test
// public void testToggleCheckmark_withReverseCheckmarks() throws Exception
// {
// prefs.setShouldReverseCheckmarks(true);
// view.setCheckmarkValues(checkmarks); // refresh after preference change
//
// setToggleListener();
// view.getButton(2).performToggle();
// waitForLatch();
// }
}

@ -0,0 +1,110 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list.views
import android.support.test.filters.*
import android.support.test.runner.*
import org.hamcrest.CoreMatchers.*
import org.hamcrest.MatcherAssert.*
import org.isoron.uhabits.*
import org.isoron.uhabits.core.models.Checkmark.*
import org.isoron.uhabits.utils.*
import org.junit.*
import org.junit.runner.*
@RunWith(AndroidJUnit4::class)
@MediumTest
class CheckmarkPanelViewTest : BaseViewTest() {
private val PATH = "habits/list/CheckmarkPanelView"
private lateinit var view: CheckmarkPanelView
@Before
override fun setUp() {
super.setUp()
prefs.isCheckmarkSequenceReversed = false
val checkmarks = intArrayOf(CHECKED_EXPLICITLY,
CHECKED_EXPLICITLY,
CHECKED_IMPLICITLY,
UNCHECKED,
UNCHECKED,
UNCHECKED,
CHECKED_EXPLICITLY)
view = component.getCheckmarkPanelViewFactory().create().apply {
values = checkmarks
buttonCount = 4
color = PaletteUtils.getAndroidTestColor(7)
}
view.onAttachedToWindow()
measureView(view, dpToPixels(200), dpToPixels(200))
}
@After
public override fun tearDown() {
// view.onDetachedFromWindow()
super.tearDown()
}
@Test
fun testRender() {
assertRenders(view, "$PATH/render.png")
}
@Test
fun testRender_withDifferentColor() {
view.color = PaletteUtils.getAndroidTestColor(1)
assertRenders(view, "$PATH/render_different_color.png")
}
@Test
fun testRender_Reversed() {
prefs.isCheckmarkSequenceReversed = true
assertRenders(view, "$PATH/render_reversed.png")
}
@Test
fun testRender_withOffset() {
view.dataOffset = 3
assertRenders(view, "$PATH/render_offset.png")
}
@Test
fun testToggle() {
var timestamps = mutableListOf<Long>()
view.onToggle = { timestamps.add(it) }
view.buttons[0].performLongClick()
view.buttons[2].performLongClick()
view.buttons[3].performLongClick()
assertThat(timestamps, equalTo(listOf(day(0), day(2), day(3))))
}
@Test
fun testToggle_withOffset() {
var timestamps = LongArray(0)
view.dataOffset = 3
view.onToggle = { timestamps += it }
view.buttons[0].performLongClick()
view.buttons[2].performLongClick()
view.buttons[3].performLongClick()
assertThat(timestamps, equalTo(longArrayOf(day(3), day(5), day(6))))
}
}

@ -1,86 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list.views;
import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.utils.*;
import org.junit.*;
import org.junit.runner.*;
@RunWith(AndroidJUnit4.class)
@MediumTest
public class HabitCardViewTest extends BaseViewTest
{
private HabitCardView view;
public static final String PATH = "habits/list/HabitCardView/";
private Habit habit;
@Override
public void setUp()
{
super.setUp();
setTheme(R.style.AppBaseTheme);
habit = fixtures.createLongHabit();
CheckmarkList checkmarks = habit.getCheckmarks();
long today = DateUtils.getStartOfToday();
long day = DateUtils.millisecondsInOneDay;
int[] values = checkmarks.getValues(today - 5 * day, today);
view = new HabitCardView(targetContext);
view.setHabit(habit);
view.setValues(values);
view.setSelected(false);
view.setScore(habit.getScores().getTodayValue());
view.setButtonCount(6);
measureView(view, dpToPixels(400), dpToPixels(50));
}
@Test
public void testRender() throws Exception
{
assertRenders(view, PATH + "render.png");
}
@Test
public void testRender_selected() throws Exception
{
view.setSelected(true);
measureView(view, dpToPixels(400), dpToPixels(50));
assertRenders(view, PATH + "render_selected.png");
}
@Test
public void testChangeModel() throws Exception
{
habit.setName("Wake up early");
habit.setColor(2);
habit.getObservable().notifyListeners();
Thread.sleep(500);
assertRenders(view, PATH + "render_changed.png");
}
}

@ -0,0 +1,86 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list.views
import android.support.test.filters.*
import android.support.test.runner.*
import org.isoron.uhabits.*
import org.isoron.uhabits.core.models.*
import org.junit.*
import org.junit.runner.*
@RunWith(AndroidJUnit4::class)
@MediumTest
class HabitCardViewTest : BaseViewTest() {
val PATH = "habits/list/HabitCardView"
lateinit private var view: HabitCardView
lateinit private var habit1: Habit
lateinit private var habit2: Habit
override fun setUp() {
super.setUp()
setTheme(R.style.AppBaseTheme)
habit1 = fixtures.createLongHabit()
habit2 = fixtures.createLongNumericalHabit()
view = component.getHabitCardViewFactory().create().apply {
habit = habit1
values = habit1.checkmarks.allValues
score = habit1.scores.todayValue
isSelected = false
buttonCount = 5
}
latch.countDown()
latch.await()
measureView(view, dpToPixels(400), dpToPixels(50))
}
@Test
fun testRender() {
assertRenders(view, "$PATH/render.png")
}
@Test
fun testRender_selected() {
view.isSelected = true
measureView(view, dpToPixels(400), dpToPixels(50))
assertRenders(view, "$PATH/render_selected.png")
}
@Test
fun testRender_numerical() {
view.apply {
habit = habit2
values = habit2.checkmarks.allValues
}
assertRenders(view, "$PATH/render_numerical.png")
}
@Test
fun testChangeModel() {
habit1.name = "Wake up early"
habit1.color = 2
habit1.observable.notifyListeners()
Thread.sleep(500)
assertRenders(view, "$PATH/render_changed.png")
}
}

@ -57,22 +57,22 @@ public class HeaderViewTest extends BaseViewTest
@Test @Test
public void testRender() throws Exception public void testRender() throws Exception
{ {
when(prefs.shouldReverseCheckmarks()).thenReturn(false); when(prefs.isCheckmarkSequenceReversed()).thenReturn(false);
assertRenders(view, PATH + "render.png"); assertRenders(view, PATH + "render.png");
verify(prefs).shouldReverseCheckmarks(); verify(prefs).isCheckmarkSequenceReversed();
verifyNoMoreInteractions(prefs); verifyNoMoreInteractions(prefs);
} }
@Test @Test
public void testRender_reverse() throws Exception public void testRender_reverse() throws Exception
{ {
when(prefs.shouldReverseCheckmarks()).thenReturn(true); when(prefs.isCheckmarkSequenceReversed()).thenReturn(true);
assertRenders(view, PATH + "render_reverse.png"); assertRenders(view, PATH + "render_reverse.png");
verify(prefs).shouldReverseCheckmarks(); verify(prefs).isCheckmarkSequenceReversed();
verifyNoMoreInteractions(prefs); verifyNoMoreInteractions(prefs);
} }
} }

@ -47,9 +47,8 @@ public class HintViewTest extends BaseViewTest
{ {
super.setUp(); super.setUp();
view = new HintView(targetContext);
list = mock(HintList.class); list = mock(HintList.class);
view.setHints(list); view = new HintView(targetContext, list);
measureView(view, 400, 200); measureView(view, 400, 200);
String text = String text =

@ -1,95 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list.views;
import android.support.test.filters.*;
import android.support.test.runner.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.utils.*;
import org.junit.*;
import org.junit.runner.*;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.*;
@RunWith(AndroidJUnit4.class)
@MediumTest
public class NumberButtonViewTest extends BaseViewTest
{
public static final String PATH = "habits/list/NumberButtonView/";
private NumberButtonView view;
@Override
@Before
public void setUp()
{
super.setUp();
view = new NumberButtonView(targetContext);
view.setUnit("steps");
view.setThreshold(100.0);
view.setColor(PaletteUtils.getAndroidTestColor(8));
measureView(view, dpToPixels(48), dpToPixels(48));
}
@Test
public void testFormatValue()
{
assertThat(NumberButtonView.formatValue(0.1235), equalTo("0.12"));
assertThat(NumberButtonView.formatValue(0.1000), equalTo("0.1"));
assertThat(NumberButtonView.formatValue(5.0), equalTo("5"));
assertThat(NumberButtonView.formatValue(5.25), equalTo("5.25"));
assertThat(NumberButtonView.formatValue(12.3456), equalTo("12.3"));
assertThat(NumberButtonView.formatValue(123.123), equalTo("123"));
assertThat(NumberButtonView.formatValue(321.2), equalTo("321"));
assertThat(NumberButtonView.formatValue(4321.2), equalTo("4.3k"));
assertThat(NumberButtonView.formatValue(54321.2), equalTo("54.3k"));
assertThat(NumberButtonView.formatValue(654321.2), equalTo("654k"));
assertThat(NumberButtonView.formatValue(7654321.2), equalTo("7.7M"));
assertThat(NumberButtonView.formatValue(87654321.2), equalTo("87.7M"));
assertThat(NumberButtonView.formatValue(987654321.2), equalTo("988M"));
assertThat(NumberButtonView.formatValue(1987654321.2), equalTo("2.0G"));
}
@Test
public void testRender_aboveThreshold() throws Exception
{
view.setValue(500);
assertRenders(view, PATH + "render_above.png");
}
@Test
public void testRender_belowThreshold() throws Exception
{
view.setValue(99);
assertRenders(view, PATH + "render_below.png");
}
@Test
public void testRender_zero() throws Exception
{
view.setValue(0);
assertRenders(view, PATH + "render_zero.png");
}
}

@ -0,0 +1,106 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list.views
import android.support.test.filters.*
import android.support.test.runner.*
import org.hamcrest.CoreMatchers.*
import org.hamcrest.MatcherAssert.*
import org.isoron.uhabits.*
import org.isoron.uhabits.utils.*
import org.junit.*
import org.junit.runner.*
@RunWith(AndroidJUnit4::class)
@MediumTest
class NumberButtonViewTest : BaseViewTest() {
private val PATH = "habits/list/NumberButtonView"
private lateinit var view: NumberButtonView
var edited = false
@Before
override fun setUp() {
super.setUp()
view = component.getNumberButtonViewFactory().create().apply {
units = "steps"
threshold = 100.0
color = PaletteUtils.getAndroidTestColor(8)
onEdit = { edited = true }
}
measureView(view, dpToPixels(48), dpToPixels(48))
}
@Test
fun testFormatValue() {
assertThat(0.1235.toShortString(), equalTo("0.12"))
assertThat(0.1000.toShortString(), equalTo("0.1"))
assertThat(5.0.toShortString(), equalTo("5"))
assertThat(5.25.toShortString(), equalTo("5.25"))
assertThat(12.3456.toShortString(), equalTo("12.3"))
assertThat(123.123.toShortString(), equalTo("123"))
assertThat(321.2.toShortString(), equalTo("321"))
assertThat(4321.2.toShortString(), equalTo("4.3k"))
assertThat(54321.2.toShortString(), equalTo("54.3k"))
assertThat(654321.2.toShortString(), equalTo("654k"))
assertThat(7654321.2.toShortString(), equalTo("7.7M"))
assertThat(87654321.2.toShortString(), equalTo("87.7M"))
assertThat(987654321.2.toShortString(), equalTo("988M"))
assertThat(1987654321.2.toShortString(), equalTo("2.0G"))
}
@Test
fun testRender_aboveThreshold() {
view.value = 500.0
assertRenders(view, "$PATH/render_above.png")
}
@Test
fun testRender_belowThreshold() {
view.value = 99.0
assertRenders(view, "$PATH/render_below.png")
}
@Test
fun testRender_zero() {
view.value = 0.0
assertRenders(view, "$PATH/render_zero.png")
}
@Test
fun testClick_shortToggleDisabled() {
prefs.isShortToggleEnabled = false
view.performClick()
assertFalse(edited)
}
@Test
fun testClick_shortToggleEnabled() {
prefs.isShortToggleEnabled = true
view.performClick()
assertTrue(edited)
}
@Test
fun testLongClick() {
view.performLongClick()
assertTrue(edited)
}
}

@ -0,0 +1,105 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list.views
import android.support.test.filters.*
import android.support.test.runner.*
import org.hamcrest.CoreMatchers.*
import org.hamcrest.MatcherAssert.*
import org.isoron.uhabits.*
import org.isoron.uhabits.utils.*
import org.junit.*
import org.junit.runner.*
@RunWith(AndroidJUnit4::class)
@MediumTest
class NumberPanelViewTest : BaseViewTest() {
private val PATH = "habits/list/NumberPanelView"
private lateinit var view: NumberPanelView
@Before
override fun setUp() {
super.setUp()
prefs.isCheckmarkSequenceReversed = false
val checkmarks = doubleArrayOf(1400.0, 5300.0, 0.0,
14600.0, 2500.0, 45000.0)
view = component.getNumberPanelViewFactory().create().apply {
values = checkmarks
buttonCount = 4
color = PaletteUtils.getAndroidTestColor(7)
units = "steps"
threshold = 5000.0
}
view.onAttachedToWindow()
measureView(view, dpToPixels(200), dpToPixels(200))
}
@After
public override fun tearDown() {
view.onDetachedFromWindow()
}
@Test
fun testRender() {
assertRenders(view, "$PATH/render.png")
}
@Test
fun testRender_withDifferentColor() {
view.color = PaletteUtils.getAndroidTestColor(1)
assertRenders(view, "$PATH/render_different_color.png")
}
@Test
fun testRender_Reversed() {
prefs.isCheckmarkSequenceReversed = true
assertRenders(view, "$PATH/render_reversed.png")
}
@Test
fun testRender_withOffset() {
view.dataOffset = 3
assertRenders(view, "$PATH/render_offset.png")
}
@Test
fun testEdit() {
var timestamps = LongArray(0)
view.onEdit = { timestamps += it }
view.buttons[0].performLongClick()
view.buttons[2].performLongClick()
view.buttons[3].performLongClick()
assertThat(timestamps, equalTo(longArrayOf(day(0), day(2), day(3))))
}
@Test
fun testEdit_withOffset() {
var timestamps = LongArray(0)
view.dataOffset = 3
view.onEdit = { timestamps += it }
view.buttons[0].performLongClick()
view.buttons[2].performLongClick()
view.buttons[3].performLongClick()
assertThat(timestamps, equalTo(longArrayOf(day(3), day(5), day(6))))
}
}

@ -1,63 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities;
import android.support.annotation.*;
import org.isoron.androidbase.activities.*;
import org.isoron.uhabits.R;
import org.isoron.uhabits.core.preferences.*;
import org.isoron.uhabits.core.ui.*;
import javax.inject.*;
@ActivityScope
public class AndroidThemeSwitcher extends ThemeSwitcher
{
@NonNull
private final BaseActivity activity;
@Inject
public AndroidThemeSwitcher(@NonNull BaseActivity activity,
@NonNull Preferences preferences)
{
super(preferences);
this.activity = activity;
}
@Override
public void applyDarkTheme()
{
activity.setTheme(R.style.AppBaseThemeDark);
}
@Override
public void applyLightTheme()
{
activity.setTheme(R.style.AppBaseTheme);
}
@Override
public void applyPureBlackTheme()
{
activity.setTheme(R.style.AppBaseThemeDark_PureBlack);
}
}

@ -17,8 +17,30 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
/** package org.isoron.uhabits.activities
* Provides custom views that are used primarily on {@link
* org.isoron.uhabits.activities.habits.show.ShowHabitActivity}. import org.isoron.androidbase.activities.*
*/ import org.isoron.uhabits.*
package org.isoron.uhabits.activities.habits.show.views; import org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.core.ui.*
import javax.inject.*
@ActivityScope
class AndroidThemeSwitcher
@Inject constructor(
private val activity: BaseActivity,
preferences: Preferences
) : ThemeSwitcher(preferences) {
override fun applyDarkTheme() {
activity.setTheme(R.style.AppBaseThemeDark)
}
override fun applyLightTheme() {
activity.setTheme(R.style.AppBaseTheme)
}
override fun applyPureBlackTheme() {
activity.setTheme(R.style.AppBaseThemeDark_PureBlack)
}
}

@ -17,7 +17,12 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
/** package org.isoron.uhabits.activities
* Provides classes that deal with importing from and exporting to files.
*/ import dagger.*
package org.isoron.uhabits.core.io; import org.isoron.uhabits.core.models.*
@Module
class HabitModule(private val habit: Habit) {
@Provides fun getHabit() = habit
}

@ -1,78 +0,0 @@
/*
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities;
import android.content.*;
import android.net.*;
import android.os.*;
import android.support.annotation.*;
import org.isoron.androidbase.activities.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.core.models.*;
public abstract class HabitsActivity extends BaseActivity
{
private HabitsActivityComponent component;
private HabitsApplicationComponent appComponent;
public HabitsActivityComponent getActivityComponent()
{
return component;
}
public HabitsApplicationComponent getAppComponent()
{
return appComponent;
}
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
appComponent =
((HabitsApplication) getApplicationContext()).getComponent();
Habit habit = getHabitFromIntent(appComponent.getHabitList());
component = DaggerHabitsActivityComponent
.builder()
.activityModule(new ActivityModule(this))
.habitModule(new HabitModule(habit))
.habitsApplicationComponent(appComponent)
.build();
component.getThemeSwitcher().apply();
}
@Nullable
private Habit getHabitFromIntent(@NonNull HabitList habitList)
{
Uri data = getIntent().getData();
if(data == null) return null;
Habit habit = habitList.getById(ContentUris.parseId(data));
if (habit == null) throw new RuntimeException("habit not found");
return habit;
}
}

@ -0,0 +1,57 @@
/*
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities
import android.content.*
import android.os.*
import org.isoron.androidbase.activities.*
import org.isoron.uhabits.*
import org.isoron.uhabits.core.models.*
abstract class HabitsActivity : BaseActivity() {
lateinit var component: HabitsActivityComponent
lateinit var appComponent: HabitsApplicationComponent
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
appComponent = (applicationContext as HabitsApplication).component
val habit = getHabitFromIntent(appComponent.habitList)
?: appComponent.modelFactory.buildHabit()
component = DaggerHabitsActivityComponent
.builder()
.activityContextModule(ActivityContextModule(this))
.baseActivityModule(BaseActivityModule(this))
.habitModule(HabitModule(habit))
.habitsApplicationComponent(appComponent)
.build()
component.themeSwitcher.apply()
}
private fun getHabitFromIntent(habitList: HabitList): Habit? {
val data = intent.data ?: return null
val habit = habitList.getById(ContentUris.parseId(data))
?: throw RuntimeException("habit not found")
return habit
}
}

@ -1,68 +0,0 @@
/*
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities;
import org.isoron.androidbase.activities.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.about.*;
import org.isoron.uhabits.activities.common.dialogs.*;
import org.isoron.uhabits.activities.habits.list.*;
import org.isoron.uhabits.activities.habits.list.views.*;
import org.isoron.uhabits.activities.habits.show.*;
import org.isoron.uhabits.core.ui.*;
import org.isoron.uhabits.core.ui.screens.habits.list.*;
import dagger.*;
@ActivityScope
@Component(modules = {
ActivityModule.class,
AboutModule.class,
HabitsActivityModule.class,
ListHabitsModule.class,
ShowHabitModule.class,
HabitModule.class
}, dependencies = { HabitsApplicationComponent.class })
public interface HabitsActivityComponent
{
AboutRootView getAboutRootView();
AboutScreen getAboutScreen();
ColorPickerDialogFactory getColorPickerDialogFactory();
HabitCardListAdapter getHabitCardListAdapter();
ListHabitsBehavior getListHabitsBehavior();
ListHabitsController getListHabitsController();
ListHabitsMenu getListHabitsMenu();
ListHabitsRootView getListHabitsRootView();
ListHabitsScreen getListHabitsScreen();
ListHabitsSelectionMenu getListHabitsSelectionMenu();
ShowHabitScreen getShowHabitScreen();
ThemeSwitcher getThemeSwitcher();
}

@ -0,0 +1,55 @@
/*
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities
import dagger.*
import org.isoron.androidbase.activities.*
import org.isoron.uhabits.*
import org.isoron.uhabits.activities.about.*
import org.isoron.uhabits.activities.common.dialogs.*
import org.isoron.uhabits.activities.habits.list.*
import org.isoron.uhabits.activities.habits.list.views.*
import org.isoron.uhabits.activities.habits.show.*
import org.isoron.uhabits.core.ui.*
import org.isoron.uhabits.core.ui.screens.habits.list.*
@ActivityScope
@Component(modules = arrayOf(
ActivityContextModule::class,
BaseActivityModule::class,
AboutModule::class,
HabitsActivityModule::class,
ListHabitsModule::class,
ShowHabitModule::class,
HabitModule::class
), dependencies = arrayOf(HabitsApplicationComponent::class))
interface HabitsActivityComponent {
val aboutRootView: AboutRootView
val aboutScreen: AboutScreen
val colorPickerDialogFactory: ColorPickerDialogFactory
val habitCardListAdapter: HabitCardListAdapter
val listHabitsBehavior: ListHabitsBehavior
val listHabitsMenu: ListHabitsMenu
val listHabitsRootView: ListHabitsRootView
val listHabitsScreen: ListHabitsScreen
val listHabitsSelectionMenu: ListHabitsSelectionMenu
val showHabitScreen: ShowHabitScreen
val themeSwitcher: ThemeSwitcher
}

@ -17,17 +17,14 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.isoron.uhabits.activities; package org.isoron.uhabits.activities
import org.isoron.androidbase.activities.*; import dagger.*
import org.isoron.uhabits.core.ui.*; import org.isoron.androidbase.activities.*
import org.isoron.uhabits.core.ui.*
import dagger.*;
@Module @Module
public abstract class HabitsActivityModule abstract class HabitsActivityModule {
{ @Binds @ActivityScope
@Binds internal abstract fun getThemeSwitcher(t: AndroidThemeSwitcher): ThemeSwitcher
@ActivityScope
abstract ThemeSwitcher getThemeSwitcher(AndroidThemeSwitcher t);
} }

@ -1,47 +0,0 @@
/*
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities;
import android.support.annotation.*;
import org.isoron.androidbase.*;
import org.isoron.uhabits.core.ui.screens.habits.list.*;
import org.isoron.uhabits.core.ui.screens.habits.show.*;
import java.io.*;
import javax.inject.*;
public class HabitsDirFinder
implements ShowHabitMenuBehavior.System, ListHabitsBehavior.DirFinder
{
private AndroidDirFinder androidDirFinder;
@Inject
public HabitsDirFinder(@NonNull AndroidDirFinder androidDirFinder)
{
this.androidDirFinder = androidDirFinder;
}
@Override
public File getCSVOutputDir()
{
return androidDirFinder.getFilesDir("CSV");
}
}

@ -16,16 +16,20 @@
* You should have received a copy of the GNU General Public License along * You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.isoron.uhabits.activities
package org.isoron.uhabits.activities.habits.list.views; import org.isoron.androidbase.*
import org.isoron.uhabits.core.ui.screens.habits.list.*
import org.isoron.uhabits.core.ui.screens.habits.show.*
import java.io.*
import javax.inject.*
import android.support.v7.widget.*; class HabitsDirFinder @Inject
import android.view.*; constructor(
private val androidDirFinder: AndroidDirFinder
) : ShowHabitMenuBehavior.System, ListHabitsBehavior.DirFinder {
public class HabitCardViewHolder extends RecyclerView.ViewHolder override fun getCSVOutputDir(): File {
{ return androidDirFinder.getFilesDir("CSV")!!
public HabitCardViewHolder(View itemView)
{
super(itemView);
} }
} }

@ -33,8 +33,8 @@ public class AboutActivity extends HabitsActivity
protected void onCreate(Bundle savedInstanceState) protected void onCreate(Bundle savedInstanceState)
{ {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
AboutScreen screen = getActivityComponent().getAboutScreen(); AboutScreen screen = getComponent().getAboutScreen();
screen.setRootView(getActivityComponent().getAboutRootView()); screen.setRootView(getComponent().getAboutRootView());
setScreen(screen); setScreen(screen);
} }
} }

@ -1,23 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Provides activity that shows information about the app.
*/
package org.isoron.uhabits.activities.about;

@ -0,0 +1,91 @@
/*
* Copyright (C) 2017 Álinson Santos Xavier
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see .
*/
package org.isoron.uhabits.activities.common.dialogs
import android.content.*
import android.support.v7.app.*
import android.text.*
import android.view.*
import android.view.inputmethod.*
import android.widget.*
import org.isoron.androidbase.activities.*
import org.isoron.androidbase.utils.*
import org.isoron.uhabits.*
import org.isoron.uhabits.core.ui.screens.habits.list.*
import javax.inject.*
class NumberPickerFactory
@Inject constructor(
@ActivityContext private val context: Context
) {
fun create(value: Double,
unit: String,
callback: ListHabitsBehavior.NumberPickerCallback): AlertDialog {
val inflater = LayoutInflater.from(context)
val view = inflater.inflate(R.layout.number_picker_dialog, null)
val picker = view.findViewById(R.id.picker) as NumberPicker
val picker2 = view.findViewById(R.id.picker2) as NumberPicker
val tvUnit = view.findViewById(R.id.tvUnit) as TextView
val intValue = Math.round(value * 100).toInt()
picker.minValue = 0
picker.maxValue = Integer.MAX_VALUE / 100
picker.value = intValue / 100
picker.wrapSelectorWheel = false
picker2.minValue = 0
picker2.maxValue = 19
picker2.setFormatter { v -> String.format("%02d", 5 * v) }
picker2.value = intValue % 100 / 5
refreshInitialValue(picker2)
tvUnit.text = unit
val dialog = AlertDialog.Builder(context)
.setView(view)
.setTitle(R.string.change_value)
.setPositiveButton(android.R.string.ok) { _, _ ->
picker.clearFocus()
val v = picker.value + 0.05 * picker2.value
callback.onNumberPicked(v)
}
.create()
InterfaceUtils.setupEditorAction(picker) { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE)
dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick()
false
}
return dialog
}
private fun refreshInitialValue(picker: NumberPicker) {
// Workaround for Android bug:
// https://code.google.com/p/android/issues/detail?id=35482
val f = NumberPicker::class.java.getDeclaredField("mInputText")
f.isAccessible = true
val inputText = f.get(picker) as EditText
inputText.filters = arrayOfNulls<InputFilter>(0)
}
}

@ -361,7 +361,7 @@ public class BarChart extends ScrollableChart
if (value / 1000 >= target) if (value / 1000 >= target)
activeColor = primaryColor; activeColor = primaryColor;
String label = NumberButtonView.formatValue(value / 1000); String label = NumberButtonViewKt.toShortString(value / 1000);
Rect rText = new Rect(); Rect rText = new Rect();
pText.getTextBounds(label, 0, label.length(), rText); pText.getTextBounds(label, 0, label.length(), rText);

@ -114,13 +114,18 @@ public class RingView extends View
public void setBackgroundColor(int backgroundColor) public void setBackgroundColor(int backgroundColor)
{ {
this.backgroundColor = backgroundColor; this.backgroundColor = backgroundColor;
postInvalidate(); invalidate();
} }
public void setColor(int color) public void setColor(int color)
{ {
this.color = color; this.color = color;
postInvalidate(); invalidate();
}
public int getColor()
{
return color;
} }
public void setIsTransparencyEnabled(boolean isTransparencyEnabled) public void setIsTransparencyEnabled(boolean isTransparencyEnabled)
@ -131,19 +136,19 @@ public class RingView extends View
public void setPercentage(float percentage) public void setPercentage(float percentage)
{ {
this.percentage = percentage; this.percentage = percentage;
postInvalidate(); invalidate();
} }
public void setPrecision(float precision) public void setPrecision(float precision)
{ {
this.precision = precision; this.precision = precision;
postInvalidate(); invalidate();
} }
public void setText(String text) public void setText(String text)
{ {
this.text = text; this.text = text;
postInvalidate(); invalidate();
} }
public void setTextSize(float textSize) public void setTextSize(float textSize)
@ -154,7 +159,7 @@ public class RingView extends View
public void setThickness(float thickness) public void setThickness(float thickness)
{ {
this.thickness = thickness; this.thickness = thickness;
postInvalidate(); invalidate();
} }
@Override @Override
@ -254,4 +259,14 @@ public class RingView extends View
Bitmap.createBitmap(diameter, diameter, Bitmap.Config.ARGB_8888); Bitmap.createBitmap(diameter, diameter, Bitmap.Config.ARGB_8888);
cacheCanvas = new Canvas(drawingCache); cacheCanvas = new Canvas(drawingCache);
} }
public float getPercentage()
{
return percentage;
}
public float getPrecision()
{
return precision;
}
} }

@ -177,7 +177,7 @@ public abstract class ScrollableChart extends View
return detector.onTouchEvent(event); return detector.onTouchEvent(event);
} }
public void setDirection(int direction) public void setScrollDirection(int direction)
{ {
if (direction != 1 && direction != -1) if (direction != 1 && direction != -1)
throw new IllegalArgumentException(); throw new IllegalArgumentException();

@ -0,0 +1,66 @@
/*
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.common.views
import android.content.*
import android.widget.*
import org.isoron.androidbase.activities.*
import org.isoron.uhabits.core.tasks.*
class TaskProgressBar(
context: Context,
private val runner: TaskRunner
) : ProgressBar(
context,
null,
android.R.attr.progressBarStyleHorizontal
), TaskRunner.Listener {
init {
visibility = BaseRootView.GONE
isIndeterminate = true
}
override fun onTaskStarted(task: Task?) = update()
override fun onTaskFinished(task: Task?) = update()
override fun onAttachedToWindow() {
super.onAttachedToWindow()
runner.addListener(this)
update()
}
override fun onDetachedFromWindow() {
runner.removeListener(this)
super.onDetachedFromWindow()
}
fun update() {
val callback = {
val activeTaskCount = runner.activeTaskCount
val newVisibility = when (activeTaskCount) {
0 -> GONE
else -> VISIBLE
}
if (visibility != newVisibility) visibility = newVisibility
}
postDelayed(callback, 500)
}
}

@ -1,23 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Provides views that are used across the app, such as RingView.
*/
package org.isoron.uhabits.activities.common.views;

@ -93,7 +93,7 @@ public class EditHabitDialog extends AppCompatDialogFragment
HabitsActivity activity = (HabitsActivity) getActivity(); HabitsActivity activity = (HabitsActivity) getActivity();
colorPickerDialogFactory = colorPickerDialogFactory =
activity.getActivityComponent().getColorPickerDialogFactory(); activity.getComponent().getColorPickerDialogFactory();
} }
@Override @Override

@ -1,23 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Provides dialogs for editing habits and related classes.
*/
package org.isoron.uhabits.activities.habits.edit;

@ -1,102 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list;
import android.os.*;
import org.isoron.uhabits.activities.*;
import org.isoron.uhabits.activities.habits.list.views.*;
import org.isoron.uhabits.core.preferences.*;
import org.isoron.uhabits.core.ui.*;
import org.isoron.uhabits.core.utils.*;
/**
* Activity that allows the user to see and modify the list of habits.
*/
public class ListHabitsActivity extends HabitsActivity
{
private HabitCardListAdapter adapter;
private ListHabitsRootView rootView;
private ListHabitsScreen screen;
private boolean pureBlack;
private Preferences prefs;
private MidnightTimer midnightTimer;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
midnightTimer = getAppComponent().getMidnightTimer();
HabitsActivityComponent component = getActivityComponent();
ListHabitsMenu menu = component.getListHabitsMenu();
ListHabitsSelectionMenu selectionMenu = component.getListHabitsSelectionMenu();
ListHabitsController controller = component.getListHabitsController();
adapter = component.getHabitCardListAdapter();
rootView = component.getListHabitsRootView();
screen = component.getListHabitsScreen();
prefs = getAppComponent().getPreferences();
pureBlack = prefs.isPureBlackEnabled();
screen.setMenu(menu);
screen.setController(controller);
screen.setSelectionMenu(selectionMenu);
rootView.setController(controller, selectionMenu);
// if(prefs.isSyncEnabled())
// startService(new Intent(this, SyncService.class));
setScreen(screen);
controller.onStartup();
}
@Override
protected void onPause()
{
midnightTimer.onPause();
screen.onDettached();
adapter.cancelRefresh();
super.onPause();
}
@Override
protected void onResume()
{
adapter.refresh();
screen.onAttached();
rootView.postInvalidate();
midnightTimer.onResume();
if (prefs.getTheme() == ThemeSwitcher.THEME_DARK &&
prefs.isPureBlackEnabled() != pureBlack)
{
restartWithFade(ListHabitsActivity.class);
}
super.onResume();
}
}

@ -0,0 +1,70 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list
import android.os.*
import org.isoron.uhabits.activities.*
import org.isoron.uhabits.activities.habits.list.views.*
import org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.core.ui.ThemeSwitcher.*
import org.isoron.uhabits.core.utils.*
class ListHabitsActivity : HabitsActivity() {
var pureBlack: Boolean = false
lateinit var adapter: HabitCardListAdapter
lateinit var rootView: ListHabitsRootView
lateinit var screen: ListHabitsScreen
lateinit var prefs: Preferences
lateinit var midnightTimer: MidnightTimer
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
prefs = appComponent.preferences
pureBlack = prefs.isPureBlackEnabled
midnightTimer = appComponent.midnightTimer
rootView = component.listHabitsRootView
screen = component.listHabitsScreen
adapter = component.habitCardListAdapter
setScreen(screen)
component.listHabitsBehavior.onStartup()
}
override fun onPause() {
midnightTimer.onPause()
screen.onDettached()
adapter.cancelRefresh()
super.onPause()
}
override fun onResume() {
adapter.refresh()
screen.onAttached()
rootView.postInvalidate()
midnightTimer.onResume()
if (prefs.theme == THEME_DARK && prefs.isPureBlackEnabled != pureBlack) {
restartWithFade(ListHabitsActivity::class.java)
}
super.onResume()
}
}

@ -1,164 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list;
import android.support.annotation.*;
import org.isoron.androidbase.activities.*;
import org.isoron.uhabits.R;
import org.isoron.uhabits.activities.habits.list.views.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.tasks.*;
import org.isoron.uhabits.core.ui.screens.habits.list.*;
import org.isoron.uhabits.tasks.*;
import java.io.*;
import javax.inject.*;
@ActivityScope
public class ListHabitsController
implements HabitCardListController.HabitListener
{
@NonNull
private final ListHabitsBehavior behavior;
@NonNull
private final ListHabitsScreen screen;
@NonNull
private final HabitCardListAdapter adapter;
@NonNull
private final TaskRunner taskRunner;
private ImportDataTaskFactory importTaskFactory;
private ExportDBTaskFactory exportDBFactory;
@Inject
public ListHabitsController(@NonNull ListHabitsBehavior behavior,
@NonNull HabitCardListAdapter adapter,
@NonNull ListHabitsScreen screen,
@NonNull TaskRunner taskRunner,
@NonNull ImportDataTaskFactory importTaskFactory,
@NonNull ExportDBTaskFactory exportDBFactory)
{
this.behavior = behavior;
this.adapter = adapter;
this.screen = screen;
this.taskRunner = taskRunner;
this.importTaskFactory = importTaskFactory;
this.exportDBFactory = exportDBFactory;
}
public void onEdit(@NonNull Habit habit, long timestamp)
{
behavior.onEdit(habit, timestamp);
}
public void onExportCSV()
{
behavior.onExportCSV();
}
public void onExportDB()
{
taskRunner.execute(exportDBFactory.create(filename ->
{
if (filename != null) screen.showSendFileScreen(filename);
else screen.showMessage(R.string.could_not_export);
}));
}
@Override
public void onHabitClick(@NonNull Habit h)
{
behavior.onClickHabit(h);
}
@Override
public void onHabitReorder(@NonNull Habit from, @NonNull Habit to)
{
behavior.onReorderHabit(from, to);
}
public void onImportData(@NonNull File file,
@NonNull OnFinishedListener finishedListener)
{
taskRunner.execute(importTaskFactory.create(file, result ->
{
switch (result)
{
case ImportDataTask.SUCCESS:
adapter.refresh();
screen.showMessage(R.string.habits_imported);
break;
case ImportDataTask.NOT_RECOGNIZED:
screen.showMessage(R.string.file_not_recognized);
break;
default:
screen.showMessage(R.string.could_not_import);
break;
}
finishedListener.onFinish();
}));
}
public void onInvalidEdit()
{
screen.showMessage(R.string.long_press_to_edit);
}
@Override
public void onInvalidToggle()
{
screen.showMessage(R.string.long_press_to_toggle);
}
public void onRepairDB()
{
behavior.onRepairDB();
}
public void onSendBugReport()
{
behavior.onSendBugReport();
}
public void onStartup()
{
behavior.onStartup();
}
@Override
public void onToggle(@NonNull Habit habit, long timestamp)
{
behavior.onToggle(habit, timestamp);
}
public interface OnFinishedListener
{
void onFinish();
}
}

@ -1,129 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list;
import android.support.annotation.*;
import android.view.*;
import org.isoron.androidbase.activities.*;
import org.isoron.uhabits.R;
import org.isoron.uhabits.core.preferences.*;
import org.isoron.uhabits.core.ui.*;
import org.isoron.uhabits.core.ui.screens.habits.list.*;
import javax.inject.*;
@ActivityScope
public class ListHabitsMenu extends BaseMenu
{
@NonNull
private final ListHabitsMenuBehavior behavior;
private final Preferences preferences;
private ThemeSwitcher themeSwitcher;
@Inject
public ListHabitsMenu(@NonNull BaseActivity activity,
@NonNull Preferences preferences,
@NonNull ThemeSwitcher themeSwitcher,
@NonNull ListHabitsMenuBehavior behavior)
{
super(activity);
this.preferences = preferences;
this.themeSwitcher = themeSwitcher;
this.behavior = behavior;
}
@Override
public void onCreate(@NonNull Menu menu)
{
MenuItem nightModeItem = menu.findItem(R.id.actionToggleNightMode);
MenuItem hideArchivedItem = menu.findItem(R.id.actionHideArchived);
MenuItem hideCompletedItem = menu.findItem(R.id.actionHideCompleted);
nightModeItem.setChecked(themeSwitcher.isNightMode());
hideArchivedItem.setChecked(!preferences.getShowArchived());
hideCompletedItem.setChecked(!preferences.getShowCompleted());
}
@Override
public boolean onItemSelected(@NonNull MenuItem item)
{
switch (item.getItemId())
{
case R.id.actionToggleNightMode:
behavior.onToggleNightMode();
return true;
case R.id.actionAdd:
behavior.onCreateHabit();
return true;
case R.id.actionFAQ:
behavior.onViewFAQ();
return true;
case R.id.actionAbout:
behavior.onViewAbout();
return true;
case R.id.actionSettings:
behavior.onViewSettings();
return true;
case R.id.actionHideArchived:
behavior.onToggleShowArchived();
invalidate();
return true;
case R.id.actionHideCompleted:
behavior.onToggleShowCompleted();
invalidate();
return true;
case R.id.actionSortColor:
behavior.onSortByColor();
return true;
case R.id.actionSortManual:
behavior.onSortByManually();
return true;
case R.id.actionSortName:
behavior.onSortByName();
return true;
case R.id.actionSortScore:
behavior.onSortByScore();
return true;
default:
return false;
}
}
@Override
protected int getMenuResourceId()
{
return R.menu.list_habits;
}
}

@ -0,0 +1,111 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list
import android.view.*
import org.isoron.androidbase.activities.*
import org.isoron.uhabits.*
import org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.core.ui.*
import org.isoron.uhabits.core.ui.screens.habits.list.*
import javax.inject.*
@ActivityScope
class ListHabitsMenu @Inject constructor(
activity: BaseActivity,
private val preferences: Preferences,
private val themeSwitcher: ThemeSwitcher,
private val behavior: ListHabitsMenuBehavior
) : BaseMenu(activity) {
override fun onCreate(menu: Menu) {
val nightModeItem = menu.findItem(R.id.actionToggleNightMode)
val hideArchivedItem = menu.findItem(R.id.actionHideArchived)
val hideCompletedItem = menu.findItem(R.id.actionHideCompleted)
nightModeItem.isChecked = themeSwitcher.isNightMode
hideArchivedItem.isChecked = !preferences.showArchived
hideCompletedItem.isChecked = !preferences.showCompleted
}
override fun onItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.actionToggleNightMode -> {
behavior.onToggleNightMode()
return true
}
R.id.actionAdd -> {
behavior.onCreateHabit()
return true
}
R.id.actionFAQ -> {
behavior.onViewFAQ()
return true
}
R.id.actionAbout -> {
behavior.onViewAbout()
return true
}
R.id.actionSettings -> {
behavior.onViewSettings()
return true
}
R.id.actionHideArchived -> {
behavior.onToggleShowArchived()
invalidate()
return true
}
R.id.actionHideCompleted -> {
behavior.onToggleShowCompleted()
invalidate()
return true
}
R.id.actionSortColor -> {
behavior.onSortByColor()
return true
}
R.id.actionSortManual -> {
behavior.onSortByManually()
return true
}
R.id.actionSortName -> {
behavior.onSortByName()
return true
}
R.id.actionSortScore -> {
behavior.onSortByScore()
return true
}
else -> return false
}
}
override fun getMenuResourceId() = R.menu.list_habits
}

@ -1,69 +0,0 @@
/*
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list;
import android.content.*;
import android.support.annotation.*;
import org.isoron.androidbase.*;
import org.isoron.uhabits.activities.*;
import org.isoron.uhabits.activities.habits.list.views.*;
import org.isoron.uhabits.core.ui.screens.habits.list.*;
import javax.inject.*;
import dagger.*;
class BugReporterProxy extends AndroidBugReporter
implements ListHabitsBehavior.BugReporter
{
@Inject
public BugReporterProxy(@AppContext @NonNull Context context)
{
super(context);
}
}
@Module
public abstract class ListHabitsModule
{
@Binds
abstract ListHabitsMenuBehavior.Adapter getAdapter(HabitCardListAdapter adapter);
@Binds
abstract ListHabitsBehavior.BugReporter getBugReporter(BugReporterProxy proxy);
@Binds
abstract ListHabitsMenuBehavior.Screen getMenuScreen(ListHabitsScreen screen);
@Binds
abstract ListHabitsBehavior.Screen getScreen(ListHabitsScreen screen);
@Binds
abstract ListHabitsSelectionMenuBehavior.Adapter getSelMenuAdapter(
HabitCardListAdapter adapter);
@Binds
abstract ListHabitsSelectionMenuBehavior.Screen getSelMenuScreen(
ListHabitsScreen screen);
@Binds
abstract ListHabitsBehavior.DirFinder getSystem(HabitsDirFinder system);
}

@ -0,0 +1,58 @@
/*
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list
import android.content.*
import dagger.*
import org.isoron.androidbase.*
import org.isoron.uhabits.activities.*
import org.isoron.uhabits.activities.habits.list.views.*
import org.isoron.uhabits.core.ui.screens.habits.list.*
import javax.inject.*
class BugReporterProxy
@Inject constructor(
@AppContext context: Context
) : AndroidBugReporter(context), ListHabitsBehavior.BugReporter
@Module
abstract class ListHabitsModule {
@Binds
abstract fun getAdapter(adapter: HabitCardListAdapter): ListHabitsMenuBehavior.Adapter
@Binds
abstract fun getBugReporter(proxy: BugReporterProxy): ListHabitsBehavior.BugReporter
@Binds
abstract fun getMenuScreen(screen: ListHabitsScreen): ListHabitsMenuBehavior.Screen
@Binds
abstract fun getScreen(screen: ListHabitsScreen): ListHabitsBehavior.Screen
@Binds
abstract fun getSelMenuAdapter(adapter: HabitCardListAdapter): ListHabitsSelectionMenuBehavior.Adapter
@Binds
abstract fun getSelMenuScreen(screen: ListHabitsScreen): ListHabitsSelectionMenuBehavior.Screen
@Binds
abstract fun getSystem(system: HabitsDirFinder): ListHabitsBehavior.DirFinder
}

@ -1,198 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list;
import android.content.*;
import android.support.annotation.*;
import android.support.v7.widget.Toolbar;
import android.view.*;
import android.widget.*;
import org.isoron.androidbase.activities.*;
import org.isoron.androidbase.utils.*;
import org.isoron.uhabits.R;
import org.isoron.uhabits.activities.common.views.*;
import org.isoron.uhabits.activities.habits.list.views.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.tasks.*;
import org.isoron.uhabits.core.ui.screens.habits.list.*;
import javax.inject.*;
import butterknife.*;
import static org.isoron.androidbase.utils.InterfaceUtils.*;
@ActivityScope
public class ListHabitsRootView extends BaseRootView
implements ModelObservable.Listener, TaskRunner.Listener
{
public static final int MAX_CHECKMARK_COUNT = 60;
@BindView(R.id.listView)
HabitCardListView listView;
@BindView(R.id.llEmpty)
ViewGroup llEmpty;
@BindView(R.id.tvStarEmpty)
TextView tvStarEmpty;
@BindView(R.id.toolbar)
Toolbar toolbar;
@BindView(R.id.progressBar)
ProgressBar progressBar;
@BindView(R.id.hintView)
HintView hintView;
@BindView(R.id.header)
HeaderView header;
@NonNull
private final HabitCardListAdapter listAdapter;
private final TaskRunner runner;
@Inject
public ListHabitsRootView(@ActivityContext Context context,
@NonNull HintListFactory hintListFactory,
@NonNull HabitCardListAdapter listAdapter,
@NonNull TaskRunner runner)
{
super(context);
addView(inflate(getContext(), R.layout.list_habits, null));
ButterKnife.bind(this);
this.listAdapter = listAdapter;
listView.setAdapter(listAdapter);
listAdapter.setListView(listView);
this.runner = runner;
progressBar.setIndeterminate(true);
tvStarEmpty.setTypeface(InterfaceUtils.getFontAwesome(getContext()));
String hints[] =
getContext().getResources().getStringArray(R.array.hints);
HintList hintList = hintListFactory.create(hints);
hintView.setHints(hintList);
initToolbar();
}
@NonNull
@Override
public Toolbar getToolbar()
{
return toolbar;
}
@Override
public void onModelChange()
{
updateEmptyView();
}
@Override
public void onTaskFinished(Task task)
{
updateProgressBar();
}
@Override
public void onTaskStarted(Task task)
{
updateProgressBar();
}
public void setController(@NonNull ListHabitsController controller,
@NonNull ListHabitsSelectionMenu menu)
{
HabitCardListController listController =
new HabitCardListController(listAdapter);
listController.setHabitListener(controller);
listController.setSelectionListener(menu);
listView.setController(listController);
menu.setListController(listController);
header.setScrollController(new ScrollableChart.ScrollController()
{
@Override
public void onDataOffsetChanged(int newDataOffset)
{
listView.setDataOffset(newDataOffset);
}
});
}
@Override
protected void onAttachedToWindow()
{
super.onAttachedToWindow();
runner.addListener(this);
updateProgressBar();
listAdapter.getObservable().addListener(this);
}
@Override
protected void onDetachedFromWindow()
{
listAdapter.getObservable().removeListener(this);
runner.removeListener(this);
super.onDetachedFromWindow();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh)
{
int count = getCheckmarkCount();
header.setButtonCount(count);
header.setMaxDataOffset(Math.max(MAX_CHECKMARK_COUNT - count, 0));
listView.setCheckmarkCount(count);
super.onSizeChanged(w, h, oldw, oldh);
}
private int getCheckmarkCount()
{
float nameWidth = getDimension(getContext(), R.dimen.habitNameWidth);
float labelWidth = Math.max(getMeasuredWidth() / 3, nameWidth);
float buttonWidth = getDimension(getContext(), R.dimen.checkmarkWidth);
return Math.min(MAX_CHECKMARK_COUNT, Math.max(0,
(int) ((getMeasuredWidth() - labelWidth) / buttonWidth)));
}
private void updateEmptyView()
{
llEmpty.setVisibility(
listAdapter.getItemCount() > 0 ? View.GONE : View.VISIBLE);
}
private void updateProgressBar()
{
postDelayed(() ->
{
int activeTaskCount = runner.getActiveTaskCount();
int newVisibility = activeTaskCount > 0 ? VISIBLE : GONE;
if (progressBar.getVisibility() != newVisibility)
progressBar.setVisibility(newVisibility);
}, 500);
}
}

@ -0,0 +1,135 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list
import android.content.*
import android.os.Build.VERSION.*
import android.os.Build.VERSION_CODES.*
import android.support.v7.widget.Toolbar
import android.view.ViewGroup.LayoutParams.*
import android.widget.*
import org.isoron.androidbase.activities.*
import org.isoron.uhabits.*
import org.isoron.uhabits.activities.common.views.*
import org.isoron.uhabits.activities.habits.list.views.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.core.tasks.*
import org.isoron.uhabits.core.ui.screens.habits.list.*
import org.isoron.uhabits.core.utils.*
import org.isoron.uhabits.utils.*
import java.lang.Math.*
import javax.inject.*
const val MAX_CHECKMARK_COUNT = 60
@ActivityScope
class ListHabitsRootView @Inject constructor(
@ActivityContext context: Context,
hintListFactory: HintListFactory,
preferences: Preferences,
midnightTimer: MidnightTimer,
runner: TaskRunner,
private val listAdapter: HabitCardListAdapter,
habitCardListViewFactory: HabitCardListViewFactory
) : BaseRootView(context), ModelObservable.Listener {
val listView: HabitCardListView = habitCardListViewFactory.create()
val llEmpty = EmptyListView(context)
val tbar = buildToolbar()
val progressBar = TaskProgressBar(context, runner)
val hintView: HintView
val header = HeaderView(context, preferences, midnightTimer)
init {
val hints = resources.getStringArray(R.array.hints)
val hintList = hintListFactory.create(hints)
hintView = HintView(context, hintList)
addView(RelativeLayout(context).apply {
background = sres.getDrawable(R.attr.windowBackgroundColor)
addAtTop(tbar)
addBelow(header, tbar)
addBelow(listView, header, height = MATCH_PARENT)
addBelow(llEmpty, header, height = MATCH_PARENT)
addBelow(progressBar, header) {
it.topMargin = dp(-6.0f).toInt()
}
addAtBottom(hintView)
if (SDK_INT < LOLLIPOP) {
addBelow(ShadowView(context), tbar)
addBelow(ShadowView(context), header)
}
}, MATCH_PARENT, MATCH_PARENT)
listAdapter.setListView(listView)
initToolbar()
}
override fun getToolbar(): Toolbar {
return tbar
}
override fun onModelChange() {
updateEmptyView()
}
private fun setupControllers() {
header.setScrollController(object : ScrollableChart.ScrollController {
override fun onDataOffsetChanged(newDataOffset: Int) {
listView.dataOffset = newDataOffset
}
})
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
setupControllers()
listAdapter.observable.addListener(this)
}
override fun onDetachedFromWindow() {
listAdapter.observable.removeListener(this)
super.onDetachedFromWindow()
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
val count = getCheckmarkCount()
header.buttonCount = count
header.setMaxDataOffset(max(MAX_CHECKMARK_COUNT - count, 0))
listView.checkmarkCount = count
super.onSizeChanged(w, h, oldw, oldh)
}
private fun getCheckmarkCount(): Int {
val nameWidth = dim(R.dimen.habitNameWidth)
val buttonWidth = dim(R.dimen.checkmarkWidth)
val labelWidth = max((measuredWidth / 3).toFloat(), nameWidth)
val buttonCount = ((measuredWidth - labelWidth) / buttonWidth).toInt()
return min(MAX_CHECKMARK_COUNT, max(0, buttonCount))
}
private fun updateEmptyView() {
llEmpty.visibility = when (listAdapter.itemCount) {
0 -> VISIBLE
else -> GONE
}
}
}

@ -1,441 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list;
import android.app.*;
import android.content.*;
import android.net.*;
import android.support.annotation.*;
import android.support.v7.app.AlertDialog;
import android.text.*;
import android.view.*;
import android.widget.*;
import org.isoron.androidbase.activities.*;
import org.isoron.androidbase.utils.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.common.dialogs.*;
import org.isoron.uhabits.activities.habits.edit.*;
import org.isoron.uhabits.core.commands.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.preferences.*;
import org.isoron.uhabits.core.ui.*;
import org.isoron.uhabits.core.ui.callbacks.*;
import org.isoron.uhabits.core.ui.screens.habits.list.*;
import org.isoron.uhabits.intents.*;
import java.io.*;
import java.lang.reflect.*;
import java.util.*;
import javax.inject.*;
import static android.content.DialogInterface.BUTTON_POSITIVE;
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
@ActivityScope
public class ListHabitsScreen extends BaseScreen
implements CommandRunner.Listener, ListHabitsBehavior.Screen,
ListHabitsMenuBehavior.Screen,
ListHabitsSelectionMenuBehavior.Screen
{
public static final int REQUEST_OPEN_DOCUMENT = 6;
public static final int REQUEST_SETTINGS = 7;
public static final int RESULT_BUG_REPORT = 4;
public static final int RESULT_EXPORT_CSV = 2;
public static final int RESULT_EXPORT_DB = 3;
public static final int RESULT_IMPORT_DATA = 1;
public static final int RESULT_REPAIR_DB = 5;
@Nullable
private ListHabitsController controller;
@NonNull
private final IntentFactory intentFactory;
@NonNull
private final CommandRunner commandRunner;
@NonNull
private final ConfirmDeleteDialogFactory confirmDeleteDialogFactory;
@NonNull
private final ColorPickerDialogFactory colorPickerFactory;
@NonNull
private final EditHabitDialogFactory editHabitDialogFactory;
@NonNull
private final ThemeSwitcher themeSwitcher;
@NonNull
private Preferences prefs;
@Inject
public ListHabitsScreen(@NonNull BaseActivity activity,
@NonNull CommandRunner commandRunner,
@NonNull ListHabitsRootView rootView,
@NonNull IntentFactory intentFactory,
@NonNull ThemeSwitcher themeSwitcher,
@NonNull ConfirmDeleteDialogFactory confirmDeleteDialogFactory,
@NonNull ColorPickerDialogFactory colorPickerFactory,
@NonNull EditHabitDialogFactory editHabitDialogFactory,
@NonNull Preferences prefs)
{
super(activity);
setRootView(rootView);
this.prefs = prefs;
this.colorPickerFactory = colorPickerFactory;
this.commandRunner = commandRunner;
this.confirmDeleteDialogFactory = confirmDeleteDialogFactory;
this.editHabitDialogFactory = editHabitDialogFactory;
this.intentFactory = intentFactory;
this.themeSwitcher = themeSwitcher;
}
@StringRes
private Integer getExecuteString(@NonNull Command command)
{
if(command instanceof ArchiveHabitsCommand)
return R.string.toast_habit_archived;
if(command instanceof ChangeHabitColorCommand)
return R.string.toast_habit_changed;
if(command instanceof CreateHabitCommand)
return R.string.toast_habit_created;
if(command instanceof DeleteHabitsCommand)
return R.string.toast_habit_deleted;
if(command instanceof EditHabitCommand)
return R.string.toast_habit_changed;
if(command instanceof UnarchiveHabitsCommand)
return R.string.toast_habit_unarchived;
return null;
}
public void onAttached()
{
commandRunner.addListener(this);
}
@Override
public void onCommandExecuted(@NonNull Command command,
@Nullable Long refreshKey)
{
if (command.isRemote()) return;
showMessage(getExecuteString(command));
}
public void onDettached()
{
commandRunner.removeListener(this);
}
@Override
public void onResult(int requestCode, int resultCode, Intent data)
{
if (requestCode == REQUEST_OPEN_DOCUMENT)
onOpenDocumentResult(resultCode, data);
if (requestCode == REQUEST_SETTINGS) onSettingsResult(resultCode);
}
public void setController(@Nullable ListHabitsController controller)
{
this.controller = controller;
}
@Override
public void applyTheme()
{
themeSwitcher.apply();
activity.restartWithFade(ListHabitsActivity.class);
}
@Override
public void showAboutScreen()
{
Intent intent = intentFactory.startAboutActivity(activity);
activity.startActivity(intent);
}
@Override
public void showColorPicker(int defaultColor,
@NonNull OnColorPickedCallback callback) {
ColorPickerDialog picker = colorPickerFactory.create(defaultColor);
picker.setListener(callback);
activity.showDialog(picker, "picker");
}
public void showCreateBooleanHabitScreen()
{
EditHabitDialog dialog;
dialog = editHabitDialogFactory.createBoolean();
activity.showDialog(dialog, "editHabit");
}
@Override
public void showCreateHabitScreen()
{
if (!prefs.isNumericalHabitsFeatureEnabled())
{
showCreateBooleanHabitScreen();
return;
}
Dialog dialog = new AlertDialog.Builder(activity)
.setTitle("Type of habit")
.setItems(R.array.habitTypes, (d, which) ->
{
if (which == 0) showCreateBooleanHabitScreen();
else showCreateNumericalHabitScreen();
})
.create();
dialog.show();
}
@Override
public void showDeleteConfirmationScreen(
@NonNull OnConfirmedCallback callback)
{
activity.showDialog(confirmDeleteDialogFactory.create(callback));
}
@Override
public void showEditHabitsScreen(List<Habit> habits)
{
EditHabitDialog dialog;
dialog = editHabitDialogFactory.edit(habits.get(0));
activity.showDialog(dialog, "editNumericalHabit");
}
@Override
public void showFAQScreen()
{
Intent intent = intentFactory.viewFAQ(activity);
activity.startActivity(intent);
}
@Override
public void showHabitScreen(@NonNull Habit habit)
{
Intent intent = intentFactory.startShowHabitActivity(activity, habit);
activity.startActivity(intent);
}
public void showImportScreen()
{
Intent intent = intentFactory.openDocument();
activity.startActivityForResult(intent, REQUEST_OPEN_DOCUMENT);
}
@Override
public void showIntroScreen()
{
Intent intent = intentFactory.startIntroActivity(activity);
activity.startActivity(intent);
}
@Override
public void showMessage(@NonNull ListHabitsBehavior.Message m)
{
switch (m)
{
case COULD_NOT_EXPORT:
showMessage(R.string.could_not_export);
break;
case IMPORT_SUCCESSFUL:
showMessage(R.string.habits_imported);
break;
case IMPORT_FAILED:
showMessage(R.string.could_not_import);
break;
case DATABASE_REPAIRED:
showMessage(R.string.database_repaired);
break;
case COULD_NOT_GENERATE_BUG_REPORT:
showMessage(R.string.bug_report_failed);
break;
case FILE_NOT_RECOGNIZED:
showMessage(R.string.file_not_recognized);
break;
}
}
@Override
public void showNumberPicker(double value,
@NonNull String unit,
@NonNull
ListHabitsBehavior.NumberPickerCallback callback)
{
LayoutInflater inflater = activity.getLayoutInflater();
View view = inflater.inflate(R.layout.number_picker_dialog, null);
final NumberPicker picker;
final NumberPicker picker2;
final TextView tvUnit;
picker = (NumberPicker) view.findViewById(R.id.picker);
picker2 = (NumberPicker) view.findViewById(R.id.picker2);
tvUnit = (TextView) view.findViewById(R.id.tvUnit);
int intValue = (int) Math.round(value * 100);
picker.setMinValue(0);
picker.setMaxValue(Integer.MAX_VALUE / 100);
picker.setValue(intValue / 100);
picker.setWrapSelectorWheel(false);
picker2.setMinValue(0);
picker2.setMaxValue(19);
picker2.setFormatter(v -> String.format("%02d", 5 * v));
picker2.setValue((intValue % 100) / 5);
refreshInitialValue(picker2);
tvUnit.setText(unit);
AlertDialog dialog = new AlertDialog.Builder(activity)
.setView(view)
.setTitle(R.string.change_value)
.setPositiveButton(android.R.string.ok, (d, which) ->
{
picker.clearFocus();
double v = picker.getValue() + 0.05 * picker2.getValue();
callback.onNumberPicked(v);
})
.create();
InterfaceUtils.setupEditorAction(picker, (v, actionId, event) ->
{
if (actionId == IME_ACTION_DONE)
dialog.getButton(BUTTON_POSITIVE).performClick();
return false;
});
dialog.show();
}
@Override
public void showSendBugReportToDeveloperScreen(String log)
{
int to = R.string.bugReportTo;
int subject = R.string.bugReportSubject;
showSendEmailScreen(to, subject, log);
}
@Override
public void showSettingsScreen()
{
Intent intent = intentFactory.startSettingsActivity(activity);
activity.startActivityForResult(intent, REQUEST_SETTINGS);
}
private void onOpenDocumentResult(int resultCode, Intent data)
{
if (controller == null) return;
if (resultCode != Activity.RESULT_OK) return;
try
{
Uri uri = data.getData();
ContentResolver cr = activity.getContentResolver();
InputStream is = cr.openInputStream(uri);
File cacheDir = activity.getExternalCacheDir();
File tempFile = File.createTempFile("import", "", cacheDir);
FileUtils.copy(is, tempFile);
controller.onImportData(tempFile, () -> tempFile.delete());
}
catch (IOException e)
{
showMessage(R.string.could_not_import);
e.printStackTrace();
}
}
private void onSettingsResult(int resultCode)
{
if (controller == null) return;
switch (resultCode)
{
case RESULT_IMPORT_DATA:
showImportScreen();
break;
case RESULT_EXPORT_CSV:
controller.onExportCSV();
break;
case RESULT_EXPORT_DB:
controller.onExportDB();
break;
case RESULT_BUG_REPORT:
controller.onSendBugReport();
break;
case RESULT_REPAIR_DB:
controller.onRepairDB();
break;
}
}
private void refreshInitialValue(NumberPicker picker2)
{
// Workaround for a bug on Android:
// https://code.google.com/p/android/issues/detail?id=35482
try
{
Field f = NumberPicker.class.getDeclaredField("mInputText");
f.setAccessible(true);
EditText inputText = (EditText) f.get(picker2);
inputText.setFilters(new InputFilter[0]);
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
private void showCreateNumericalHabitScreen()
{
EditHabitDialog dialog;
dialog = editHabitDialogFactory.createNumerical();
activity.showDialog(dialog, "editHabit");
}
}

@ -0,0 +1,264 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list
import android.app.*
import android.content.*
import android.support.annotation.*
import dagger.*
import org.isoron.androidbase.activities.*
import org.isoron.androidbase.utils.*
import org.isoron.uhabits.*
import org.isoron.uhabits.activities.common.dialogs.*
import org.isoron.uhabits.activities.habits.edit.*
import org.isoron.uhabits.activities.habits.list.views.*
import org.isoron.uhabits.core.commands.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.core.tasks.*
import org.isoron.uhabits.core.ui.*
import org.isoron.uhabits.core.ui.callbacks.*
import org.isoron.uhabits.core.ui.screens.habits.list.*
import org.isoron.uhabits.core.ui.screens.habits.list.ListHabitsBehavior.Message.*
import org.isoron.uhabits.intents.*
import org.isoron.uhabits.tasks.*
import java.io.*
import javax.inject.*
const val RESULT_IMPORT_DATA = 1
const val RESULT_EXPORT_CSV = 2
const val RESULT_EXPORT_DB = 3
const val RESULT_BUG_REPORT = 4
const val RESULT_REPAIR_DB = 5
const val REQUEST_OPEN_DOCUMENT = 6
const val REQUEST_SETTINGS = 7
@ActivityScope
class ListHabitsScreen
@Inject constructor(
activity: BaseActivity,
rootView: ListHabitsRootView,
private val commandRunner: CommandRunner,
private val intentFactory: IntentFactory,
private val themeSwitcher: ThemeSwitcher,
private val preferences: Preferences,
private val adapter: HabitCardListAdapter,
private val taskRunner: TaskRunner,
private val exportDBFactory: ExportDBTaskFactory,
private val importTaskFactory: ImportDataTaskFactory,
private val confirmDeleteDialogFactory: ConfirmDeleteDialogFactory,
private val colorPickerFactory: ColorPickerDialogFactory,
private val editHabitDialogFactory: EditHabitDialogFactory,
private val numberPickerFactory: NumberPickerFactory,
private val behavior: Lazy<ListHabitsBehavior>,
private val menu: Lazy<ListHabitsMenu>,
private val selectionMenu: Lazy<ListHabitsSelectionMenu>
) : BaseScreen(activity),
CommandRunner.Listener,
ListHabitsBehavior.Screen,
ListHabitsMenuBehavior.Screen,
ListHabitsSelectionMenuBehavior.Screen {
init {
setRootView(rootView)
}
fun onAttached() {
setMenu(menu.get())
setSelectionMenu(selectionMenu.get())
commandRunner.addListener(this)
}
fun onDettached() {
commandRunner.removeListener(this)
}
override fun onCommandExecuted(command: Command, refreshKey: Long?) {
if (command.isRemote) return
showMessage(getExecuteString(command))
}
override fun onResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
REQUEST_OPEN_DOCUMENT -> onOpenDocumentResult(resultCode, data)
REQUEST_SETTINGS -> onSettingsResult(resultCode)
}
}
private fun onOpenDocumentResult(resultCode: Int, data: Intent?) {
if (data == null) return
if (resultCode != Activity.RESULT_OK) return
try {
val inStream = activity.contentResolver.openInputStream(data.data)
val cacheDir = activity.externalCacheDir
val tempFile = File.createTempFile("import", "", cacheDir)
FileUtils.copy(inStream, tempFile)
onImportData(tempFile) { tempFile.delete() }
} catch (e: IOException) {
showMessage(R.string.could_not_import)
e.printStackTrace()
}
}
private fun onSettingsResult(resultCode: Int) {
when (resultCode) {
RESULT_IMPORT_DATA -> showImportScreen()
RESULT_EXPORT_CSV -> behavior.get().onExportCSV()
RESULT_EXPORT_DB -> onExportDB()
RESULT_BUG_REPORT -> behavior.get().onSendBugReport()
RESULT_REPAIR_DB -> behavior.get().onRepairDB()
}
}
override fun applyTheme() {
themeSwitcher.apply()
activity.restartWithFade(ListHabitsActivity::class.java)
}
override fun showAboutScreen() {
val intent = intentFactory.startAboutActivity(activity)
activity.startActivity(intent)
}
fun showCreateBooleanHabitScreen() {
val dialog = editHabitDialogFactory.createBoolean()
activity.showDialog(dialog, "editHabit")
}
override fun showCreateHabitScreen() {
if (!preferences.isNumericalHabitsFeatureEnabled) {
showCreateBooleanHabitScreen()
return
}
val dialog = AlertDialog.Builder(activity)
.setTitle("Type of habit")
.setItems(R.array.habitTypes) { _, which ->
if (which == 0) showCreateBooleanHabitScreen()
else showCreateNumericalHabitScreen()
}
.create()
dialog.show()
}
override fun showDeleteConfirmationScreen(callback: OnConfirmedCallback) {
activity.showDialog(confirmDeleteDialogFactory.create(callback))
}
override fun showEditHabitsScreen(habits: List<Habit>) {
val dialog = editHabitDialogFactory.edit(habits[0])
activity.showDialog(dialog, "editNumericalHabit")
}
override fun showFAQScreen() {
val intent = intentFactory.viewFAQ(activity)
activity.startActivity(intent)
}
override fun showHabitScreen(habit: Habit) {
val intent = intentFactory.startShowHabitActivity(activity, habit)
activity.startActivity(intent)
}
fun showImportScreen() {
val intent = intentFactory.openDocument()
activity.startActivityForResult(intent, REQUEST_OPEN_DOCUMENT)
}
override fun showIntroScreen() {
val intent = intentFactory.startIntroActivity(activity)
activity.startActivity(intent)
}
override fun showMessage(m: ListHabitsBehavior.Message) {
showMessage(when (m) {
COULD_NOT_EXPORT -> R.string.could_not_export
IMPORT_SUCCESSFUL -> R.string.habits_imported
IMPORT_FAILED -> R.string.could_not_import
DATABASE_REPAIRED -> R.string.database_repaired
COULD_NOT_GENERATE_BUG_REPORT -> R.string.bug_report_failed
FILE_NOT_RECOGNIZED -> R.string.file_not_recognized
})
}
override fun showSendBugReportToDeveloperScreen(log: String) {
val to = R.string.bugReportTo
val subject = R.string.bugReportSubject
showSendEmailScreen(to, subject, log)
}
override fun showSettingsScreen() {
val intent = intentFactory.startSettingsActivity(activity)
activity.startActivityForResult(intent, REQUEST_SETTINGS)
}
override fun showColorPicker(defaultColor: Int,
callback: OnColorPickedCallback) {
val picker = colorPickerFactory.create(defaultColor)
picker.setListener(callback)
activity.showDialog(picker, "picker")
}
override fun showNumberPicker(value: Double,
unit: String,
callback: ListHabitsBehavior.NumberPickerCallback) {
numberPickerFactory.create(value, unit, callback).show()
}
@StringRes
private fun getExecuteString(command: Command): Int? {
when (command) {
is ArchiveHabitsCommand -> return R.string.toast_habit_archived
is ChangeHabitColorCommand -> return R.string.toast_habit_changed
is CreateHabitCommand -> return R.string.toast_habit_created
is DeleteHabitsCommand -> return R.string.toast_habit_deleted
is EditHabitCommand -> return R.string.toast_habit_changed
is UnarchiveHabitsCommand -> return R.string.toast_habit_unarchived
else -> return null
}
}
private fun showCreateNumericalHabitScreen() {
val dialog = editHabitDialogFactory.createNumerical()
activity.showDialog(dialog, "editHabit")
}
private fun onImportData(file: File, onFinished: () -> Unit) {
taskRunner.execute(importTaskFactory.create(file) { result ->
if (result == ImportDataTask.SUCCESS) {
adapter.refresh()
showMessage(R.string.habits_imported)
} else if (result == ImportDataTask.NOT_RECOGNIZED) {
showMessage(R.string.file_not_recognized)
} else {
showMessage(R.string.could_not_import)
}
onFinished()
})
}
private fun onExportDB() {
taskRunner.execute(exportDBFactory.create { filename ->
if (filename != null) showSendFileScreen(filename)
else showMessage(R.string.could_not_export)
})
}
}

@ -1,145 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list;
import android.support.annotation.*;
import android.view.*;
import org.isoron.androidbase.activities.*;
import org.isoron.uhabits.R;
import org.isoron.uhabits.activities.habits.list.views.*;
import org.isoron.uhabits.core.commands.*;
import org.isoron.uhabits.core.ui.screens.habits.list.*;
import javax.inject.*;
@ActivityScope
public class ListHabitsSelectionMenu extends BaseSelectionMenu
implements HabitCardListController.SelectionListener
{
@NonNull
private final ListHabitsScreen screen;
@NonNull
CommandRunner commandRunner;
private ListHabitsSelectionMenuBehavior behavior;
@NonNull
private final HabitCardListAdapter listAdapter;
@Nullable
private HabitCardListController listController;
@Inject
public ListHabitsSelectionMenu(@NonNull ListHabitsScreen screen,
@NonNull HabitCardListAdapter listAdapter,
@NonNull CommandRunner commandRunner,
@NonNull ListHabitsSelectionMenuBehavior behavior)
{
this.screen = screen;
this.listAdapter = listAdapter;
this.commandRunner = commandRunner;
this.behavior = behavior;
}
@Override
public void onFinish()
{
if (listController != null) listController.onSelectionFinished();
super.onFinish();
}
@Override
public boolean onItemClicked(@NonNull MenuItem item)
{
switch (item.getItemId())
{
case R.id.action_edit_habit:
behavior.onEditHabits();
return true;
case R.id.action_archive_habit:
behavior.onArchiveHabits();
return true;
case R.id.action_unarchive_habit:
behavior.onUnarchiveHabits();
return true;
case R.id.action_delete:
behavior.onDeleteHabits();
return true;
case R.id.action_color:
behavior.onChangeColor();
return true;
default:
return false;
}
}
@Override
public boolean onPrepare(@NonNull Menu menu)
{
MenuItem itemEdit = menu.findItem(R.id.action_edit_habit);
MenuItem itemColor = menu.findItem(R.id.action_color);
MenuItem itemArchive = menu.findItem(R.id.action_archive_habit);
MenuItem itemUnarchive = menu.findItem(R.id.action_unarchive_habit);
itemColor.setVisible(true);
itemEdit.setVisible(behavior.canEdit());
itemArchive.setVisible(behavior.canArchive());
itemUnarchive.setVisible(behavior.canUnarchive());
setTitle(Integer.toString(listAdapter.getSelected().size()));
return true;
}
@Override
public void onSelectionChange()
{
invalidate();
}
@Override
public void onSelectionFinish()
{
finish();
}
@Override
public void onSelectionStart()
{
screen.startSelection();
}
public void setListController(HabitCardListController listController)
{
this.listController = listController;
}
@Override
protected int getResourceId()
{
return R.menu.list_habits_selection;
}
}

@ -0,0 +1,95 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list
import android.view.*
import dagger.*
import org.isoron.androidbase.activities.*
import org.isoron.uhabits.*
import org.isoron.uhabits.activities.habits.list.views.*
import org.isoron.uhabits.core.commands.*
import org.isoron.uhabits.core.ui.screens.habits.list.*
import javax.inject.*
@ActivityScope
class ListHabitsSelectionMenu @Inject constructor(
private val screen: ListHabitsScreen,
private val listAdapter: HabitCardListAdapter,
var commandRunner: CommandRunner,
private val behavior: ListHabitsSelectionMenuBehavior,
private val listController: Lazy<HabitCardListController>
) : BaseSelectionMenu() {
override fun onFinish() {
listController.get().onSelectionFinished()
super.onFinish()
}
override fun onItemClicked(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_edit_habit -> {
behavior.onEditHabits()
return true
}
R.id.action_archive_habit -> {
behavior.onArchiveHabits()
return true
}
R.id.action_unarchive_habit -> {
behavior.onUnarchiveHabits()
return true
}
R.id.action_delete -> {
behavior.onDeleteHabits()
return true
}
R.id.action_color -> {
behavior.onChangeColor()
return true
}
else -> return false
}
}
override fun onPrepare(menu: Menu): Boolean {
val itemEdit = menu.findItem(R.id.action_edit_habit)
val itemColor = menu.findItem(R.id.action_color)
val itemArchive = menu.findItem(R.id.action_archive_habit)
val itemUnarchive = menu.findItem(R.id.action_unarchive_habit)
itemColor.isVisible = true
itemEdit.isVisible = behavior.canEdit()
itemArchive.isVisible = behavior.canArchive()
itemUnarchive.isVisible = behavior.canUnarchive()
setTitle(Integer.toString(listAdapter.selected.size))
return true
}
fun onSelectionStart() = screen.startSelection()
fun onSelectionChange() = invalidate()
fun onSelectionFinish() = finish()
override fun getResourceId() = R.menu.list_habits_selection
}

@ -1,23 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Provides acitivity for listing habits and related classes.
*/
package org.isoron.uhabits.activities.habits.list;

@ -0,0 +1,88 @@
/*
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list.views
import android.content.*
import android.view.*
import android.view.View.MeasureSpec.*
import android.widget.*
import org.isoron.uhabits.*
import org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.utils.*
abstract class ButtonPanelView<T : View>(
context: Context,
val preferences: Preferences
) : LinearLayout(context),
Preferences.Listener {
var buttonCount = 0
set(value) {
field = value
inflateButtons()
}
var dataOffset = 0
set(value) {
field = value
setupButtons()
}
var buttons = mutableListOf<T>()
override fun onCheckmarkSequenceChanged() {
inflateButtons()
}
@Synchronized
protected fun inflateButtons() {
val reverse = preferences.isCheckmarkSequenceReversed
buttons.clear()
repeat(buttonCount) { buttons.add(createButton()) }
removeAllViews()
if (reverse) buttons.reversed().forEach { addView(it) }
else buttons.forEach { addView(it) }
setupButtons()
requestLayout()
}
public override fun onAttachedToWindow() {
super.onAttachedToWindow()
preferences.addListener(this)
}
public override fun onDetachedFromWindow() {
preferences.removeListener(this)
super.onDetachedFromWindow()
}
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
val buttonWidth = dim(R.dimen.checkmarkWidth)
val buttonHeight = dim(R.dimen.checkmarkHeight)
val width = (buttonWidth * buttonCount)
super.onMeasure(width.toMeasureSpec(EXACTLY),
buttonHeight.toMeasureSpec(EXACTLY))
}
protected abstract fun setupButtons()
protected abstract fun createButton(): T
}

@ -1,194 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list.views;
import android.content.*;
import android.content.res.*;
import android.graphics.*;
import android.support.annotation.*;
import android.text.*;
import android.util.*;
import android.view.*;
import org.isoron.androidbase.utils.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.*;
import org.isoron.uhabits.core.preferences.*;
import org.isoron.uhabits.utils.*;
import static android.view.View.MeasureSpec.EXACTLY;
import static android.view.View.MeasureSpec.makeMeasureSpec;
import static org.isoron.androidbase.utils.InterfaceUtils.getDimension;
import static org.isoron.androidbase.utils.InterfaceUtils.getFontAwesome;
import static org.isoron.uhabits.core.models.Checkmark.CHECKED_EXPLICITLY;
import static org.isoron.uhabits.core.models.Checkmark.UNCHECKED;
import static org.isoron.uhabits.utils.AttributeSetUtils.getIntAttribute;
public class CheckmarkButtonView extends View
{
private int color;
private int value;
private StyledResources styledRes;
private TextPaint paint;
private int lowContrastColor;
private RectF rect;
@Nullable
private Preferences prefs;
@NonNull
private OnToggleListener onToggleListener;
@NonNull
private OnInvalidToggleListener onInvalidToggleListener;
public CheckmarkButtonView(@Nullable Context context)
{
super(context);
init();
}
public CheckmarkButtonView(@Nullable Context ctx, @Nullable AttributeSet attrs)
{
super(ctx, attrs);
init();
if(ctx == null) throw new IllegalStateException();
if(attrs == null) throw new IllegalStateException();
int paletteColor = getIntAttribute(ctx, attrs, "color", 0);
setColor(PaletteUtils.getAndroidTestColor(paletteColor));
int value = getIntAttribute(ctx, attrs, "value", 0);
setValue(value);
}
public void setColor(int color)
{
this.color = color;
postInvalidate();
}
public void setValue(int value)
{
this.value = value;
postInvalidate();
}
public void performToggle()
{
onToggleListener.onToggle();
value = (value == CHECKED_EXPLICITLY ? UNCHECKED : CHECKED_EXPLICITLY);
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
postInvalidate();
}
@Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
Resources resources = getResources();
paint.setColor(value == CHECKED_EXPLICITLY ? color : lowContrastColor);
int id = (value == UNCHECKED ? R.string.fa_times : R.string.fa_check);
String label = resources.getString(id);
float em = paint.measureText("m");
rect.set(0, 0, getWidth(), getHeight());
rect.offset(0, 0.4f * em);
canvas.drawText(label, rect.centerX(), rect.centerY(), paint);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
Resources res = getResources();
int height = res.getDimensionPixelSize(R.dimen.checkmarkHeight);
int width = res.getDimensionPixelSize(R.dimen.checkmarkWidth);
widthMeasureSpec = makeMeasureSpec(width, EXACTLY);
heightMeasureSpec = makeMeasureSpec(height, EXACTLY);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
private void init()
{
setFocusable(false);
styledRes = new StyledResources(getContext());
paint = new TextPaint();
paint.setTypeface(getFontAwesome(getContext()));
paint.setAntiAlias(true);
paint.setTextAlign(Paint.Align.CENTER);
paint.setTextSize(getDimension(getContext(), R.dimen.smallTextSize));
rect = new RectF();
color = Color.BLACK;
lowContrastColor = styledRes.getColor(R.attr.lowContrastTextColor);
onToggleListener = () -> {};
onInvalidToggleListener = () -> {};
if (getContext() instanceof HabitsActivity)
{
HabitsApplicationComponent component =
((HabitsActivity) getContext()).getAppComponent();
prefs = component.getPreferences();
}
setOnClickListener((v) -> {
if (prefs == null) return;
if (prefs.isShortToggleEnabled()) performToggle();
else onInvalidToggleListener.onInvalidToggle();
});
setOnLongClickListener(v -> {
performToggle();
return true;
});
}
public void setOnInvalidToggleListener(
@NonNull OnInvalidToggleListener onInvalidToggleListener)
{
this.onInvalidToggleListener = onInvalidToggleListener;
}
public void setOnToggleListener(@NonNull OnToggleListener onToggleListener)
{
this.onToggleListener = onToggleListener;
}
public interface OnInvalidToggleListener
{
void onInvalidToggle();
}
public interface OnToggleListener
{
void onToggle();
}
}

@ -0,0 +1,123 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list.views
import android.content.*
import android.graphics.*
import android.text.*
import android.view.*
import android.view.View.MeasureSpec.*
import com.google.auto.factory.*
import org.isoron.androidbase.activities.*
import org.isoron.uhabits.*
import org.isoron.uhabits.core.models.Checkmark.*
import org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.utils.*
@AutoFactory
class CheckmarkButtonView(
@Provided @ActivityContext context: Context,
@Provided val preferences: Preferences
) : View(context),
View.OnClickListener,
View.OnLongClickListener {
var color: Int = Color.BLACK
set(value) {
field = value
invalidate()
}
var value: Int = 0
set(value) {
field = value
invalidate()
}
var onToggle: () -> Unit = {}
private var drawer = Drawer()
init {
isFocusable = false
setOnClickListener(this)
setOnLongClickListener(this)
}
fun performToggle() {
onToggle()
value = when (value) {
CHECKED_EXPLICITLY -> UNCHECKED
else -> CHECKED_EXPLICITLY
}
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
invalidate()
}
override fun onClick(v: View) {
if (preferences.isShortToggleEnabled) performToggle()
else showMessage(R.string.long_press_to_toggle)
}
override fun onLongClick(v: View): Boolean {
performToggle()
return true
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
drawer.draw(canvas)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val height = resources.getDimensionPixelSize(R.dimen.checkmarkHeight)
val width = resources.getDimensionPixelSize(R.dimen.checkmarkWidth)
super.onMeasure(width.toMeasureSpec(EXACTLY),
height.toMeasureSpec(EXACTLY))
}
private inner class Drawer {
private val rect = RectF()
private val lowContrastColor = sres.getColor(R.attr.lowContrastTextColor)
private val paint = TextPaint().apply {
typeface = getFontAwesome()
isAntiAlias = true
textAlign = Paint.Align.CENTER
textSize = dim(R.dimen.smallTextSize)
}
fun draw(canvas: Canvas) {
paint.color = when (value) {
CHECKED_EXPLICITLY -> color
else -> lowContrastColor
}
val id = when (value) {
UNCHECKED -> R.string.fa_times
else -> R.string.fa_check
}
val label = resources.getString(id)
val em = paint.measureText("m")
rect.set(0f, 0f, width.toFloat(), height.toFloat())
rect.offset(0f, 0.4f * em)
canvas.drawText(label, rect.centerX(), rect.centerY(), paint)
}
}
}

@ -1,243 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list.views;
import android.content.*;
import android.support.annotation.*;
import android.util.*;
import android.widget.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.core.preferences.*;
import org.isoron.uhabits.core.utils.*;
import java.util.*;
import static android.view.View.MeasureSpec.EXACTLY;
import static android.view.View.MeasureSpec.makeMeasureSpec;
import static org.isoron.androidbase.utils.InterfaceUtils.getDimension;
import static org.isoron.uhabits.utils.AttributeSetUtils.getIntAttribute;
import static org.isoron.uhabits.utils.PaletteUtils.getAndroidTestColor;
public class CheckmarkPanelView extends LinearLayout
implements Preferences.Listener
{
private static final int LEFT_TO_RIGHT = 0;
private static final int RIGHT_TO_LEFT = 1;
@Nullable
private Preferences prefs;
private int values[];
private int nButtons;
private int color;
private int dataOffset;
@NonNull
private OnInvalidToggleListener onInvalidToggleListener;
@NonNull
private OnToggleListener onToggleLister;
public CheckmarkPanelView(Context context)
{
super(context);
init();
}
public CheckmarkPanelView(Context ctx, AttributeSet attrs)
{
super(ctx, attrs);
init();
if (ctx != null && attrs != null)
{
int paletteColor = getIntAttribute(ctx, attrs, "color", 0);
setColor(getAndroidTestColor(paletteColor));
setButtonCount(getIntAttribute(ctx, attrs, "button_count", 5));
}
if (isInEditMode()) initEditMode();
}
public CheckmarkButtonView indexToButton(int i)
{
int position = i;
if (getCheckmarkOrder() == RIGHT_TO_LEFT) position = nButtons - i - 1;
return (CheckmarkButtonView) getChildAt(position);
}
@Override
public void onCheckmarkOrderChanged()
{
setupButtons();
}
public void setButtonCount(int newButtonCount)
{
if (nButtons != newButtonCount)
{
nButtons = newButtonCount;
addButtons();
}
setupButtons();
}
public void setColor(int color)
{
this.color = color;
setupButtons();
}
public void setDataOffset(int dataOffset)
{
this.dataOffset = dataOffset;
setupButtons();
}
public void setOnInvalidToggleListener(
@NonNull OnInvalidToggleListener onInvalidToggleListener)
{
this.onInvalidToggleListener = onInvalidToggleListener;
}
public void setOnToggleLister(@NonNull OnToggleListener onToggleLister)
{
this.onToggleLister = onToggleLister;
}
public void setValues(int[] values)
{
this.values = values;
setupButtons();
}
@Override
protected void onAttachedToWindow()
{
super.onAttachedToWindow();
if (prefs != null) prefs.addListener(this);
}
@Override
protected void onDetachedFromWindow()
{
if (prefs != null) prefs.removeListener(this);
super.onDetachedFromWindow();
}
@Override
protected void onMeasure(int widthSpec, int heightSpec)
{
float buttonWidth = getDimension(getContext(), R.dimen.checkmarkWidth);
float buttonHeight =
getDimension(getContext(), R.dimen.checkmarkHeight);
float width = buttonWidth * nButtons;
widthSpec = makeMeasureSpec((int) width, EXACTLY);
heightSpec = makeMeasureSpec((int) buttonHeight, EXACTLY);
super.onMeasure(widthSpec, heightSpec);
}
private void addButtons()
{
removeAllViews();
for (int i = 0; i < nButtons; i++)
addView(new CheckmarkButtonView(getContext()));
}
private int getCheckmarkOrder()
{
if (prefs == null) return LEFT_TO_RIGHT;
return prefs.shouldReverseCheckmarks() ? RIGHT_TO_LEFT : LEFT_TO_RIGHT;
}
private void init()
{
Context appContext = getContext().getApplicationContext();
if (appContext instanceof HabitsApplication)
{
HabitsApplication app = (HabitsApplication) appContext;
prefs = app.getComponent().getPreferences();
}
onInvalidToggleListener = () -> {};
onToggleLister = (t) -> {};
setWillNotDraw(false);
values = new int[0];
}
private void initEditMode()
{
int values[] = new int[nButtons];
for (int i = 0; i < nButtons; i++)
values[i] = Math.min(2, new Random().nextInt(4));
setValues(values);
}
private void setupButtonControllers(long timestamp,
CheckmarkButtonView buttonView)
{
buttonView.setOnInvalidToggleListener(
() -> onInvalidToggleListener.onInvalidToggle());
buttonView.setOnToggleListener(
() -> onToggleLister.onToggle(timestamp));
}
private void setupButtons()
{
long timestamp = DateUtils.getStartOfToday();
long day = DateUtils.millisecondsInOneDay;
timestamp -= day * dataOffset;
for (int i = 0; i < nButtons; i++)
{
CheckmarkButtonView buttonView = indexToButton(i);
if (i + dataOffset >= values.length) break;
buttonView.setValue(values[i + dataOffset]);
buttonView.setColor(color);
setupButtonControllers(timestamp, buttonView);
timestamp -= day;
}
}
public interface OnInvalidToggleListener
{
void onInvalidToggle();
}
public interface OnToggleListener
{
void onToggle(long timestamp);
}
}

@ -0,0 +1,71 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list.views
import android.content.*
import com.google.auto.factory.*
import org.isoron.androidbase.activities.*
import org.isoron.uhabits.core.models.Checkmark.*
import org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.core.utils.*
@AutoFactory
class CheckmarkPanelView(
@Provided @ActivityContext context: Context,
@Provided preferences: Preferences,
@Provided private val buttonFactory: CheckmarkButtonViewFactory
) : ButtonPanelView<CheckmarkButtonView>(context, preferences) {
var values = IntArray(0)
set(values) {
field = values
setupButtons()
}
var color = 0
set(value) {
field = value
setupButtons()
}
var onToggle: (Long) -> Unit = {}
set(value) {
field = value
setupButtons()
}
override fun createButton(): CheckmarkButtonView = buttonFactory.create()
@Synchronized
override fun setupButtons() {
val today = DateUtils.getStartOfToday()
val day = DateUtils.millisecondsInOneDay
buttons.forEachIndexed { index, button ->
val timestamp = today - (index + dataOffset) * day
button.value = when {
index + dataOffset < values.size -> values[index + dataOffset]
else -> UNCHECKED
}
button.color = color
button.onToggle = { onToggle(timestamp) }
}
}
}

@ -0,0 +1,52 @@
/*
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list.views
import android.content.*
import android.view.*
import android.view.Gravity.*
import android.view.ViewGroup.LayoutParams.*
import android.widget.*
import org.isoron.androidbase.activities.*
import org.isoron.uhabits.*
import org.isoron.uhabits.utils.*
class EmptyListView(context: Context) : LinearLayout(context) {
init {
orientation = VERTICAL
gravity = Gravity.CENTER
visibility = BaseRootView.GONE
addView(TextView(context).apply {
text = str(R.string.fa_star_half_o)
typeface = getFontAwesome()
textSize = sp(40.0f)
gravity = CENTER
setTextColor(sres.getColor(R.attr.mediumContrastTextColor))
}, MATCH_PARENT, WRAP_CONTENT)
addView(TextView(context).apply {
text = str(R.string.no_habits_found)
gravity = CENTER
setPadding(0, dp(20.0f).toInt(), 0, 0)
setTextColor(sres.getColor(R.attr.mediumContrastTextColor))
}, MATCH_PARENT, WRAP_CONTENT)
}
}

@ -78,7 +78,8 @@ public class HabitCardListAdapter
this.midnightTimer = midnightTimer; this.midnightTimer = midnightTimer;
cache.setListener(this); cache.setListener(this);
cache.setCheckmarkCount(ListHabitsRootView.MAX_CHECKMARK_COUNT); cache.setCheckmarkCount(
ListHabitsRootViewKt.MAX_CHECKMARK_COUNT);
cache.setOrder(preferences.getDefaultOrder()); cache.setOrder(preferences.getDefaultOrder());
setHasStableIds(true); setHasStableIds(true);
@ -202,7 +203,7 @@ public class HabitCardListAdapter
int viewType) int viewType)
{ {
if (listView == null) return null; if (listView == null) return null;
View view = listView.createCardView(); View view = listView.createHabitCardView();
return new HabitCardViewHolder(view); return new HabitCardViewHolder(view);
} }

@ -1,334 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list.views;
import android.support.annotation.*;
import org.isoron.androidbase.activities.*;
import org.isoron.uhabits.core.models.*;
import javax.inject.*;
/**
* Controller responsible for receiving and processing the events generated by a
* HabitListView. These include selecting and reordering items, toggling
* checkmarks and clicking habits.
*/
@ActivityScope
public class HabitCardListController implements HabitCardListView.Controller,
ModelObservable.Listener
{
private final Mode NORMAL_MODE = new NormalMode();
private final Mode SELECTION_MODE = new SelectionMode();
@NonNull
private final HabitCardListAdapter adapter;
@Nullable
private HabitListener habitListener;
@Nullable
private SelectionListener selectionListener;
@NonNull
private Mode activeMode;
@Inject
public HabitCardListController(@NonNull HabitCardListAdapter adapter)
{
this.adapter = adapter;
this.activeMode = new NormalMode();
adapter.getObservable().addListener(this);
}
/**
* Called when the user drags a habit and drops it somewhere. Note that the
* dragging operation is already complete.
*
* @param from the original position of the habit
* @param to the position where the habit was released
*/
@Override
public void drop(int from, int to)
{
if (from == to) return;
cancelSelection();
Habit habitFrom = adapter.getItem(from);
Habit habitTo = adapter.getItem(to);
adapter.performReorder(from, to);
if (habitListener != null)
habitListener.onHabitReorder(habitFrom, habitTo);
}
@Override
public void onEdit(@NonNull Habit habit, long timestamp)
{
if (habitListener != null) habitListener.onEdit(habit, timestamp);
}
@Override
public void onInvalidEdit()
{
if (habitListener != null) habitListener.onInvalidEdit();
}
/**
* Called when the user attempts to perform a toggle, but attempt is
* rejected.
*/
@Override
public void onInvalidToggle()
{
if (habitListener != null) habitListener.onInvalidToggle();
}
/**
* Called when the user clicks at some item.
*
* @param position the position of the clicked item
*/
@Override
public void onItemClick(int position)
{
activeMode.onItemClick(position);
}
/**
* Called when the user long clicks at some item.
*
* @param position the position of the clicked item
*/
@Override
public void onItemLongClick(int position)
{
activeMode.onItemLongClick(position);
}
@Override
public void onModelChange()
{
if(adapter.isSelectionEmpty())
{
activeMode = new NormalMode();
if (selectionListener != null) selectionListener.onSelectionFinish();
}
}
/**
* Called when the selection operation is cancelled externally, by something
* other than this controller. This happens, for example, when the user
* presses the back button.
*/
public void onSelectionFinished()
{
cancelSelection();
}
/**
* Called when the user wants to toggle a checkmark.
*
* @param habit the habit of the checkmark
* @param timestamp the timestamps of the checkmark
*/
@Override
public void onToggle(@NonNull Habit habit, long timestamp)
{
if (habitListener != null) habitListener.onToggle(habit, timestamp);
}
public void setHabitListener(@Nullable HabitListener habitListener)
{
this.habitListener = habitListener;
}
public void setSelectionListener(@Nullable SelectionListener listener)
{
this.selectionListener = listener;
}
/**
* Called when the user starts dragging an item.
*
* @param position the position of the habit dragged
*/
@Override
public void startDrag(int position)
{
activeMode.startDrag(position);
}
/**
* Selects or deselects the item at a given position
*
* @param position the position of the item to be selected/deselected
*/
protected void toggleSelection(int position)
{
adapter.toggleSelection(position);
activeMode = adapter.isSelectionEmpty() ? NORMAL_MODE : SELECTION_MODE;
}
/**
* Marks all items as not selected and finishes the selection operation.
*/
private void cancelSelection()
{
adapter.clearSelection();
activeMode = new NormalMode();
if (selectionListener != null) selectionListener.onSelectionFinish();
}
public interface HabitListener
{
void onEdit(Habit habit, long timestamp);
/**
* Called when the user clicks a habit.
*
* @param habit the habit clicked
*/
void onHabitClick(@NonNull Habit habit);
/**
* Called when the user wants to change the position of a habit on the
* list.
*
* @param from habit to be moved
* @param to habit that currently occupies the desired position
*/
void onHabitReorder(@NonNull Habit from, @NonNull Habit to);
void onInvalidEdit();
void onInvalidToggle();
void onToggle(Habit habit, long timestamp);
}
/**
* A Mode describes the behaviour of the list upon clicking, long clicking
* and dragging an item. This depends on whether some items are already
* selected or not.
*/
private interface Mode
{
void onItemClick(int position);
boolean onItemLongClick(int position);
void startDrag(int position);
}
public interface SelectionListener
{
/**
* Called when the user changes the list of selected item. This is only
* called if there were previously selected items. If the selection was
* previously empty, then onHabitSelectionStart is called instead.
*/
void onSelectionChange();
/**
* Called when the user deselects all items or cancels the selection.
*/
void onSelectionFinish();
/**
* Called after the user selects the first item.
*/
void onSelectionStart();
}
/**
* Mode activated when there are no items selected. Clicks trigger habit
* click. Long clicks start selection.
*/
class NormalMode implements Mode
{
@Override
public void onItemClick(int position)
{
Habit habit = adapter.getItem(position);
if (habitListener != null) habitListener.onHabitClick(habit);
}
@Override
public boolean onItemLongClick(int position)
{
startSelection(position);
return true;
}
@Override
public void startDrag(int position)
{
startSelection(position);
}
protected void startSelection(int position)
{
toggleSelection(position);
activeMode = SELECTION_MODE;
if (selectionListener != null) selectionListener.onSelectionStart();
}
}
/**
* Mode activated when some items are already selected.
* <p>
* Clicks toggle item selection. Long clicks select more items.
*/
class SelectionMode implements Mode
{
@Override
public void onItemClick(int position)
{
toggleSelection(position);
notifyListener();
}
@Override
public boolean onItemLongClick(int position)
{
toggleSelection(position);
notifyListener();
return true;
}
@Override
public void startDrag(int position)
{
toggleSelection(position);
notifyListener();
}
protected void notifyListener()
{
if (selectionListener == null) return;
if (activeMode == SELECTION_MODE)
selectionListener.onSelectionChange();
else selectionListener.onSelectionFinish();
}
}
}

@ -0,0 +1,167 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.list.views
import dagger.*
import org.isoron.androidbase.activities.*
import org.isoron.uhabits.activities.habits.list.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.core.ui.screens.habits.list.*
import javax.inject.*
/**
* Controller responsible for receiving and processing the events generated by a
* HabitListView. These include selecting and reordering items, toggling
* checkmarks and clicking habits.
*/
@ActivityScope
class HabitCardListController @Inject constructor(
private val adapter: HabitCardListAdapter,
private val behavior: ListHabitsBehavior,
private val selectionMenu: Lazy<ListHabitsSelectionMenu>
) : HabitCardListView.Controller, ModelObservable.Listener {
private val NORMAL_MODE = NormalMode()
private val SELECTION_MODE = SelectionMode()
private var activeMode: Mode
init {
this.activeMode = NORMAL_MODE
adapter.observable.addListener(this)
}
override fun drop(from: Int, to: Int) {
if (from == to) return
cancelSelection()
val habitFrom = adapter.getItem(from)
val habitTo = adapter.getItem(to)
if(habitFrom == null || habitTo == null) return
adapter.performReorder(from, to)
behavior.onReorderHabit(habitFrom, habitTo)
}
override fun onItemClick(position: Int) {
activeMode.onItemClick(position)
}
override fun onItemLongClick(position: Int) {
activeMode.onItemLongClick(position)
}
override fun onModelChange() {
if (adapter.isSelectionEmpty) {
activeMode = NormalMode()
selectionMenu.get().onSelectionFinish()
}
}
fun onSelectionFinished() {
cancelSelection()
}
override fun startDrag(position: Int) {
activeMode.startDrag(position)
}
protected fun toggleSelection(position: Int) {
adapter.toggleSelection(position)
activeMode = if (adapter.isSelectionEmpty) NORMAL_MODE else SELECTION_MODE
}
private fun cancelSelection() {
adapter.clearSelection()
activeMode = NormalMode()
selectionMenu.get().onSelectionFinish()
}
interface HabitListener {
fun onHabitClick(habit: Habit)
fun onHabitReorder(from: Habit, to: Habit)
}
/**
* A Mode describes the behavior of the list upon clicking, long clicking
* and dragging an item. This depends on whether some items are already
* selected or not.
*/
private interface Mode {
fun onItemClick(position: Int)
fun onItemLongClick(position: Int): Boolean
fun startDrag(position: Int)
}
/**
* Mode activated when there are no items selected. Clicks trigger habit
* click. Long clicks start selection.
*/
internal inner class NormalMode : Mode {
override fun onItemClick(position: Int) {
val habit = adapter.getItem(position)
if (habit == null) return
behavior.onClickHabit(habit)
}
override fun onItemLongClick(position: Int): Boolean {
startSelection(position)
return true
}
override fun startDrag(position: Int) {
startSelection(position)
}
protected fun startSelection(position: Int) {
toggleSelection(position)
activeMode = SELECTION_MODE
selectionMenu.get().onSelectionStart()
}
}
/**
* Mode activated when some items are already selected. Clicks toggle
* item selection. Long clicks select more items.
*/
internal inner class SelectionMode : Mode {
override fun onItemClick(position: Int) {
toggleSelection(position)
notifyListener()
}
override fun onItemLongClick(position: Int): Boolean {
toggleSelection(position)
notifyListener()
return true
}
override fun startDrag(position: Int) {
toggleSelection(position)
notifyListener()
}
protected fun notifyListener() {
if (activeMode === SELECTION_MODE)
selectionMenu.get().onSelectionChange()
else
selectionMenu.get().onSelectionFinish()
}
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save