diff --git a/app/build.gradle b/app/build.gradle index f8cefba08..c446e6d11 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,4 +1,6 @@ apply plugin: 'com.android.application' +apply plugin: 'com.neenbedankt.android-apt' +apply plugin: 'me.tatarka.retrolambda' android { compileSdkVersion 23 @@ -13,7 +15,7 @@ android { buildConfigField "String", "databaseFilename", "\"uhabits.db\"" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" - //testInstrumentationRunnerArgument "size", "small" + testInstrumentationRunnerArgument "size", "medium" } buildTypes { @@ -29,6 +31,22 @@ android { lintOptions { checkReleaseBuilds false } + + compileOptions { + targetCompatibility 1.8 + sourceCompatibility 1.8 + } + + testOptions { + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen { false } + showStandardStreams = true + } + } + } + } dependencies { @@ -40,12 +58,29 @@ dependencies { compile 'org.apmem.tools:layouts:1.10@aar' compile 'com.opencsv:opencsv:3.7' compile 'com.michaelpardo:activeandroid:3.1.0-SNAPSHOT' + compile 'org.jetbrains:annotations-java5:15.0' + + compile 'com.jakewharton:butterknife:8.0.1' + apt 'com.jakewharton:butterknife-compiler:8.0.1' + + compile 'com.google.dagger:dagger:2.2' + apt 'com.google.dagger:dagger-compiler:2.2' + testApt 'com.google.dagger:dagger-compiler:2.2' + androidTestApt 'com.google.dagger:dagger-compiler:2.2' + provided 'javax.annotation:jsr250-api:1.0' compile project(':libs:drag-sort-listview:library') + testCompile 'junit:junit:4.12' + testCompile 'org.hamcrest:hamcrest-library:1.3' + testCompile 'org.mockito:mockito-core:1.10.19' + androidTestCompile 'com.android.support:support-annotations:23.3.0' androidTestCompile 'com.android.support.test:runner:0.5' androidTestCompile 'com.android.support.test:rules:0.5' + androidTestCompile 'org.mockito:mockito-core:1.10.19' + androidTestCompile "com.google.dexmaker:dexmaker:1.2" + androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2' androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.1') { exclude group: 'com.android.support' @@ -60,13 +95,6 @@ dependencies { } } - -task grantAnimationPermission(type: Exec, dependsOn: 'installDebug') { - commandLine "adb shell pm grant org.isoron.uhabits android.permission.SET_ANIMATION_SCALE".split(' ') -} - -tasks.whenTaskAdded { task -> - if (task.name.startsWith('connected')) { - task.dependsOn grantAnimationPermission - } +retrolambda { + defaultMethods true } \ No newline at end of file diff --git a/app/src/androidTest/assets/views/CheckmarkView/checked.png b/app/src/androidTest/assets/views/CheckmarkView/checked.png deleted file mode 100644 index 69f650a0b..000000000 Binary files a/app/src/androidTest/assets/views/CheckmarkView/checked.png and /dev/null differ diff --git a/app/src/androidTest/assets/views/CheckmarkView/implicitly_checked.png b/app/src/androidTest/assets/views/CheckmarkView/implicitly_checked.png deleted file mode 100644 index 992a9e5b2..000000000 Binary files a/app/src/androidTest/assets/views/CheckmarkView/implicitly_checked.png and /dev/null differ diff --git a/app/src/androidTest/assets/views/CheckmarkView/large_size.png b/app/src/androidTest/assets/views/CheckmarkView/large_size.png deleted file mode 100644 index 8aa13d5e2..000000000 Binary files a/app/src/androidTest/assets/views/CheckmarkView/large_size.png and /dev/null differ diff --git a/app/src/androidTest/assets/views/CheckmarkView/unchecked.png b/app/src/androidTest/assets/views/CheckmarkView/unchecked.png deleted file mode 100644 index 576d369ec..000000000 Binary files a/app/src/androidTest/assets/views/CheckmarkView/unchecked.png and /dev/null differ diff --git a/app/src/androidTest/assets/views/NumberView/render.png b/app/src/androidTest/assets/views/NumberView/render.png deleted file mode 100644 index 52e65b579..000000000 Binary files a/app/src/androidTest/assets/views/NumberView/render.png and /dev/null differ diff --git a/app/src/androidTest/assets/views/NumberView/renderDifferentParams.png b/app/src/androidTest/assets/views/NumberView/renderDifferentParams.png deleted file mode 100644 index 4b4814f1a..000000000 Binary files a/app/src/androidTest/assets/views/NumberView/renderDifferentParams.png and /dev/null differ diff --git a/app/src/androidTest/assets/views/NumberView/renderLongLabel.png b/app/src/androidTest/assets/views/NumberView/renderLongLabel.png deleted file mode 100644 index 89fe4fd32..000000000 Binary files a/app/src/androidTest/assets/views/NumberView/renderLongLabel.png and /dev/null differ diff --git a/app/src/androidTest/assets/views/HabitFrequencyView/render.png b/app/src/androidTest/assets/views/common/FrequencyChart/render.png similarity index 100% rename from app/src/androidTest/assets/views/HabitFrequencyView/render.png rename to app/src/androidTest/assets/views/common/FrequencyChart/render.png diff --git a/app/src/androidTest/assets/views/HabitFrequencyView/renderDataOffset.png b/app/src/androidTest/assets/views/common/FrequencyChart/renderDataOffset.png similarity index 100% rename from app/src/androidTest/assets/views/HabitFrequencyView/renderDataOffset.png rename to app/src/androidTest/assets/views/common/FrequencyChart/renderDataOffset.png diff --git a/app/src/androidTest/assets/views/HabitFrequencyView/renderDifferentSize.png b/app/src/androidTest/assets/views/common/FrequencyChart/renderDifferentSize.png similarity index 100% rename from app/src/androidTest/assets/views/HabitFrequencyView/renderDifferentSize.png rename to app/src/androidTest/assets/views/common/FrequencyChart/renderDifferentSize.png diff --git a/app/src/androidTest/assets/views/HabitFrequencyView/renderTransparent.png b/app/src/androidTest/assets/views/common/FrequencyChart/renderTransparent.png similarity index 100% rename from app/src/androidTest/assets/views/HabitFrequencyView/renderTransparent.png rename to app/src/androidTest/assets/views/common/FrequencyChart/renderTransparent.png diff --git a/app/src/androidTest/assets/views/HabitHistoryView/render.png b/app/src/androidTest/assets/views/common/HistoryChart/render.png similarity index 100% rename from app/src/androidTest/assets/views/HabitHistoryView/render.png rename to app/src/androidTest/assets/views/common/HistoryChart/render.png diff --git a/app/src/androidTest/assets/views/HabitHistoryView/renderDataOffset.png b/app/src/androidTest/assets/views/common/HistoryChart/renderDataOffset.png similarity index 100% rename from app/src/androidTest/assets/views/HabitHistoryView/renderDataOffset.png rename to app/src/androidTest/assets/views/common/HistoryChart/renderDataOffset.png diff --git a/app/src/androidTest/assets/views/HabitHistoryView/renderDifferentSize.png b/app/src/androidTest/assets/views/common/HistoryChart/renderDifferentSize.png similarity index 100% rename from app/src/androidTest/assets/views/HabitHistoryView/renderDifferentSize.png rename to app/src/androidTest/assets/views/common/HistoryChart/renderDifferentSize.png diff --git a/app/src/androidTest/assets/views/HabitHistoryView/renderTransparent.png b/app/src/androidTest/assets/views/common/HistoryChart/renderTransparent.png similarity index 100% rename from app/src/androidTest/assets/views/HabitHistoryView/renderTransparent.png rename to app/src/androidTest/assets/views/common/HistoryChart/renderTransparent.png diff --git a/app/src/androidTest/assets/views/RingView/render.png b/app/src/androidTest/assets/views/common/RingView/render.png similarity index 100% rename from app/src/androidTest/assets/views/RingView/render.png rename to app/src/androidTest/assets/views/common/RingView/render.png diff --git a/app/src/androidTest/assets/views/RingView/renderDifferentParams.png b/app/src/androidTest/assets/views/common/RingView/renderDifferentParams.png similarity index 100% rename from app/src/androidTest/assets/views/RingView/renderDifferentParams.png rename to app/src/androidTest/assets/views/common/RingView/renderDifferentParams.png diff --git a/app/src/androidTest/assets/views/HabitScoreView/render.png b/app/src/androidTest/assets/views/common/ScoreChart/render.png similarity index 100% rename from app/src/androidTest/assets/views/HabitScoreView/render.png rename to app/src/androidTest/assets/views/common/ScoreChart/render.png diff --git a/app/src/androidTest/assets/views/HabitScoreView/renderDataOffset.png b/app/src/androidTest/assets/views/common/ScoreChart/renderDataOffset.png similarity index 100% rename from app/src/androidTest/assets/views/HabitScoreView/renderDataOffset.png rename to app/src/androidTest/assets/views/common/ScoreChart/renderDataOffset.png diff --git a/app/src/androidTest/assets/views/HabitScoreView/renderDifferentSize.png b/app/src/androidTest/assets/views/common/ScoreChart/renderDifferentSize.png similarity index 100% rename from app/src/androidTest/assets/views/HabitScoreView/renderDifferentSize.png rename to app/src/androidTest/assets/views/common/ScoreChart/renderDifferentSize.png diff --git a/app/src/androidTest/assets/views/HabitScoreView/renderMonthly.png b/app/src/androidTest/assets/views/common/ScoreChart/renderMonthly.png similarity index 100% rename from app/src/androidTest/assets/views/HabitScoreView/renderMonthly.png rename to app/src/androidTest/assets/views/common/ScoreChart/renderMonthly.png diff --git a/app/src/androidTest/assets/views/HabitScoreView/renderTransparent.png b/app/src/androidTest/assets/views/common/ScoreChart/renderTransparent.png similarity index 100% rename from app/src/androidTest/assets/views/HabitScoreView/renderTransparent.png rename to app/src/androidTest/assets/views/common/ScoreChart/renderTransparent.png diff --git a/app/src/androidTest/assets/views/HabitScoreView/renderYearly.png b/app/src/androidTest/assets/views/common/ScoreChart/renderYearly.png similarity index 100% rename from app/src/androidTest/assets/views/HabitScoreView/renderYearly.png rename to app/src/androidTest/assets/views/common/ScoreChart/renderYearly.png diff --git a/app/src/androidTest/assets/views/HabitStreakView/render.png b/app/src/androidTest/assets/views/common/StreakChart/render.png similarity index 100% rename from app/src/androidTest/assets/views/HabitStreakView/render.png rename to app/src/androidTest/assets/views/common/StreakChart/render.png diff --git a/app/src/androidTest/assets/views/HabitStreakView/renderSmallSize.png b/app/src/androidTest/assets/views/common/StreakChart/renderSmallSize.png similarity index 100% rename from app/src/androidTest/assets/views/HabitStreakView/renderSmallSize.png rename to app/src/androidTest/assets/views/common/StreakChart/renderSmallSize.png diff --git a/app/src/androidTest/assets/views/HabitStreakView/renderTransparent.png b/app/src/androidTest/assets/views/common/StreakChart/renderTransparent.png similarity index 100% rename from app/src/androidTest/assets/views/HabitStreakView/renderTransparent.png rename to app/src/androidTest/assets/views/common/StreakChart/renderTransparent.png diff --git a/app/src/androidTest/assets/views/habits/list/CheckmarkButtonView/render_explicit_check.png b/app/src/androidTest/assets/views/habits/list/CheckmarkButtonView/render_explicit_check.png new file mode 100644 index 000000000..1f53b9ae5 Binary files /dev/null and b/app/src/androidTest/assets/views/habits/list/CheckmarkButtonView/render_explicit_check.png differ diff --git a/app/src/androidTest/assets/views/habits/list/CheckmarkButtonView/render_implicit_check.png b/app/src/androidTest/assets/views/habits/list/CheckmarkButtonView/render_implicit_check.png new file mode 100644 index 000000000..2570ca857 Binary files /dev/null and b/app/src/androidTest/assets/views/habits/list/CheckmarkButtonView/render_implicit_check.png differ diff --git a/app/src/androidTest/assets/views/habits/list/CheckmarkButtonView/render_unchecked.png b/app/src/androidTest/assets/views/habits/list/CheckmarkButtonView/render_unchecked.png new file mode 100644 index 000000000..841cc20e0 Binary files /dev/null and b/app/src/androidTest/assets/views/habits/list/CheckmarkButtonView/render_unchecked.png differ diff --git a/app/src/androidTest/assets/views/habits/list/CheckmarkPanelView/render.png b/app/src/androidTest/assets/views/habits/list/CheckmarkPanelView/render.png new file mode 100644 index 000000000..00b87e88e Binary files /dev/null and b/app/src/androidTest/assets/views/habits/list/CheckmarkPanelView/render.png differ diff --git a/app/src/androidTest/assets/views/habits/list/HintView/render.png b/app/src/androidTest/assets/views/habits/list/HintView/render.png new file mode 100644 index 000000000..dae19beb8 Binary files /dev/null and b/app/src/androidTest/assets/views/habits/list/HintView/render.png differ diff --git a/app/src/androidTest/assets/views/widgets/CheckmarkWidget/render.png b/app/src/androidTest/assets/views/widgets/CheckmarkWidget/render.png new file mode 100644 index 000000000..9f497ed59 Binary files /dev/null and b/app/src/androidTest/assets/views/widgets/CheckmarkWidget/render.png differ diff --git a/app/src/androidTest/assets/views/widgets/CheckmarkWidgetView/checked.png b/app/src/androidTest/assets/views/widgets/CheckmarkWidgetView/checked.png new file mode 100644 index 000000000..1437a510b Binary files /dev/null and b/app/src/androidTest/assets/views/widgets/CheckmarkWidgetView/checked.png differ diff --git a/app/src/androidTest/assets/views/widgets/CheckmarkWidgetView/implicitly_checked.png b/app/src/androidTest/assets/views/widgets/CheckmarkWidgetView/implicitly_checked.png new file mode 100644 index 000000000..97fdcbd19 Binary files /dev/null and b/app/src/androidTest/assets/views/widgets/CheckmarkWidgetView/implicitly_checked.png differ diff --git a/app/src/androidTest/assets/views/widgets/CheckmarkWidgetView/large_size.png b/app/src/androidTest/assets/views/widgets/CheckmarkWidgetView/large_size.png new file mode 100644 index 000000000..bcc1fcf9b Binary files /dev/null and b/app/src/androidTest/assets/views/widgets/CheckmarkWidgetView/large_size.png differ diff --git a/app/src/androidTest/assets/views/widgets/CheckmarkWidgetView/unchecked.png b/app/src/androidTest/assets/views/widgets/CheckmarkWidgetView/unchecked.png new file mode 100644 index 000000000..2f64db223 Binary files /dev/null and b/app/src/androidTest/assets/views/widgets/CheckmarkWidgetView/unchecked.png differ diff --git a/app/src/androidTest/assets/views/widgets/FrequencyWidget/render.png b/app/src/androidTest/assets/views/widgets/FrequencyWidget/render.png new file mode 100644 index 000000000..d32b9b837 Binary files /dev/null and b/app/src/androidTest/assets/views/widgets/FrequencyWidget/render.png differ diff --git a/app/src/androidTest/assets/views/widgets/HistoryWidget/render.png b/app/src/androidTest/assets/views/widgets/HistoryWidget/render.png new file mode 100644 index 000000000..ce30793a7 Binary files /dev/null and b/app/src/androidTest/assets/views/widgets/HistoryWidget/render.png differ diff --git a/app/src/androidTest/assets/views/widgets/ScoreWidget/render.png b/app/src/androidTest/assets/views/widgets/ScoreWidget/render.png new file mode 100644 index 000000000..382bb867d Binary files /dev/null and b/app/src/androidTest/assets/views/widgets/ScoreWidget/render.png differ diff --git a/app/src/androidTest/assets/views/widgets/StreakWidget/render.png b/app/src/androidTest/assets/views/widgets/StreakWidget/render.png new file mode 100644 index 000000000..7aa31f345 Binary files /dev/null and b/app/src/androidTest/assets/views/widgets/StreakWidget/render.png differ diff --git a/app/src/androidTest/java/org/isoron/uhabits/AndroidTestComponent.java b/app/src/androidTest/java/org/isoron/uhabits/AndroidTestComponent.java new file mode 100644 index 000000000..4c73578d6 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/AndroidTestComponent.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2016 Á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; + +import javax.inject.Singleton; + +import dagger.Component; + +@Singleton +@Component(modules = {AndroidModule.class}) +public interface AndroidTestComponent extends BaseComponent +{ + void inject(BaseAndroidTest baseAndroidTest); +} + diff --git a/app/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.java b/app/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.java new file mode 100644 index 000000000..585d4f49a --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2016 Á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; + +import android.appwidget.*; +import android.content.*; +import android.os.*; +import android.support.annotation.*; +import android.support.test.*; + +import org.isoron.uhabits.commands.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.tasks.*; +import org.isoron.uhabits.utils.*; +import org.junit.*; + +import java.util.*; +import java.util.concurrent.*; + +import javax.inject.*; + +import static junit.framework.Assert.*; +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +public class BaseAndroidTest +{ + // 8:00am, January 25th, 2015 (UTC) + public static final long FIXED_LOCAL_TIME = 1422172800000L; + + private static boolean isLooperPrepared; + + protected Context testContext; + + protected Context targetContext; + + @Inject + protected Preferences prefs; + + @Inject + protected HabitList habitList; + + @Inject + protected CommandRunner commandRunner; + + protected AndroidTestComponent androidTestComponent; + + protected HabitFixtures fixtures; + + protected CountDownLatch latch; + + @Before + public void setUp() + { + if (!isLooperPrepared) + { + Looper.prepare(); + isLooperPrepared = true; + } + + targetContext = InstrumentationRegistry.getTargetContext(); + testContext = InstrumentationRegistry.getContext(); + + InterfaceUtils.setFixedTheme(R.style.AppBaseTheme); + DateUtils.setFixedLocalTime(FIXED_LOCAL_TIME); + + androidTestComponent = DaggerAndroidTestComponent.builder().build(); + HabitsApplication.setComponent(androidTestComponent); + androidTestComponent.inject(this); + + fixtures = new HabitFixtures(habitList); + + latch = new CountDownLatch(1); + } + + protected void assertWidgetProviderIsInstalled(Class componentClass) + { + ComponentName provider = + new ComponentName(targetContext, componentClass); + AppWidgetManager manager = AppWidgetManager.getInstance(targetContext); + + List installedProviders = new LinkedList<>(); + for (AppWidgetProviderInfo info : manager.getInstalledProviders()) + installedProviders.add(info.provider); + + assertThat(installedProviders, hasItems(provider)); + } + + protected void setTheme(@StyleRes int themeId) + { + InterfaceUtils.setFixedTheme(themeId); + targetContext.setTheme(themeId); + } + + protected void sleep(int time) + { + try + { + Thread.sleep(time); + } + catch (InterruptedException e) + { + fail(); + } + } + + protected void waitForAsyncTasks() + throws InterruptedException, TimeoutException + { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) + { + Thread.sleep(1000); + return; + } + + BaseTask.waitForTasks(10000); + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/BaseTest.java b/app/src/androidTest/java/org/isoron/uhabits/BaseTest.java deleted file mode 100644 index 5515b5121..000000000 --- a/app/src/androidTest/java/org/isoron/uhabits/BaseTest.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (C) 2016 Á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; - -import android.content.Context; -import android.os.Build; -import android.os.Looper; -import android.support.test.InstrumentationRegistry; - -import org.isoron.uhabits.helpers.DateHelper; -import org.isoron.uhabits.helpers.UIHelper; -import org.isoron.uhabits.tasks.BaseTask; -import org.junit.Before; - -import java.util.concurrent.TimeoutException; - -public class BaseTest -{ - protected Context testContext; - protected Context targetContext; - private static boolean isLooperPrepared; - - public static final long FIXED_LOCAL_TIME = 1422172800000L; // 8:00am, January 25th, 2015 (UTC) - - @Before - public void setup() - { - if(!isLooperPrepared) - { - Looper.prepare(); - isLooperPrepared = true; - } - - targetContext = InstrumentationRegistry.getTargetContext(); - testContext = InstrumentationRegistry.getContext(); - - UIHelper.setFixedTheme(R.style.AppBaseTheme); - DateHelper.setFixedLocalTime(FIXED_LOCAL_TIME); - } - - protected void waitForAsyncTasks() throws InterruptedException, TimeoutException - { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) - { - Thread.sleep(1000); - return; - } - - BaseTask.waitForTasks(10000); - } -} diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/views/ViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/BaseViewTest.java similarity index 55% rename from app/src/androidTest/java/org/isoron/uhabits/unit/views/ViewTest.java rename to app/src/androidTest/java/org/isoron/uhabits/BaseViewTest.java index d87c3b9fc..9894843d0 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/views/ViewTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/BaseViewTest.java @@ -17,47 +17,46 @@ * with this program. If not, see . */ -package org.isoron.uhabits.unit.views; +package org.isoron.uhabits; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.os.SystemClock; -import android.view.GestureDetector; -import android.view.MotionEvent; -import android.view.View; +import android.graphics.*; +import android.os.*; +import android.support.annotation.*; +import android.view.*; +import android.widget.*; -import org.isoron.uhabits.BaseTest; -import org.isoron.uhabits.helpers.DatabaseHelper; -import org.isoron.uhabits.helpers.UIHelper; -import org.isoron.uhabits.tasks.BaseTask; -import org.isoron.uhabits.views.HabitDataView; +import org.isoron.uhabits.ui.widgets.*; +import org.isoron.uhabits.utils.*; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; +import java.io.*; -import static junit.framework.Assert.fail; +import static android.view.View.MeasureSpec.*; +import static junit.framework.Assert.*; -public class ViewTest extends BaseTest +public class BaseViewTest extends BaseAndroidTest { - protected static final double SIMILARITY_CUTOFF = 0.09; + protected static final double DEFAULT_SIMILARITY_CUTOFF = 0.09; + public static final int HISTOGRAM_BIN_SIZE = 8; - protected void measureView(int width, int height, View view) - { - int specWidth = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY); - int specHeight = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY); + private double similarityCutoff; - view.measure(specWidth, specHeight); - view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight()); + @Override + public void setUp() + { + super.setUp(); + similarityCutoff = DEFAULT_SIMILARITY_CUTOFF; } - protected void assertRenders(View view, String expectedImagePath) throws IOException + protected void assertRenders(View view, String expectedImagePath) + throws IOException { StringBuilder errorMessage = new StringBuilder(); expectedImagePath = getVersionedViewAssetPath(expectedImagePath); + if (view.isLayoutRequested()) measureView(view, view.getMeasuredWidth(), + view.getMeasuredHeight()); + view.setDrawingCacheEnabled(true); view.buildDrawingCache(); Bitmap actual = view.getDrawingCache(); @@ -65,97 +64,127 @@ public class ViewTest extends BaseTest int width = actual.getWidth(); int height = actual.getHeight(); - Bitmap scaledExpected = Bitmap.createScaledBitmap(expected, width, height, true); + Bitmap scaledExpected = + Bitmap.createScaledBitmap(expected, width, height, true); double distance; boolean similarEnough = true; - if ((distance = compareHistograms(getHistogram(actual), getHistogram(scaledExpected))) > SIMILARITY_CUTOFF) + if ((distance = compareHistograms(getHistogram(actual), + getHistogram(scaledExpected))) > similarityCutoff) { similarEnough = false; errorMessage.append(String.format( - "Rendered image has wrong histogram (distance=%f). ", - distance)); + "Rendered image has wrong histogram (distance=%f). ", + distance)); } - if(!similarEnough) + if (!similarEnough) { saveBitmap(expectedImagePath, ".expected", scaledExpected); String path = saveBitmap(expectedImagePath, "", actual); - errorMessage.append(String.format("Actual rendered image " + "saved to %s", path)); + errorMessage.append( + String.format("Actual rendered image saved to %s", path)); fail(errorMessage.toString()); } - actual.recycle(); expected.recycle(); scaledExpected.recycle(); } - private Bitmap getBitmapFromAssets(String path) throws IOException + @NonNull + protected FrameLayout convertToView(BaseWidget widget, + int width, + int height) { - InputStream stream = testContext.getAssets().open(path); - return BitmapFactory.decodeStream(stream); + widget.setDimensions( + new WidgetDimensions(width, height, width, height)); + FrameLayout view = new FrameLayout(targetContext); + RemoteViews remoteViews = widget.getPortraitRemoteViews(); + view.addView(remoteViews.apply(targetContext, view)); + measureView(view, width, height); + return view; } - private String getVersionedViewAssetPath(String path) + protected int dpToPixels(int dp) { - String result = null; + return (int) InterfaceUtils.dpToPixels(targetContext, dp); + } - if (android.os.Build.VERSION.SDK_INT >= 21) - { - try - { - String vpath = "views-v21/" + path; - testContext.getAssets().open(vpath); - result = vpath; - } - catch (IOException e) - { - // ignored - } - } + protected void measureView(View view, int width, int height) + { + int specWidth = makeMeasureSpec(width, View.MeasureSpec.EXACTLY); + int specHeight = makeMeasureSpec(height, View.MeasureSpec.EXACTLY); - if(result == null) - result = "views/" + path; + view.measure(specWidth, specHeight); + view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight()); + } - return result; + protected void setSimilarityCutoff(double similarityCutoff) + { + this.similarityCutoff = similarityCutoff; } - private String saveBitmap(String filename, String suffix, Bitmap bitmap) - throws IOException + protected void skipAnimation(View view) { - File dir = DatabaseHelper.getSDCardDir("test-screenshots"); - if(dir == null) dir = DatabaseHelper.getFilesDir("test-screenshots"); - if(dir == null) throw new RuntimeException("Could not find suitable dir for screenshots"); + ViewPropertyAnimator animator = view.animate(); + animator.setDuration(0); + animator.start(); + } - filename = filename.replaceAll("\\.png$", suffix + ".png"); - String absolutePath = String.format("%s/%s", dir.getAbsolutePath(), filename); + protected void tap(GestureDetector.OnGestureListener view, int x, int y) + throws InterruptedException + { + long now = SystemClock.uptimeMillis(); + MotionEvent e = + MotionEvent.obtain(now, now, MotionEvent.ACTION_UP, dpToPixels(x), + dpToPixels(y), 0); + view.onSingleTapUp(e); + e.recycle(); + } - File parent = new File(absolutePath).getParentFile(); - if(!parent.exists() && !parent.mkdirs()) - throw new RuntimeException(String.format("Could not create dir: %s", - parent.getAbsolutePath())); + private double compareHistograms(int[][] actualHistogram, + int[][] expectedHistogram) + { + long diff = 0; + long total = 0; - FileOutputStream out = new FileOutputStream(absolutePath); - bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); + for (int i = 0; i < 256 / HISTOGRAM_BIN_SIZE; i++) + { + diff += Math.abs(actualHistogram[0][i] - expectedHistogram[0][i]); + diff += Math.abs(actualHistogram[1][i] - expectedHistogram[1][i]); + diff += Math.abs(actualHistogram[2][i] - expectedHistogram[2][i]); + diff += Math.abs(actualHistogram[3][i] - expectedHistogram[3][i]); - return absolutePath; + total += actualHistogram[0][i]; + total += actualHistogram[1][i]; + total += actualHistogram[2][i]; + total += actualHistogram[3][i]; + } + + return (double) diff / total / 2; + } + + private Bitmap getBitmapFromAssets(String path) throws IOException + { + InputStream stream = testContext.getAssets().open(path); + return BitmapFactory.decodeStream(stream); } private int[][] getHistogram(Bitmap bitmap) { int histogram[][] = new int[4][256 / HISTOGRAM_BIN_SIZE]; - for(int x = 0; x < bitmap.getWidth(); x++) + for (int x = 0; x < bitmap.getWidth(); x++) { - for(int y = 0; y < bitmap.getHeight(); y++) + for (int y = 0; y < bitmap.getHeight(); y++) { int color = bitmap.getPixel(x, y); int[] argb = new int[]{ - (color >> 24) & 0xff, //alpha - (color >> 16) & 0xff, //red - (color >> 8) & 0xff, //green - (color ) & 0xff //blue + (color >> 24) & 0xff, //alpha + (color >> 16) & 0xff, //red + (color >> 8) & 0xff, //green + (color) & 0xff //blue }; histogram[0][argb[0] / HISTOGRAM_BIN_SIZE]++; @@ -168,59 +197,49 @@ public class ViewTest extends BaseTest return histogram; } - private double compareHistograms(int[][] actualHistogram, int[][] expectedHistogram) + private String getVersionedViewAssetPath(String path) { - long diff = 0; - long total = 0; + String result = null; - for(int i = 0; i < 256 / HISTOGRAM_BIN_SIZE; i ++) + if (android.os.Build.VERSION.SDK_INT >= 21) { - diff += Math.abs(actualHistogram[0][i] - expectedHistogram[0][i]); - diff += Math.abs(actualHistogram[1][i] - expectedHistogram[1][i]); - diff += Math.abs(actualHistogram[2][i] - expectedHistogram[2][i]); - diff += Math.abs(actualHistogram[3][i] - expectedHistogram[3][i]); - - total += actualHistogram[0][i]; - total += actualHistogram[1][i]; - total += actualHistogram[2][i]; - total += actualHistogram[3][i]; + try + { + String vpath = "views-v21/" + path; + testContext.getAssets().open(vpath); + result = vpath; + } + catch (IOException e) + { + // ignored + } } - return (double) diff / total / 2; - } + if (result == null) result = "views/" + path; - protected int dpToPixels(int dp) - { - return (int) UIHelper.dpToPixels(targetContext, dp); + return result; } - protected void tap(GestureDetector.OnGestureListener view, int x, int y) throws InterruptedException + private String saveBitmap(String filename, String suffix, Bitmap bitmap) + throws IOException { - long now = SystemClock.uptimeMillis(); - MotionEvent e = MotionEvent.obtain(now, now, MotionEvent.ACTION_UP, dpToPixels(x), - dpToPixels(y), 0); - view.onSingleTapUp(e); - e.recycle(); - } + File dir = FileUtils.getSDCardDir("test-screenshots"); + if (dir == null) dir = FileUtils.getFilesDir("test-screenshots"); + if (dir == null) throw new RuntimeException( + "Could not find suitable dir for screenshots"); - protected void refreshData(final HabitDataView view) - { - new BaseTask() - { - @Override - protected void doInBackground() - { - view.refreshData(); - } - }.execute(); + filename = filename.replaceAll("\\.png$", suffix + ".png"); + String absolutePath = + String.format("%s/%s", dir.getAbsolutePath(), filename); - try - { - waitForAsyncTasks(); - } - catch (Exception e) - { - throw new RuntimeException("Time out"); - } + File parent = new File(absolutePath).getParentFile(); + if (!parent.exists() && !parent.mkdirs()) throw new RuntimeException( + String.format("Could not create dir: %s", + parent.getAbsolutePath())); + + FileOutputStream out = new FileOutputStream(absolutePath); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); + + return absolutePath; } } diff --git a/app/src/androidTest/java/org/isoron/uhabits/HabitFixtures.java b/app/src/androidTest/java/org/isoron/uhabits/HabitFixtures.java new file mode 100644 index 000000000..6cf077513 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/HabitFixtures.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2016 Á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; + +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.DateUtils; + +public class HabitFixtures +{ + public boolean NON_DAILY_HABIT_CHECKS[] = { + true, false, false, true, true, true, false, false, true, true + }; + + private final HabitList habitList; + + public HabitFixtures(HabitList habitList) + { + this.habitList = habitList; + } + + public Habit createEmptyHabit() + { + Habit habit = new Habit(); + habit.setName("Meditate"); + habit.setDescription("Did you meditate this morning?"); + habit.setColor(3); + habit.setFrequency(Frequency.DAILY); + habitList.add(habit); + return habit; + } + + public Habit createLongHabit() + { + Habit habit = createEmptyHabit(); + habit.setFrequency(new Frequency(3, 7)); + habit.setColor(4); + + long day = DateUtils.millisecondsInOneDay; + long today = DateUtils.getStartOfToday(); + int marks[] = { 0, 1, 3, 5, 7, 8, 9, 10, 12, 14, 15, 17, 19, 20, 26, 27, + 28, 50, 51, 52, 53, 54, 58, 60, 63, 65, 70, 71, 72, 73, 74, 75, 80, + 81, 83, 89, 90, 91, 95, 102, 103, 108, 109, 120}; + + for (int mark : marks) + habit.getRepetitions().toggleTimestamp(today - mark * day); + + return habit; + } + + public Habit createShortHabit() + { + Habit habit = new Habit(); + habit.setName("Wake up early"); + habit.setDescription("Did you wake up before 6am?"); + habit.setFrequency(new Frequency(2, 3)); + habitList.add(habit); + + long timestamp = DateUtils.getStartOfToday(); + for (boolean c : NON_DAILY_HABIT_CHECKS) + { + if (c) habit.getRepetitions().toggleTimestamp(timestamp); + timestamp -= DateUtils.millisecondsInOneDay; + } + + return habit; + } + + public void purgeHabits(HabitList habitList) + { + for (Habit h : habitList.getAll(true)) + habitList.remove(h); + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/HabitsApplicationTest.java b/app/src/androidTest/java/org/isoron/uhabits/HabitsApplicationTest.java similarity index 66% rename from app/src/androidTest/java/org/isoron/uhabits/unit/HabitsApplicationTest.java rename to app/src/androidTest/java/org/isoron/uhabits/HabitsApplicationTest.java index 1c4f78ad8..3807cbcd4 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/HabitsApplicationTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/HabitsApplicationTest.java @@ -17,24 +17,24 @@ * with this program. If not, see . */ -package org.isoron.uhabits.unit; +package org.isoron.uhabits; -import android.os.Build; -import android.support.test.runner.AndroidJUnit4; -import android.test.suitebuilder.annotation.SmallTest; +import android.os.*; +import android.support.test.runner.*; +import android.test.suitebuilder.annotation.*; -import org.isoron.uhabits.HabitsApplication; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.isoron.uhabits.ui.*; +import org.junit.*; +import org.junit.runner.*; -import java.io.IOException; +import java.io.*; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; @RunWith(AndroidJUnit4.class) @SmallTest -public class HabitsApplicationTest +public class HabitsApplicationTest extends BaseAndroidTest { @Test public void test_getLogcat() throws IOException @@ -45,7 +45,11 @@ public class HabitsApplicationTest String msg = "LOGCAT TEST"; new RuntimeException(msg).printStackTrace(); - String log = HabitsApplication.getLogcat(); + HabitsApplication app = HabitsApplication.getInstance(); + assert(app != null); + + BaseSystem system = new BaseSystem(targetContext); + String log = system.getLogcat(); assertThat(log, containsString(msg)); } } diff --git a/app/src/androidTest/java/org/isoron/uhabits/ui/HabitMatchers.java b/app/src/androidTest/java/org/isoron/uhabits/espresso/HabitMatchers.java similarity index 96% rename from app/src/androidTest/java/org/isoron/uhabits/ui/HabitMatchers.java rename to app/src/androidTest/java/org/isoron/uhabits/espresso/HabitMatchers.java index ee8b810b8..b3fae7c1b 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/ui/HabitMatchers.java +++ b/app/src/androidTest/java/org/isoron/uhabits/espresso/HabitMatchers.java @@ -17,7 +17,7 @@ * with this program. If not, see . */ -package org.isoron.uhabits.ui; +package org.isoron.uhabits.espresso; import android.preference.Preference; import android.view.View; @@ -39,7 +39,7 @@ public class HabitMatchers @Override public boolean matchesSafely(Habit habit) { - return habit.name.equals(name); + return habit.getName().equals(name); } @Override @@ -51,7 +51,7 @@ public class HabitMatchers @Override public void describeMismatchSafely(Habit habit, Description description) { - description.appendText("was ").appendText(habit.name); + description.appendText("was ").appendText(habit.getName()); } }; } diff --git a/app/src/androidTest/java/org/isoron/uhabits/ui/HabitViewActions.java b/app/src/androidTest/java/org/isoron/uhabits/espresso/HabitViewActions.java similarity index 97% rename from app/src/androidTest/java/org/isoron/uhabits/ui/HabitViewActions.java rename to app/src/androidTest/java/org/isoron/uhabits/espresso/HabitViewActions.java index afa630ea0..156e3bea8 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/ui/HabitViewActions.java +++ b/app/src/androidTest/java/org/isoron/uhabits/espresso/HabitViewActions.java @@ -17,7 +17,7 @@ * with this program. If not, see . */ -package org.isoron.uhabits.ui; +package org.isoron.uhabits.espresso; import android.support.test.espresso.UiController; import android.support.test.espresso.ViewAction; @@ -61,7 +61,7 @@ public class HabitViewActions @Override public void perform(UiController uiController, View view) { - if (view.getId() != R.id.llButtons) + if (view.getId() != R.id.checkmarkPanel) throw new InvalidParameterException("View must have id llButtons"); LinearLayout llButtons = (LinearLayout) view; diff --git a/app/src/androidTest/java/org/isoron/uhabits/espresso/MainActivityActions.java b/app/src/androidTest/java/org/isoron/uhabits/espresso/MainActivityActions.java new file mode 100644 index 000000000..65059cebc --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/espresso/MainActivityActions.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2016 Á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.espresso; + +import android.support.test.espresso.*; +import android.support.test.espresso.contrib.*; + +import org.hamcrest.*; +import org.isoron.uhabits.R; +import org.isoron.uhabits.models.*; + +import java.util.*; + +import static android.support.test.espresso.Espresso.*; +import static android.support.test.espresso.Espresso.pressBack; +import static android.support.test.espresso.action.ViewActions.*; +import static android.support.test.espresso.assertion.ViewAssertions.*; +import static android.support.test.espresso.matcher.RootMatchers.*; +import static android.support.test.espresso.matcher.ViewMatchers.Visibility.*; +import static android.support.test.espresso.matcher.ViewMatchers.*; +import static org.hamcrest.Matchers.*; + +public class MainActivityActions +{ + public static String addHabit() + { + return addHabit(false); + } + + public static String addHabit(boolean openDialogs) + { + String name = "New Habit " + new Random().nextInt(1000000); + String description = "Did you perform your new habit today?"; + String num = "4"; + String den = "8"; + + onView(withId(R.id.action_add)).perform(click()); + + typeHabitData(name, description, num, den); + + if (openDialogs) + { + onView(withId(R.id.buttonPickColor)).perform(click()); + pressBack(); + onView(withId(R.id.tvReminderTime)).perform(click()); + onView(withText("Done")).perform(click()); + onView(withId(R.id.tvReminderDays)).perform(click()); + onView(withText("OK")).perform(click()); + } + + onView(withId(R.id.buttonSave)).perform(click()); + + onData(Matchers.allOf(is(instanceOf(Habit.class)), + HabitMatchers.withName(name))).onChildView(withId(R.id.label)); + + return name; + } + + public static void assertHabitExists(String name) + { + List names = new LinkedList<>(); + names.add(name); + assertHabitsExist(names); + } + + public static void assertHabitsDontExist(List names) + { + for (String name : names) + onView(withId(R.id.listView)).check(matches(Matchers.not( + HabitMatchers.containsHabit(HabitMatchers.withName(name))))); + } + + public static void assertHabitsExist(List names) + { + for (String name : names) + onData(Matchers.allOf(is(instanceOf(Habit.class)), + HabitMatchers.withName(name))).check(matches(isDisplayed())); + } + + private static void clickHiddenMenuItem(int stringId) + { + try + { + // Try the ActionMode overflow menu first + onView(allOf(withContentDescription("More options"), withParent( + withParent(withClassName(containsString("Action")))))).perform( + click()); + } + catch (Exception e1) + { + // Try the toolbar overflow menu + onView(allOf(withContentDescription("More options"), withParent( + withParent(withClassName(containsString("Toolbar")))))).perform( + click()); + } + + onView(withText(stringId)).perform(click()); + } + + public static void clickMenuItem(int stringId) + { + try + { + onView(withText(stringId)).perform(click()); + } + catch (Exception e1) + { + try + { + onView(withContentDescription(stringId)).perform(click()); + } + catch (Exception e2) + { + clickHiddenMenuItem(stringId); + } + } + } + + public static void clickSettingsItem(String text) + { + onView(withClassName(containsString("RecyclerView"))).perform( + RecyclerViewActions.actionOnItem( + hasDescendant(withText(containsString(text))), click())); + } + + public static void deleteHabit(String name) + { + deleteHabits(Collections.singletonList(name)); + } + + public static void deleteHabits(List names) + { + selectHabits(names); + clickMenuItem(R.string.delete); + onView(withText("OK")).perform(click()); + assertHabitsDontExist(names); + } + + public static void selectHabit(String name) + { + selectHabits(Collections.singletonList(name)); + } + + public static void selectHabits(List names) + { + boolean first = true; + for (String name : names) + { + onData(Matchers.allOf(is(instanceOf(Habit.class)), + HabitMatchers.withName(name))) + .onChildView(withId(R.id.label)) + .perform(first ? longClick() : click()); + + first = false; + } + } + + public static void typeHabitData(String name, + String description, + String num, + String den) + { + onView(withId(R.id.tvName)).perform(replaceText(name)); + onView(withId(R.id.tvDescription)).perform(replaceText(description)); + + try + { + onView(allOf(withId(R.id.sFrequency), + withEffectiveVisibility(VISIBLE))).perform(click()); + onData(allOf(instanceOf(String.class), startsWith("Custom"))) + .inRoot(isPlatformPopup()) + .perform(click()); + } + catch (NoMatchingViewException e) + { + // ignored + } + + onView(withId(R.id.tvFreqNum)).perform(replaceText(num)); + onView(withId(R.id.tvFreqDen)).perform(replaceText(den)); + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/espresso/MainTest.java b/app/src/androidTest/java/org/isoron/uhabits/espresso/MainTest.java new file mode 100644 index 000000000..420f90d36 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/espresso/MainTest.java @@ -0,0 +1,317 @@ +/* + * Copyright (C) 2016 Á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.espresso; + +import android.app.*; +import android.content.*; +import android.support.test.*; +import android.support.test.espresso.*; +import android.support.test.espresso.intent.rule.*; +import android.support.test.runner.*; +import android.test.suitebuilder.annotation.*; + +import org.hamcrest.*; +import org.isoron.uhabits.*; +import org.isoron.uhabits.R; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; +import org.junit.*; +import org.junit.runner.*; + +import java.util.*; + +import static android.support.test.espresso.Espresso.*; +import static android.support.test.espresso.Espresso.pressBack; +import static android.support.test.espresso.action.ViewActions.*; +import static android.support.test.espresso.assertion.ViewAssertions.*; +import static android.support.test.espresso.intent.Intents.*; +import static android.support.test.espresso.intent.matcher.IntentMatchers.*; +import static android.support.test.espresso.matcher.ViewMatchers.*; +import static org.hamcrest.Matchers.*; +import static org.isoron.uhabits.espresso.HabitViewActions.*; +import static org.isoron.uhabits.espresso.MainActivityActions.*; +import static org.isoron.uhabits.espresso.ShowHabitActivityActions.*; + +@RunWith(AndroidJUnit4.class) +@LargeTest +public class MainTest +{ + private SystemHelper sys; + + @Rule + public IntentsTestRule activityRule = + new IntentsTestRule<>(MainActivity.class); + + @Before + public void setup() + { + Context context = + InstrumentationRegistry.getInstrumentation().getContext(); + sys = new SystemHelper(context); + sys.disableAllAnimations(); + sys.acquireWakeLock(); + sys.unlockScreen(); + + Instrumentation.ActivityResult okResult = + new Instrumentation.ActivityResult(Activity.RESULT_OK, + new Intent()); + + intending(hasAction(equalTo(Intent.ACTION_SEND))).respondWith(okResult); + intending(hasAction(equalTo(Intent.ACTION_SENDTO))).respondWith( + okResult); + intending(hasAction(equalTo(Intent.ACTION_VIEW))).respondWith(okResult); + + skipTutorial(); + } + + public void skipTutorial() + { + try + { + for (int i = 0; i < 10; i++) + onView(allOf(withClassName(endsWith("AppCompatImageButton")), + isDisplayed())).perform(click()); + } + catch (NoMatchingViewException e) + { + // ignored + } + } + + @After + public void tearDown() + { + sys.releaseWakeLock(); + } + + /** + * User opens menu, clicks about, sees about screen. + */ + @Test + public void testAbout() + { + clickMenuItem(R.string.about); + onView(isRoot()).perform(swipeUp()); + } + + /** + * User creates a habit, toggles a bunch of checkmarks, clicks the habit to + * open the statistics screen, scrolls down to some views, then scrolls the + * views backwards and forwards in time. + */ + @Test + public void testAddHabitAndViewStats() throws InterruptedException + { + String name = addHabit(true); + + onData(Matchers.allOf(is(instanceOf(Habit.class)), + HabitMatchers.withName(name))) + .onChildView(withId(R.id.checkmarkPanel)) + .perform(toggleAllCheckmarks()); + + Thread.sleep(1200); + + onData(Matchers.allOf(is(instanceOf(Habit.class)), + HabitMatchers.withName(name))) + .onChildView(withId(R.id.label)) + .perform(click()); + + onView(withId(R.id.scoreView)).perform(scrollTo(), swipeRight()); + + onView(withId(R.id.frequencyChart)).perform(scrollTo(), swipeRight()); + } + + /** + * User opens the app, clicks the add button, types some bogus information, + * tries to save, dialog displays an error. + */ + @Test + public void testAddInvalidHabit() + { + onView(withId(R.id.action_add)).perform(click()); + + typeHabitData("", "", "15", "7"); + + onView(withId(R.id.buttonSave)).perform(click()); + onView(withId(R.id.tvName)).check(matches(isDisplayed())); + } + + /** + * User opens the app, creates some habits, selects them, archives them, + * select 'show archived' on the menu, selects the previously archived + * habits and then deletes them. + */ + @Test + public void testArchiveHabits() + { + List names = new LinkedList<>(); + + for (int i = 0; i < 3; i++) + names.add(addHabit()); + + selectHabits(names); + + clickMenuItem(R.string.archive); + assertHabitsDontExist(names); + + clickMenuItem(R.string.show_archived); + + assertHabitsExist(names); + selectHabits(names); + clickMenuItem(R.string.unarchive); + + clickMenuItem(R.string.show_archived); + + assertHabitsExist(names); + deleteHabits(names); + } + + /** + * User creates a habit, selects the habit, clicks edit button, changes some + * information about the habit, click save button, sees changes on the main + * window, selects habit again, changes color, then deletes the habit. + */ + @Test + public void testEditHabit() + { + String name = addHabit(); + + onData(Matchers.allOf(is(instanceOf(Habit.class)), + HabitMatchers.withName(name))) + .onChildView(withId(R.id.label)) + .perform(longClick()); + + clickMenuItem(R.string.edit); + + String modifiedName = "Modified " + new Random().nextInt(10000); + typeHabitData(modifiedName, "", "1", "1"); + + onView(withId(R.id.buttonSave)).perform(click()); + + assertHabitExists(modifiedName); + + selectHabit(modifiedName); + clickMenuItem(R.string.color_picker_default_title); + pressBack(); + + deleteHabit(modifiedName); + } + + /** + * User creates a habit, opens statistics page, clicks button to edit + * history, adds some checkmarks, closes dialog, sees the modified history + * calendar. + */ + @Test + public void testEditHistory() + { + String name = addHabit(); + + onData(Matchers.allOf(is(instanceOf(Habit.class)), + HabitMatchers.withName(name))) + .onChildView(withId(R.id.label)) + .perform(click()); + + openHistoryEditor(); + onView(withClassName(endsWith("HabitHistoryView"))).perform( + clickAtRandomLocations(20)); + + pressBack(); + onView(withId(R.id.historyChart)).perform(scrollTo(), swipeRight(), + swipeLeft()); + } + + /** + * User creates a habit, opens settings, clicks export as CSV, is asked what + * activity should handle the file. + */ + @Test + public void testExportCSV() + { + addHabit(); + clickMenuItem(R.string.settings); + clickSettingsItem("Export as CSV"); + intended(hasAction(Intent.ACTION_SEND)); + } + + /** + * User creates a habit, exports full backup, deletes the habit, restores + * backup, sees that the previously created habit has appeared back. + */ + @Test + public void testExportImportDB() + { + String name = addHabit(); + + clickMenuItem(R.string.settings); + + String date = + DateUtils.getBackupDateFormat().format(DateUtils.getLocalTime()); + date = date.substring(0, date.length() - 2); + + clickSettingsItem("Export full backup"); + intended(hasAction(Intent.ACTION_SEND)); + + deleteHabit(name); + + clickMenuItem(R.string.settings); + clickSettingsItem("Import data"); + + onData( + allOf(is(instanceOf(String.class)), startsWith("Backups"))).perform( + click()); + + onData( + allOf(is(instanceOf(String.class)), containsString(date))).perform( + click()); + + selectHabit(name); + } + + /** + * User opens the settings and generates a bug report. + */ + @Test + public void testGenerateBugReport() + { + clickMenuItem(R.string.settings); + clickSettingsItem("Generate bug report"); + intended(hasAction(Intent.ACTION_SEND)); + } + + /** + * User opens menu, clicks Help, sees website. + */ + @Test + public void testHelp() + { + clickMenuItem(R.string.help); + intended(hasAction(Intent.ACTION_VIEW)); + } + + /** + * User opens menu, clicks settings, sees settings screen. + */ + @Test + public void testSettings() + { + clickMenuItem(R.string.settings); + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/ui/ShowHabitActivityActions.java b/app/src/androidTest/java/org/isoron/uhabits/espresso/ShowHabitActivityActions.java similarity index 93% rename from app/src/androidTest/java/org/isoron/uhabits/ui/ShowHabitActivityActions.java rename to app/src/androidTest/java/org/isoron/uhabits/espresso/ShowHabitActivityActions.java index 31a89c397..6475b0890 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/ui/ShowHabitActivityActions.java +++ b/app/src/androidTest/java/org/isoron/uhabits/espresso/ShowHabitActivityActions.java @@ -17,7 +17,7 @@ * with this program. If not, see . */ -package org.isoron.uhabits.ui; +package org.isoron.uhabits.espresso; import android.support.test.espresso.matcher.ViewMatchers; @@ -31,7 +31,7 @@ public class ShowHabitActivityActions { public static void openHistoryEditor() { - onView(ViewMatchers.withId(R.id.btEditHistory)) + onView(ViewMatchers.withId(R.id.edit)) .perform(scrollTo(), click()); } } diff --git a/app/src/androidTest/java/org/isoron/uhabits/ui/SystemHelper.java b/app/src/androidTest/java/org/isoron/uhabits/espresso/SystemHelper.java similarity index 82% rename from app/src/androidTest/java/org/isoron/uhabits/ui/SystemHelper.java rename to app/src/androidTest/java/org/isoron/uhabits/espresso/SystemHelper.java index 807e3b36c..6fb12fede 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/ui/SystemHelper.java +++ b/app/src/androidTest/java/org/isoron/uhabits/espresso/SystemHelper.java @@ -1,4 +1,23 @@ -package org.isoron.uhabits.ui; +/* + * Copyright (C) 2016 Á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.espresso; import android.app.KeyguardManager; import android.content.Context; diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/io/HabitsCSVExporterTest.java b/app/src/androidTest/java/org/isoron/uhabits/io/HabitsCSVExporterTest.java similarity index 75% rename from app/src/androidTest/java/org/isoron/uhabits/unit/io/HabitsCSVExporterTest.java rename to app/src/androidTest/java/org/isoron/uhabits/io/HabitsCSVExporterTest.java index 2091d7aa3..5cda1ed99 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/io/HabitsCSVExporterTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/io/HabitsCSVExporterTest.java @@ -17,18 +17,16 @@ * with this program. If not, see . */ -package org.isoron.uhabits.unit.io; +package org.isoron.uhabits.io; import android.content.Context; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; -import org.isoron.uhabits.BaseTest; -import org.isoron.uhabits.helpers.DatabaseHelper; -import org.isoron.uhabits.io.HabitsCSVExporter; +import org.isoron.uhabits.BaseAndroidTest; import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.unit.HabitFixtures; +import org.isoron.uhabits.utils.FileUtils; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -45,50 +43,27 @@ import static junit.framework.Assert.assertTrue; @RunWith(AndroidJUnit4.class) @SmallTest -public class HabitsCSVExporterTest extends BaseTest +public class HabitsCSVExporterTest extends BaseAndroidTest { private File baseDir; @Before - public void setup() + public void setUp() { - super.setup(); + super.setUp(); - HabitFixtures.purgeHabits(); - HabitFixtures.createShortHabit(); - HabitFixtures.createEmptyHabit(); + fixtures.purgeHabits(habitList); + fixtures.createShortHabit(); + fixtures.createEmptyHabit(); Context targetContext = InstrumentationRegistry.getTargetContext(); baseDir = targetContext.getCacheDir(); } - private void unzip(File file) throws IOException - { - ZipFile zip = new ZipFile(file); - Enumeration e = zip.entries(); - - while(e.hasMoreElements()) - { - ZipEntry entry = e.nextElement(); - InputStream stream = zip.getInputStream(entry); - - String outputFilename = String.format("%s/%s", baseDir.getAbsolutePath(), - entry.getName()); - File outputFile = new File(outputFilename); - - File parent = outputFile.getParentFile(); - if(parent != null) parent.mkdirs(); - - DatabaseHelper.copy(stream, outputFile); - } - - zip.close(); - } - @Test public void testExportCSV() throws IOException { - List habits = Habit.getAll(true); + List habits = habitList.getAll(true); HabitsCSVExporter exporter = new HabitsCSVExporter(habits, baseDir); String filename = exporter.writeArchive(); @@ -105,14 +80,41 @@ public class HabitsCSVExporterTest extends BaseTest assertPathExists("002 Meditate/Scores.csv"); } + private void assertAbsolutePathExists(String s) + { + File file = new File(s); + assertTrue( + String.format("File %s should exist", file.getAbsolutePath()), + file.exists()); + } + private void assertPathExists(String s) { - assertAbsolutePathExists(String.format("%s/%s", baseDir.getAbsolutePath(), s)); + assertAbsolutePathExists( + String.format("%s/%s", baseDir.getAbsolutePath(), s)); } - private void assertAbsolutePathExists(String s) + private void unzip(File file) throws IOException { - File file = new File(s); - assertTrue(String.format("File %s should exist", file.getAbsolutePath()), file.exists()); + ZipFile zip = new ZipFile(file); + Enumeration e = zip.entries(); + + while (e.hasMoreElements()) + { + ZipEntry entry = e.nextElement(); + InputStream stream = zip.getInputStream(entry); + + String outputFilename = + String.format("%s/%s", baseDir.getAbsolutePath(), + entry.getName()); + File outputFile = new File(outputFilename); + + File parent = outputFile.getParentFile(); + if (parent != null) parent.mkdirs(); + + FileUtils.copy(stream, outputFile); + } + + zip.close(); } } diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/io/ImportTest.java b/app/src/androidTest/java/org/isoron/uhabits/io/ImportTest.java similarity index 55% rename from app/src/androidTest/java/org/isoron/uhabits/unit/io/ImportTest.java rename to app/src/androidTest/java/org/isoron/uhabits/io/ImportTest.java index 8f6f3fbe0..e19b035ff 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/io/ImportTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/io/ImportTest.java @@ -17,95 +17,76 @@ * with this program. If not, see . */ -package org.isoron.uhabits.unit.io; - -import android.content.Context; -import android.support.test.InstrumentationRegistry; -import android.support.test.runner.AndroidJUnit4; -import android.test.suitebuilder.annotation.SmallTest; - -import org.isoron.uhabits.BaseTest; -import org.isoron.uhabits.helpers.DatabaseHelper; -import org.isoron.uhabits.helpers.DateHelper; -import org.isoron.uhabits.io.GenericImporter; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.unit.HabitFixtures; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.util.GregorianCalendar; -import java.util.List; - -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +package org.isoron.uhabits.io; + +import android.content.*; +import android.support.test.*; +import android.support.test.runner.*; +import android.test.suitebuilder.annotation.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; +import org.junit.*; +import org.junit.runner.*; + +import java.io.*; +import java.util.*; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; @RunWith(AndroidJUnit4.class) @SmallTest -public class ImportTest extends BaseTest +public class ImportTest extends BaseAndroidTest { private File baseDir; + private Context context; @Before - public void setup() + public void setUp() { - super.setup(); - DateHelper.setFixedLocalTime(null); + super.setUp(); + DateUtils.setFixedLocalTime(null); - HabitFixtures.purgeHabits(); + fixtures.purgeHabits(habitList); context = InstrumentationRegistry.getInstrumentation().getContext(); - baseDir = DatabaseHelper.getFilesDir("Backups"); - if(baseDir == null) fail("baseDir should not be null"); + baseDir = FileUtils.getFilesDir("Backups"); + if (baseDir == null) fail("baseDir should not be null"); } - private void copyAssetToFile(String assetPath, File dst) throws IOException - { - InputStream in = context.getAssets().open(assetPath); - DatabaseHelper.copy(in, dst); - } - - private void importFromFile(String assetFilename) throws IOException + @Test + public void testHabitBullCSV() throws IOException { - File file = new File(String.format("%s/%s", baseDir.getPath(), assetFilename)); - copyAssetToFile(assetFilename, file); - assertTrue(file.exists()); - assertTrue(file.canRead()); - - GenericImporter importer = new GenericImporter(); - assertThat(importer.canHandle(file), is(true)); + importFromFile("habitbull.csv"); - importer.importHabitsFromFile(file); - } + List habits = habitList.getAll(true); + assertThat(habits.size(), equalTo(4)); - private boolean containsRepetition(Habit h, int year, int month, int day) - { - GregorianCalendar date = DateHelper.getStartOfTodayCalendar(); - date.set(year, month - 1, day); - return h.repetitions.contains(date.getTimeInMillis()); + Habit habit = habits.get(0); + assertThat(habit.getName(), equalTo("Breed dragons")); + assertThat(habit.getDescription(), equalTo("with love and fire")); + assertThat(habit.getFrequency(), equalTo(Frequency.DAILY)); + assertTrue(containsRepetition(habit, 2016, 3, 18)); + assertTrue(containsRepetition(habit, 2016, 3, 19)); + assertFalse(containsRepetition(habit, 2016, 3, 20)); } @Test - public void testTickmateDB() throws IOException + public void testLoopDB() throws IOException { - importFromFile("tickmate.db"); + importFromFile("loop.db"); - List habits = Habit.getAll(true); - assertThat(habits.size(), equalTo(3)); + List habits = habitList.getAll(true); + assertThat(habits.size(), equalTo(9)); - Habit h = habits.get(0); - assertThat(h.name, equalTo("Vegan")); - assertTrue(containsRepetition(h, 2016, 1, 24)); - assertTrue(containsRepetition(h, 2016, 2, 5)); - assertTrue(containsRepetition(h, 2016, 3, 18)); - assertFalse(containsRepetition(h, 2016, 3, 14)); + Habit habit = habits.get(0); + assertThat(habit.getName(), equalTo("Wake up early")); + assertThat(habit.getFrequency(), equalTo(Frequency.THREE_TIMES_PER_WEEK)); + assertTrue(containsRepetition(habit, 2016, 3, 14)); + assertTrue(containsRepetition(habit, 2016, 3, 16)); + assertFalse(containsRepetition(habit, 2016, 3, 17)); } @Test @@ -113,13 +94,13 @@ public class ImportTest extends BaseTest { importFromFile("rewire.db"); - List habits = Habit.getAll(true); + List habits = habitList.getAll(true); assertThat(habits.size(), equalTo(3)); Habit habit = habits.get(0); - assertThat(habit.name, equalTo("Wake up early")); - assertThat(habit.freqNum, equalTo(3)); - assertThat(habit.freqDen, equalTo(7)); + assertThat(habit.getName(), equalTo("Wake up early")); + assertThat(habit.getFrequency(), + equalTo(Frequency.THREE_TIMES_PER_WEEK)); assertFalse(habit.hasReminder()); assertFalse(containsRepetition(habit, 2015, 12, 31)); assertTrue(containsRepetition(habit, 2016, 1, 18)); @@ -127,47 +108,59 @@ public class ImportTest extends BaseTest assertFalse(containsRepetition(habit, 2016, 3, 10)); habit = habits.get(1); - assertThat(habit.name, equalTo("brush teeth")); - assertThat(habit.freqNum, equalTo(3)); - assertThat(habit.freqDen, equalTo(7)); - assertThat(habit.reminderHour, equalTo(8)); - assertThat(habit.reminderMin, equalTo(0)); - boolean[] reminderDays = {false, true, true, true, true, true, false}; - assertThat(habit.reminderDays, equalTo(DateHelper.packWeekdayList(reminderDays))); + assertThat(habit.getName(), equalTo("brush teeth")); + assertThat(habit.getFrequency(), + equalTo(Frequency.THREE_TIMES_PER_WEEK)); + assertThat(habit.hasReminder(), equalTo(true)); + + Reminder reminder = habit.getReminder(); + assertThat(reminder.getHour(), equalTo(8)); + assertThat(reminder.getMinute(), equalTo(0)); + boolean[] reminderDays = { false, true, true, true, true, true, false }; + assertThat(reminder.getDays(), + equalTo(DateUtils.packWeekdayList(reminderDays))); } @Test - public void testHabitBullCSV() throws IOException + public void testTickmateDB() throws IOException { - importFromFile("habitbull.csv"); + importFromFile("tickmate.db"); - List habits = Habit.getAll(true); - assertThat(habits.size(), equalTo(4)); + List habits = habitList.getAll(true); + assertThat(habits.size(), equalTo(3)); - Habit habit = habits.get(0); - assertThat(habit.name, equalTo("Breed dragons")); - assertThat(habit.description, equalTo("with love and fire")); - assertThat(habit.freqNum, equalTo(1)); - assertThat(habit.freqDen, equalTo(1)); - assertTrue(containsRepetition(habit, 2016, 3, 18)); - assertTrue(containsRepetition(habit, 2016, 3, 19)); - assertFalse(containsRepetition(habit, 2016, 3, 20)); + Habit h = habits.get(0); + assertThat(h.getName(), equalTo("Vegan")); + assertTrue(containsRepetition(h, 2016, 1, 24)); + assertTrue(containsRepetition(h, 2016, 2, 5)); + assertTrue(containsRepetition(h, 2016, 3, 18)); + assertFalse(containsRepetition(h, 2016, 3, 14)); } - @Test - public void testLoopDB() throws IOException + private boolean containsRepetition(Habit h, int year, int month, int day) { - importFromFile("loop.db"); + GregorianCalendar date = DateUtils.getStartOfTodayCalendar(); + date.set(year, month - 1, day); + return h.getRepetitions().containsTimestamp(date.getTimeInMillis()); + } - List habits = Habit.getAll(true); - assertThat(habits.size(), equalTo(9)); + private void copyAssetToFile(String assetPath, File dst) throws IOException + { + InputStream in = context.getAssets().open(assetPath); + FileUtils.copy(in, dst); + } - Habit habit = habits.get(0); - assertThat(habit.name, equalTo("Wake up early")); - assertThat(habit.freqNum, equalTo(3)); - assertThat(habit.freqDen, equalTo(7)); - assertTrue(containsRepetition(habit, 2016, 3, 14)); - assertTrue(containsRepetition(habit, 2016, 3, 16)); - assertFalse(containsRepetition(habit, 2016, 3, 17)); + private void importFromFile(String assetFilename) throws IOException + { + File file = + new File(String.format("%s/%s", baseDir.getPath(), assetFilename)); + copyAssetToFile(assetFilename, file); + assertTrue(file.exists()); + assertTrue(file.canRead()); + + GenericImporter importer = new GenericImporter(); + assertThat(importer.canHandle(file), is(true)); + + importer.importHabitsFromFile(file); } } diff --git a/app/src/androidTest/java/org/isoron/uhabits/models/sqlite/SQLiteCheckmarkListTest.java b/app/src/androidTest/java/org/isoron/uhabits/models/sqlite/SQLiteCheckmarkListTest.java new file mode 100644 index 000000000..d66e3bc12 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/models/sqlite/SQLiteCheckmarkListTest.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2016 Á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.models.sqlite; + +import android.support.test.runner.*; +import android.test.suitebuilder.annotation.*; + +import com.activeandroid.query.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.models.sqlite.records.*; +import org.isoron.uhabits.utils.*; +import org.junit.*; +import org.junit.runner.*; + +import java.util.*; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +@RunWith(AndroidJUnit4.class) +@MediumTest +public class SQLiteCheckmarkListTest extends BaseAndroidTest +{ + private Habit habit; + + private CheckmarkList checkmarks; + + private long today; + + private long day; + + @Override + public void setUp() + { + super.setUp(); + + habit = fixtures.createLongHabit(); + checkmarks = habit.getCheckmarks(); + checkmarks.getToday(); // compute checkmarks + + today = DateUtils.getStartOfToday(); + day = DateUtils.millisecondsInOneDay; + } + + @Test + public void testAdd() + { + checkmarks.invalidateNewerThan(0); + + List list = new LinkedList<>(); + list.add(new Checkmark(0, 0)); + list.add(new Checkmark(1, 1)); + list.add(new Checkmark(2, 2)); + + checkmarks.add(list); + + List records = getAllRecords(); + assertThat(records.size(), equalTo(3)); + assertThat(records.get(0).timestamp, equalTo(2L)); + } + + @Test + public void testGetByInterval() + { + long from = today - 10 * day; + long to = today - 3 * day; + + List list = checkmarks.getByInterval(from, to); + assertThat(list.size(), equalTo(8)); + + assertThat(list.get(0).getTimestamp(), equalTo(today - 3 * day)); + assertThat(list.get(3).getTimestamp(), equalTo(today - 6 * day)); + assertThat(list.get(7).getTimestamp(), equalTo(today - 10 * day)); + } + + @Test + public void testInvalidateNewerThan() + { + List records = getAllRecords(); + assertThat(records.size(), equalTo(121)); + + checkmarks.invalidateNewerThan(today - 20 * day); + + records = getAllRecords(); + assertThat(records.size(), equalTo(100)); + assertThat(records.get(0).timestamp, equalTo(today - 21 * day)); + } + + private List getAllRecords() + { + return new Select() + .from(CheckmarkRecord.class) + .where("habit = ?", habit.getId()) + .orderBy("timestamp desc") + .execute(); + } + +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/models/sqlite/SQLiteHabitListTest.java b/app/src/androidTest/java/org/isoron/uhabits/models/sqlite/SQLiteHabitListTest.java new file mode 100644 index 000000000..cf8693a60 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/models/sqlite/SQLiteHabitListTest.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2016 Á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.models.sqlite; + +import android.support.test.runner.*; +import android.test.suitebuilder.annotation.*; + +import com.activeandroid.query.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.models.sqlite.records.*; +import org.junit.*; +import org.junit.rules.*; +import org.junit.runner.*; + +import java.util.*; + +import static junit.framework.Assert.*; +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.core.IsEqual.*; + +@SuppressWarnings("JavaDoc") +@RunWith(AndroidJUnit4.class) +@MediumTest +public class SQLiteHabitListTest extends BaseAndroidTest +{ + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Override + public void setUp() + { + super.setUp(); + fixtures.purgeHabits(habitList); + + for (int i = 0; i < 10; i++) + { + Habit h = new Habit(); + h.setName("habit " + i); + h.setId((long) i); + if (i % 2 == 0) h.setArchived(true); + + HabitRecord record = new HabitRecord(); + record.copyFrom(h); + record.position = i; + record.save(i); + } + } + + @Test + public void testAdd_withDuplicate() + { + Habit habit = new Habit(); + habitList.add(habit); + exception.expect(IllegalArgumentException.class); + habitList.add(habit); + } + + @Test + public void testAdd_withId() + { + Habit habit = new Habit(); + habit.setName("Hello world with id"); + habit.setId(12300L); + + habitList.add(habit); + assertThat(habit.getId(), equalTo(12300L)); + + HabitRecord record = getRecord(12300L); + assertNotNull(record); + assertThat(record.name, equalTo(habit.getName())); + } + + @Test + public void testAdd_withoutId() + { + Habit habit = new Habit(); + habit.setName("Hello world"); + assertNull(habit.getId()); + + habitList.add(habit); + assertNotNull(habit.getId()); + + HabitRecord record = getRecord(habit.getId()); + assertNotNull(record); + assertThat(record.name, equalTo(habit.getName())); + } + + @Test + public void testCountActive() + { + assertThat(habitList.countActive(), equalTo(5)); + } + + @Test + public void testCountWithArchived() + { + assertThat(habitList.countWithArchived(), equalTo(10)); + } + + @Test + public void testGetAll_withArchived() + { + List habits = habitList.getAll(true); + assertThat(habits.size(), equalTo(10)); + assertThat(habits.get(3).getName(), equalTo("habit 3")); + } + + @Test + public void testGetAll_withoutArchived() + { + List habits = habitList.getAll(false); + assertThat(habits.size(), equalTo(5)); + assertThat(habits.get(3).getName(), equalTo("habit 7")); + + List another = habitList.getAll(false); + assertThat(habits, equalTo(another)); + } + + @Test + public void testGetById() + { + Habit h1 = habitList.getById(0); + assertNotNull(h1); + assertThat(h1.getName(), equalTo("habit 0")); + + Habit h2 = habitList.getById(0); + assertNotNull(h2); + assertThat(h1, equalTo(h2)); + } + + @Test + public void testGetById_withInvalid() + { + long invalidId = 9183792001L; + Habit h1 = habitList.getById(invalidId); + assertNull(h1); + } + + @Test + public void testGetByPosition() + { + Habit h = habitList.getByPosition(5); + assertNotNull(h); + assertThat(h.getName(), equalTo("habit 5")); + + h = habitList.getByPosition(5000); + assertNull(h); + } + + @Test + public void testIndexOf() + { + Habit h1 = habitList.getByPosition(5); + assertNotNull(h1); + assertThat(habitList.indexOf(h1), equalTo(5)); + + Habit h2 = new Habit(); + assertThat(habitList.indexOf(h2), equalTo(-1)); + + h2.setId(1000L); + assertThat(habitList.indexOf(h2), equalTo(-1)); + } + + @Test + public void test_reorder() + { + // Same as HabitListTest.java + // TODO: remove duplication + + int operations[][] = { + {5, 2}, {3, 7}, {4, 4}, {3, 2} + }; + + int expectedPosition[][] = { + {0, 1, 3, 4, 5, 2, 6, 7, 8, 9}, + {0, 1, 7, 3, 4, 2, 5, 6, 8, 9}, + {0, 1, 7, 3, 4, 2, 5, 6, 8, 9}, + {0, 1, 7, 2, 4, 3, 5, 6, 8, 9}, + }; + + for (int i = 0; i < operations.length; i++) + { + int from = operations[i][0]; + int to = operations[i][1]; + + Habit fromHabit = habitList.getByPosition(from); + Habit toHabit = habitList.getByPosition(to); + habitList.reorder(fromHabit, toHabit); + + int actualPositions[] = new int[10]; + + for (int j = 0; j < 10; j++) + { + Habit h = habitList.getById(j); + assertNotNull(h); + actualPositions[j] = habitList.indexOf(h); + } + + assertThat(actualPositions, equalTo(expectedPosition[i])); + } + } + + private HabitRecord getRecord(long id) + { + return new Select() + .from(HabitRecord.class) + .where("id = ?", id) + .executeSingle(); + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/org/isoron/uhabits/models/sqlite/SQLiteRepetitionListTest.java b/app/src/androidTest/java/org/isoron/uhabits/models/sqlite/SQLiteRepetitionListTest.java new file mode 100644 index 000000000..992c1e673 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/models/sqlite/SQLiteRepetitionListTest.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2016 Á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.models.sqlite; + +import android.support.annotation.*; +import android.support.test.runner.*; +import android.test.suitebuilder.annotation.*; + +import com.activeandroid.query.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.models.sqlite.records.*; +import org.isoron.uhabits.utils.*; +import org.junit.*; +import org.junit.runner.*; + +import java.util.*; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; +import static org.hamcrest.core.IsNot.not; + +@RunWith(AndroidJUnit4.class) +@MediumTest +public class SQLiteRepetitionListTest extends BaseAndroidTest +{ + private Habit habit; + + private long today; + + private RepetitionList repetitions; + + private long day; + + @Override + public void setUp() + { + super.setUp(); + + habit = fixtures.createLongHabit(); + repetitions = habit.getRepetitions(); + today = DateUtils.getStartOfToday(); + day = DateUtils.millisecondsInOneDay; + } + + @Test + public void testAdd() + { + RepetitionRecord record = getByTimestamp(today + day); + assertThat(record, is(nullValue())); + + Repetition rep = new Repetition(today + day); + habit.getRepetitions().add(rep); + + record = getByTimestamp(today + day); + assertThat(record, is(not(nullValue()))); + } + + @Test + public void testGetByInterval() + { + List reps = + repetitions.getByInterval(today - 10 * day, today); + + assertThat(reps.size(), equalTo(8)); + assertThat(reps.get(0).getTimestamp(), equalTo(today - 10 * day)); + assertThat(reps.get(4).getTimestamp(), equalTo(today - 5 * day)); + assertThat(reps.get(5).getTimestamp(), equalTo(today - 3 * day)); + } + + @Test + public void testGetByTimestamp() + { + Repetition rep = repetitions.getByTimestamp(today); + assertThat(rep, is(not(nullValue()))); + assertThat(rep.getTimestamp(), equalTo(today)); + + rep = repetitions.getByTimestamp(today - 2 * day); + assertThat(rep, is(nullValue())); + } + + @Test + public void testGetOldest() + { + Repetition rep = repetitions.getOldest(); + assertThat(rep, is(not(nullValue()))); + assertThat(rep.getTimestamp(), equalTo(today - 120 * day)); + } + + @Test + public void testGetOldest_withEmptyHabit() + { + Habit empty = fixtures.createEmptyHabit(); + Repetition rep = empty.getRepetitions().getOldest(); + assertThat(rep, is(nullValue())); + } + + @Test + public void testRemove() + { + RepetitionRecord record = getByTimestamp(today); + assertThat(record, is(not(nullValue()))); + + Repetition rep = record.toRepetition(); + repetitions.remove(rep); + + record = getByTimestamp(today); + assertThat(record, is(nullValue())); + } + + @Nullable + private RepetitionRecord getByTimestamp(long timestamp) + { + return selectByTimestamp(timestamp).executeSingle(); + } + + @NonNull + private From selectByTimestamp(long timestamp) + { + return new Select() + .from(RepetitionRecord.class) + .where("habit = ?", habit.getId()) + .and("timestamp = ?", timestamp); + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/models/sqlite/SQLiteScoreListTest.java b/app/src/androidTest/java/org/isoron/uhabits/models/sqlite/SQLiteScoreListTest.java new file mode 100644 index 000000000..1e05d9ecc --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/models/sqlite/SQLiteScoreListTest.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2016 Á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.models.sqlite; + +import android.support.test.runner.*; +import android.test.suitebuilder.annotation.*; + +import com.activeandroid.query.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.models.sqlite.records.*; +import org.isoron.uhabits.utils.*; +import org.junit.*; +import org.junit.runner.*; + +import java.util.*; + +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertNull; +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +@SuppressWarnings("JavaDoc") +@RunWith(AndroidJUnit4.class) +@MediumTest +public class SQLiteScoreListTest extends BaseAndroidTest +{ + private Habit habit; + + private ScoreList scores; + + private long today; + + private long day; + + @Override + public void setUp() + { + super.setUp(); + + habit = fixtures.createLongHabit(); + scores = habit.getScores(); + + today = DateUtils.getStartOfToday(); + day = DateUtils.millisecondsInOneDay; + } + + @Test + public void testGetAll() + { + List list = scores.getAll(); + assertThat(list.size(), equalTo(121)); + assertThat(list.get(0).getTimestamp(), equalTo(today)); + assertThat(list.get(10).getTimestamp(), equalTo(today - 10 * day)); + } + + @Test + public void testInvalidateNewerThan() + { + scores.getTodayValue(); // force recompute + List records = getAllRecords(); + assertThat(records.size(), equalTo(121)); + + scores.invalidateNewerThan(today - 10 * day); + + records = getAllRecords(); + assertThat(records.size(), equalTo(110)); + assertThat(records.get(0).timestamp, equalTo(today - 11 * day)); + } + + @Test + public void testAdd() + { + new Delete().from(ScoreRecord.class).execute(); + + List list = new LinkedList<>(); + list.add(new Score(today, 0)); + list.add(new Score(today - day, 0)); + list.add(new Score(today - 2 * day, 0)); + + scores.add(list); + + List records = getAllRecords(); + assertThat(records.size(), equalTo(3)); + assertThat(records.get(0).timestamp, equalTo(today)); + } + + @Test + public void testGetByTimestamp() + { + Score s = scores.getByTimestamp(today); + assertNotNull(s); + assertThat(s.getTimestamp(), equalTo(today)); + + s = scores.getByTimestamp(today - 200 * day); + assertNull(s); + } + + private List getAllRecords() + { + return new Select() + .from(ScoreRecord.class) + .where("habit = ?", habit.getId()) + .orderBy("timestamp desc") + .execute(); + } + +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/tasks/ExportCSVTaskTest.java b/app/src/androidTest/java/org/isoron/uhabits/tasks/ExportCSVTaskTest.java new file mode 100644 index 000000000..68b1caa46 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/tasks/ExportCSVTaskTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2016 Á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.tasks; + +import android.support.test.runner.*; +import android.test.suitebuilder.annotation.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.junit.*; +import org.junit.runner.*; + +import java.io.*; +import java.util.*; + +import static junit.framework.Assert.*; +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; +import static org.hamcrest.core.IsNot.not; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class ExportCSVTaskTest extends BaseAndroidTest +{ + @Before + public void setUp() + { + super.setUp(); + } + + @Test + public void testExportCSV() throws Throwable + { + fixtures.createShortHabit(); + List habits = habitList.getAll(true); + + ExportCSVTask task = new ExportCSVTask(habits, null); + task.setListener(archiveFilename -> { + assertThat(archiveFilename, is(not(nullValue()))); + + File f = new File(archiveFilename); + assertTrue(f.exists()); + assertTrue(f.canRead()); + }); + + task.execute(); + waitForAsyncTasks(); + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/tasks/ExportDBTaskTest.java b/app/src/androidTest/java/org/isoron/uhabits/tasks/ExportDBTaskTest.java similarity index 78% rename from app/src/androidTest/java/org/isoron/uhabits/unit/tasks/ExportDBTaskTest.java rename to app/src/androidTest/java/org/isoron/uhabits/tasks/ExportDBTaskTest.java index 26269e353..d7b480f4c 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/tasks/ExportDBTaskTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/tasks/ExportDBTaskTest.java @@ -17,16 +17,12 @@ * with this program. If not, see . */ -package org.isoron.uhabits.unit.tasks; +package org.isoron.uhabits.tasks; -import android.content.Context; -import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; -import android.widget.ProgressBar; -import org.isoron.uhabits.BaseTest; -import org.isoron.uhabits.tasks.ExportDBTask; +import org.isoron.uhabits.BaseAndroidTest; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -41,21 +37,18 @@ import static org.hamcrest.core.IsNot.not; @RunWith(AndroidJUnit4.class) @SmallTest -public class ExportDBTaskTest extends BaseTest +public class ExportDBTaskTest extends BaseAndroidTest { @Before - public void setup() + public void setUp() { - super.setup(); + super.setUp(); } @Test public void testExportCSV() throws Throwable { - Context context = InstrumentationRegistry.getContext(); - - ProgressBar bar = new ProgressBar(context); - ExportDBTask task = new ExportDBTask(bar); + ExportDBTask task = new ExportDBTask(null); task.setListener(new ExportDBTask.Listener() { @Override diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/tasks/ImportDataTaskTest.java b/app/src/androidTest/java/org/isoron/uhabits/tasks/ImportDataTaskTest.java similarity index 81% rename from app/src/androidTest/java/org/isoron/uhabits/unit/tasks/ImportDataTaskTest.java rename to app/src/androidTest/java/org/isoron/uhabits/tasks/ImportDataTaskTest.java index d6a3cabaa..094692083 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/tasks/ImportDataTaskTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/tasks/ImportDataTaskTest.java @@ -17,16 +17,14 @@ * with this program. If not, see . */ -package org.isoron.uhabits.unit.tasks; +package org.isoron.uhabits.tasks; import android.support.annotation.NonNull; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; -import android.widget.ProgressBar; -import org.isoron.uhabits.BaseTest; -import org.isoron.uhabits.helpers.DatabaseHelper; -import org.isoron.uhabits.tasks.ImportDataTask; +import org.isoron.uhabits.BaseAndroidTest; +import org.isoron.uhabits.utils.FileUtils; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -41,23 +39,23 @@ import static org.junit.Assert.fail; @RunWith(AndroidJUnit4.class) @SmallTest -public class ImportDataTaskTest extends BaseTest +public class ImportDataTaskTest extends BaseAndroidTest { private File baseDir; @Before - public void setup() + public void setUp() { - super.setup(); + super.setUp(); - baseDir = DatabaseHelper.getFilesDir("Backups"); + baseDir = FileUtils.getFilesDir("Backups"); if(baseDir == null) fail("baseDir should not be null"); } private void copyAssetToFile(String assetPath, File dst) throws IOException { InputStream in = testContext.getAssets().open(assetPath); - DatabaseHelper.copy(in, dst); + FileUtils.copy(in, dst); } private void assertTaskResult(final int expectedResult, String assetFilename) throws Throwable @@ -67,7 +65,7 @@ public class ImportDataTaskTest extends BaseTest task.setListener(new ImportDataTask.Listener() { @Override - public void onImportFinished(int result) + public void onImportDataFinished(int result) { assertThat(result, equalTo(expectedResult)); } @@ -80,11 +78,9 @@ public class ImportDataTaskTest extends BaseTest @NonNull private ImportDataTask createTask(String assetFilename) throws IOException { - ProgressBar bar = new ProgressBar(targetContext); File file = new File(String.format("%s/%s", baseDir.getPath(), assetFilename)); copyAssetToFile(assetFilename, file); - - return new ImportDataTask(file, bar); + return new ImportDataTask(file, null); } @Test diff --git a/app/src/androidTest/java/org/isoron/uhabits/ui/MainActivityActions.java b/app/src/androidTest/java/org/isoron/uhabits/ui/MainActivityActions.java deleted file mode 100644 index a933cd3a9..000000000 --- a/app/src/androidTest/java/org/isoron/uhabits/ui/MainActivityActions.java +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Copyright (C) 2016 Á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.ui; - -import android.support.test.espresso.NoMatchingViewException; -import android.support.test.espresso.contrib.RecyclerViewActions; - -import org.isoron.uhabits.R; -import org.isoron.uhabits.models.Habit; - -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.Random; - -import static android.support.test.espresso.Espresso.onData; -import static android.support.test.espresso.Espresso.onView; -import static android.support.test.espresso.Espresso.pressBack; -import static android.support.test.espresso.action.ViewActions.click; -import static android.support.test.espresso.action.ViewActions.longClick; -import static android.support.test.espresso.action.ViewActions.replaceText; -import static android.support.test.espresso.assertion.ViewAssertions.matches; -import static android.support.test.espresso.matcher.RootMatchers.isPlatformPopup; -import static android.support.test.espresso.matcher.ViewMatchers.Visibility.VISIBLE; -import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant; -import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; -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.withEffectiveVisibility; -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.Matchers.allOf; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.startsWith; -import static org.isoron.uhabits.ui.HabitMatchers.containsHabit; -import static org.isoron.uhabits.ui.HabitMatchers.withName; - -public class MainActivityActions -{ - public static String addHabit() - { - return addHabit(false); - } - - public static String addHabit(boolean openDialogs) - { - String name = "New Habit " + new Random().nextInt(1000000); - String description = "Did you perform your new habit today?"; - String num = "4"; - String den = "8"; - - onView(withId(R.id.action_add)) - .perform(click()); - - typeHabitData(name, description, num, den); - - if(openDialogs) - { - onView(withId(R.id.buttonPickColor)) - .perform(click()); - pressBack(); - onView(withId(R.id.inputReminderTime)) - .perform(click()); - onView(withText("Done")) - .perform(click()); - onView(withId(R.id.inputReminderDays)) - .perform(click()); - onView(withText("OK")) - .perform(click()); - } - - onView(withId(R.id.buttonSave)) - .perform(click()); - - onData(allOf(is(instanceOf(Habit.class)), withName(name))) - .onChildView(withId(R.id.label)); - - return name; - } - - public static void typeHabitData(String name, String description, String num, String den) - { - onView(withId(R.id.input_name)) - .perform(replaceText(name)); - onView(withId(R.id.input_description)) - .perform(replaceText(description)); - - try - { - onView(allOf(withId(R.id.sFrequency), withEffectiveVisibility(VISIBLE))) - .perform(click()); - onData(allOf(instanceOf(String.class), startsWith("Custom"))) - .inRoot(isPlatformPopup()) - .perform(click()); - } - catch(NoMatchingViewException e) - { - // ignored - } - - onView(withId(R.id.input_freq_num)) - .perform(replaceText(num)); - onView(withId(R.id.input_freq_den)) - .perform(replaceText(den)); - } - - public static void selectHabit(String name) - { - selectHabits(Collections.singletonList(name)); - } - - public static void selectHabits(List names) - { - boolean first = true; - for(String name : names) - { - onData(allOf(is(instanceOf(Habit.class)), withName(name))) - .onChildView(withId(R.id.label)) - .perform(first ? longClick() : click()); - - first = false; - } - } - - public static void assertHabitsDontExist(List names) - { - for(String name : names) - onView(withId(R.id.listView)) - .check(matches(not(containsHabit(withName(name))))); - } - - public static void assertHabitExists(String name) - { - List names = new LinkedList<>(); - names.add(name); - assertHabitsExist(names); - } - - public static void assertHabitsExist(List names) - { - for(String name : names) - onData(allOf(is(instanceOf(Habit.class)), withName(name))) - .check(matches(isDisplayed())); - } - - public static void deleteHabit(String name) - { - deleteHabits(Collections.singletonList(name)); - } - - public static void deleteHabits(List names) - { - selectHabits(names); - clickMenuItem(R.string.delete); - onView(withText("OK")) - .perform(click()); - assertHabitsDontExist(names); - } - - public static void clickMenuItem(int stringId) - { - try - { - onView(withText(stringId)).perform(click()); - } - catch (Exception e1) - { - try - { - onView(withContentDescription(stringId)).perform(click()); - } - catch(Exception e2) - { - clickHiddenMenuItem(stringId); - } - } - } - - private static void clickHiddenMenuItem(int stringId) - { - try - { - // Try the ActionMode overflow menu first - onView(allOf(withContentDescription("More options"), withParent(withParent( - withClassName(containsString("Action")))))).perform(click()); - } - catch (Exception e1) - { - // Try the toolbar overflow menu - onView(allOf(withContentDescription("More options"), withParent(withParent( - withClassName(containsString("Toolbar")))))).perform(click()); - } - - onView(withText(stringId)).perform(click()); - } - - public static void clickSettingsItem(String text) - { - onView(withClassName(containsString("RecyclerView"))) - .perform(RecyclerViewActions.actionOnItem( - hasDescendant(withText(containsString(text))), - click())); - } -} diff --git a/app/src/androidTest/java/org/isoron/uhabits/ui/MainTest.java b/app/src/androidTest/java/org/isoron/uhabits/ui/MainTest.java deleted file mode 100644 index 5dde5c695..000000000 --- a/app/src/androidTest/java/org/isoron/uhabits/ui/MainTest.java +++ /dev/null @@ -1,346 +0,0 @@ -/* - * Copyright (C) 2016 Á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.ui; - -import android.app.Activity; -import android.app.Instrumentation; -import android.content.Context; -import android.content.Intent; -import android.support.test.InstrumentationRegistry; -import android.support.test.espresso.NoMatchingViewException; -import android.support.test.espresso.intent.rule.IntentsTestRule; -import android.support.test.runner.AndroidJUnit4; -import android.test.suitebuilder.annotation.LargeTest; - -import org.isoron.uhabits.MainActivity; -import org.isoron.uhabits.R; -import org.isoron.uhabits.helpers.DateHelper; -import org.isoron.uhabits.models.Habit; -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.util.LinkedList; -import java.util.List; -import java.util.Random; - -import static android.support.test.espresso.Espresso.onData; -import static android.support.test.espresso.Espresso.onView; -import static android.support.test.espresso.Espresso.pressBack; -import static android.support.test.espresso.action.ViewActions.click; -import static android.support.test.espresso.action.ViewActions.longClick; -import static android.support.test.espresso.action.ViewActions.scrollTo; -import static android.support.test.espresso.action.ViewActions.swipeLeft; -import static android.support.test.espresso.action.ViewActions.swipeRight; -import static android.support.test.espresso.action.ViewActions.swipeUp; -import static android.support.test.espresso.assertion.ViewAssertions.matches; -import static android.support.test.espresso.intent.Intents.intended; -import static android.support.test.espresso.intent.Intents.intending; -import static android.support.test.espresso.intent.matcher.IntentMatchers.hasAction; -import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; -import static android.support.test.espresso.matcher.ViewMatchers.isRoot; -import static android.support.test.espresso.matcher.ViewMatchers.withClassName; -import static android.support.test.espresso.matcher.ViewMatchers.withId; -import static org.hamcrest.Matchers.allOf; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.endsWith; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.startsWith; -import static org.isoron.uhabits.ui.HabitMatchers.withName; -import static org.isoron.uhabits.ui.HabitViewActions.clickAtRandomLocations; -import static org.isoron.uhabits.ui.HabitViewActions.toggleAllCheckmarks; -import static org.isoron.uhabits.ui.MainActivityActions.addHabit; -import static org.isoron.uhabits.ui.MainActivityActions.assertHabitExists; -import static org.isoron.uhabits.ui.MainActivityActions.assertHabitsDontExist; -import static org.isoron.uhabits.ui.MainActivityActions.assertHabitsExist; -import static org.isoron.uhabits.ui.MainActivityActions.clickMenuItem; -import static org.isoron.uhabits.ui.MainActivityActions.clickSettingsItem; -import static org.isoron.uhabits.ui.MainActivityActions.deleteHabit; -import static org.isoron.uhabits.ui.MainActivityActions.deleteHabits; -import static org.isoron.uhabits.ui.MainActivityActions.selectHabit; -import static org.isoron.uhabits.ui.MainActivityActions.selectHabits; -import static org.isoron.uhabits.ui.MainActivityActions.typeHabitData; -import static org.isoron.uhabits.ui.ShowHabitActivityActions.openHistoryEditor; - -@RunWith(AndroidJUnit4.class) -@LargeTest -public class MainTest -{ - private SystemHelper sys; - - @Rule - public IntentsTestRule activityRule = new IntentsTestRule<>( - MainActivity.class); - - private Context targetContext; - - @Before - public void setup() - { - Context context = InstrumentationRegistry.getInstrumentation().getContext(); - sys = new SystemHelper(context); - sys.disableAllAnimations(); - sys.acquireWakeLock(); - sys.unlockScreen(); - - targetContext = InstrumentationRegistry.getTargetContext(); - - Instrumentation.ActivityResult okResult = new Instrumentation.ActivityResult( - Activity.RESULT_OK, new Intent()); - - intending(hasAction(equalTo(Intent.ACTION_SEND))).respondWith(okResult); - intending(hasAction(equalTo(Intent.ACTION_SENDTO))).respondWith(okResult); - intending(hasAction(equalTo(Intent.ACTION_VIEW))).respondWith(okResult); - - skipTutorial(); - } - - @After - public void tearDown() - { - sys.releaseWakeLock(); - } - - public void skipTutorial() - { - try - { - for (int i = 0; i < 10; i++) - onView(allOf(withClassName(endsWith("AppCompatImageButton")), - isDisplayed())).perform(click()); - } - catch (NoMatchingViewException e) - { - // ignored - } - } - - /** - * User opens the app, creates some habits, selects them, archives them, select 'show archived' - * on the menu, selects the previously archived habits and then deletes them. - */ - @Test - public void testArchiveHabits() - { - List names = new LinkedList<>(); - - for(int i = 0; i < 3; i++) - names.add(addHabit()); - - selectHabits(names); - - clickMenuItem(R.string.archive); - assertHabitsDontExist(names); - - clickMenuItem(R.string.show_archived); - - assertHabitsExist(names); - selectHabits(names); - clickMenuItem(R.string.unarchive); - - clickMenuItem(R.string.show_archived); - - assertHabitsExist(names); - deleteHabits(names); - } - - /** - * User opens the app, clicks the add button, types some bogus information, tries to save, - * dialog displays an error. - */ - @Test - public void testAddInvalidHabit() - { - onView(withId(R.id.action_add)) - .perform(click()); - - typeHabitData("", "", "15", "7"); - - onView(withId(R.id.buttonSave)).perform(click()); - onView(withId(R.id.input_name)).check(matches(isDisplayed())); - } - - /** - * User creates a habit, toggles a bunch of checkmarks, clicks the habit to open the statistics - * screen, scrolls down to some views, then scrolls the views backwards and forwards in time. - */ - @Test - public void testAddHabitAndViewStats() throws InterruptedException - { - String name = addHabit(true); - - onData(allOf(is(instanceOf(Habit.class)), withName(name))) - .onChildView(withId(R.id.llButtons)) - .perform(toggleAllCheckmarks()); - - Thread.sleep(1200); - - onData(allOf(is(instanceOf(Habit.class)), withName(name))) - .onChildView(withId(R.id.label)) - .perform(click()); - - onView(withId(R.id.scoreView)) - .perform(scrollTo(), swipeRight()); - - onView(withId(R.id.punchcardView)) - .perform(scrollTo(), swipeRight()); - } - - /** - * User creates a habit, selects the habit, clicks edit button, changes some information about - * the habit, click save button, sees changes on the main window, selects habit again, - * changes color, then deletes the habit. - */ - @Test - public void testEditHabit() - { - String name = addHabit(); - - onData(allOf(is(instanceOf(Habit.class)), withName(name))) - .onChildView(withId(R.id.label)) - .perform(longClick()); - - clickMenuItem(R.string.edit); - - String modifiedName = "Modified " + new Random().nextInt(10000); - typeHabitData(modifiedName, "", "1", "1"); - - onView(withId(R.id.buttonSave)) - .perform(click()); - - assertHabitExists(modifiedName); - - selectHabit(modifiedName); - clickMenuItem(R.string.color_picker_default_title); - pressBack(); - - deleteHabit(modifiedName); - } - - /** - * User creates a habit, opens statistics page, clicks button to edit history, adds some - * checkmarks, closes dialog, sees the modified history calendar. - */ - @Test - public void testEditHistory() - { - String name = addHabit(); - - onData(allOf(is(instanceOf(Habit.class)), withName(name))) - .onChildView(withId(R.id.label)) - .perform(click()); - - openHistoryEditor(); - onView(withClassName(endsWith("HabitHistoryView"))) - .perform(clickAtRandomLocations(20)); - - pressBack(); - onView(withId(R.id.historyView)) - .perform(scrollTo(), swipeRight(), swipeLeft()); - } - - /** - * User opens menu, clicks settings, sees settings screen. - */ - @Test - public void testSettings() - { - clickMenuItem(R.string.settings); - } - - /** - * User opens menu, clicks about, sees about screen. - */ - @Test - public void testAbout() - { - clickMenuItem(R.string.about); - onView(isRoot()).perform(swipeUp()); - } - - /** - * User opens menu, clicks Help, sees website. - */ - @Test - public void testHelp() - { - clickMenuItem(R.string.help); - intended(hasAction(Intent.ACTION_VIEW)); - } - - /** - * User creates a habit, exports full backup, deletes the habit, restores backup, sees that the - * previously created habit has appeared back. - */ - @Test - public void testExportImportDB() - { - String name = addHabit(); - - clickMenuItem(R.string.settings); - - String date = DateHelper.getBackupDateFormat().format(DateHelper.getLocalTime()); - date = date.substring(0, date.length() - 2); - - clickSettingsItem("Export full backup"); - intended(hasAction(Intent.ACTION_SEND)); - - deleteHabit(name); - - clickMenuItem(R.string.settings); - clickSettingsItem("Import data"); - - onData(allOf(is(instanceOf(String.class)), startsWith("Backups"))) - .perform(click()); - - onData(allOf(is(instanceOf(String.class)), containsString(date))) - .perform(click()); - - selectHabit(name); - } - - /** - * User creates a habit, opens settings, clicks export as CSV, is asked what activity should - * handle the file. - */ - @Test - public void testExportCSV() - { - addHabit(); - clickMenuItem(R.string.settings); - clickSettingsItem("Export as CSV"); - intended(hasAction(Intent.ACTION_SEND)); - } - - /** - * User opens the settings and generates a bug report. - */ - @Test - public void testGenerateBugReport() - { - clickMenuItem(R.string.settings); - clickSettingsItem("Generate bug report"); - intended(hasAction(Intent.ACTION_SEND)); - } -} diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/views/HabitFrequencyViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/ui/common/views/FrequencyChartTest.java similarity index 52% rename from app/src/androidTest/java/org/isoron/uhabits/unit/views/HabitFrequencyViewTest.java rename to app/src/androidTest/java/org/isoron/uhabits/ui/common/views/FrequencyChartTest.java index 590e2c9d2..2d21a6dd1 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/views/HabitFrequencyViewTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/ui/common/views/FrequencyChartTest.java @@ -17,64 +17,66 @@ * with this program. If not, see . */ -package org.isoron.uhabits.unit.views; +package org.isoron.uhabits.ui.common.views; -import android.support.test.runner.AndroidJUnit4; -import android.test.suitebuilder.annotation.SmallTest; +import android.support.test.runner.*; +import android.test.suitebuilder.annotation.*; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.unit.HabitFixtures; -import org.isoron.uhabits.views.HabitFrequencyView; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; +import org.junit.*; +import org.junit.runner.*; @RunWith(AndroidJUnit4.class) -@SmallTest -public class HabitFrequencyViewTest extends ViewTest +@MediumTest +public class FrequencyChartTest extends BaseViewTest { - private HabitFrequencyView view; + public static final String BASE_PATH = "common/FrequencyChart/"; + private FrequencyChart view; + + @Override @Before - public void setup() + public void setUp() { - super.setup(); + super.setUp(); - HabitFixtures.purgeHabits(); - Habit habit = HabitFixtures.createLongHabit(); + fixtures.purgeHabits(habitList); + Habit habit = fixtures.createLongHabit(); - view = new HabitFrequencyView(targetContext); - view.setHabit(habit); - refreshData(view); - measureView(dpToPixels(300), dpToPixels(100), view); + view = new FrequencyChart(targetContext); + view.setFrequency(habit.getRepetitions().getWeekdayFrequency()); + view.setColor(ColorUtils.getAndroidTestColor(habit.getColor())); + measureView(view, dpToPixels(300), dpToPixels(100)); } @Test public void testRender() throws Throwable { - assertRenders(view, "HabitFrequencyView/render.png"); + assertRenders(view, BASE_PATH + "render.png"); } @Test - public void testRender_withTransparentBackground() throws Throwable + public void testRender_withDataOffset() throws Throwable { - view.setIsBackgroundTransparent(true); - assertRenders(view, "HabitFrequencyView/renderTransparent.png"); + view.onScroll(null, null, -dpToPixels(150), 0); + view.invalidate(); + + assertRenders(view, BASE_PATH + "renderDataOffset.png"); } @Test public void testRender_withDifferentSize() throws Throwable { - measureView(dpToPixels(200), dpToPixels(200), view); - assertRenders(view, "HabitFrequencyView/renderDifferentSize.png"); + measureView(view, dpToPixels(200), dpToPixels(200)); + assertRenders(view, BASE_PATH + "renderDifferentSize.png"); } @Test - public void testRender_withDataOffset() throws Throwable + public void testRender_withTransparentBackground() throws Throwable { - view.onScroll(null, null, -dpToPixels(150), 0); - view.invalidate(); - - assertRenders(view, "HabitFrequencyView/renderDataOffset.png"); + view.setIsBackgroundTransparent(true); + assertRenders(view, BASE_PATH + "renderTransparent.png"); } } diff --git a/app/src/androidTest/java/org/isoron/uhabits/ui/common/views/HistoryChartTest.java b/app/src/androidTest/java/org/isoron/uhabits/ui/common/views/HistoryChartTest.java new file mode 100644 index 000000000..9a6c99640 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/ui/common/views/HistoryChartTest.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2016 Á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.ui.common.views; + +import android.support.test.runner.*; +import android.test.suitebuilder.annotation.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; +import org.junit.*; +import org.junit.runner.*; + +@RunWith(AndroidJUnit4.class) +@MediumTest +public class HistoryChartTest extends BaseViewTest +{ + private static final String BASE_PATH = "common/HistoryChart/"; + + private HistoryChart chart; + + @Override + @Before + public void setUp() + { + super.setUp(); + + fixtures.purgeHabits(habitList); + Habit habit = fixtures.createLongHabit(); + + chart = new HistoryChart(targetContext); + chart.setCheckmarks(habit.getCheckmarks().getAllValues()); + chart.setColor(ColorUtils.getAndroidTestColor(habit.getColor())); + measureView(chart, dpToPixels(400), dpToPixels(200)); + } + +// @Test +// public void tapDate_atInvalidLocations() throws Throwable +// { +// int expectedCheckmarkValues[] = habit.getCheckmarks().getAllValues(); +// +// chart.setIsEditable(true); +// tap(chart, 118, 13); // header +// tap(chart, 336, 60); // tomorrow's square +// tap(chart, 370, 60); // right axis +// waitForAsyncTasks(); +// +// int actualCheckmarkValues[] = habit.getCheckmarks().getAllValues(); +// assertThat(actualCheckmarkValues, equalTo(expectedCheckmarkValues)); +// } +// +// @Test +// public void tapDate_withEditableView() throws Throwable +// { +// chart.setIsEditable(true); +// tap(chart, 340, 40); // today's square +// waitForAsyncTasks(); +// +// long today = DateUtils.getStartOfToday(); +// assertFalse(habit.getRepetitions().containsTimestamp(today)); +// } +// +// @Test +// public void tapDate_withReadOnlyView() throws Throwable +// { +// chart.setIsEditable(false); +// tap(chart, 340, 40); // today's square +// waitForAsyncTasks(); +// +// long today = DateUtils.getStartOfToday(); +// assertTrue(habit.getRepetitions().containsTimestamp(today)); +// } + + @Test + public void testRender() throws Throwable + { + assertRenders(chart, BASE_PATH + "render.png"); + } + + @Test + public void testRender_withDataOffset() throws Throwable + { + chart.onScroll(null, null, -dpToPixels(150), 0); + chart.invalidate(); + + assertRenders(chart, BASE_PATH + "renderDataOffset.png"); + } + + @Test + public void testRender_withDifferentSize() throws Throwable + { + measureView(chart, dpToPixels(200), dpToPixels(200)); + assertRenders(chart, BASE_PATH + "renderDifferentSize.png"); + } + + @Test + public void testRender_withTransparentBackground() throws Throwable + { + chart.setIsBackgroundTransparent(true); + assertRenders(chart, BASE_PATH + "renderTransparent.png"); + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/views/RingViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/ui/common/views/RingViewTest.java similarity index 60% rename from app/src/androidTest/java/org/isoron/uhabits/unit/views/RingViewTest.java rename to app/src/androidTest/java/org/isoron/uhabits/ui/common/views/RingViewTest.java index 9f831ba49..771e1d17c 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/views/RingViewTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/ui/common/views/RingViewTest.java @@ -17,35 +17,37 @@ * with this program. If not, see . */ -package org.isoron.uhabits.unit.views; +package org.isoron.uhabits.ui.common.views; -import android.graphics.Color; -import android.support.test.runner.AndroidJUnit4; -import android.test.suitebuilder.annotation.SmallTest; +import android.graphics.*; +import android.support.test.runner.*; +import android.test.suitebuilder.annotation.*; -import org.isoron.uhabits.helpers.ColorHelper; -import org.isoron.uhabits.views.RingView; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.isoron.uhabits.*; +import org.isoron.uhabits.utils.*; +import org.junit.*; +import org.junit.runner.*; -import java.io.IOException; +import java.io.*; @RunWith(AndroidJUnit4.class) -@SmallTest -public class RingViewTest extends ViewTest +@MediumTest +public class RingViewTest extends BaseViewTest { + private static final String BASE_PATH = "common/RingView/"; + private RingView view; + @Override @Before - public void setup() + public void setUp() { - super.setup(); + super.setUp(); view = new RingView(targetContext); view.setPercentage(0.6f); view.setText("60%"); - view.setColor(ColorHelper.CSV_PALETTE[0]); + view.setColor(ColorUtils.getAndroidTestColor(0)); view.setBackgroundColor(Color.WHITE); view.setThickness(dpToPixels(3)); } @@ -53,17 +55,17 @@ public class RingViewTest extends ViewTest @Test public void testRender_base() throws IOException { - measureView(dpToPixels(100), dpToPixels(100), view); - assertRenders(view, "RingView/render.png"); + measureView(view, dpToPixels(100), dpToPixels(100)); + assertRenders(view, BASE_PATH + "render.png"); } @Test public void testRender_withDifferentParams() throws IOException { view.setPercentage(0.25f); - view.setColor(ColorHelper.CSV_PALETTE[5]); + view.setColor(ColorUtils.getAndroidTestColor(5)); - measureView(dpToPixels(200), dpToPixels(200), view); - assertRenders(view, "RingView/renderDifferentParams.png"); + measureView(view, dpToPixels(200), dpToPixels(200)); + assertRenders(view, BASE_PATH + "renderDifferentParams.png"); } } diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/views/HabitScoreViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/ui/common/views/ScoreChartTest.java similarity index 52% rename from app/src/androidTest/java/org/isoron/uhabits/unit/views/HabitScoreViewTest.java rename to app/src/androidTest/java/org/isoron/uhabits/ui/common/views/ScoreChartTest.java index a7f1228b9..b56bd5704 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/views/HabitScoreViewTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/ui/common/views/ScoreChartTest.java @@ -17,88 +17,92 @@ * with this program. If not, see . */ -package org.isoron.uhabits.unit.views; +package org.isoron.uhabits.ui.common.views; -import android.support.test.runner.AndroidJUnit4; -import android.test.suitebuilder.annotation.SmallTest; -import android.util.Log; +import android.support.test.runner.*; +import android.test.suitebuilder.annotation.*; +import android.util.*; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.unit.HabitFixtures; -import org.isoron.uhabits.views.HabitScoreView; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; +import org.junit.*; +import org.junit.runner.*; @RunWith(AndroidJUnit4.class) -@SmallTest -public class HabitScoreViewTest extends ViewTest +@MediumTest +public class ScoreChartTest extends BaseViewTest { + private static final String BASE_PATH = "common/ScoreChart/"; + private Habit habit; - private HabitScoreView view; + private ScoreChart view; + + @Override @Before - public void setup() + public void setUp() { - super.setup(); + super.setUp(); - HabitFixtures.purgeHabits(); - habit = HabitFixtures.createLongHabit(); + fixtures.purgeHabits(habitList); + habit = fixtures.createLongHabit(); - view = new HabitScoreView(targetContext); - view.setHabit(habit); + view = new ScoreChart(targetContext); + view.setScores(habit.getScores().getAll()); + view.setColor(ColorUtils.getColor(targetContext, habit.getColor())); view.setBucketSize(7); - refreshData(view); - measureView(dpToPixels(300), dpToPixels(200), view); + measureView(view, dpToPixels(300), dpToPixels(200)); } @Test public void testRender() throws Throwable { - Log.d("HabitScoreViewTest", String.format("height=%d", dpToPixels(100))); - assertRenders(view, "HabitScoreView/render.png"); + Log.d("HabitScoreViewTest", + String.format("height=%d", dpToPixels(100))); + assertRenders(view, BASE_PATH + "render.png"); } @Test - public void testRender_withTransparentBackground() throws Throwable + public void testRender_withDataOffset() throws Throwable { - view.setIsTransparencyEnabled(true); - assertRenders(view, "HabitScoreView/renderTransparent.png"); + view.onScroll(null, null, -dpToPixels(150), 0); + view.invalidate(); + + assertRenders(view, BASE_PATH + "renderDataOffset.png"); } @Test public void testRender_withDifferentSize() throws Throwable { - measureView(dpToPixels(200), dpToPixels(200), view); - assertRenders(view, "HabitScoreView/renderDifferentSize.png"); + measureView(view, dpToPixels(200), dpToPixels(200)); + assertRenders(view, BASE_PATH + "renderDifferentSize.png"); } @Test - public void testRender_withDataOffset() throws Throwable + public void testRender_withMonthlyBucket() throws Throwable { - view.onScroll(null, null, -dpToPixels(150), 0); + view.setScores(habit.getScores().groupBy(DateUtils.TruncateField.MONTH)); + view.setBucketSize(30); view.invalidate(); - assertRenders(view, "HabitScoreView/renderDataOffset.png"); + assertRenders(view, BASE_PATH + "renderMonthly.png"); } @Test - public void testRender_withMonthlyBucket() throws Throwable + public void testRender_withTransparentBackground() throws Throwable { - view.setBucketSize(30); - view.refreshData(); - view.invalidate(); - - assertRenders(view, "HabitScoreView/renderMonthly.png"); + view.setIsTransparencyEnabled(true); + assertRenders(view, BASE_PATH + "renderTransparent.png"); } @Test public void testRender_withYearlyBucket() throws Throwable { + view.setScores(habit.getScores().groupBy(DateUtils.TruncateField.YEAR)); view.setBucketSize(365); - view.refreshData(); view.invalidate(); - assertRenders(view, "HabitScoreView/renderYearly.png"); + assertRenders(view, BASE_PATH + "renderYearly.png"); } } diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/views/HabitStreakViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/ui/common/views/StreakChartTest.java similarity index 51% rename from app/src/androidTest/java/org/isoron/uhabits/unit/views/HabitStreakViewTest.java rename to app/src/androidTest/java/org/isoron/uhabits/ui/common/views/StreakChartTest.java index ececee945..3ca002117 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/views/HabitStreakViewTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/ui/common/views/StreakChartTest.java @@ -17,58 +17,57 @@ * with this program. If not, see . */ -package org.isoron.uhabits.unit.views; +package org.isoron.uhabits.ui.common.views; -import android.support.test.runner.AndroidJUnit4; -import android.test.suitebuilder.annotation.SmallTest; +import android.support.test.runner.*; +import android.test.suitebuilder.annotation.*; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.unit.HabitFixtures; -import org.isoron.uhabits.views.HabitStreakView; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; +import org.junit.*; +import org.junit.runner.*; @RunWith(AndroidJUnit4.class) -@SmallTest -public class HabitStreakViewTest extends ViewTest +@MediumTest +public class StreakChartTest extends BaseViewTest { - private HabitStreakView view; + private static final String BASE_PATH = "common/StreakChart/"; + private StreakChart view; + + @Override @Before - public void setup() + public void setUp() { - super.setup(); - - HabitFixtures.purgeHabits(); - Habit habit = HabitFixtures.createLongHabit(); + super.setUp(); - view = new HabitStreakView(targetContext); - measureView(dpToPixels(300), dpToPixels(100), view); + fixtures.purgeHabits(habitList); + Habit habit = fixtures.createLongHabit(); - view.setHabit(habit); - refreshData(view); + view = new StreakChart(targetContext); + view.setColor(ColorUtils.getAndroidTestColor(habit.getColor())); + view.setStreaks(habit.getStreaks().getBest(5)); + measureView(view, dpToPixels(300), dpToPixels(100)); } @Test public void testRender() throws Throwable { - assertRenders(view, "HabitStreakView/render.png"); + assertRenders(view, BASE_PATH + "render.png"); } @Test - public void testRender_withTransparentBackground() throws Throwable + public void testRender_withSmallSize() throws Throwable { - view.setIsBackgroundTransparent(true); - assertRenders(view, "HabitStreakView/renderTransparent.png"); + measureView(view, dpToPixels(100), dpToPixels(100)); + assertRenders(view, BASE_PATH + "renderSmallSize.png"); } @Test - public void testRender_withSmallSize() throws Throwable + public void testRender_withTransparentBackground() throws Throwable { - measureView(dpToPixels(100), dpToPixels(100), view); - refreshData(view); - - assertRenders(view, "HabitStreakView/renderSmallSize.png"); + view.setIsBackgroundTransparent(true); + assertRenders(view, BASE_PATH + "renderTransparent.png"); } } diff --git a/app/src/androidTest/java/org/isoron/uhabits/ui/habits/list/views/CheckmarkButtonViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/ui/habits/list/views/CheckmarkButtonViewTest.java new file mode 100644 index 000000000..b55eefce1 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/ui/habits/list/views/CheckmarkButtonViewTest.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.list.views; + +import android.support.test.runner.AndroidJUnit4; +import android.test.suitebuilder.annotation.*; + +import org.isoron.uhabits.models.Checkmark; +import org.isoron.uhabits.BaseViewTest; +import org.isoron.uhabits.utils.ColorUtils; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.IOException; +import java.util.concurrent.CountDownLatch; + +@RunWith(AndroidJUnit4.class) +@MediumTest +public class CheckmarkButtonViewTest extends BaseViewTest +{ + public static final String PATH = "habits/list/CheckmarkButtonView/"; + + private CountDownLatch latch; + + private CheckmarkButtonView view; + + @Override + @Before + public void setUp() + { + super.setUp(); + setSimilarityCutoff(0.03f); + + latch = new CountDownLatch(1); + view = new CheckmarkButtonView(targetContext); + view.setValue(Checkmark.UNCHECKED); + view.setColor(ColorUtils.getAndroidTestColor(7)); + + measureView(view, dpToPixels(40), dpToPixels(40)); + } + + @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"); + } + +// @Test +// public void testLongClick() throws Exception +// { +// setOnToggleListener(); +// view.performLongClick(); +// waitForLatch(); +// assertRendersCheckedExplicitly(); +// } +// +// @Test +// public void testClick_withShortToggle_fromUnchecked() throws Exception +// { +// Preferences.getInstance().setShortToggleEnabled(true); +// view.setValue(Checkmark.UNCHECKED); +// setOnToggleListenerAndPerformClick(); +// assertRendersCheckedExplicitly(); +// } +// +// @Test +// public void testClick_withShortToggle_fromChecked() throws Exception +// { +// Preferences.getInstance().setShortToggleEnabled(true); +// view.setValue(Checkmark.CHECKED_EXPLICITLY); +// setOnToggleListenerAndPerformClick(); +// assertRendersUnchecked(); +// } +// +// @Test +// public void testClick_withShortToggle_withoutListener() throws Exception +// { +// Preferences.getInstance().setShortToggleEnabled(true); +// view.setValue(Checkmark.CHECKED_EXPLICITLY); +// view.setController(null); +// view.performClick(); +// assertRendersUnchecked(); +// } +// +// protected void setOnToggleListenerAndPerformClick() throws InterruptedException +// { +// setOnToggleListener(); +// view.performClick(); +// waitForLatch(); +// } +// +// @Test +// public void testClick_withoutShortToggle() throws Exception +// { +// Preferences.getInstance().setShortToggleEnabled(false); +// setOnInvalidToggleListener(); +// view.performClick(); +// waitForLatch(); +// assertRendersUnchecked(); +// } + +// protected void setOnInvalidToggleListener() +// { +// view.setController(new CheckmarkButtonView.Controller() +// { +// @Override +// public void onToggleCheckmark(CheckmarkButtonView view, long timestamp) +// { +// fail(); +// } +// +// @Override +// public void onInvalidToggle(CheckmarkButtonView v) +// { +// assertThat(v, equalTo(view)); +// latch.countDown(); +// } +// }); +// } + +// protected void setOnToggleListener() +// { +// view.setController(new CheckmarkButtonView.Controller() +// { +// @Override +// public void onToggleCheckmark(CheckmarkButtonView v, long t) +// { +// assertThat(v, equalTo(view)); +// assertThat(t, equalTo(DateUtils.getStartOfToday())); +// latch.countDown(); +// } +// +// @Override +// public void onInvalidToggle(CheckmarkButtonView view) +// { +// fail(); +// } +// }); +// } +} \ No newline at end of file diff --git a/app/src/androidTest/java/org/isoron/uhabits/ui/habits/list/views/CheckmarkPanelViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/ui/habits/list/views/CheckmarkPanelViewTest.java new file mode 100644 index 000000000..878dc5c9f --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/ui/habits/list/views/CheckmarkPanelViewTest.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.list.views; + +import android.support.test.runner.AndroidJUnit4; +import android.test.suitebuilder.annotation.*; + +import org.isoron.uhabits.models.Checkmark; +import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.BaseViewTest; +import org.isoron.uhabits.utils.ColorUtils; +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(); + setSimilarityCutoff(0.03f); + prefs.setShouldReverseCheckmarks(false); + + Habit habit = new Habit(); + + latch = new CountDownLatch(1); + checkmarks = new int[]{ + Checkmark.CHECKED_EXPLICITLY, Checkmark.UNCHECKED, + Checkmark.CHECKED_IMPLICITLY, Checkmark.CHECKED_EXPLICITLY}; + + view = new CheckmarkPanelView(targetContext); + view.setHabit(habit); + view.setCheckmarkValues(checkmarks); + view.setColor(ColorUtils.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(); +// } +} \ No newline at end of file diff --git a/app/src/androidTest/java/org/isoron/uhabits/ui/habits/list/views/HintViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/ui/habits/list/views/HintViewTest.java new file mode 100644 index 000000000..8e73673e2 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/ui/habits/list/views/HintViewTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.list.views; + +import android.support.test.runner.*; +import android.test.suitebuilder.annotation.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.ui.habits.list.model.*; +import org.junit.*; +import org.junit.runner.*; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; +import static org.mockito.Mockito.*; + +@RunWith(AndroidJUnit4.class) +@MediumTest +public class HintViewTest extends BaseViewTest +{ + public static final String PATH = "habits/list/HintView/"; + + private HintView view; + + private HintList list; + + @Before + @Override + public void setUp() + { + super.setUp(); + + view = new HintView(targetContext); + list = mock(HintList.class); + view.setHints(list); + measureView(view, 400, 200); + + String text = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit."; + + when(list.shouldShow()).thenReturn(true); + when(list.pop()).thenReturn(text); + + view.showNext(); + skipAnimation(view); + } + + @Test + public void testRender() throws Exception + { + assertRenders(view, PATH + "render.png"); + } + + @Test + public void testClick() throws Exception + { + assertThat(view.getAlpha(), equalTo(1f)); + view.performClick(); + skipAnimation(view); + assertThat(view.getAlpha(), equalTo(0f)); + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/ui/widgets/CheckmarkWidgetTest.java b/app/src/androidTest/java/org/isoron/uhabits/ui/widgets/CheckmarkWidgetTest.java new file mode 100644 index 000000000..382e13375 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/ui/widgets/CheckmarkWidgetTest.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2016 Á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.ui.widgets; + +import android.support.test.runner.*; +import android.test.suitebuilder.annotation.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.widgets.*; +import org.junit.*; +import org.junit.runner.*; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; +import static org.isoron.uhabits.models.Checkmark.*; + +@RunWith(AndroidJUnit4.class) +@MediumTest +public class CheckmarkWidgetTest extends BaseViewTest +{ + + private static final String PATH = "widgets/CheckmarkWidgetView/"; + + private Habit habit; + + private CheckmarkList checkmarks; + + private FrameLayout view; + + @Override + public void setUp() + { + super.setUp(); + habit = fixtures.createShortHabit(); + checkmarks = habit.getCheckmarks(); + CheckmarkWidget widget = new CheckmarkWidget(targetContext, 0, habit); + view = convertToView(widget, 200, 250); + + assertThat(checkmarks.getTodayValue(), equalTo(CHECKED_EXPLICITLY)); + } + + @Test + public void testClick() throws Exception + { + Button button = (Button) view.findViewById(R.id.button); + assertThat(button, is(not(nullValue()))); + + // A better test would be to capture the intent, but it doesn't seem + // possible to capture intents sent to BroadcastReceivers. + button.performClick(); + sleep(1000); + + assertThat(checkmarks.getTodayValue(), equalTo(UNCHECKED)); + } + + @Test + public void testIsInstalled() + { + assertWidgetProviderIsInstalled(CheckmarkWidgetProvider.class); + } + + @Test + public void testRender() throws Exception + { + assertRenders(view, PATH + "checked.png"); + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/ui/widgets/FrequencyWidgetTest.java b/app/src/androidTest/java/org/isoron/uhabits/ui/widgets/FrequencyWidgetTest.java new file mode 100644 index 000000000..71492eb85 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/ui/widgets/FrequencyWidgetTest.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2016 Á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.ui.widgets; + +import android.support.test.runner.*; +import android.test.suitebuilder.annotation.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.widgets.*; +import org.junit.*; +import org.junit.runner.*; + +@RunWith(AndroidJUnit4.class) +@MediumTest +public class FrequencyWidgetTest extends BaseViewTest +{ + private static final String PATH = "widgets/FrequencyWidget/"; + + private Habit habit; + + private FrameLayout view; + + @Override + public void setUp() + { + super.setUp(); + setTheme(R.style.TransparentWidgetTheme); + + habit = fixtures.createLongHabit(); + FrequencyWidget widget = new FrequencyWidget(targetContext, 0, habit); + view = convertToView(widget, 400, 400); + } + + @Test + public void testIsInstalled() + { + assertWidgetProviderIsInstalled(FrequencyWidgetProvider.class); + } + + @Test + public void testRender() throws Exception + { + assertRenders(view, PATH + "render.png"); + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/ui/widgets/HistoryWidgetTest.java b/app/src/androidTest/java/org/isoron/uhabits/ui/widgets/HistoryWidgetTest.java new file mode 100644 index 000000000..a1daf7d64 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/ui/widgets/HistoryWidgetTest.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2016 Á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.ui.widgets; + +import android.support.test.runner.*; +import android.test.suitebuilder.annotation.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.widgets.*; +import org.junit.*; +import org.junit.runner.*; + +@RunWith(AndroidJUnit4.class) +@MediumTest +public class HistoryWidgetTest extends BaseViewTest +{ + private static final String PATH = "widgets/HistoryWidget/"; + + private Habit habit; + + private FrameLayout view; + + @Override + public void setUp() + { + super.setUp(); + setTheme(R.style.TransparentWidgetTheme); + + habit = fixtures.createLongHabit(); + HistoryWidget widget = new HistoryWidget(targetContext, 0, habit); + view = convertToView(widget, 400, 400); + } + + @Test + public void testIsInstalled() + { + assertWidgetProviderIsInstalled(HistoryWidgetProvider.class); + } + + @Test + public void testRender() throws Exception + { + assertRenders(view, PATH + "render.png"); + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/ui/widgets/ScoreWidgetTest.java b/app/src/androidTest/java/org/isoron/uhabits/ui/widgets/ScoreWidgetTest.java new file mode 100644 index 000000000..3a0ce96dd --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/ui/widgets/ScoreWidgetTest.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2016 Á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.ui.widgets; + +import android.support.test.runner.*; +import android.test.suitebuilder.annotation.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.widgets.*; +import org.junit.*; +import org.junit.runner.*; + +@RunWith(AndroidJUnit4.class) +@MediumTest +public class ScoreWidgetTest extends BaseViewTest +{ + private static final String PATH = "widgets/ScoreWidget/"; + + private Habit habit; + + private FrameLayout view; + + @Override + public void setUp() + { + super.setUp(); + setTheme(R.style.TransparentWidgetTheme); + + habit = fixtures.createLongHabit(); + ScoreWidget widget = new ScoreWidget(targetContext, 0, habit); + view = convertToView(widget, 400, 400); + } + + @Test + public void testIsInstalled() + { + assertWidgetProviderIsInstalled(ScoreWidgetProvider.class); + } + + @Test + public void testRender() throws Exception + { + assertRenders(view, PATH + "render.png"); + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/ui/widgets/StreakWidgetTest.java b/app/src/androidTest/java/org/isoron/uhabits/ui/widgets/StreakWidgetTest.java new file mode 100644 index 000000000..e8bf18f44 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/ui/widgets/StreakWidgetTest.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2016 Á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.ui.widgets; + +import android.support.test.runner.*; +import android.test.suitebuilder.annotation.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.widgets.*; +import org.junit.*; +import org.junit.runner.*; + +@RunWith(AndroidJUnit4.class) +@MediumTest +public class StreakWidgetTest extends BaseViewTest +{ + private static final String PATH = "widgets/StreakWidget/"; + + private Habit habit; + + private FrameLayout view; + + @Override + public void setUp() + { + super.setUp(); + setTheme(R.style.TransparentWidgetTheme); + + habit = fixtures.createLongHabit(); + StreakWidget widget = new StreakWidget(targetContext, 0, habit); + view = convertToView(widget, 400, 400); + } + + @Test + public void testIsInstalled() + { + assertWidgetProviderIsInstalled(StreakWidgetProvider.class); + } + + @Test + public void testRender() throws Exception + { + assertRenders(view, PATH + "render.png"); + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/ui/widgets/views/CheckmarkWidgetViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/ui/widgets/views/CheckmarkWidgetViewTest.java new file mode 100644 index 000000000..fe64291af --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/ui/widgets/views/CheckmarkWidgetViewTest.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2016 Á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.ui.widgets.views; + +import android.support.test.runner.*; +import android.test.suitebuilder.annotation.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; +import org.junit.*; +import org.junit.runner.*; + +import java.io.*; + +@RunWith(AndroidJUnit4.class) +@MediumTest +public class CheckmarkWidgetViewTest extends BaseViewTest +{ + private static final String PATH = "widgets/CheckmarkWidgetView/"; + + private CheckmarkWidgetView view; + + private Habit habit; + + @Override + @Before + public void setUp() + { + super.setUp(); + InterfaceUtils.setFixedTheme(R.style.TransparentWidgetTheme); + + habit = fixtures.createShortHabit(); + view = new CheckmarkWidgetView(targetContext); + int color = ColorUtils.getAndroidTestColor(habit.getColor()); + int score = habit.getScores().getTodayValue(); + float percentage = (float) score / Score.MAX_VALUE; + + view.setActiveColor(color); + view.setCheckmarkValue(habit.getCheckmarks().getTodayValue()); + view.setPercentage(percentage); + view.setName(habit.getName()); + view.refresh(); + measureView(view, dpToPixels(100), dpToPixels(200)); + } + + @Test + public void testRender_checked() throws IOException + { + assertRenders(view, PATH + "checked.png"); + } + + @Test + public void testRender_implicitlyChecked() throws IOException + { + view.setCheckmarkValue(Checkmark.CHECKED_IMPLICITLY); + view.refresh(); + assertRenders(view, PATH + "implicitly_checked.png"); + } + + @Test + public void testRender_largeSize() throws IOException + { + measureView(view, dpToPixels(300), dpToPixels(300)); + assertRenders(view, PATH + "large_size.png"); + } + + @Test + public void testRender_unchecked() throws IOException + { + view.setCheckmarkValue(Checkmark.UNCHECKED); + view.refresh(); + assertRenders(view, PATH + "unchecked.png"); + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/HabitFixtures.java b/app/src/androidTest/java/org/isoron/uhabits/unit/HabitFixtures.java deleted file mode 100644 index 09298bb08..000000000 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/HabitFixtures.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright (C) 2016 Á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.unit; - -import android.content.Context; -import android.support.annotation.Nullable; -import android.util.Log; - -import org.isoron.uhabits.helpers.DatabaseHelper; -import org.isoron.uhabits.helpers.DateHelper; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.tasks.BaseTask; -import org.isoron.uhabits.tasks.ExportDBTask; -import org.isoron.uhabits.tasks.ImportDataTask; - -import java.io.File; -import java.io.InputStream; -import java.util.Random; - -import static org.junit.Assert.fail; - -public class HabitFixtures -{ - public static boolean NON_DAILY_HABIT_CHECKS[] = { true, false, false, true, true, true, false, - false, true, true }; - - public static Habit createShortHabit() - { - Habit habit = new Habit(); - habit.name = "Wake up early"; - habit.description = "Did you wake up before 6am?"; - habit.freqNum = 2; - habit.freqDen = 3; - habit.save(); - - long timestamp = DateHelper.getStartOfToday(); - for(boolean c : NON_DAILY_HABIT_CHECKS) - { - if(c) habit.repetitions.toggle(timestamp); - timestamp -= DateHelper.millisecondsInOneDay; - } - - return habit; - } - - public static Habit createEmptyHabit() - { - Habit habit = new Habit(); - habit.name = "Meditate"; - habit.description = "Did you meditate this morning?"; - habit.color = 3; - habit.freqNum = 1; - habit.freqDen = 1; - habit.save(); - return habit; - } - - public static Habit createLongHabit() - { - Habit habit = createEmptyHabit(); - habit.freqNum = 3; - habit.freqDen = 7; - habit.color = 4; - habit.save(); - - long day = DateHelper.millisecondsInOneDay; - long today = DateHelper.getStartOfToday(); - int marks[] = { 0, 1, 3, 5, 7, 8, 9, 10, 12, 14, 15, 17, 19, 20, 26, 27, 28, 50, 51, 52, - 53, 54, 58, 60, 63, 65, 70, 71, 72, 73, 74, 75, 80, 81, 83, 89, 90, 91, 95, - 102, 103, 108, 109, 120}; - - for(int mark : marks) - habit.repetitions.toggle(today - mark * day); - - return habit; - } - - public static void generateHugeDataSet() throws Throwable - { - final int nHabits = 30; - final int nYears = 5; - - DatabaseHelper.executeAsTransaction(new DatabaseHelper.Command() - { - @Override - public void execute() - { - Random rand = new Random(); - - for(int i = 0; i < nHabits; i++) - { - Log.i("HabitFixture", String.format("Creating habit %d / %d", i, nHabits)); - - Habit habit = new Habit(); - habit.name = String.format("Habit %d", i); - habit.save(); - - long today = DateHelper.getStartOfToday(); - long day = DateHelper.millisecondsInOneDay; - - - for(int j = 0; j < 365 * nYears; j++) - { - if(rand.nextBoolean()) - habit.repetitions.toggle(today - j * day); - } - - habit.scores.getTodayValue(); - habit.streaks.getAll(1); - } - } - }); - - ExportDBTask task = new ExportDBTask(null); - task.setListener(new ExportDBTask.Listener() - { - @Override - public void onExportDBFinished(@Nullable String filename) - { - if(filename != null) - Log.i("HabitFixture", String.format("Huge data set exported to %s", filename)); - else - Log.i("HabitFixture", "Failed to save database"); - } - }); - task.execute(); - - BaseTask.waitForTasks(30000); - } - - public static void loadHugeDataSet(Context testContext) throws Throwable - { - File baseDir = DatabaseHelper.getFilesDir("Backups"); - if(baseDir == null) fail("baseDir should not be null"); - - File dst = new File(String.format("%s/%s", baseDir.getPath(), "loopHuge.db")); - InputStream in = testContext.getAssets().open("fixtures/loopHuge.db"); - DatabaseHelper.copy(in, dst); - - ImportDataTask task = new ImportDataTask(dst, null); - task.execute(); - - BaseTask.waitForTasks(30000); - } - - public static void purgeHabits() - { - for(Habit h : Habit.getAll(true)) - h.cascadeDelete(); - } -} diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/commands/EditHabitCommandTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/commands/EditHabitCommandTest.java deleted file mode 100644 index 94d765870..000000000 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/commands/EditHabitCommandTest.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (C) 2016 Á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.unit.commands; - -import android.support.test.runner.AndroidJUnit4; -import android.test.suitebuilder.annotation.SmallTest; - -import org.isoron.uhabits.BaseTest; -import org.isoron.uhabits.commands.EditHabitCommand; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.unit.HabitFixtures; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import static junit.framework.Assert.assertTrue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.greaterThan; - -@RunWith(AndroidJUnit4.class) -@SmallTest -public class EditHabitCommandTest extends BaseTest -{ - - private EditHabitCommand command; - private Habit habit; - private Habit modified; - private Long id; - - @Before - public void setup() - { - super.setup(); - - habit = HabitFixtures.createShortHabit(); - habit.name = "original"; - habit.freqDen = 1; - habit.freqNum = 1; - habit.save(); - - id = habit.getId(); - - modified = new Habit(habit); - modified.name = "modified"; - } - - @Test - public void testExecuteUndoRedo() - { - command = new EditHabitCommand(habit, modified); - - int originalScore = habit.scores.getTodayValue(); - assertThat(habit.name, equalTo("original")); - - command.execute(); - refreshHabit(); - assertThat(habit.name, equalTo("modified")); - assertThat(habit.scores.getTodayValue(), equalTo(originalScore)); - - command.undo(); - refreshHabit(); - assertThat(habit.name, equalTo("original")); - assertThat(habit.scores.getTodayValue(), equalTo(originalScore)); - - command.execute(); - refreshHabit(); - assertThat(habit.name, equalTo("modified")); - assertThat(habit.scores.getTodayValue(), equalTo(originalScore)); - } - - @Test - public void testExecuteUndoRedo_withModifiedInterval() - { - modified.freqNum = 1; - modified.freqDen = 7; - command = new EditHabitCommand(habit, modified); - - int originalScore = habit.scores.getTodayValue(); - assertThat(habit.name, equalTo("original")); - - command.execute(); - refreshHabit(); - assertThat(habit.name, equalTo("modified")); - assertThat(habit.scores.getTodayValue(), greaterThan(originalScore)); - - command.undo(); - refreshHabit(); - assertThat(habit.name, equalTo("original")); - assertThat(habit.scores.getTodayValue(), equalTo(originalScore)); - - command.execute(); - refreshHabit(); - assertThat(habit.name, equalTo("modified")); - assertThat(habit.scores.getTodayValue(), greaterThan(originalScore)); - } - - private void refreshHabit() - { - habit = Habit.get(id); - assertTrue(habit != null); - } -} diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/models/CheckmarkListTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/models/CheckmarkListTest.java deleted file mode 100644 index 5763892b8..000000000 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/models/CheckmarkListTest.java +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright (C) 2016 Á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.unit.models; - -import android.support.test.runner.AndroidJUnit4; -import android.test.suitebuilder.annotation.SmallTest; - -import org.isoron.uhabits.BaseTest; -import org.isoron.uhabits.helpers.DateHelper; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.unit.HabitFixtures; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.io.IOException; -import java.io.StringWriter; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.isoron.uhabits.models.Checkmark.CHECKED_EXPLICITLY; -import static org.isoron.uhabits.models.Checkmark.CHECKED_IMPLICITLY; -import static org.isoron.uhabits.models.Checkmark.UNCHECKED; - -@RunWith(AndroidJUnit4.class) -@SmallTest -public class CheckmarkListTest extends BaseTest -{ - Habit nonDailyHabit; - private Habit emptyHabit; - - @Before - public void setup() - { - super.setup(); - - HabitFixtures.purgeHabits(); - nonDailyHabit = HabitFixtures.createShortHabit(); - emptyHabit = HabitFixtures.createEmptyHabit(); - } - - @After - public void tearDown() - { - DateHelper.setFixedLocalTime(null); - } - - @Test - public void test_getAllValues_withNonDailyHabit() - { - int[] expectedValues = { CHECKED_EXPLICITLY, UNCHECKED, CHECKED_IMPLICITLY, - CHECKED_EXPLICITLY, CHECKED_EXPLICITLY, CHECKED_EXPLICITLY, UNCHECKED, - CHECKED_IMPLICITLY, CHECKED_EXPLICITLY, CHECKED_EXPLICITLY }; - - int[] actualValues = nonDailyHabit.checkmarks.getAllValues(); - - assertThat(actualValues, equalTo(expectedValues)); - } - - @Test - public void test_getAllValues_withEmptyHabit() - { - int[] expectedValues = new int[0]; - int[] actualValues = emptyHabit.checkmarks.getAllValues(); - - assertThat(actualValues, equalTo(expectedValues)); - } - - @Test - public void test_getAllValues_moveForwardInTime() - { - travelInTime(3); - - int[] expectedValues = { UNCHECKED, UNCHECKED, UNCHECKED, CHECKED_EXPLICITLY, UNCHECKED, - CHECKED_IMPLICITLY, CHECKED_EXPLICITLY, CHECKED_EXPLICITLY, CHECKED_EXPLICITLY, - UNCHECKED, CHECKED_IMPLICITLY, CHECKED_EXPLICITLY, CHECKED_EXPLICITLY }; - - int[] actualValues = nonDailyHabit.checkmarks.getAllValues(); - - assertThat(actualValues, equalTo(expectedValues)); - } - - @Test - public void test_getAllValues_moveBackwardsInTime() - { - travelInTime(-3); - - int[] expectedValues = { CHECKED_EXPLICITLY, CHECKED_EXPLICITLY, CHECKED_EXPLICITLY, - UNCHECKED, CHECKED_IMPLICITLY, CHECKED_EXPLICITLY, CHECKED_EXPLICITLY }; - - int[] actualValues = nonDailyHabit.checkmarks.getAllValues(); - - assertThat(actualValues, equalTo(expectedValues)); - } - - @Test - public void test_getValues_withInvalidInterval() - { - int values[] = nonDailyHabit.checkmarks.getValues(100L, -100L); - assertThat(values, equalTo(new int[0])); - } - - @Test - public void test_getValues_withValidInterval() - { - long from = DateHelper.getStartOfToday() - 15 * DateHelper.millisecondsInOneDay; - long to = DateHelper.getStartOfToday() - 5 * DateHelper.millisecondsInOneDay; - - int[] expectedValues = { CHECKED_EXPLICITLY, UNCHECKED, CHECKED_IMPLICITLY, - CHECKED_EXPLICITLY, CHECKED_EXPLICITLY, UNCHECKED, UNCHECKED, UNCHECKED, UNCHECKED, - UNCHECKED, UNCHECKED }; - - int[] actualValues = nonDailyHabit.checkmarks.getValues(from, to); - - assertThat(actualValues, equalTo(expectedValues)); - } - - @Test - public void test_getTodayValue() - { - travelInTime(-1); - assertThat(nonDailyHabit.checkmarks.getTodayValue(), equalTo(UNCHECKED)); - - travelInTime(0); - assertThat(nonDailyHabit.checkmarks.getTodayValue(), equalTo(CHECKED_EXPLICITLY)); - - travelInTime(1); - assertThat(nonDailyHabit.checkmarks.getTodayValue(), equalTo(UNCHECKED)); - } - - @Test - public void test_writeCSV() throws IOException - { - String expectedCSV = - "2015-01-16,2\n" + - "2015-01-17,2\n" + - "2015-01-18,1\n" + - "2015-01-19,0\n" + - "2015-01-20,2\n" + - "2015-01-21,2\n" + - "2015-01-22,2\n" + - "2015-01-23,1\n" + - "2015-01-24,0\n" + - "2015-01-25,2\n"; - - StringWriter writer = new StringWriter(); - nonDailyHabit.checkmarks.writeCSV(writer); - - assertThat(writer.toString(), equalTo(expectedCSV)); - } - - private void travelInTime(int days) - { - DateHelper.setFixedLocalTime(FIXED_LOCAL_TIME + - days * DateHelper.millisecondsInOneDay); - } -} diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitTest.java deleted file mode 100644 index 0d34272ec..000000000 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitTest.java +++ /dev/null @@ -1,378 +0,0 @@ -/* - * Copyright (C) 2016 Á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.unit.models; - -import android.support.test.runner.AndroidJUnit4; -import android.test.suitebuilder.annotation.SmallTest; - -import org.hamcrest.MatcherAssert; -import org.isoron.uhabits.BaseTest; -import org.isoron.uhabits.helpers.DateHelper; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.unit.HabitFixtures; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.io.IOException; -import java.io.StringWriter; -import java.util.LinkedList; -import java.util.List; - -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; -import static org.hamcrest.core.IsNot.not; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.fail; - -@RunWith(AndroidJUnit4.class) -@SmallTest -public class HabitTest extends BaseTest -{ - @Before - public void setup() - { - super.setup(); - HabitFixtures.purgeHabits(); - } - - @Test - public void testConstructor_default() - { - Habit habit = new Habit(); - assertThat(habit.archived, is(0)); - assertThat(habit.highlight, is(0)); - - assertThat(habit.reminderHour, is(nullValue())); - assertThat(habit.reminderMin, is(nullValue())); - - assertThat(habit.reminderDays, is(not(nullValue()))); - assertThat(habit.streaks, is(not(nullValue()))); - assertThat(habit.scores, is(not(nullValue()))); - assertThat(habit.repetitions, is(not(nullValue()))); - assertThat(habit.checkmarks, is(not(nullValue()))); - } - - @Test - public void testConstructor_habit() - { - Habit model = new Habit(); - model.archived = 1; - model.highlight = 1; - model.color = 0; - model.freqNum = 10; - model.freqDen = 20; - model.reminderDays = 1; - model.reminderHour = 8; - model.reminderMin = 30; - model.position = 0; - - Habit habit = new Habit(model); - assertThat(habit.archived, is(model.archived)); - assertThat(habit.highlight, is(model.highlight)); - assertThat(habit.color, is(model.color)); - assertThat(habit.freqNum, is(model.freqNum)); - assertThat(habit.freqDen, is(model.freqDen)); - assertThat(habit.reminderDays, is(model.reminderDays)); - assertThat(habit.reminderHour, is(model.reminderHour)); - assertThat(habit.reminderMin, is(model.reminderMin)); - assertThat(habit.position, is(model.position)); - } - - @Test - public void test_get_withValidId() - { - Habit habit = new Habit(); - habit.save(); - - Habit habit2 = Habit.get(habit.getId()); - assertThat(habit, equalTo(habit2)); - } - - @Test - public void test_get_withInvalidId() - { - Habit habit = Habit.get(123456L); - assertThat(habit, is(nullValue())); - } - - @Test - public void test_getAll_withoutArchived() - { - List habits = new LinkedList<>(); - List habitsWithArchived = new LinkedList<>(); - - for(int i = 0; i < 10; i++) - { - Habit h = new Habit(); - - if(i % 2 == 0) - h.archived = 1; - else - habits.add(h); - - habitsWithArchived.add(h); - h.save(); - } - - assertThat(habits, equalTo(Habit.getAll(false))); - assertThat(habitsWithArchived, equalTo(Habit.getAll(true))); - } - - @Test - public void test_getByPosition() - { - List habits = new LinkedList<>(); - - for(int i = 0; i < 10; i++) - { - Habit h = new Habit(); - h.save(); - habits.add(h); - } - - for(int i = 0; i < 10; i++) - { - Habit h = Habit.getByPosition(i); - if(h == null) fail(); - assertThat(h, equalTo(habits.get(i))); - } - } - - @Test - public void test_count() - { - for(int i = 0; i < 10; i++) - { - Habit h = new Habit(); - if(i % 2 == 0) h.archived = 1; - h.save(); - } - - assertThat(Habit.count(), equalTo(5)); - } - - - @Test - public void test_countWithArchived() - { - for(int i = 0; i < 10; i++) - { - Habit h = new Habit(); - if(i % 2 == 0) h.archived = 1; - h.save(); - } - - assertThat(Habit.countWithArchived(), equalTo(10)); - } - - @Test - public void test_updateId() - { - Habit habit = new Habit(); - habit.name = "Hello World"; - habit.save(); - - Long oldId = habit.getId(); - Long newId = 123456L; - Habit.updateId(oldId, newId); - - Habit newHabit = Habit.get(newId); - if(newHabit == null) fail(); - assertThat(newHabit, is(not(nullValue()))); - assertThat(newHabit.name, equalTo(habit.name)); - } - - @Test - public void test_reorder() - { - List ids = new LinkedList<>(); - - int n = 10; - for (int i = 0; i < n; i++) - { - Habit h = new Habit(); - h.save(); - ids.add(h.getId()); - assertThat(h.position, is(i)); - } - - int operations[][] = { - {5, 2}, - {3, 7}, - {4, 4}, - {3, 2} - }; - - int expectedPosition[][] = { - {0, 1, 3, 4, 5, 2, 6, 7, 8, 9}, - {0, 1, 7, 3, 4, 2, 5, 6, 8, 9}, - {0, 1, 7, 3, 4, 2, 5, 6, 8, 9}, - {0, 1, 7, 2, 4, 3, 5, 6, 8, 9}, - }; - - for(int i = 0; i < operations.length; i++) - { - int from = operations[i][0]; - int to = operations[i][1]; - - Habit fromHabit = Habit.getByPosition(from); - Habit toHabit = Habit.getByPosition(to); - Habit.reorder(fromHabit, toHabit); - - int actualPositions[] = new int[n]; - - for (int j = 0; j < n; j++) - { - Habit h = Habit.get(ids.get(j)); - if (h == null) fail(); - actualPositions[j] = h.position; - } - - assertThat(actualPositions, equalTo(expectedPosition[i])); - } - } - - @Test - public void test_rebuildOrder() - { - List ids = new LinkedList<>(); - int originalPositions[] = { 0, 1, 1, 4, 6, 8, 10, 10, 13}; - - for (int p : originalPositions) - { - Habit h = new Habit(); - h.position = p; - h.save(); - ids.add(h.getId()); - } - - Habit.rebuildOrder(); - - for (int i = 0; i < originalPositions.length; i++) - { - Habit h = Habit.get(ids.get(i)); - if(h == null) fail(); - assertThat(h.position, is(i)); - } - } - - @Test - public void test_getHabitsWithReminder() - { - List habitsWithReminder = new LinkedList<>(); - - for(int i = 0; i < 10; i++) - { - Habit habit = new Habit(); - if(i % 2 == 0) - { - habit.reminderDays = DateHelper.ALL_WEEK_DAYS; - habit.reminderHour = 8; - habit.reminderMin = 30; - habitsWithReminder.add(habit); - } - habit.save(); - } - - assertThat(habitsWithReminder, equalTo(Habit.getHabitsWithReminder())); - } - - @Test - public void test_archive_unarchive() - { - List allHabits = new LinkedList<>(); - List archivedHabits = new LinkedList<>(); - List unarchivedHabits = new LinkedList<>(); - - for(int i = 0; i < 10; i++) - { - Habit habit = new Habit(); - habit.save(); - allHabits.add(habit); - - if(i % 2 == 0) - archivedHabits.add(habit); - else - unarchivedHabits.add(habit); - } - - Habit.archive(archivedHabits); - assertThat(Habit.getAll(false), equalTo(unarchivedHabits)); - assertThat(Habit.getAll(true), equalTo(allHabits)); - - Habit.unarchive(archivedHabits); - assertThat(Habit.getAll(false), equalTo(allHabits)); - assertThat(Habit.getAll(true), equalTo(allHabits)); - } - - @Test - public void test_setColor() - { - List habits = new LinkedList<>(); - - for(int i = 0; i < 10; i++) - { - Habit habit = new Habit(); - habit.color = i; - habit.save(); - habits.add(habit); - } - - int newColor = 100; - Habit.setColor(habits, newColor); - - for(Habit h : habits) - assertThat(h.color, equalTo(newColor)); - } - - @Test - public void test_hasReminder_clearReminder() - { - Habit h = new Habit(); - assertThat(h.hasReminder(), is(false)); - - h.reminderDays = DateHelper.ALL_WEEK_DAYS; - h.reminderHour = 8; - h.reminderMin = 30; - assertThat(h.hasReminder(), is(true)); - - h.clearReminder(); - assertThat(h.hasReminder(), is(false)); - } - - @Test - public void test_writeCSV() throws IOException - { - HabitFixtures.createEmptyHabit(); - HabitFixtures.createShortHabit(); - - String expectedCSV = - "Position,Name,Description,NumRepetitions,Interval,Color\n" + - "001,Meditate,Did you meditate this morning?,1,1,#AFB42B\n" + - "002,Wake up early,Did you wake up before 6am?,2,3,#00897B\n"; - - StringWriter writer = new StringWriter(); - Habit.writeCSV(Habit.getAll(true), writer); - - MatcherAssert.assertThat(writer.toString(), equalTo(expectedCSV)); - } -} diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/models/RepetitionListTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/models/RepetitionListTest.java deleted file mode 100644 index 2b152a25f..000000000 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/models/RepetitionListTest.java +++ /dev/null @@ -1,191 +0,0 @@ -/* - * Copyright (C) 2016 Á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.unit.models; - -import android.support.test.runner.AndroidJUnit4; -import android.test.suitebuilder.annotation.SmallTest; - -import org.isoron.uhabits.BaseTest; -import org.isoron.uhabits.helpers.DateHelper; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.models.Repetition; -import org.isoron.uhabits.unit.HabitFixtures; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.util.Arrays; -import java.util.Calendar; -import java.util.GregorianCalendar; -import java.util.HashMap; -import java.util.Random; - -import static junit.framework.Assert.assertFalse; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; - -@RunWith(AndroidJUnit4.class) -@SmallTest -public class RepetitionListTest extends BaseTest -{ - private Habit habit; - private Habit emptyHabit; - - @Before - public void setup() - { - super.setup(); - - HabitFixtures.purgeHabits(); - habit = HabitFixtures.createShortHabit(); - emptyHabit = HabitFixtures.createEmptyHabit(); - } - - @After - public void tearDown() - { - DateHelper.setFixedLocalTime(null); - } - - @Test - public void test_contains() - { - long current = DateHelper.getStartOfToday(); - - for(boolean b : HabitFixtures.NON_DAILY_HABIT_CHECKS) - { - assertThat(habit.repetitions.contains(current), equalTo(b)); - current -= DateHelper.millisecondsInOneDay; - } - - for(int i = 0; i < 3; i++) - { - assertThat(habit.repetitions.contains(current), equalTo(false)); - current -= DateHelper.millisecondsInOneDay; - } - } - - @Test - public void test_delete() - { - long timestamp = DateHelper.getStartOfToday(); - assertThat(habit.repetitions.contains(timestamp), equalTo(true)); - - habit.repetitions.delete(timestamp); - assertThat(habit.repetitions.contains(timestamp), equalTo(false)); - } - - @Test - public void test_toggle() - { - long timestamp = DateHelper.getStartOfToday(); - assertThat(habit.repetitions.contains(timestamp), equalTo(true)); - - habit.repetitions.toggle(timestamp); - assertThat(habit.repetitions.contains(timestamp), equalTo(false)); - - habit.repetitions.toggle(timestamp); - assertThat(habit.repetitions.contains(timestamp), equalTo(true)); - } - - @Test - public void test_getWeekDayFrequency() - { - Random random = new Random(); - Integer weekdayCount[][] = new Integer[12][7]; - Integer monthCount[] = new Integer[12]; - - Arrays.fill(monthCount, 0); - for(Integer row[] : weekdayCount) - Arrays.fill(row, 0); - - GregorianCalendar day = DateHelper.getStartOfTodayCalendar(); - - // Sets the current date to the end of November - day.set(2015, 10, 30); - DateHelper.setFixedLocalTime(day.getTimeInMillis()); - - // Add repetitions randomly from January to December - // Leaves the month of March empty, to check that it returns null - day.set(2015, 0, 1); - for(int i = 0; i < 365; i ++) - { - if(random.nextBoolean()) - { - int month = day.get(Calendar.MONTH); - int week = day.get(Calendar.DAY_OF_WEEK) % 7; - - if(month != 2) - { - if (month <= 10) - { - weekdayCount[month][week]++; - monthCount[month]++; - } - emptyHabit.repetitions.toggle(day.getTimeInMillis()); - } - } - - day.add(Calendar.DAY_OF_YEAR, 1); - } - - HashMap freq = emptyHabit.repetitions.getWeekdayFrequency(); - - // Repetitions until November should be counted correctly - for(int month = 0; month < 11; month++) - { - day.set(2015, month, 1); - Integer actualCount[] = freq.get(day.getTimeInMillis()); - if(monthCount[month] == 0) - assertThat(actualCount, equalTo(null)); - else - assertThat(actualCount, equalTo(weekdayCount[month])); - } - - // Repetitions in December should be discarded - day.set(2015, 11, 1); - assertThat(freq.get(day.getTimeInMillis()), equalTo(null)); - } - - @Test - public void test_count() - { - long to = DateHelper.getStartOfToday(); - long from = to - 9 * DateHelper.millisecondsInOneDay; - assertThat(habit.repetitions.count(from, to), equalTo(6)); - - to = DateHelper.getStartOfToday() - DateHelper.millisecondsInOneDay; - from = to - 5 * DateHelper.millisecondsInOneDay; - assertThat(habit.repetitions.count(from, to), equalTo(3)); - } - - @Test - public void test_getOldest() - { - long expectedOldestTimestamp = DateHelper.getStartOfToday() - 9 * DateHelper.millisecondsInOneDay; - - assertThat(habit.repetitions.getOldestTimestamp(), equalTo(expectedOldestTimestamp)); - - Repetition oldest = habit.repetitions.getOldest(); - assertFalse(oldest == null); - assertThat(oldest.timestamp, equalTo(expectedOldestTimestamp)); - } -} diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreListTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreListTest.java deleted file mode 100644 index 25e99cffc..000000000 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreListTest.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright (C) 2016 Á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.unit.models; - -import android.support.test.runner.AndroidJUnit4; -import android.test.suitebuilder.annotation.SmallTest; - -import org.isoron.uhabits.BaseTest; -import org.isoron.uhabits.helpers.DatabaseHelper; -import org.isoron.uhabits.helpers.DateHelper; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.models.Score; -import org.isoron.uhabits.unit.HabitFixtures; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.io.IOException; -import java.io.StringWriter; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; - -@RunWith(AndroidJUnit4.class) -@SmallTest -public class ScoreListTest extends BaseTest -{ - private Habit habit; - - @Before - public void setup() - { - super.setup(); - - HabitFixtures.purgeHabits(); - habit = HabitFixtures.createEmptyHabit(); - } - - @After - public void tearDown() - { - DateHelper.setFixedLocalTime(null); - } - - @Test - public void test_invalidateNewerThan() - { - assertThat(habit.scores.getTodayValue(), equalTo(0)); - - toggleRepetitions(0, 2); - assertThat(habit.scores.getTodayValue(), equalTo(1948077)); - - habit.freqNum = 1; - habit.freqDen = 2; - habit.scores.invalidateNewerThan(0); - - assertThat(habit.scores.getTodayValue(), equalTo(1974654)); - } - - @Test - public void test_getTodayStarValue() - { - assertThat(habit.scores.getTodayStarStatus(), equalTo(Score.EMPTY_STAR)); - - int k = 0; - while(habit.scores.getTodayValue() < Score.HALF_STAR_CUTOFF) toggleRepetitions(k, ++k); - assertThat(habit.scores.getTodayStarStatus(), equalTo(Score.HALF_STAR)); - - while(habit.scores.getTodayValue() < Score.FULL_STAR_CUTOFF) toggleRepetitions(k, ++k); - assertThat(habit.scores.getTodayStarStatus(), equalTo(Score.FULL_STAR)); - } - - @Test - public void test_getTodayValue() - { - toggleRepetitions(0, 20); - assertThat(habit.scores.getTodayValue(), equalTo(12629351)); - } - - @Test - public void test_getValue() - { - toggleRepetitions(0, 20); - - int expectedValues[] = { 12629351, 12266245, 11883254, 11479288, 11053198, 10603773, - 10129735, 9629735, 9102352, 8546087, 7959357, 7340494, 6687738, 5999234, 5273023, - 4507040, 3699107, 2846927, 1948077, 1000000 }; - - long current = DateHelper.getStartOfToday(); - for(int expectedValue : expectedValues) - { - assertThat(habit.scores.getValue(current), equalTo(expectedValue)); - current -= DateHelper.millisecondsInOneDay; - } - } - - @Test - public void test_getAllValues_withoutGroups() - { - toggleRepetitions(0, 20); - - int expectedValues[] = { 12629351, 12266245, 11883254, 11479288, 11053198, 10603773, - 10129735, 9629735, 9102352, 8546087, 7959357, 7340494, 6687738, 5999234, 5273023, - 4507040, 3699107, 2846927, 1948077, 1000000 }; - - int actualValues[] = habit.scores.getAllValues(1); - assertThat(actualValues, equalTo(expectedValues)); - } - - @Test - public void test_getAllValues_withGroups() - { - toggleRepetitions(0, 20); - - int expectedValues[] = { 11434978, 7894999, 3212362 }; - - int actualValues[] = habit.scores.getAllValues(7); - assertThat(actualValues, equalTo(expectedValues)); - } - - @Test - public void test_writeCSV() throws IOException - { - HabitFixtures.purgeHabits(); - Habit habit = HabitFixtures.createShortHabit(); - - String expectedCSV = - "2015-01-16,0.0519\n" + - "2015-01-17,0.1021\n" + - "2015-01-18,0.0986\n" + - "2015-01-19,0.0952\n" + - "2015-01-20,0.1439\n" + - "2015-01-21,0.1909\n" + - "2015-01-22,0.2364\n" + - "2015-01-23,0.2283\n" + - "2015-01-24,0.2205\n" + - "2015-01-25,0.2649\n"; - - StringWriter writer = new StringWriter(); - habit.scores.writeCSV(writer); - - assertThat(writer.toString(), equalTo(expectedCSV)); - } - - private void toggleRepetitions(final int from, final int to) - { - DatabaseHelper.executeAsTransaction(new DatabaseHelper.Command() - { - @Override - public void execute() - { - long today = DateHelper.getStartOfToday(); - for (int i = from; i < to; i++) - habit.repetitions.toggle(today - i * DateHelper.millisecondsInOneDay); - } - }); - } -} diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/tasks/ExportCSVTaskTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/tasks/ExportCSVTaskTest.java deleted file mode 100644 index f827dddf4..000000000 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/tasks/ExportCSVTaskTest.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (C) 2016 Á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.unit.tasks; - -import android.support.test.runner.AndroidJUnit4; -import android.test.suitebuilder.annotation.SmallTest; -import android.widget.ProgressBar; - -import org.isoron.uhabits.BaseTest; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.tasks.ExportCSVTask; -import org.isoron.uhabits.unit.HabitFixtures; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.io.File; -import java.util.List; - -import static junit.framework.Assert.assertTrue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; -import static org.hamcrest.core.IsNot.not; - -@RunWith(AndroidJUnit4.class) -@SmallTest -public class ExportCSVTaskTest extends BaseTest -{ - @Before - public void setup() - { - super.setup(); - } - - @Test - public void testExportCSV() throws Throwable - { - HabitFixtures.createShortHabit(); - List habits = Habit.getAll(true); - ProgressBar bar = new ProgressBar(targetContext); - - ExportCSVTask task = new ExportCSVTask(habits, bar); - task.setListener(new ExportCSVTask.Listener() - { - @Override - public void onExportCSVFinished(String archiveFilename) - { - assertThat(archiveFilename, is(not(nullValue()))); - - File f = new File(archiveFilename); - assertTrue(f.exists()); - assertTrue(f.canRead()); - } - }); - - task.execute(); - waitForAsyncTasks(); - } -} diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/views/CheckmarkWidgetViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/views/CheckmarkWidgetViewTest.java deleted file mode 100644 index 874c8666f..000000000 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/views/CheckmarkWidgetViewTest.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (C) 2016 Á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.unit.views; - -import android.support.test.runner.AndroidJUnit4; -import android.test.suitebuilder.annotation.SmallTest; - -import org.isoron.uhabits.R; -import org.isoron.uhabits.helpers.DateHelper; -import org.isoron.uhabits.helpers.UIHelper; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.unit.HabitFixtures; -import org.isoron.uhabits.views.CheckmarkWidgetView; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.io.IOException; - -@RunWith(AndroidJUnit4.class) -@SmallTest -public class CheckmarkWidgetViewTest extends ViewTest -{ - private CheckmarkWidgetView view; - private Habit habit; - - @Before - public void setup() - { - super.setup(); - UIHelper.setFixedTheme(R.style.TransparentWidgetTheme); - - habit = HabitFixtures.createShortHabit(); - view = new CheckmarkWidgetView(targetContext); - view.setHabit(habit); - refreshData(view); - measureView(dpToPixels(100), dpToPixels(200), view); - } - - @Test - public void testRender_checked() throws IOException - { - assertRenders(view, "CheckmarkView/checked.png"); - } - - @Test - public void testRender_unchecked() throws IOException - { - habit.repetitions.toggle(DateHelper.getStartOfToday()); - view.refreshData(); - - assertRenders(view, "CheckmarkView/unchecked.png"); - } - - @Test - public void testRender_implicitlyChecked() throws IOException - { - long today = DateHelper.getStartOfToday(); - long day = DateHelper.millisecondsInOneDay; - habit.repetitions.toggle(today); - habit.repetitions.toggle(today - day); - habit.repetitions.toggle(today - 2 * day); - view.refreshData(); - - assertRenders(view, "CheckmarkView/implicitly_checked.png"); - } - - @Test - public void testRender_largeSize() throws IOException - { - measureView(dpToPixels(300), dpToPixels(300), view); - assertRenders(view, "CheckmarkView/large_size.png"); - } -} diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/views/HabitHistoryViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/views/HabitHistoryViewTest.java deleted file mode 100644 index ea8f0f54f..000000000 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/views/HabitHistoryViewTest.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (C) 2016 Á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.unit.views; - -import android.support.test.runner.AndroidJUnit4; -import android.test.suitebuilder.annotation.SmallTest; - -import org.isoron.uhabits.helpers.DateHelper; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.unit.HabitFixtures; -import org.isoron.uhabits.views.HabitHistoryView; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import static junit.framework.Assert.assertFalse; -import static junit.framework.Assert.assertTrue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; - -@RunWith(AndroidJUnit4.class) -@SmallTest -public class HabitHistoryViewTest extends ViewTest -{ - private Habit habit; - private HabitHistoryView view; - - @Before - public void setup() - { - super.setup(); - - HabitFixtures.purgeHabits(); - habit = HabitFixtures.createLongHabit(); - - view = new HabitHistoryView(targetContext); - view.setHabit(habit); - measureView(dpToPixels(400), dpToPixels(200), view); - refreshData(view); - } - - @Test - public void testRender() throws Throwable - { - assertRenders(view, "HabitHistoryView/render.png"); - } - - @Test - public void testRender_withTransparentBackground() throws Throwable - { - view.setIsBackgroundTransparent(true); - assertRenders(view, "HabitHistoryView/renderTransparent.png"); - } - - @Test - public void testRender_withDifferentSize() throws Throwable - { - measureView(dpToPixels(200), dpToPixels(200), view); - assertRenders(view, "HabitHistoryView/renderDifferentSize.png"); - } - - @Test - public void testRender_withDataOffset() throws Throwable - { - view.onScroll(null, null, -dpToPixels(150), 0); - view.invalidate(); - - assertRenders(view, "HabitHistoryView/renderDataOffset.png"); - } - - @Test - public void tapDate_withEditableView() throws Throwable - { - view.setIsEditable(true); - tap(view, 340, 40); // today's square - waitForAsyncTasks(); - - long today = DateHelper.getStartOfToday(); - assertFalse(habit.repetitions.contains(today)); - } - - @Test - public void tapDate_atInvalidLocations() throws Throwable - { - int expectedCheckmarkValues[] = habit.checkmarks.getAllValues(); - - view.setIsEditable(true); - tap(view, 118, 13); // header - tap(view, 336, 60); // tomorrow's square - tap(view, 370, 60); // right axis - waitForAsyncTasks(); - - int actualCheckmarkValues[] = habit.checkmarks.getAllValues(); - assertThat(actualCheckmarkValues, equalTo(expectedCheckmarkValues)); - } - - @Test - public void tapDate_withReadOnlyView() throws Throwable - { - view.setIsEditable(false); - tap(view, 340, 40); // today's square - waitForAsyncTasks(); - - long today = DateHelper.getStartOfToday(); - assertTrue(habit.repetitions.contains(today)); - } - -} diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/views/NumberViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/views/NumberViewTest.java deleted file mode 100644 index a8f371af6..000000000 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/views/NumberViewTest.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (C) 2016 Á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.unit.views; - -import android.support.test.runner.AndroidJUnit4; -import android.test.suitebuilder.annotation.SmallTest; - -import org.isoron.uhabits.R; -import org.isoron.uhabits.helpers.ColorHelper; -import org.isoron.uhabits.views.NumberView; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.io.IOException; - -@RunWith(AndroidJUnit4.class) -@SmallTest -public class NumberViewTest extends ViewTest -{ - private NumberView view; - - @Before - public void setup() - { - super.setup(); - - view = new NumberView(targetContext); - view.setLabel("Hello world"); - view.setNumber(31); - view.setColor(ColorHelper.CSV_PALETTE[0]); - measureView(dpToPixels(100), dpToPixels(100), view); - } - - @Test - public void testRender_base() throws IOException - { - assertRenders(view, "NumberView/render.png"); - } - - @Test - public void testRender_withLongLabel() throws IOException - { - view.setLabel("The quick brown fox jumps over the lazy fox"); - - measureView(dpToPixels(100), dpToPixels(100), view); - assertRenders(view, "NumberView/renderLongLabel.png"); - } - - @Test - public void testRender_withDifferentParams() throws IOException - { - view.setNumber(500); - view.setColor(ColorHelper.CSV_PALETTE[5]); - view.setTextSize(targetContext.getResources().getDimension(R.dimen.tinyTextSize)); - - measureView(dpToPixels(200), dpToPixels(200), view); - assertRenders(view, "NumberView/renderDifferentParams.png"); - } -} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 12e827336..b2a07390a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -60,28 +60,28 @@ + android:value=".MainActivity"/> + android:value=".MainActivity"/> @@ -89,7 +89,7 @@ + * + * 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; + +import javax.inject.Singleton; + +import dagger.Component; + +/** + * Dependency injection component for classes that are specific to Android. + */ +@Singleton +@Component(modules = {AndroidModule.class}) +public interface AndroidComponent extends BaseComponent +{ +} + diff --git a/app/src/main/java/org/isoron/uhabits/AndroidModule.java b/app/src/main/java/org/isoron/uhabits/AndroidModule.java new file mode 100644 index 000000000..3a371d2e8 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/AndroidModule.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2016 Á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; + +import org.isoron.uhabits.commands.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.models.sqlite.*; +import org.isoron.uhabits.utils.*; + +import javax.inject.*; + +import dagger.*; + +/** + * Module that provides dependencies when the application is running on + * Android. + *

+ * This module is also used for instrumented tests. + */ +@Module +public class AndroidModule +{ + @Provides + @Singleton + CommandRunner provideCommandRunner() + { + return new CommandRunner(); + } + + @Provides + @Singleton + HabitList provideHabitList() + { + return SQLiteHabitList.getInstance(); + } + + @Provides + ModelFactory provideModelFactory() + { + return new SQLModelFactory(); + } + + @Provides + @Singleton + Preferences providePreferences() + { + return new Preferences(); + } + + @Provides + @Singleton + WidgetPreferences provideWidgetPreferences() + { + return new WidgetPreferences(); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/BaseActivity.java b/app/src/main/java/org/isoron/uhabits/BaseActivity.java deleted file mode 100644 index 6d59a00d0..000000000 --- a/app/src/main/java/org/isoron/uhabits/BaseActivity.java +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Copyright (C) 2016 Á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; - -import android.app.backup.BackupManager; -import android.graphics.Color; -import android.graphics.drawable.ColorDrawable; -import android.os.AsyncTask; -import android.os.Build; -import android.os.Bundle; -import android.support.v7.app.ActionBar; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.Toolbar; -import android.view.View; -import android.widget.Toast; - -import org.isoron.uhabits.commands.Command; -import org.isoron.uhabits.helpers.ColorHelper; -import org.isoron.uhabits.helpers.UIHelper; - -import java.util.LinkedList; - -abstract public class BaseActivity extends AppCompatActivity implements Thread.UncaughtExceptionHandler -{ - private static int MAX_UNDO_LEVEL = 15; - - private LinkedList undoList; - private LinkedList redoList; - private Toast toast; - - Thread.UncaughtExceptionHandler androidExceptionHandler; - - @Override - protected void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - - UIHelper.applyCurrentTheme(this); - - androidExceptionHandler = Thread.getDefaultUncaughtExceptionHandler(); - Thread.setDefaultUncaughtExceptionHandler(this); - - undoList = new LinkedList<>(); - redoList = new LinkedList<>(); - } - - public void executeCommand(Command command, Long refreshKey) - { - executeCommand(command, false, refreshKey); - } - - protected void undo() - { - if (undoList.isEmpty()) - { - showToast(R.string.toast_nothing_to_undo); - return; - } - - Command last = undoList.pop(); - redoList.push(last); - last.undo(); - showToast(last.getUndoStringId()); - } - - protected void redo() - { - if (redoList.isEmpty()) - { - showToast(R.string.toast_nothing_to_redo); - return; - } - Command last = redoList.pop(); - executeCommand(last, false, null); - } - - public void showToast(Integer stringId) - { - if (stringId == null) return; - if (toast == null) toast = Toast.makeText(this, stringId, Toast.LENGTH_SHORT); - else toast.setText(stringId); - toast.show(); - } - - public void executeCommand(final Command command, Boolean clearRedoStack, final Long refreshKey) - { - undoList.push(command); - - if (undoList.size() > MAX_UNDO_LEVEL) undoList.removeLast(); - if (clearRedoStack) redoList.clear(); - - new AsyncTask() - { - @Override - protected Void doInBackground(Void... params) - { - command.execute(); - return null; - } - - @Override - protected void onPostExecute(Void aVoid) - { - BaseActivity.this.onPostExecuteCommand(refreshKey); - BackupManager.dataChanged("org.isoron.uhabits"); - } - }.execute(); - - - showToast(command.getExecuteStringId()); - } - - protected void setupSupportActionBar(boolean homeButtonEnabled) - { - Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); - if(toolbar == null) return; - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) - toolbar.setElevation(UIHelper.dpToPixels(this, 2)); - - setSupportActionBar(toolbar); - - ActionBar actionBar = getSupportActionBar(); - if(actionBar == null) return; - - if(homeButtonEnabled) - actionBar.setDisplayHomeAsUpEnabled(true); - } - - public void onPostExecuteCommand(Long refreshKey) - { - } - - @Override - public void uncaughtException(Thread thread, Throwable ex) - { - try - { - ex.printStackTrace(); - HabitsApplication.dumpBugReportToFile(); - } - catch(Exception e) - { - // ignored - } - - if(androidExceptionHandler != null) - androidExceptionHandler.uncaughtException(thread, ex); - else - System.exit(1); - } - - protected void setupActionBarColor(int color) - { - ActionBar actionBar = getSupportActionBar(); - if(actionBar == null) return; - - if (!UIHelper.getStyledBoolean(this, R.attr.useHabitColorAsPrimary)) return; - - ColorDrawable drawable = new ColorDrawable(color); - actionBar.setBackgroundDrawable(drawable); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) - { - int darkerColor = ColorHelper.mixColors(color, Color.BLACK, 0.75f); - getWindow().setStatusBarColor(darkerColor); - } - } - - @Override - protected void onPostResume() - { - super.onPostResume(); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) - hideFakeToolbarShadow(); - } - - protected void hideFakeToolbarShadow() - { - View view = findViewById(R.id.toolbarShadow); - if(view != null) view.setVisibility(View.GONE); - - view = findViewById(R.id.headerShadow); - if(view != null) view.setVisibility(View.GONE); - } -} diff --git a/app/src/main/java/org/isoron/uhabits/BaseComponent.java b/app/src/main/java/org/isoron/uhabits/BaseComponent.java new file mode 100644 index 000000000..feb93f511 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/BaseComponent.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2016 Á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; + +import org.isoron.uhabits.commands.*; +import org.isoron.uhabits.io.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.tasks.*; +import org.isoron.uhabits.ui.*; +import org.isoron.uhabits.ui.habits.edit.*; +import org.isoron.uhabits.ui.habits.list.*; +import org.isoron.uhabits.ui.habits.list.controllers.*; +import org.isoron.uhabits.ui.habits.list.model.*; +import org.isoron.uhabits.ui.habits.list.views.*; +import org.isoron.uhabits.ui.habits.show.*; +import org.isoron.uhabits.ui.widgets.*; +import org.isoron.uhabits.widgets.*; + +/** + * Base component for dependency injection. + */ +public interface BaseComponent +{ + void inject(CheckmarkButtonController checkmarkButtonController); + + void inject(ListHabitsController listHabitsController); + + void inject(CheckmarkPanelView checkmarkPanelView); + + void inject(ToggleRepetitionTask toggleRepetitionTask); + + void inject(HabitCardListCache habitCardListCache); + + void inject(HabitBroadcastReceiver habitBroadcastReceiver); + + void inject(ListHabitsSelectionMenu listHabitsSelectionMenu); + + void inject(HintList hintList); + + void inject(HabitCardListAdapter habitCardListAdapter); + + void inject(ArchiveHabitsCommand archiveHabitsCommand); + + void inject(ChangeHabitColorCommand changeHabitColorCommand); + + void inject(UnarchiveHabitsCommand unarchiveHabitsCommand); + + void inject(EditHabitCommand editHabitCommand); + + void inject(CreateHabitCommand createHabitCommand); + + void inject(HabitPickerDialog habitPickerDialog); + + void inject(BaseWidgetProvider baseWidgetProvider); + + void inject(ShowHabitActivity showHabitActivity); + + void inject(DeleteHabitsCommand deleteHabitsCommand); + + void inject(ListHabitsActivity listHabitsActivity); + + void inject(BaseSystem baseSystem); + + void inject(HistoryEditorDialog historyEditorDialog); + + void inject(HabitsApplication application); + + void inject(Habit habit); + + void inject(AbstractImporter abstractImporter); + + void inject(HabitsCSVExporter habitsCSVExporter); + + void inject(BaseDialogFragment baseDialogFragment); + + void inject(ShowHabitController showHabitController); + + void inject(BaseWidget baseWidget); + + void inject(WidgetUpdater widgetManager); +} diff --git a/app/src/main/java/org/isoron/uhabits/HabitBroadcastReceiver.java b/app/src/main/java/org/isoron/uhabits/HabitBroadcastReceiver.java index 695f935b4..90f06191a 100644 --- a/app/src/main/java/org/isoron/uhabits/HabitBroadcastReceiver.java +++ b/app/src/main/java/org/isoron/uhabits/HabitBroadcastReceiver.java @@ -19,37 +19,62 @@ package org.isoron.uhabits; -import android.app.Activity; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.BroadcastReceiver; -import android.content.ContentUris; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.graphics.BitmapFactory; -import android.net.Uri; -import android.os.Handler; -import android.preference.PreferenceManager; -import android.support.v4.app.NotificationCompat; -import android.support.v4.app.TaskStackBuilder; -import android.support.v4.content.LocalBroadcastManager; - -import org.isoron.uhabits.helpers.DateHelper; -import org.isoron.uhabits.helpers.ReminderHelper; -import org.isoron.uhabits.models.Checkmark; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.tasks.BaseTask; - -import java.util.Date; - +import android.app.*; +import android.content.*; +import android.graphics.*; +import android.net.*; +import android.os.*; +import android.preference.*; +import android.support.v4.app.*; + +import org.isoron.uhabits.commands.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.tasks.*; +import org.isoron.uhabits.utils.*; + +import java.util.*; + +import javax.inject.*; + +/** + * The Android BroadcastReceiver for Loop Habit Tracker. + *

+ * All broadcast messages are received and processed by this class. + */ public class HabitBroadcastReceiver extends BroadcastReceiver { public static final String ACTION_CHECK = "org.isoron.uhabits.ACTION_CHECK"; - public static final String ACTION_DISMISS = "org.isoron.uhabits.ACTION_DISMISS"; - public static final String ACTION_SHOW_REMINDER = "org.isoron.uhabits.ACTION_SHOW_REMINDER"; - public static final String ACTION_SNOOZE = "org.isoron.uhabits.ACTION_SNOOZE"; + + public static final String ACTION_DISMISS = + "org.isoron.uhabits.ACTION_DISMISS"; + + public static final String ACTION_SHOW_REMINDER = + "org.isoron.uhabits.ACTION_SHOW_REMINDER"; + + public static final String ACTION_SNOOZE = + "org.isoron.uhabits.ACTION_SNOOZE"; + + @Inject + HabitList habitList; + + @Inject + CommandRunner commandRunner; + + public HabitBroadcastReceiver() + { + super(); + HabitsApplication.getComponent().inject(this); + } + + public static void dismissNotification(Context context, Habit habit) + { + NotificationManager notificationManager = + (NotificationManager) context.getSystemService( + Activity.NOTIFICATION_SERVICE); + + int notificationId = (int) (habit.getId() % Integer.MAX_VALUE); + notificationManager.cancel(notificationId); + } @Override public void onReceive(final Context context, Intent intent) @@ -74,81 +99,52 @@ public class HabitBroadcastReceiver extends BroadcastReceiver break; case Intent.ACTION_BOOT_COMPLETED: - ReminderHelper.createReminderAlarms(context); + ReminderUtils.createReminderAlarms(context, habitList); break; } } - private void createReminderAlarmsDelayed(final Context context) - { - new Handler().postDelayed(new Runnable() - { - @Override - public void run() - { - ReminderHelper.createReminderAlarms(context); - } - }, 5000); - } - - private void snoozeHabit(Context context, Intent intent) - { - Uri data = intent.getData(); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - long delayMinutes = Long.parseLong(prefs.getString("pref_snooze_interval", "15")); - - long habitId = ContentUris.parseId(data); - Habit habit = Habit.get(habitId); - if(habit != null) - ReminderHelper.createReminderAlarm(context, habit, - new Date().getTime() + delayMinutes * 60 * 1000); - dismissNotification(context, habitId); - } - private void checkHabit(Context context, Intent intent) { Uri data = intent.getData(); - Long timestamp = intent.getLongExtra("timestamp", DateHelper.getStartOfToday()); + long today = DateUtils.getStartOfToday(); + Long timestamp = intent.getLongExtra("timestamp", today); long habitId = ContentUris.parseId(data); - Habit habit = Habit.get(habitId); - if(habit != null) - habit.repetitions.toggle(timestamp); - dismissNotification(context, habitId); - - sendRefreshBroadcast(context); - } - - public static void sendRefreshBroadcast(Context context) - { - LocalBroadcastManager manager = LocalBroadcastManager.getInstance(context); - Intent refreshIntent = new Intent(MainActivity.ACTION_REFRESH); - manager.sendBroadcast(refreshIntent); + Habit habit = habitList.getById(habitId); + if (habit != null) + { + ToggleRepetitionCommand command = + new ToggleRepetitionCommand(habit, timestamp); + commandRunner.execute(command, habitId); + } - MainActivity.updateWidgets(context); + dismissNotification(context, habitId); } - private void dismissAllHabits() + private boolean checkWeekday(Intent intent, Habit habit) { + if (!habit.hasReminder()) return false; + Reminder reminder = habit.getReminder(); - } + Long timestamp = + intent.getLongExtra("timestamp", DateUtils.getStartOfToday()); - private void dismissNotification(Context context, Long habitId) - { - NotificationManager notificationManager = - (NotificationManager) context.getSystemService(Activity.NOTIFICATION_SERVICE); + boolean reminderDays[] = + DateUtils.unpackWeekdayList(reminder.getDays()); + int weekday = DateUtils.getWeekday(timestamp); - int notificationId = (int) (habitId % Integer.MAX_VALUE); - notificationManager.cancel(notificationId); + return reminderDays[weekday]; } - private void createNotification(final Context context, final Intent intent) { final Uri data = intent.getData(); - final Habit habit = Habit.get(ContentUris.parseId(data)); - final Long timestamp = intent.getLongExtra("timestamp", DateHelper.getStartOfToday()); - final Long reminderTime = intent.getLongExtra("reminderTime", DateHelper.getStartOfToday()); + final Habit habit = habitList.getById(ContentUris.parseId(data)); + final Long timestamp = + intent.getLongExtra("timestamp", DateUtils.getStartOfToday()); + final Long reminderTime = + intent.getLongExtra("reminderTime", DateUtils.getStartOfToday()); if (habit == null) return; @@ -159,7 +155,7 @@ public class HabitBroadcastReceiver extends BroadcastReceiver @Override protected void doInBackground() { - todayValue = habit.checkmarks.getTodayValue(); + todayValue = habit.getCheckmarks().getTodayValue(); } @Override @@ -173,42 +169,48 @@ public class HabitBroadcastReceiver extends BroadcastReceiver contentIntent.setData(data); PendingIntent contentPendingIntent = PendingIntent.getActivity(context, 0, contentIntent, - PendingIntent.FLAG_UPDATE_CURRENT); + PendingIntent.FLAG_CANCEL_CURRENT); - PendingIntent dismissPendingIntent = buildDismissIntent(context); - PendingIntent checkIntentPending = buildCheckIntent(context, - habit, timestamp, 1); - PendingIntent snoozeIntentPending = buildSnoozeIntent(context, habit); + PendingIntent dismissPendingIntent; + dismissPendingIntent = + HabitPendingIntents.dismissNotification(context); + PendingIntent checkIntentPending = + HabitPendingIntents.toggleCheckmark(context, habit, + timestamp, 1); + PendingIntent snoozeIntentPending = + HabitPendingIntents.snoozeNotification(context, habit); - Uri ringtoneUri = ReminderHelper.getRingtoneUri(context); + Uri ringtoneUri = ReminderUtils.getRingtoneUri(context); NotificationCompat.WearableExtender wearableExtender = - new NotificationCompat.WearableExtender().setBackground( - BitmapFactory.decodeResource(context.getResources(), - R.drawable.stripe)); + new NotificationCompat.WearableExtender().setBackground( + BitmapFactory.decodeResource(context.getResources(), + R.drawable.stripe)); Notification notification = - new NotificationCompat.Builder(context) - .setSmallIcon(R.drawable.ic_notification) - .setContentTitle(habit.name) - .setContentText(habit.description) - .setContentIntent(contentPendingIntent) - .setDeleteIntent(dismissPendingIntent) - .addAction(R.drawable.ic_action_check, - context.getString(R.string.check), checkIntentPending) - .addAction(R.drawable.ic_action_snooze, - context.getString(R.string.snooze), snoozeIntentPending) - .setSound(ringtoneUri) - .extend(wearableExtender) - .setWhen(reminderTime) - .setShowWhen(true) - .build(); + new NotificationCompat.Builder(context) + .setSmallIcon(R.drawable.ic_notification) + .setContentTitle(habit.getName()) + .setContentText(habit.getDescription()) + .setContentIntent(contentPendingIntent) + .setDeleteIntent(dismissPendingIntent) + .addAction(R.drawable.ic_action_check, + context.getString(R.string.check), + checkIntentPending) + .addAction(R.drawable.ic_action_snooze, + context.getString(R.string.snooze), + snoozeIntentPending) + .setSound(ringtoneUri) + .extend(wearableExtender) + .setWhen(reminderTime) + .setShowWhen(true) + .build(); notification.flags |= Notification.FLAG_AUTO_CANCEL; NotificationManager notificationManager = - (NotificationManager) context.getSystemService( - Activity.NOTIFICATION_SERVICE); + (NotificationManager) context.getSystemService( + Activity.NOTIFICATION_SERVICE); int notificationId = (int) (habit.getId() % Integer.MAX_VALUE); notificationManager.notify(notificationId, notification); @@ -218,63 +220,39 @@ public class HabitBroadcastReceiver extends BroadcastReceiver }.execute(); } - public static PendingIntent buildSnoozeIntent(Context context, Habit habit) - { - Uri data = habit.getUri(); - Intent snoozeIntent = new Intent(context, HabitBroadcastReceiver.class); - snoozeIntent.setData(data); - snoozeIntent.setAction(ACTION_SNOOZE); - return PendingIntent.getBroadcast(context, 0, snoozeIntent, - PendingIntent.FLAG_UPDATE_CURRENT); - } - - public static PendingIntent buildCheckIntent(Context context, Habit - habit, Long timestamp, int requestCode) - { - Uri data = habit.getUri(); - Intent checkIntent = new Intent(context, HabitBroadcastReceiver.class); - checkIntent.setData(data); - checkIntent.setAction(ACTION_CHECK); - if(timestamp != null) checkIntent.putExtra("timestamp", timestamp); - return PendingIntent.getBroadcast(context, requestCode, checkIntent, - PendingIntent.FLAG_UPDATE_CURRENT); - } - - public static PendingIntent buildDismissIntent(Context context) + private void createReminderAlarmsDelayed(final Context context) { - Intent deleteIntent = new Intent(context, HabitBroadcastReceiver.class); - deleteIntent.setAction(ACTION_DISMISS); - return PendingIntent.getBroadcast(context, 0, deleteIntent, - PendingIntent.FLAG_UPDATE_CURRENT); + new Handler().postDelayed( + () -> ReminderUtils.createReminderAlarms(context, habitList), 5000); } - public static PendingIntent buildViewHabitIntent(Context context, Habit habit) + private void dismissAllHabits() { - Intent intent = new Intent(context, ShowHabitActivity.class); - intent.setData(Uri.parse("content://org.isoron.uhabits/habit/" + habit.getId())); - return TaskStackBuilder.create(context.getApplicationContext()) - .addNextIntentWithParentStack(intent) - .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); } - private boolean checkWeekday(Intent intent, Habit habit) + private void dismissNotification(Context context, Long habitId) { - Long timestamp = intent.getLongExtra("timestamp", DateHelper.getStartOfToday()); - - boolean reminderDays[] = DateHelper.unpackWeekdayList(habit.reminderDays); - int weekday = DateHelper.getWeekday(timestamp); + NotificationManager notificationManager = + (NotificationManager) context.getSystemService( + Activity.NOTIFICATION_SERVICE); - return reminderDays[weekday]; + int notificationId = (int) (habitId % Integer.MAX_VALUE); + notificationManager.cancel(notificationId); } - public static void dismissNotification(Context context, Habit habit) + private void snoozeHabit(Context context, Intent intent) { - NotificationManager notificationManager = - (NotificationManager) context.getSystemService( - Activity.NOTIFICATION_SERVICE); + Uri data = intent.getData(); + SharedPreferences prefs = + PreferenceManager.getDefaultSharedPreferences(context); + long delayMinutes = + Long.parseLong(prefs.getString("pref_snooze_interval", "15")); - int notificationId = (int) (habit.getId() % Integer.MAX_VALUE); - notificationManager.cancel(notificationId); + long habitId = ContentUris.parseId(data); + Habit habit = habitList.getById(habitId); + if (habit != null) ReminderUtils.createReminderAlarm(context, habit, + new Date().getTime() + delayMinutes * 60 * 1000); + dismissNotification(context, habitId); } } diff --git a/app/src/main/java/org/isoron/uhabits/HabitPendingIntents.java b/app/src/main/java/org/isoron/uhabits/HabitPendingIntents.java new file mode 100644 index 000000000..58e553479 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/HabitPendingIntents.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2016 Á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; + +import android.app.*; +import android.content.*; +import android.net.*; + +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.ui.habits.show.*; + +public abstract class HabitPendingIntents +{ + + private static final String BASE_URL = + "content://org.isoron.uhabits/habit/"; + + public static PendingIntent dismissNotification(Context context) + { + Intent deleteIntent = new Intent(context, HabitBroadcastReceiver.class); + deleteIntent.setAction(HabitBroadcastReceiver.ACTION_DISMISS); + return PendingIntent.getBroadcast(context, 0, deleteIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + } + + public static PendingIntent snoozeNotification(Context context, Habit habit) + { + Uri data = habit.getUri(); + Intent snoozeIntent = new Intent(context, HabitBroadcastReceiver.class); + snoozeIntent.setData(data); + snoozeIntent.setAction(HabitBroadcastReceiver.ACTION_SNOOZE); + return PendingIntent.getBroadcast(context, 0, snoozeIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + } + + public static PendingIntent toggleCheckmark(Context context, + Habit habit, + Long timestamp, + int requestCode) + { + Uri data = habit.getUri(); + Intent checkIntent = new Intent(context, HabitBroadcastReceiver.class); + checkIntent.setData(data); + checkIntent.setAction(HabitBroadcastReceiver.ACTION_CHECK); + if (timestamp != null) checkIntent.putExtra("timestamp", timestamp); + return PendingIntent.getBroadcast(context, requestCode, checkIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + } + + public static PendingIntent viewHabit(Context context, Habit habit) + { + Intent intent = new Intent(context, ShowHabitActivity.class); + intent.setData(Uri.parse(BASE_URL + habit.getId())); + return android.support.v4.app.TaskStackBuilder + .create(context.getApplicationContext()) + .addNextIntentWithParentStack(intent) + .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/HabitsApplication.java b/app/src/main/java/org/isoron/uhabits/HabitsApplication.java index dee4ca17d..53fb23d24 100644 --- a/app/src/main/java/org/isoron/uhabits/HabitsApplication.java +++ b/app/src/main/java/org/isoron/uhabits/HabitsApplication.java @@ -19,36 +19,78 @@ package org.isoron.uhabits; -import android.app.Application; -import android.content.Context; -import android.os.Environment; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.view.WindowManager; +import android.app.*; +import android.content.*; +import android.support.annotation.*; -import com.activeandroid.ActiveAndroid; +import com.activeandroid.*; -import org.isoron.uhabits.helpers.DatabaseHelper; -import org.isoron.uhabits.helpers.DateHelper; +import org.isoron.uhabits.ui.widgets.*; +import org.isoron.uhabits.utils.*; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.io.InputStreamReader; -import java.util.LinkedList; +import java.io.*; +/** + * The Android application for Loop Habit Tracker. + */ public class HabitsApplication extends Application { + 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; + + @Nullable + private static HabitsApplication application; + + private static BaseComponent component; + @Nullable private static Context context; + private static WidgetUpdater widgetManager; + + public static BaseComponent getComponent() + { + return component; + } + + public static void setComponent(BaseComponent component) + { + HabitsApplication.component = component; + } + + @Nullable + public static Context getContext() + { + return context; + } + + @Nullable + public static HabitsApplication getInstance() + { + return application; + } + + @NonNull + public static WidgetUpdater getWidgetManager() + { + if (widgetManager == null) + throw new RuntimeException("widgetManager is null"); + + return widgetManager; + } + public static boolean isTestMode() { try { - if(context != null) - context.getClassLoader().loadClass("org.isoron.uhabits.unit.models.HabitTest"); + if (context != null) context + .getClassLoader() + .loadClass("org.isoron.uhabits.BaseAndroidTest"); return true; } catch (final Exception e) @@ -57,25 +99,25 @@ public class HabitsApplication extends Application } } - @Nullable - public static Context getContext() - { - return context; - } - @Override public void onCreate() { super.onCreate(); HabitsApplication.context = this; + HabitsApplication.application = this; + component = DaggerAndroidComponent.builder().build(); + component.inject(this); if (isTestMode()) { - File db = DatabaseHelper.getDatabaseFile(); - if(db.exists()) db.delete(); + File db = DatabaseUtils.getDatabaseFile(); + if (db.exists()) db.delete(); } - DatabaseHelper.initializeActiveAndroid(); + widgetManager = new WidgetUpdater(this); + widgetManager.startListening(); + + DatabaseUtils.initializeActiveAndroid(); } @Override @@ -83,84 +125,7 @@ public class HabitsApplication extends Application { HabitsApplication.context = null; ActiveAndroid.dispose(); + widgetManager.stopListening(); super.onTerminate(); } - - public static String getLogcat() throws IOException - { - int maxNLines = 250; - StringBuilder builder = new StringBuilder(); - - String[] command = new String[] { "logcat", "-d"}; - Process process = Runtime.getRuntime().exec(command); - - InputStreamReader in = new InputStreamReader(process.getInputStream()); - BufferedReader bufferedReader = new BufferedReader(in); - - LinkedList log = new LinkedList<>(); - - String line; - while ((line = bufferedReader.readLine()) != null) - { - log.addLast(line); - if(log.size() > maxNLines) log.removeFirst(); - } - - for(String l : log) - { - builder.append(l); - builder.append('\n'); - } - - return builder.toString(); - } - - public static String getDeviceInfo() - { - if(context == null) return ""; - - StringBuilder b = new StringBuilder(); - WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); - - b.append(String.format("App Version Name: %s\n", BuildConfig.VERSION_NAME)); - b.append(String.format("App Version Code: %s\n", BuildConfig.VERSION_CODE)); - b.append(String.format("OS Version: %s (%s)\n", System.getProperty("os.version"), - android.os.Build.VERSION.INCREMENTAL)); - b.append(String.format("OS API Level: %s\n", android.os.Build.VERSION.SDK)); - b.append(String.format("Device: %s\n", android.os.Build.DEVICE)); - b.append(String.format("Model (Product): %s (%s)\n", android.os.Build.MODEL, - android.os.Build.PRODUCT)); - b.append(String.format("Manufacturer: %s\n", android.os.Build.MANUFACTURER)); - b.append(String.format("Other tags: %s\n", android.os.Build.TAGS)); - b.append(String.format("Screen Width: %s\n", wm.getDefaultDisplay().getWidth())); - b.append(String.format("Screen Height: %s\n", wm.getDefaultDisplay().getHeight())); - b.append(String.format("SD Card state: %s\n\n", Environment.getExternalStorageState())); - - return b.toString(); - } - - @NonNull - public static File dumpBugReportToFile() throws IOException - { - String date = DateHelper.getBackupDateFormat().format(DateHelper.getLocalTime()); - - if(context == null) throw new RuntimeException("application context should not be null"); - File dir = DatabaseHelper.getFilesDir("Logs"); - if (dir == null) throw new IOException("log dir should not be null"); - - File logFile = new File(String.format("%s/Log %s.txt", dir.getPath(), date)); - FileWriter output = new FileWriter(logFile); - output.write(generateBugReport()); - output.close(); - - return logFile; - } - - @NonNull - public static String generateBugReport() throws IOException - { - String logcat = getLogcat(); - String deviceInfo = getDeviceInfo(); - return deviceInfo + "\n" + logcat; - } } diff --git a/app/src/main/java/org/isoron/uhabits/HabitsBackupAgent.java b/app/src/main/java/org/isoron/uhabits/HabitsBackupAgent.java index 6baa562f2..f337949ea 100644 --- a/app/src/main/java/org/isoron/uhabits/HabitsBackupAgent.java +++ b/app/src/main/java/org/isoron/uhabits/HabitsBackupAgent.java @@ -23,12 +23,17 @@ import android.app.backup.BackupAgentHelper; import android.app.backup.FileBackupHelper; import android.app.backup.SharedPreferencesBackupHelper; +/** + * An Android BackupAgentHelper customized for this application. + */ public class HabitsBackupAgent extends BackupAgentHelper { @Override public void onCreate() { - addHelper("preferences", new SharedPreferencesBackupHelper(this, "preferences")); - addHelper("database", new FileBackupHelper(this, "../databases/uhabits.db")); + addHelper("preferences", + new SharedPreferencesBackupHelper(this, "preferences")); + addHelper("database", + new FileBackupHelper(this, "../databases/uhabits.db")); } } diff --git a/app/src/main/java/org/isoron/uhabits/MainActivity.java b/app/src/main/java/org/isoron/uhabits/MainActivity.java index 77c8625a1..ba7b6da06 100644 --- a/app/src/main/java/org/isoron/uhabits/MainActivity.java +++ b/app/src/main/java/org/isoron/uhabits/MainActivity.java @@ -19,328 +19,16 @@ package org.isoron.uhabits; -import android.appwidget.AppWidgetManager; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.graphics.drawable.ColorDrawable; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.preference.PreferenceManager; -import android.support.annotation.NonNull; -import android.support.v4.content.LocalBroadcastManager; -import android.support.v7.app.ActionBar; -import android.view.Menu; -import android.view.MenuItem; +import org.isoron.uhabits.ui.habits.list.ListHabitsActivity; -import org.isoron.uhabits.fragments.ListHabitsFragment; -import org.isoron.uhabits.helpers.DateHelper; -import org.isoron.uhabits.helpers.ReminderHelper; -import org.isoron.uhabits.helpers.UIHelper; -import org.isoron.uhabits.models.Checkmark; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.tasks.BaseTask; -import org.isoron.uhabits.widgets.CheckmarkWidgetProvider; -import org.isoron.uhabits.widgets.FrequencyWidgetProvider; -import org.isoron.uhabits.widgets.HistoryWidgetProvider; -import org.isoron.uhabits.widgets.ScoreWidgetProvider; -import org.isoron.uhabits.widgets.StreakWidgetProvider; - -import java.io.IOException; - -public class MainActivity extends BaseActivity - implements ListHabitsFragment.OnHabitClickListener +/** + * Application that starts upon clicking the launcher icon. + */ +public class MainActivity extends ListHabitsActivity { - private ListHabitsFragment listHabitsFragment; - private SharedPreferences prefs; - private BroadcastReceiver receiver; - private LocalBroadcastManager localBroadcastManager; - - public static final String ACTION_REFRESH = "org.isoron.uhabits.ACTION_REFRESH"; - - public static final int RESULT_IMPORT_DATA = 1; - public static final int RESULT_EXPORT_CSV = 2; - public static final int RESULT_EXPORT_DB = 3; - public static final int RESULT_BUG_REPORT = 4; - - @Override - protected void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - - setContentView(R.layout.list_habits_activity); - - setupSupportActionBar(false); - - prefs = PreferenceManager.getDefaultSharedPreferences(this); - listHabitsFragment = - (ListHabitsFragment) getSupportFragmentManager().findFragmentById(R.id.fragment1); - - receiver = new Receiver(); - localBroadcastManager = LocalBroadcastManager.getInstance(this); - localBroadcastManager.registerReceiver(receiver, new IntentFilter(ACTION_REFRESH)); - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) - onPreLollipopStartup(); - - onStartup(); - } - - private void onPreLollipopStartup() - { - ActionBar actionBar = getSupportActionBar(); - if(actionBar == null) return; - if(UIHelper.isNightMode()) return; - - int color = getResources().getColor(R.color.grey_900); - actionBar.setBackgroundDrawable(new ColorDrawable(color)); - } - - private void onStartup() - { - PreferenceManager.setDefaultValues(this, R.xml.preferences, false); - UIHelper.incrementLaunchCount(this); - UIHelper.updateLastAppVersion(this); - showTutorial(); - - new AsyncTask() { - @Override - protected Void doInBackground(Void... params) - { - ReminderHelper.createReminderAlarms(MainActivity.this); - updateWidgets(MainActivity.this); - return null; - } - }.execute(); - - } - - private void showTutorial() - { - Boolean firstRun = prefs.getBoolean("pref_first_run", true); - - if (firstRun) - { - SharedPreferences.Editor editor = prefs.edit(); - editor.putBoolean("pref_first_run", false); - editor.putLong("last_hint_timestamp", DateHelper.getStartOfToday()).apply(); - editor.apply(); - - Intent intent = new Intent(this, IntroActivity.class); - this.startActivity(intent); - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) - { - menu.clear(); - getMenuInflater().inflate(R.menu.list_habits_menu, menu); - - MenuItem nightModeItem = menu.findItem(R.id.action_night_mode); - nightModeItem.setChecked(UIHelper.isNightMode()); - - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) - { - switch (item.getItemId()) - { - case R.id.action_night_mode: - { - if(UIHelper.isNightMode()) - UIHelper.setCurrentTheme(UIHelper.THEME_LIGHT); - else - UIHelper.setCurrentTheme(UIHelper.THEME_DARK); - - refreshTheme(); - return true; - } - - case R.id.action_settings: - { - Intent intent = new Intent(this, SettingsActivity.class); - startActivityForResult(intent, 0); - return true; - } - - case R.id.action_about: - { - Intent intent = new Intent(this, AboutActivity.class); - startActivity(intent); - return true; - } - - case R.id.action_faq: - { - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_VIEW); - intent.setData(Uri.parse(getString(R.string.helpURL))); - startActivity(intent); - return true; - } - - default: - return super.onOptionsItemSelected(item); - } - } - - private void refreshTheme() - { - new Handler().postDelayed(new Runnable() - { - @Override - public void run() - { - Intent intent = new Intent(MainActivity.this, MainActivity.class); - - MainActivity.this.finish(); - overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out); - startActivity(intent); - - } - }, 500); // Let the menu disappear first - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) - { - switch (resultCode) - { - case RESULT_IMPORT_DATA: - listHabitsFragment.showImportDialog(); - break; - - case RESULT_EXPORT_CSV: - listHabitsFragment.exportAllHabits(); - break; - - case RESULT_EXPORT_DB: - listHabitsFragment.exportDB(); - break; - - case RESULT_BUG_REPORT: - generateBugReport(); - break; - } - } - - private void generateBugReport() - { - try - { - HabitsApplication.dumpBugReportToFile(); - } - catch (IOException e) - { - // ignored - } - - try - { - String log = "---------- BUG REPORT BEGINS ----------\n"; - log += HabitsApplication.generateBugReport(); - log += "---------- BUG REPORT ENDS ------------\n"; - - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_SEND); - intent.setType("message/rfc822"); - intent.putExtra(Intent.EXTRA_EMAIL, new String[] { "dev@loophabits.org" }); - intent.putExtra(Intent.EXTRA_SUBJECT, "Bug Report - Loop Habit Tracker"); - intent.putExtra(Intent.EXTRA_TEXT, log); - startActivity(intent); - } - catch (IOException e) - { - e.printStackTrace(); - showToast(R.string.bug_report_failed); - } - } - - @Override - public void onHabitClicked(Habit habit) - { - Intent intent = new Intent(this, ShowHabitActivity.class); - intent.setData(Uri.parse("content://org.isoron.uhabits/habit/" + habit.getId())); - startActivity(intent); - } - - @Override - public void onPostExecuteCommand(Long refreshKey) - { - listHabitsFragment.onPostExecuteCommand(refreshKey); - - new BaseTask() - { - @Override - protected void doInBackground() - { - dismissNotifications(MainActivity.this); - updateWidgets(MainActivity.this); - } - }.execute(); - } - - private void dismissNotifications(Context context) - { - for(Habit h : Habit.getHabitsWithReminder()) - { - if(h.checkmarks.getTodayValue() != Checkmark.UNCHECKED) - HabitBroadcastReceiver.dismissNotification(context, h); - } - } - - public static void updateWidgets(Context context) - { - updateWidgets(context, CheckmarkWidgetProvider.class); - updateWidgets(context, HistoryWidgetProvider.class); - updateWidgets(context, ScoreWidgetProvider.class); - updateWidgets(context, StreakWidgetProvider.class); - updateWidgets(context, FrequencyWidgetProvider.class); - } - - private static void updateWidgets(Context context, Class providerClass) - { - ComponentName provider = new ComponentName(context, providerClass); - Intent intent = new Intent(context, providerClass); - intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE); - int ids[] = AppWidgetManager.getInstance(context).getAppWidgetIds(provider); - intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids); - context.sendBroadcast(intent); - } - - @Override - protected void onDestroy() - { - localBroadcastManager.unregisterReceiver(receiver); - super.onDestroy(); - } - - class Receiver extends BroadcastReceiver - { - @Override - public void onReceive(Context context, Intent intent) - { - listHabitsFragment.onPostExecuteCommand(null); - } - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, - @NonNull int[] grantResults) - { - if (grantResults.length <= 0) return; - if (grantResults[0] != PackageManager.PERMISSION_GRANTED) return; - - listHabitsFragment.showImportDialog(); - } + /* + * Since changing the main activity on the manifest file causes things to + * break we always point the launcher icon to this activity instead, and + * redirect to the desired one using inheritance. + */ } diff --git a/app/src/main/java/org/isoron/uhabits/ShowHabitActivity.java b/app/src/main/java/org/isoron/uhabits/ShowHabitActivity.java deleted file mode 100644 index 8be54b586..000000000 --- a/app/src/main/java/org/isoron/uhabits/ShowHabitActivity.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (C) 2016 Á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; - -import android.content.ContentUris; -import android.net.Uri; -import android.os.Bundle; -import android.support.v7.app.ActionBar; - -import org.isoron.uhabits.helpers.ColorHelper; -import org.isoron.uhabits.models.Habit; - -public class ShowHabitActivity extends BaseActivity -{ - private Habit habit; - - @Override - protected void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - - Uri data = getIntent().getData(); - habit = Habit.get(ContentUris.parseId(data)); - - setContentView(R.layout.show_habit_activity); - - setupSupportActionBar(true); - setupHabitActionBar(); - } - - private void setupHabitActionBar() - { - if(habit == null) return; - - ActionBar actionBar = getSupportActionBar(); - if(actionBar == null) return; - - actionBar.setTitle(habit.name); - - setupActionBarColor(ColorHelper.getColor(this, habit.color)); - } - - public Habit getHabit() - { - return habit; - } -} diff --git a/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java b/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java index 25e998b7b..fa2ca827f 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java @@ -19,38 +19,52 @@ package org.isoron.uhabits.commands; +import org.isoron.uhabits.HabitsApplication; import org.isoron.uhabits.R; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.HabitList; import java.util.List; +import javax.inject.Inject; + +/** + * Command to archive a list of habits. + */ public class ArchiveHabitsCommand extends Command { + @Inject + HabitList habitList; private List habits; public ArchiveHabitsCommand(List habits) { + HabitsApplication.getComponent().inject(this); this.habits = habits; } @Override public void execute() { - Habit.archive(habits); + for(Habit h : habits) h.setArchived(true); + habitList.update(habits); } @Override public void undo() { - Habit.unarchive(habits); + for(Habit h : habits) h.setArchived(false); + habitList.update(habits); } + @Override public Integer getExecuteStringId() { return R.string.toast_habit_archived; } + @Override public Integer getUndoStringId() { return R.string.toast_habit_unarchived; diff --git a/app/src/main/java/org/isoron/uhabits/commands/ChangeHabitColorCommand.java b/app/src/main/java/org/isoron/uhabits/commands/ChangeHabitColorCommand.java index 04ba83d7d..e09ec470b 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/ChangeHabitColorCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/ChangeHabitColorCommand.java @@ -19,62 +19,65 @@ package org.isoron.uhabits.commands; -import com.activeandroid.ActiveAndroid; - +import org.isoron.uhabits.HabitsApplication; import org.isoron.uhabits.R; -import org.isoron.uhabits.helpers.DatabaseHelper; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.HabitList; import java.util.ArrayList; import java.util.List; +import javax.inject.Inject; + +/** + * Command to change the color of a list of habits. + */ public class ChangeHabitColorCommand extends Command { + @Inject + HabitList habitList; + List habits; + List originalColors; + Integer newColor; public ChangeHabitColorCommand(List habits, Integer newColor) { + HabitsApplication.getComponent().inject(this); + this.habits = habits; this.newColor = newColor; this.originalColors = new ArrayList<>(habits.size()); - for(Habit h : habits) - originalColors.add(h.color); + for (Habit h : habits) originalColors.add(h.getColor()); } @Override public void execute() { - Habit.setColor(habits, newColor); + for(Habit h : habits) h.setColor(newColor); + habitList.update(habits); } @Override - public void undo() - { - DatabaseHelper.executeAsTransaction(new DatabaseHelper.Command() - { - @Override - public void execute() - { - int k = 0; - for(Habit h : habits) - { - h.color = originalColors.get(k++); - h.save(); - } - } - }); - } - public Integer getExecuteStringId() { return R.string.toast_habit_changed; } + @Override public Integer getUndoStringId() { return R.string.toast_habit_changed; } + + @Override + public void undo() + { + int k = 0; + for (Habit h : habits) h.setColor(originalColors.get(k++)); + habitList.update(habits); + } } diff --git a/app/src/main/java/org/isoron/uhabits/commands/Command.java b/app/src/main/java/org/isoron/uhabits/commands/Command.java index b9427e38a..e319a5095 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/Command.java +++ b/app/src/main/java/org/isoron/uhabits/commands/Command.java @@ -19,12 +19,19 @@ package org.isoron.uhabits.commands; +/** + * A Command represents a desired set of changes that should be performed on the + * models. + *

+ * A command can be executed and undone. Each of these operations also provide + * an string that should be displayed to the user upon their completion. + *

+ * In general, commands should always be executed by a {@link CommandRunner}. + */ public abstract class Command { public abstract void execute(); - public abstract void undo(); - public Integer getExecuteStringId() { return null; @@ -34,4 +41,6 @@ public abstract class Command { return null; } + + public abstract void undo(); } diff --git a/app/src/main/java/org/isoron/uhabits/commands/CommandRunner.java b/app/src/main/java/org/isoron/uhabits/commands/CommandRunner.java new file mode 100644 index 000000000..d1051e744 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/commands/CommandRunner.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2016 Á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.commands; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.isoron.uhabits.tasks.BaseTask; + +import java.util.LinkedList; + +/** + * A CommandRunner executes and undoes commands. + *

+ * CommandRunners also allows objects to subscribe to it, and receive events + * whenever a command is performed. + */ +public class CommandRunner +{ + private LinkedList listeners; + + public CommandRunner() + { + listeners = new LinkedList<>(); + } + + private static CommandRunner getInstance() + { + return null; + } + + public void addListener(Listener l) + { + listeners.add(l); + } + + public void execute(final Command command, final Long refreshKey) + { + new BaseTask() + { + @Override + protected void doInBackground() + { + command.execute(); + } + + @Override + protected void onPostExecute(Void aVoid) + { + for (Listener l : listeners) + l.onCommandExecuted(command, refreshKey); + + super.onPostExecute(null); + } + }.execute(); + } + + public void removeListener(Listener l) + { + listeners.remove(l); + } + + /** + * Interface implemented by objects that want to receive an event whenever a + * command is executed. + */ + public interface Listener + { + void onCommandExecuted(@NonNull Command command, + @Nullable Long refreshKey); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/commands/CreateHabitCommand.java b/app/src/main/java/org/isoron/uhabits/commands/CreateHabitCommand.java index 7cc9ad51c..7668e5d2f 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/CreateHabitCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/CreateHabitCommand.java @@ -19,41 +19,48 @@ package org.isoron.uhabits.commands; +import org.isoron.uhabits.HabitsApplication; import org.isoron.uhabits.R; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.HabitList; +import javax.inject.Inject; + +/** + * Command to create a habit. + */ public class CreateHabitCommand extends Command { + @Inject + HabitList habitList; + private Habit model; private Long savedId; public CreateHabitCommand(Habit model) { this.model = model; + HabitsApplication.getComponent().inject(this); } @Override public void execute() { - Habit savedHabit = new Habit(model); - if (savedId == null) - { - savedHabit.save(); - savedId = savedHabit.getId(); - } - else - { - savedHabit.save(savedId); - } + Habit savedHabit = new Habit(); + savedHabit.copyFrom(model); + savedHabit.setId(savedId); + + habitList.add(savedHabit); + savedId = savedHabit.getId(); } @Override public void undo() { - Habit habit = Habit.get(savedId); + Habit habit = habitList.getById(savedId); if(habit == null) throw new RuntimeException("Habit not found"); - habit.cascadeDelete(); + habitList.remove(habit); } @Override diff --git a/app/src/main/java/org/isoron/uhabits/commands/DeleteHabitsCommand.java b/app/src/main/java/org/isoron/uhabits/commands/DeleteHabitsCommand.java index 34e26c50c..0d800cd17 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/DeleteHabitsCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/DeleteHabitsCommand.java @@ -19,27 +19,36 @@ package org.isoron.uhabits.commands; +import org.isoron.uhabits.HabitsApplication; import org.isoron.uhabits.R; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.HabitList; import java.util.List; +import javax.inject.Inject; + +/** + * Command to delete a list of habits. + */ public class DeleteHabitsCommand extends Command { + @Inject + HabitList habitList; + private List habits; public DeleteHabitsCommand(List habits) { this.habits = habits; + HabitsApplication.getComponent().inject(this); } @Override public void execute() { for(Habit h : habits) - h.cascadeDelete(); - - Habit.rebuildOrder(); + habitList.remove(h); } @Override @@ -48,11 +57,13 @@ public class DeleteHabitsCommand extends Command throw new UnsupportedOperationException(); } + @Override public Integer getExecuteStringId() { return R.string.toast_habit_deleted; } + @Override public Integer getUndoStringId() { return R.string.toast_habit_restored; diff --git a/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java b/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java index 7a7787d6a..784d909bf 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java @@ -19,24 +19,41 @@ package org.isoron.uhabits.commands; -import org.isoron.uhabits.R; -import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import javax.inject.*; + +/** + * Command to modify a habit. + */ public class EditHabitCommand extends Command { + @Inject + HabitList habitList; + private Habit original; + private Habit modified; + private long savedId; - private boolean hasIntervalChanged; + + private boolean hasFrequencyChanged; public EditHabitCommand(Habit original, Habit modified) { + HabitsApplication.getComponent().inject(this); + this.savedId = original.getId(); - this.modified = new Habit(modified); - this.original = new Habit(original); + this.modified = new Habit(); + this.original = new Habit(); + + this.modified.copyFrom(modified); + this.original.copyFrom(original); - hasIntervalChanged = (!this.original.freqDen.equals(this.modified.freqDen) || - !this.original.freqNum.equals(this.modified.freqNum)); + Frequency originalFreq = this.original.getFrequency(); + Frequency modifiedFreq = this.modified.getFrequency(); + hasFrequencyChanged = (!originalFreq.equals(modifiedFreq)); } @Override @@ -45,6 +62,18 @@ public class EditHabitCommand extends Command copyAttributes(this.modified); } + @Override + public Integer getExecuteStringId() + { + return R.string.toast_habit_changed; + } + + @Override + public Integer getUndoStringId() + { + return R.string.toast_habit_changed_back; + } + @Override public void undo() { @@ -53,32 +82,22 @@ public class EditHabitCommand extends Command private void copyAttributes(Habit model) { - Habit habit = Habit.get(savedId); - if(habit == null) throw new RuntimeException("Habit not found"); + Habit habit = habitList.getById(savedId); + if (habit == null) throw new RuntimeException("Habit not found"); - habit.copyAttributes(model); - habit.save(); + habit.copyFrom(model); + habitList.update(habit); invalidateIfNeeded(habit); } private void invalidateIfNeeded(Habit habit) { - if (hasIntervalChanged) + if (hasFrequencyChanged) { - habit.checkmarks.deleteNewerThan(0); - habit.streaks.deleteNewerThan(0); - habit.scores.invalidateNewerThan(0); + habit.getCheckmarks().invalidateNewerThan(0); + habit.getStreaks().invalidateNewerThan(0); + habit.getScores().invalidateNewerThan(0); } } - - public Integer getExecuteStringId() - { - return R.string.toast_habit_changed; - } - - public Integer getUndoStringId() - { - return R.string.toast_habit_changed_back; - } } \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java b/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java index 451908433..8a3e981f8 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java @@ -21,6 +21,9 @@ package org.isoron.uhabits.commands; import org.isoron.uhabits.models.Habit; +/** + * Command to toggle a repetition. + */ public class ToggleRepetitionCommand extends Command { private Long offset; @@ -35,7 +38,7 @@ public class ToggleRepetitionCommand extends Command @Override public void execute() { - habit.repetitions.toggle(offset); + habit.getRepetitions().toggleTimestamp(offset); } @Override diff --git a/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java b/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java index 612481fa7..5d442717c 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java @@ -19,38 +19,52 @@ package org.isoron.uhabits.commands; +import org.isoron.uhabits.HabitsApplication; import org.isoron.uhabits.R; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.HabitList; import java.util.List; +import javax.inject.Inject; + +/** + * Command to unarchive a list of habits. + */ public class UnarchiveHabitsCommand extends Command { + @Inject + HabitList habitList; private List habits; public UnarchiveHabitsCommand(List habits) { this.habits = habits; + HabitsApplication.getComponent().inject(this); } @Override public void execute() { - Habit.unarchive(habits); + for(Habit h : habits) h.setArchived(false); + habitList.update(habits); } @Override public void undo() { - Habit.archive(habits); + for(Habit h : habits) h.setArchived(true); + habitList.update(habits); } + @Override public Integer getExecuteStringId() { return R.string.toast_habit_unarchived; } + @Override public Integer getUndoStringId() { return R.string.toast_habit_archived; diff --git a/app/src/main/java/org/isoron/uhabits/commands/package-info.java b/app/src/main/java/org/isoron/uhabits/commands/package-info.java new file mode 100644 index 000000000..8fce85ae1 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/commands/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2016 Á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 . + */ + +/** + * Provides commands to modify the models, such as {@link + * org.isoron.uhabits.commands.CreateHabitCommand}. + */ +package org.isoron.uhabits.commands; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/dialogs/EditHabitDialogFragment.java b/app/src/main/java/org/isoron/uhabits/dialogs/EditHabitDialogFragment.java deleted file mode 100644 index 1baa97cbe..000000000 --- a/app/src/main/java/org/isoron/uhabits/dialogs/EditHabitDialogFragment.java +++ /dev/null @@ -1,457 +0,0 @@ -/* - * Copyright (C) 2016 Á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.dialogs; - -import android.annotation.SuppressLint; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.support.v7.app.AppCompatDialogFragment; -import android.text.format.DateFormat; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.Button; -import android.widget.ImageButton; -import android.widget.Spinner; -import android.widget.TextView; - -import com.android.colorpicker.ColorPickerDialog; -import com.android.colorpicker.ColorPickerSwatch; -import com.android.datetimepicker.time.RadialPickerLayout; -import com.android.datetimepicker.time.TimePickerDialog; - -import org.isoron.uhabits.R; -import org.isoron.uhabits.commands.Command; -import org.isoron.uhabits.commands.CreateHabitCommand; -import org.isoron.uhabits.commands.EditHabitCommand; -import org.isoron.uhabits.helpers.ColorHelper; -import org.isoron.uhabits.helpers.DateHelper; -import org.isoron.uhabits.helpers.UIHelper.OnSavedListener; -import org.isoron.uhabits.models.Habit; - -import java.util.Arrays; - -public class EditHabitDialogFragment extends AppCompatDialogFragment - implements OnClickListener, WeekdayPickerDialog.OnWeekdaysPickedListener, - TimePickerDialog.OnTimeSetListener, Spinner.OnItemSelectedListener -{ - private Integer mode; - static final int EDIT_MODE = 0; - static final int CREATE_MODE = 1; - - private OnSavedListener onSavedListener; - - private Habit originalHabit; - private Habit modifiedHabit; - - private TextView tvName; - private TextView tvDescription; - private TextView tvFreqNum; - private TextView tvFreqDen; - private TextView tvReminderTime; - private TextView tvReminderDays; - - private Spinner sFrequency; - private ViewGroup llCustomFrequency; - private ViewGroup llReminderDays; - - private SharedPreferences prefs; - private boolean is24HourMode; - - public static EditHabitDialogFragment editSingleHabitFragment(long id) - { - EditHabitDialogFragment frag = new EditHabitDialogFragment(); - Bundle args = new Bundle(); - args.putLong("habitId", id); - args.putInt("editMode", EDIT_MODE); - frag.setArguments(args); - return frag; - } - - public static EditHabitDialogFragment createHabitFragment() - { - EditHabitDialogFragment frag = new EditHabitDialogFragment(); - Bundle args = new Bundle(); - args.putInt("editMode", CREATE_MODE); - frag.setArguments(args); - return frag; - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) - { - View view = inflater.inflate(R.layout.edit_habit, container, false); - tvName = (TextView) view.findViewById(R.id.input_name); - tvDescription = (TextView) view.findViewById(R.id.input_description); - tvFreqNum = (TextView) view.findViewById(R.id.input_freq_num); - tvFreqDen = (TextView) view.findViewById(R.id.input_freq_den); - tvReminderTime = (TextView) view.findViewById(R.id.inputReminderTime); - tvReminderDays = (TextView) view.findViewById(R.id.inputReminderDays); - - sFrequency = (Spinner) view.findViewById(R.id.sFrequency); - llCustomFrequency = (ViewGroup) view.findViewById(R.id.llCustomFrequency); - llReminderDays = (ViewGroup) view.findViewById(R.id.llReminderDays); - - Button buttonSave = (Button) view.findViewById(R.id.buttonSave); - Button buttonDiscard = (Button) view.findViewById(R.id.buttonDiscard); - ImageButton buttonPickColor = (ImageButton) view.findViewById(R.id.buttonPickColor); - - buttonSave.setOnClickListener(this); - buttonDiscard.setOnClickListener(this); - tvReminderTime.setOnClickListener(this); - tvReminderDays.setOnClickListener(this); - buttonPickColor.setOnClickListener(this); - sFrequency.setOnItemSelectedListener(this); - - prefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); - - Bundle args = getArguments(); - mode = (Integer) args.get("editMode"); - - is24HourMode = DateFormat.is24HourFormat(getActivity()); - - if (mode == CREATE_MODE) - { - getDialog().setTitle(R.string.create_habit); - modifiedHabit = new Habit(); - modifiedHabit.freqNum = 1; - modifiedHabit.freqDen = 1; - modifiedHabit.color = prefs.getInt("pref_default_habit_palette_color", modifiedHabit.color); - } - else if (mode == EDIT_MODE) - { - Long habitId = (Long) args.get("habitId"); - if(habitId == null) throw new IllegalArgumentException("habitId must be specified"); - - originalHabit = Habit.get(habitId); - modifiedHabit = new Habit(originalHabit); - - getDialog().setTitle(R.string.edit_habit); - tvName.append(modifiedHabit.name); - tvDescription.append(modifiedHabit.description); - } - - if(savedInstanceState != null) - { - modifiedHabit.color = savedInstanceState.getInt("color", modifiedHabit.color); - modifiedHabit.reminderMin = savedInstanceState.getInt("reminderMin", -1); - modifiedHabit.reminderHour = savedInstanceState.getInt("reminderHour", -1); - modifiedHabit.reminderDays = savedInstanceState.getInt("reminderDays", -1); - - if(modifiedHabit.reminderMin < 0) - modifiedHabit.clearReminder(); - } - - tvFreqNum.append(modifiedHabit.freqNum.toString()); - tvFreqDen.append(modifiedHabit.freqDen.toString()); - - changeColor(modifiedHabit.color); - updateFrequency(); - updateReminder(); - - return view; - } - - private void changeColor(int paletteColor) - { - modifiedHabit.color = paletteColor; - tvName.setTextColor(ColorHelper.getColor(getActivity(), paletteColor)); - - SharedPreferences.Editor editor = prefs.edit(); - editor.putInt("pref_default_habit_palette_color", paletteColor); - editor.apply(); - } - - @SuppressWarnings("ConstantConditions") - private void updateReminder() - { - if (modifiedHabit.hasReminder()) - { - tvReminderTime.setText(DateHelper.formatTime(getActivity(), modifiedHabit.reminderHour, - modifiedHabit.reminderMin)); - llReminderDays.setVisibility(View.VISIBLE); - - boolean weekdays[] = DateHelper.unpackWeekdayList(modifiedHabit.reminderDays); - tvReminderDays.setText(DateHelper.formatWeekdayList(getActivity(), weekdays)); - } - else - { - tvReminderTime.setText(R.string.reminder_off); - llReminderDays.setVisibility(View.GONE); - } - } - - public void setOnSavedListener(OnSavedListener onSavedListener) - { - this.onSavedListener = onSavedListener; - } - - @Override - public void onClick(View v) - { - switch(v.getId()) - { - case R.id.inputReminderTime: - onDateSpinnerClick(); - break; - - case R.id.inputReminderDays: - onWeekdayClick(); - break; - - case R.id.buttonSave: - onSaveButtonClick(); - break; - - case R.id.buttonDiscard: - dismiss(); - break; - - case R.id.buttonPickColor: - onColorButtonClick(); - break; - } - } - - private void onColorButtonClick() - { - int originalAndroidColor = ColorHelper.getColor(getActivity(), modifiedHabit.color); - - ColorPickerDialog picker = ColorPickerDialog.newInstance( - R.string.color_picker_default_title, ColorHelper.getPalette(getActivity()), - originalAndroidColor, 4, ColorPickerDialog.SIZE_SMALL); - - picker.setOnColorSelectedListener(new ColorPickerSwatch.OnColorSelectedListener() - { - public void onColorSelected(int androidColor) - { - int paletteColor = ColorHelper.colorToPaletteIndex(getActivity(), androidColor); - changeColor(paletteColor); - } - }); - picker.show(getFragmentManager(), "picker"); - } - - private void onSaveButtonClick() - { - modifiedHabit.name = tvName.getText().toString().trim(); - modifiedHabit.description = tvDescription.getText().toString().trim(); - String freqNum = tvFreqNum.getText().toString(); - String freqDen = tvFreqDen.getText().toString(); - if(!freqNum.isEmpty()) modifiedHabit.freqNum = Integer.parseInt(freqNum); - if(!freqDen.isEmpty()) modifiedHabit.freqDen = Integer.parseInt(freqDen); - - if (!validate()) return; - - Command command = null; - Habit savedHabit = null; - - if (mode == EDIT_MODE) - { - command = new EditHabitCommand(originalHabit, modifiedHabit); - savedHabit = originalHabit; - } - else if (mode == CREATE_MODE) - { - command = new CreateHabitCommand(modifiedHabit); - } - - if (onSavedListener != null) onSavedListener.onSaved(command, savedHabit); - - dismiss(); - } - - private boolean validate() - { - Boolean valid = true; - - if (modifiedHabit.name.length() == 0) - { - tvName.setError(getString(R.string.validation_name_should_not_be_blank)); - valid = false; - } - - if (modifiedHabit.freqNum <= 0) - { - tvFreqNum.setError(getString(R.string.validation_number_should_be_positive)); - valid = false; - } - - if (modifiedHabit.freqNum > modifiedHabit.freqDen) - { - tvFreqNum.setError(getString(R.string.validation_at_most_one_rep_per_day)); - valid = false; - } - - return valid; - } - - @SuppressWarnings("ConstantConditions") - private void onDateSpinnerClick() - { - int defaultHour = 8; - int defaultMin = 0; - - if (modifiedHabit.hasReminder()) - { - defaultHour = modifiedHabit.reminderHour; - defaultMin = modifiedHabit.reminderMin; - } - - TimePickerDialog timePicker = - TimePickerDialog.newInstance(this, defaultHour, defaultMin, is24HourMode); - timePicker.show(getFragmentManager(), "timePicker"); - } - - @SuppressWarnings("ConstantConditions") - private void onWeekdayClick() - { - if(!modifiedHabit.hasReminder()) return; - - WeekdayPickerDialog dialog = new WeekdayPickerDialog(); - dialog.setListener(this); - dialog.setSelectedDays(DateHelper.unpackWeekdayList(modifiedHabit.reminderDays)); - dialog.show(getFragmentManager(), "weekdayPicker"); - } - - @Override - public void onTimeSet(RadialPickerLayout view, int hour, int minute) - { - modifiedHabit.reminderHour = hour; - modifiedHabit.reminderMin = minute; - modifiedHabit.reminderDays = DateHelper.ALL_WEEK_DAYS; - updateReminder(); - } - - @Override - public void onTimeCleared(RadialPickerLayout view) - { - modifiedHabit.clearReminder(); - updateReminder(); - } - - @Override - public void onWeekdaysPicked(boolean[] selectedDays) - { - int count = 0; - for(int i = 0; i < 7; i++) - if(selectedDays[i]) count++; - - if(count == 0) Arrays.fill(selectedDays, true); - - modifiedHabit.reminderDays = DateHelper.packWeekdayList(selectedDays); - updateReminder(); - } - - @Override - @SuppressWarnings("ConstantConditions") - public void onSaveInstanceState(Bundle outState) - { - super.onSaveInstanceState(outState); - - outState.putInt("color", modifiedHabit.color); - - if(modifiedHabit.hasReminder()) - { - outState.putInt("reminderMin", modifiedHabit.reminderMin); - outState.putInt("reminderHour", modifiedHabit.reminderHour); - outState.putInt("reminderDays", modifiedHabit.reminderDays); - } - } - - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) - { - if(parent.getId() == R.id.sFrequency) - { - switch (position) - { - case 0: - modifiedHabit.freqNum = 1; - modifiedHabit.freqDen = 1; - break; - - case 1: - modifiedHabit.freqNum = 1; - modifiedHabit.freqDen = 7; - break; - - case 2: - modifiedHabit.freqNum = 2; - modifiedHabit.freqDen = 7; - break; - - case 3: - modifiedHabit.freqNum = 5; - modifiedHabit.freqDen = 7; - break; - - case 4: - modifiedHabit.freqNum = 3; - modifiedHabit.freqDen = 7; - break; - } - } - - updateFrequency(); - } - - @SuppressLint("SetTextI18n") - private void updateFrequency() - { - int quickSelectPosition = -1; - - if(modifiedHabit.freqNum.equals(modifiedHabit.freqDen)) - quickSelectPosition = 0; - - else if(modifiedHabit.freqNum == 1 && modifiedHabit.freqDen == 7) - quickSelectPosition = 1; - - else if(modifiedHabit.freqNum == 2 && modifiedHabit.freqDen == 7) - quickSelectPosition = 2; - - else if(modifiedHabit.freqNum == 5 && modifiedHabit.freqDen == 7) - quickSelectPosition = 3; - - if(quickSelectPosition >= 0) - { - sFrequency.setVisibility(View.VISIBLE); - sFrequency.setSelection(quickSelectPosition); - llCustomFrequency.setVisibility(View.GONE); - tvFreqNum.setText(modifiedHabit.freqNum.toString()); - tvFreqDen.setText(modifiedHabit.freqDen.toString()); - } - else - { - sFrequency.setVisibility(View.GONE); - llCustomFrequency.setVisibility(View.VISIBLE); - } - } - - @Override - public void onNothingSelected(AdapterView parent) - { - - } -} diff --git a/app/src/main/java/org/isoron/uhabits/dialogs/HistoryEditorDialog.java b/app/src/main/java/org/isoron/uhabits/dialogs/HistoryEditorDialog.java deleted file mode 100644 index cd95e9212..000000000 --- a/app/src/main/java/org/isoron/uhabits/dialogs/HistoryEditorDialog.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright (C) 2016 Á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.dialogs; - -import android.app.Dialog; -import android.content.Context; -import android.content.DialogInterface; -import android.os.Bundle; -import android.support.v7.app.AlertDialog; -import android.support.v7.app.AppCompatDialogFragment; -import android.util.DisplayMetrics; - -import org.isoron.uhabits.R; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.tasks.BaseTask; -import org.isoron.uhabits.views.HabitHistoryView; - -public class HistoryEditorDialog extends AppCompatDialogFragment - implements DialogInterface.OnClickListener -{ - private Habit habit; - private Listener listener; - HabitHistoryView historyView; - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) - { - Context context = getActivity(); - historyView = new HabitHistoryView(context, null); - - if(savedInstanceState != null) - { - long id = savedInstanceState.getLong("habit", -1); - if(id > 0) this.habit = Habit.get(id); - } - - int padding = (int) getResources().getDimension(R.dimen.history_editor_padding); - historyView.setPadding(padding, 0, padding, 0); - historyView.setHabit(habit); - historyView.setIsEditable(true); - - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(R.string.history) - .setView(historyView) - .setPositiveButton(android.R.string.ok, this); - - refreshData(); - - return builder.create(); - } - - private void refreshData() - { - new BaseTask() - { - @Override - protected void doInBackground() - { - historyView.refreshData(); - } - }.execute(); - } - - @Override - public void onResume() - { - super.onResume(); - - DisplayMetrics metrics = getResources().getDisplayMetrics(); - int maxHeight = getResources().getDimensionPixelSize(R.dimen.history_editor_max_height); - int width = metrics.widthPixels; - int height = Math.min(metrics.heightPixels, maxHeight); - - getDialog().getWindow().setLayout(width, height); - } - - @Override - public void onClick(DialogInterface dialog, int which) - { - dismiss(); - } - - public void setHabit(Habit habit) - { - this.habit = habit; - if(historyView != null) historyView.setHabit(habit); - } - - @Override - public void onPause() - { - super.onPause(); - if(listener != null) listener.onHistoryEditorClosed(); - } - - @Override - public void onSaveInstanceState(Bundle outState) - { - outState.putLong("habit", habit.getId()); - } - - public void setListener(Listener listener) - { - this.listener = listener; - } - - public interface Listener { - void onHistoryEditorClosed(); - } -} diff --git a/app/src/main/java/org/isoron/uhabits/fragments/HabitListAdapter.java b/app/src/main/java/org/isoron/uhabits/fragments/HabitListAdapter.java deleted file mode 100644 index e01ceef84..000000000 --- a/app/src/main/java/org/isoron/uhabits/fragments/HabitListAdapter.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (C) 2016 Á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.fragments; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; - -import org.isoron.uhabits.R; -import org.isoron.uhabits.helpers.DateHelper; -import org.isoron.uhabits.helpers.ListHabitsHelper; -import org.isoron.uhabits.loaders.HabitListLoader; -import org.isoron.uhabits.models.Habit; - -import java.util.List; - -class HabitListAdapter extends BaseAdapter -{ - private LayoutInflater inflater; - private HabitListLoader loader; - private ListHabitsHelper helper; - private List selectedPositions; - private View.OnLongClickListener onCheckmarkLongClickListener; - private View.OnClickListener onCheckmarkClickListener; - - public HabitListAdapter(Context context, HabitListLoader loader) - { - this.loader = loader; - - inflater = LayoutInflater.from(context); - helper = new ListHabitsHelper(context, loader); - } - - @Override - public int getCount() - { - return loader.habits.size(); - } - - @Override - public Habit getItem(int position) - { - return loader.habitsList.get(position); - } - - @Override - public long getItemId(int position) - { - return (getItem(position)).getId(); - } - - @Override - public View getView(int position, View view, ViewGroup parent) - { - final Habit habit = loader.habitsList.get(position); - boolean selected = selectedPositions.contains(position); - - if (view == null || (Long) view.getTag(R.id.timestamp_key) != DateHelper.getStartOfToday()) - { - view = helper.inflateHabitCard(inflater, onCheckmarkLongClickListener, - onCheckmarkClickListener); - } - - helper.updateHabitCard(view, habit, selected); - return view; - } - - public void setSelectedPositions(List selectedPositions) - { - this.selectedPositions = selectedPositions; - } - - public void setOnCheckmarkLongClickListener(View.OnLongClickListener listener) - { - this.onCheckmarkLongClickListener = listener; - } - - public void setOnCheckmarkClickListener(View.OnClickListener listener) - { - this.onCheckmarkClickListener = listener; - } -} diff --git a/app/src/main/java/org/isoron/uhabits/fragments/HabitSelectionCallback.java b/app/src/main/java/org/isoron/uhabits/fragments/HabitSelectionCallback.java deleted file mode 100644 index bf0973aaf..000000000 --- a/app/src/main/java/org/isoron/uhabits/fragments/HabitSelectionCallback.java +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright (C) 2016 Á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.fragments; - -import android.content.DialogInterface; -import android.support.v7.app.AlertDialog; -import android.support.v7.view.ActionMode; -import android.view.Menu; -import android.view.MenuItem; -import android.widget.ProgressBar; - -import com.android.colorpicker.ColorPickerDialog; -import com.android.colorpicker.ColorPickerSwatch; - -import org.isoron.uhabits.BaseActivity; -import org.isoron.uhabits.R; -import org.isoron.uhabits.commands.ArchiveHabitsCommand; -import org.isoron.uhabits.commands.ChangeHabitColorCommand; -import org.isoron.uhabits.commands.DeleteHabitsCommand; -import org.isoron.uhabits.commands.UnarchiveHabitsCommand; -import org.isoron.uhabits.dialogs.EditHabitDialogFragment; -import org.isoron.uhabits.helpers.ColorHelper; -import org.isoron.uhabits.helpers.UIHelper; -import org.isoron.uhabits.loaders.HabitListLoader; -import org.isoron.uhabits.models.Habit; - -import java.util.LinkedList; -import java.util.List; - -public class HabitSelectionCallback implements ActionMode.Callback -{ - private HabitListLoader loader; - private List selectedPositions; - private BaseActivity activity; - private Listener listener; - private UIHelper.OnSavedListener onSavedListener; - private ProgressBar progressBar; - - public interface Listener - { - void onActionModeDestroyed(ActionMode mode); - } - - public HabitSelectionCallback(BaseActivity activity, HabitListLoader loader) - { - this.activity = activity; - this.loader = loader; - selectedPositions = new LinkedList<>(); - } - - public void setListener(Listener listener) - { - this.listener = listener; - } - - public void setProgressBar(ProgressBar progressBar) - { - this.progressBar = progressBar; - } - - public void setOnSavedListener(UIHelper.OnSavedListener onSavedListener) - { - this.onSavedListener = onSavedListener; - } - - public void setSelectedPositions(List selectedPositions) - { - this.selectedPositions = selectedPositions; - } - - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) - { - activity.getMenuInflater().inflate(R.menu.list_habits_context, menu); - updateTitle(mode); - updateActions(menu); - return true; - } - - @Override - public boolean onPrepareActionMode(ActionMode mode, Menu menu) - { - updateTitle(mode); - updateActions(menu); - return true; - } - - private void updateActions(Menu menu) - { - boolean showEdit = (selectedPositions.size() == 1); - boolean showArchive = true; - boolean showUnarchive = true; - for (int i : selectedPositions) - { - Habit h = loader.habitsList.get(i); - if (h.isArchived()) - { - showArchive = false; - } - else showUnarchive = false; - } - - 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(showEdit); - itemArchive.setVisible(showArchive); - itemUnarchive.setVisible(showUnarchive); - } - - private void updateTitle(ActionMode mode) - { - mode.setTitle("" + selectedPositions.size()); - } - - @Override - public boolean onActionItemClicked(final ActionMode mode, MenuItem item) - { - final LinkedList selectedHabits = new LinkedList<>(); - for (int i : selectedPositions) - selectedHabits.add(loader.habitsList.get(i)); - - Habit firstHabit = selectedHabits.getFirst(); - - switch (item.getItemId()) - { - case R.id.action_archive_habit: - activity.executeCommand(new ArchiveHabitsCommand(selectedHabits), null); - mode.finish(); - return true; - - case R.id.action_unarchive_habit: - activity.executeCommand(new UnarchiveHabitsCommand(selectedHabits), null); - mode.finish(); - return true; - - case R.id.action_edit_habit: - { - EditHabitDialogFragment - frag = EditHabitDialogFragment.editSingleHabitFragment(firstHabit.getId()); - frag.setOnSavedListener(onSavedListener); - frag.show(activity.getSupportFragmentManager(), "editHabit"); - return true; - } - - case R.id.action_color: - { - int originalAndroidColor = ColorHelper.getColor(activity, firstHabit.color); - - ColorPickerDialog picker = ColorPickerDialog.newInstance( - R.string.color_picker_default_title, ColorHelper.getPalette(activity), - originalAndroidColor, 4, ColorPickerDialog.SIZE_SMALL); - - picker.setOnColorSelectedListener(new ColorPickerSwatch.OnColorSelectedListener() - { - public void onColorSelected(int androidColor) - { - int paletteColor = ColorHelper.colorToPaletteIndex(activity, - androidColor); - activity.executeCommand(new ChangeHabitColorCommand(selectedHabits, - paletteColor), null); - mode.finish(); - } - }); - picker.show(activity.getSupportFragmentManager(), "picker"); - return true; - } - - case R.id.action_delete: - { - new AlertDialog.Builder(activity).setTitle(R.string.delete_habits) - .setMessage(R.string.delete_habits_message) - .setPositiveButton(android.R.string.yes, - new DialogInterface.OnClickListener() - { - @Override - public void onClick(DialogInterface dialog, int which) - { - activity.executeCommand( - new DeleteHabitsCommand(selectedHabits), null); - mode.finish(); - } - }).setNegativeButton(android.R.string.no, null) - .show(); - - return true; - } - } - - return false; - } - - @Override - public void onDestroyActionMode(ActionMode mode) - { - if(listener != null) listener.onActionModeDestroyed(mode); - } -} diff --git a/app/src/main/java/org/isoron/uhabits/fragments/ListHabitsFragment.java b/app/src/main/java/org/isoron/uhabits/fragments/ListHabitsFragment.java deleted file mode 100644 index b742f217f..000000000 --- a/app/src/main/java/org/isoron/uhabits/fragments/ListHabitsFragment.java +++ /dev/null @@ -1,495 +0,0 @@ -/* - * Copyright (C) 2016 Á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.fragments; - -import android.app.*; -import android.content.*; -import android.net.*; -import android.os.*; -import android.preference.*; -import android.support.annotation.*; -import android.support.v4.app.Fragment; -import android.support.v7.view.ActionMode; -import android.view.*; -import android.view.ContextMenu.*; -import android.view.View.*; -import android.widget.*; -import android.widget.AdapterView.*; - -import com.mobeta.android.dslv.*; -import com.mobeta.android.dslv.DragSortListView.*; - -import org.isoron.uhabits.*; -import org.isoron.uhabits.R; -import org.isoron.uhabits.commands.*; -import org.isoron.uhabits.dialogs.*; -import org.isoron.uhabits.helpers.*; -import org.isoron.uhabits.helpers.UIHelper.*; -import org.isoron.uhabits.loaders.*; -import org.isoron.uhabits.models.*; -import org.isoron.uhabits.tasks.*; - -import java.io.*; -import java.util.*; - -public class ListHabitsFragment extends Fragment - implements OnSavedListener, OnItemClickListener, OnLongClickListener, DropListener, - OnClickListener, HabitListLoader.Listener, AdapterView.OnItemLongClickListener, - HabitSelectionCallback.Listener, ImportDataTask.Listener, ExportCSVTask.Listener, - ExportDBTask.Listener -{ - long lastLongClick = 0; - private boolean isShortToggleEnabled; - private boolean showArchived; - - private ActionMode actionMode; - private HabitListAdapter adapter; - private HabitListLoader loader; - private HintManager hintManager; - private ListHabitsHelper helper; - private List selectedPositions; - private OnHabitClickListener habitClickListener; - private BaseActivity activity; - private SharedPreferences prefs; - - private DragSortListView listView; - private LinearLayout llButtonsHeader; - private ProgressBar progressBar; - private View llEmpty; - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) - { - View view = inflater.inflate(R.layout.list_habits_fragment, container, false); - View llHint = view.findViewById(R.id.llHint); - TextView tvStarEmpty = (TextView) view.findViewById(R.id.tvStarEmpty); - listView = (DragSortListView) view.findViewById(R.id.listView); - llButtonsHeader = (LinearLayout) view.findViewById(R.id.llButtonsHeader); - llEmpty = view.findViewById(R.id.llEmpty); - - progressBar = (ProgressBar) view.findViewById(R.id.progressBar); - progressBar.setVisibility(View.GONE); - - selectedPositions = new LinkedList<>(); - loader = new HabitListLoader(); - helper = new ListHabitsHelper(activity, loader); - hintManager = new HintManager(activity, llHint); - - loader.setListener(this); - loader.setCheckmarkCount(helper.getButtonCount()); - - llHint.setOnClickListener(this); - tvStarEmpty.setTypeface(UIHelper.getFontAwesome(activity)); - - adapter = new HabitListAdapter(getActivity(), loader); - adapter.setSelectedPositions(selectedPositions); - adapter.setOnCheckmarkClickListener(this); - adapter.setOnCheckmarkLongClickListener(this); - - DragSortListView.DragListener dragListener = new HabitsDragListener(); - DragSortController dragSortController = new HabitsDragSortController(); - - listView.setAdapter(adapter); - listView.setOnItemClickListener(this); - listView.setOnItemLongClickListener(this); - listView.setDropListener(this); - listView.setDragListener(dragListener); - listView.setFloatViewManager(dragSortController); - listView.setDragEnabled(true); - listView.setLongClickable(true); - - if(savedInstanceState != null) - { - EditHabitDialogFragment frag = (EditHabitDialogFragment) getFragmentManager() - .findFragmentByTag("editHabit"); - if(frag != null) frag.setOnSavedListener(this); - } - - setHasOptionsMenu(true); - return view; - } - - @Override - @SuppressWarnings("deprecation") - public void onAttach(Activity activity) - { - super.onAttach(activity); - this.activity = (BaseActivity) activity; - - habitClickListener = (OnHabitClickListener) activity; - prefs = PreferenceManager.getDefaultSharedPreferences(activity); - } - - @Override - public void onResume() - { - super.onResume(); - - loader.updateAllHabits(true); - helper.updateEmptyMessage(llEmpty); - helper.updateHeader(llButtonsHeader); - hintManager.showHintIfAppropriate(); - - adapter.notifyDataSetChanged(); - isShortToggleEnabled = prefs.getBoolean("pref_short_toggle", false); - } - - @Override - public void onLoadFinished() - { - adapter.notifyDataSetChanged(); - helper.updateEmptyMessage(llEmpty); - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) - { - super.onCreateOptionsMenu(menu, inflater); - inflater.inflate(R.menu.list_habits_options, menu); - - MenuItem showArchivedItem = menu.findItem(R.id.action_show_archived); - showArchivedItem.setChecked(showArchived); - } - - @Override - public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) - { - super.onCreateContextMenu(menu, view, menuInfo); - getActivity().getMenuInflater().inflate(R.menu.list_habits_context, menu); - - AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; - final Habit habit = loader.habits.get(info.id); - - if (habit.isArchived()) menu.findItem(R.id.action_archive_habit).setVisible(false); - else menu.findItem(R.id.action_unarchive_habit).setVisible(false); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) - { - switch (item.getItemId()) - { - case R.id.action_add: - { - EditHabitDialogFragment frag = EditHabitDialogFragment.createHabitFragment(); - frag.setOnSavedListener(this); - frag.show(getFragmentManager(), "editHabit"); - return true; - } - - case R.id.action_show_archived: - { - showArchived = !showArchived; - loader.setIncludeArchived(showArchived); - loader.updateAllHabits(true); - activity.invalidateOptionsMenu(); - return true; - } - - default: - return super.onOptionsItemSelected(item); - } - } - - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) - { - if (new Date().getTime() - lastLongClick < 1000) return; - - if(actionMode == null) - { - Habit habit = loader.habitsList.get(position); - habitClickListener.onHabitClicked(habit); - } - else - { - int k = selectedPositions.indexOf(position); - if(k < 0) - selectedPositions.add(position); - else - selectedPositions.remove(k); - - if(selectedPositions.isEmpty()) actionMode.finish(); - else actionMode.invalidate(); - - adapter.notifyDataSetChanged(); - } - } - - @Override - public boolean onItemLongClick(AdapterView parent, View view, int position, long id) - { - selectItem(position); - return true; - } - - private void selectItem(int position) - { - if(!selectedPositions.contains(position)) - selectedPositions.add(position); - - adapter.notifyDataSetChanged(); - - if(actionMode == null) - { - HabitSelectionCallback callback = new HabitSelectionCallback(activity, loader); - callback.setSelectedPositions(selectedPositions); - callback.setProgressBar(progressBar); - callback.setOnSavedListener(this); - callback.setListener(this); - - actionMode = activity.startSupportActionMode(callback); - } - - if(actionMode != null) actionMode.invalidate(); - } - - @Override - public void onSaved(Command command, Object savedObject) - { - Habit h = (Habit) savedObject; - - if (h == null) activity.executeCommand(command, null); - else activity.executeCommand(command, h.getId()); - adapter.notifyDataSetChanged(); - - ReminderHelper.createReminderAlarms(activity); - - if(actionMode != null) actionMode.finish(); - } - - @Override - public boolean onLongClick(View v) - { - lastLongClick = new Date().getTime(); - - switch (v.getId()) - { - case R.id.tvCheck: - onCheckmarkLongClick(v); - return true; - } - - return false; - } - - private void onCheckmarkLongClick(View v) - { - if (isShortToggleEnabled) return; - - toggleCheck(v); - } - - private void toggleCheck(View v) - { - Long id = helper.getHabitIdFromCheckmarkView(v); - Habit habit = loader.habits.get(id); - if(habit == null) return; - - float x = v.getX() + v.getWidth() / 2.0f + ((View) v.getParent()).getX(); - float y = v.getY() + v.getHeight() / 2.0f + ((View) v.getParent()).getY(); - helper.triggerRipple((View) v.getParent().getParent(), x, y); - - listView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); - helper.toggleCheckmarkView(v, habit); - - long timestamp = helper.getTimestampFromCheckmarkView(v); - executeCommand(new ToggleRepetitionCommand(habit, timestamp), habit.getId()); - } - - private void executeCommand(Command c, Long refreshKey) - { - activity.executeCommand(c, refreshKey); - } - - @Override - public void drop(int from, int to) - { - if(from == to) return; - if(actionMode != null) actionMode.finish(); - - loader.reorder(from, to); - adapter.notifyDataSetChanged(); - loader.updateAllHabits(false); - } - - @Override - public void onClick(View v) - { - switch (v.getId()) - { - case R.id.tvCheck: - if (isShortToggleEnabled) toggleCheck(v); - else activity.showToast(R.string.long_press_to_toggle); - break; - - case R.id.llHint: - hintManager.dismissHint(); - break; - } - } - - public void onPostExecuteCommand(Long refreshKey) - { - if (refreshKey == null) loader.updateAllHabits(true); - else loader.updateHabit(refreshKey); - } - - @Override - public void onActionModeDestroyed(ActionMode mode) - { - actionMode = null; - selectedPositions.clear(); - adapter.notifyDataSetChanged(); - listView.setDragEnabled(true); - } - - public interface OnHabitClickListener - { - void onHabitClicked(Habit habit); - } - - private class HabitsDragSortController extends DragSortController - { - public HabitsDragSortController() - { - super(ListHabitsFragment.this.listView); - setRemoveEnabled(false); - } - - @Override - public View onCreateFloatView(int position) - { - return adapter.getView(position, null, null); - } - - @Override - public void onDestroyFloatView(View floatView) - { - } - } - - private class HabitsDragListener implements DragSortListView.DragListener - { - @Override - public void drag(int from, int to) - { - } - - @Override - public void startDrag(int position) - { - selectItem(position); - } - } - - public void showImportDialog() - { - File dir = DatabaseHelper.getFilesDir(null); - if(dir == null) - { - activity.showToast(R.string.could_not_import); - return; - } - - FilePickerDialog picker = new FilePickerDialog(activity, dir); - picker.setListener(new FilePickerDialog.OnFileSelectedListener() - { - @Override - public void onFileSelected(File file) - { - ImportDataTask task = new ImportDataTask(file, progressBar); - task.setListener(ListHabitsFragment.this); - task.execute(); - } - }); - - picker.show(); - } - - @Override - public void onImportFinished(int result) - { - switch (result) - { - case ImportDataTask.SUCCESS: - loader.updateAllHabits(true); - activity.showToast(R.string.habits_imported); - break; - - case ImportDataTask.NOT_RECOGNIZED: - activity.showToast(R.string.file_not_recognized); - break; - - default: - activity.showToast(R.string.could_not_import); - break; - } - } - - public void exportAllHabits() - { - ExportCSVTask task = new ExportCSVTask(Habit.getAll(true), progressBar); - task.setListener(this); - task.execute(); - } - - @Override - public void onExportCSVFinished(@Nullable String archiveFilename) - { - if(archiveFilename != null) - { - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_SEND); - intent.setType("application/zip"); - intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(new File(archiveFilename))); - activity.startActivity(intent); - } - else - { - activity.showToast(R.string.could_not_export); - } - } - - public void exportDB() - { - ExportDBTask task = new ExportDBTask(progressBar); - task.setListener(this); - task.execute(); - } - - @Override - public void onExportDBFinished(@Nullable String filename) - { - if(filename != null) - { - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_SEND); - intent.setType("application/octet-stream"); - intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(new File(filename))); - activity.startActivity(intent); - } - else - { - activity.showToast(R.string.could_not_export); - } - } -} diff --git a/app/src/main/java/org/isoron/uhabits/fragments/ShowHabitFragment.java b/app/src/main/java/org/isoron/uhabits/fragments/ShowHabitFragment.java deleted file mode 100644 index f95c83881..000000000 --- a/app/src/main/java/org/isoron/uhabits/fragments/ShowHabitFragment.java +++ /dev/null @@ -1,374 +0,0 @@ -/* - * Copyright (C) 2016 Á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.fragments; - -import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.Button; -import android.widget.Spinner; -import android.widget.TextView; - -import org.isoron.uhabits.HabitBroadcastReceiver; -import org.isoron.uhabits.R; -import org.isoron.uhabits.ShowHabitActivity; -import org.isoron.uhabits.commands.Command; -import org.isoron.uhabits.dialogs.EditHabitDialogFragment; -import org.isoron.uhabits.dialogs.HistoryEditorDialog; -import org.isoron.uhabits.helpers.ColorHelper; -import org.isoron.uhabits.helpers.DateHelper; -import org.isoron.uhabits.helpers.ReminderHelper; -import org.isoron.uhabits.helpers.UIHelper; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.models.Score; -import org.isoron.uhabits.tasks.BaseTask; -import org.isoron.uhabits.views.HabitDataView; -import org.isoron.uhabits.views.HabitFrequencyView; -import org.isoron.uhabits.views.HabitHistoryView; -import org.isoron.uhabits.views.HabitScoreView; -import org.isoron.uhabits.views.HabitStreakView; -import org.isoron.uhabits.views.RingView; - -import java.util.LinkedList; -import java.util.List; - -public class ShowHabitFragment extends Fragment - implements UIHelper.OnSavedListener, HistoryEditorDialog.Listener, - Spinner.OnItemSelectedListener -{ - @Nullable - protected ShowHabitActivity activity; - - @Nullable - private Habit habit; - - @Nullable - private List dataViews; - - @Nullable - private HabitScoreView scoreView; - - private int previousScoreInterval; - - private float todayScore; - private float lastMonthScore; - private float lastYearScore; - - private int activeColor; - private int inactiveColor; - - @Override - public void onStart() - { - super.onStart(); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) - { - View view = inflater.inflate(R.layout.show_habit, container, false); - activity = (ShowHabitActivity) getActivity(); - - habit = activity.getHabit(); - activeColor = ColorHelper.getColor(getContext(), habit.color); - inactiveColor = UIHelper.getStyledColor(getContext(), R.attr.mediumContrastTextColor); - - updateHeader(view); - - dataViews = new LinkedList<>(); - - Button btEditHistory = (Button) view.findViewById(R.id.btEditHistory); - Spinner sStrengthInterval = (Spinner) view.findViewById(R.id.sStrengthInterval); - - scoreView = (HabitScoreView) view.findViewById(R.id.scoreView); - - int defaultScoreInterval = UIHelper.getDefaultScoreInterval(getContext()); - previousScoreInterval = defaultScoreInterval; - setScoreBucketSize(defaultScoreInterval); - - sStrengthInterval.setSelection(defaultScoreInterval); - sStrengthInterval.setOnItemSelectedListener(this); - - dataViews.add((HabitScoreView) view.findViewById(R.id.scoreView)); - dataViews.add((HabitHistoryView) view.findViewById(R.id.historyView)); - dataViews.add((HabitFrequencyView) view.findViewById(R.id.punchcardView)); - dataViews.add((HabitStreakView) view.findViewById(R.id.streakView)); - - updateHeaders(view); - - for(HabitDataView dataView : dataViews) - dataView.setHabit(habit); - - btEditHistory.setOnClickListener(new View.OnClickListener() - { - @Override - public void onClick(View v) - { - HistoryEditorDialog frag = new HistoryEditorDialog(); - frag.setHabit(habit); - frag.setListener(ShowHabitFragment.this); - frag.show(getFragmentManager(), "historyEditor"); - } - }); - - if(savedInstanceState != null) - { - EditHabitDialogFragment fragEdit = (EditHabitDialogFragment) getFragmentManager() - .findFragmentByTag("editHabit"); - HistoryEditorDialog fragEditor = (HistoryEditorDialog) getFragmentManager() - .findFragmentByTag("historyEditor"); - - if(fragEdit != null) fragEdit.setOnSavedListener(this); - if(fragEditor != null) fragEditor.setListener(this); - } - - setHasOptionsMenu(true); - - return view; - } - - private void updateHeader(View view) - { - if(habit == null) return; - - TextView questionLabel = (TextView) view.findViewById(R.id.questionLabel); - questionLabel.setTextColor(activeColor); - questionLabel.setText(habit.description); - - TextView reminderLabel = (TextView) view.findViewById(R.id.reminderLabel); - if(habit.hasReminder()) - reminderLabel.setText(DateHelper.formatTime(getActivity(), habit.reminderHour, - habit.reminderMin)); - else - reminderLabel.setText(getResources().getString(R.string.reminder_off)); - - TextView frequencyLabel = (TextView) view.findViewById(R.id.frequencyLabel); - frequencyLabel.setText(getFreqText()); - - if(habit.description.isEmpty()) - questionLabel.setVisibility(View.GONE); - } - - private String getFreqText() - { - if(habit == null) - return ""; - - if(habit.freqNum.equals(habit.freqDen)) - return getResources().getString(R.string.every_day); - - if(habit.freqNum == 1) - { - if (habit.freqDen == 7) - return getResources().getString(R.string.every_week); - - if (habit.freqDen % 7 == 0) - return getResources().getString(R.string.every_x_weeks, habit.freqDen / 7); - - return getResources().getString(R.string.every_x_days, habit.freqDen); - } - - String times_every = getResources().getString(R.string.times_every); - - if(habit.freqNum == 1) - times_every = getResources().getString(R.string.time_every); - - return String.format("%d %s %d %s", habit.freqNum, times_every, habit.freqDen, - getResources().getString(R.string.days)); - } - - @Override - public void onResume() - { - super.onResume(); - refreshData(); - } - - private void updateScore(View view) - { - if(habit == null) return; - if(view == null) return; - - float todayPercentage = todayScore / Score.MAX_VALUE; - float monthDiff = todayPercentage - (lastMonthScore / Score.MAX_VALUE); - float yearDiff = todayPercentage - (lastYearScore / Score.MAX_VALUE); - - RingView scoreRing = (RingView) view.findViewById(R.id.scoreRing); - int androidColor = ColorHelper.getColor(getActivity(), habit.color); - scoreRing.setColor(androidColor); - scoreRing.setPercentage(todayPercentage); - - TextView scoreLabel = (TextView) view.findViewById(R.id.scoreLabel); - TextView monthDiffLabel = (TextView) view.findViewById(R.id.monthDiffLabel); - TextView yearDiffLabel = (TextView) view.findViewById(R.id.yearDiffLabel); - - scoreLabel.setText(String.format("%.0f%%", todayPercentage * 100)); - - String minus = "\u2212"; - monthDiffLabel.setText(String.format("%s%.0f%%", (monthDiff >= 0 ? "+" : minus), - Math.abs(monthDiff) * 100)); - yearDiffLabel.setText( - String.format("%s%.0f%%", (yearDiff >= 0 ? "+" : minus), Math.abs(yearDiff) * 100)); - - monthDiffLabel.setTextColor(monthDiff >= 0 ? activeColor : inactiveColor); - yearDiffLabel.setTextColor(yearDiff >= 0 ? activeColor : inactiveColor); - } - - private void updateHeaders(View view) - { - updateColor(view, R.id.tvHistory); - updateColor(view, R.id.tvOverview); - updateColor(view, R.id.tvStrength); - updateColor(view, R.id.tvStreaks); - updateColor(view, R.id.tvWeekdayFreq); - updateColor(view, R.id.scoreLabel); - } - - private void updateColor(View view, int viewId) - { - if(habit == null || activity == null) return; - - TextView textView = (TextView) view.findViewById(viewId); - int androidColor = ColorHelper.getColor(activity, habit.color); - textView.setTextColor(androidColor); - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) - { - inflater.inflate(R.menu.show_habit_fragment_menu, menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) - { - if(habit == null) return false; - - switch (item.getItemId()) - { - case R.id.action_edit_habit: - { - EditHabitDialogFragment - frag = EditHabitDialogFragment.editSingleHabitFragment(habit.getId()); - frag.setOnSavedListener(this); - frag.show(getFragmentManager(), "editHabit"); - return true; - } - } - - return false; - } - - @Override - public void onSaved(Command command, Object savedObject) - { - if(activity == null) return; - Habit h = (Habit) savedObject; - - if (h == null) activity.executeCommand(command, null); - else activity.executeCommand(command, h.getId()); - - ReminderHelper.createReminderAlarms(activity); - HabitBroadcastReceiver.sendRefreshBroadcast(getActivity()); - - activity.recreate(); - } - - @Override - public void onHistoryEditorClosed() - { - refreshData(); - HabitBroadcastReceiver.sendRefreshBroadcast(getActivity()); - } - - public void refreshData() - { - new BaseTask() - { - @Override - protected void doInBackground() - { - if(habit == null) return; - if(dataViews == null) return; - - long today = DateHelper.getStartOfToday(); - long lastMonth = today - 30 * DateHelper.millisecondsInOneDay; - long lastYear = today - 365 * DateHelper.millisecondsInOneDay; - - todayScore = (float) habit.scores.getTodayValue(); - lastMonthScore = (float) habit.scores.getValue(lastMonth); - lastYearScore = (float) habit.scores.getValue(lastYear); - - int count = 0; - for(HabitDataView view : dataViews) - { - view.refreshData(); - publishProgress(count++); - } - } - - @Override - protected void onProgressUpdate(Integer... values) - { - updateScore(getView()); - if(dataViews == null) return; - dataViews.get(values[0]).postInvalidate(); - } - }.execute(); - - } - - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) - { - if(parent.getId() == R.id.sStrengthInterval) - setScoreBucketSize(position); - } - - private void setScoreBucketSize(int position) - { - if(scoreView == null) return; - - scoreView.setBucketSize(HabitScoreView.DEFAULT_BUCKET_SIZES[position]); - - if(position != previousScoreInterval) - { - refreshData(); - HabitBroadcastReceiver.sendRefreshBroadcast(getActivity()); - } - - UIHelper.setDefaultScoreInterval(getContext(), position); - previousScoreInterval = position; - } - - @Override - public void onNothingSelected(AdapterView parent) - { - - } -} diff --git a/app/src/main/java/org/isoron/uhabits/helpers/ColorHelper.java b/app/src/main/java/org/isoron/uhabits/helpers/ColorHelper.java deleted file mode 100644 index d64f4ac3d..000000000 --- a/app/src/main/java/org/isoron/uhabits/helpers/ColorHelper.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright (C) 2016 Á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.helpers; - -import android.content.Context; -import android.graphics.Color; -import android.util.Log; - -import org.isoron.uhabits.R; - -public class ColorHelper -{ - public static int CSV_PALETTE[] = - { - Color.parseColor("#D32F2F"), // 0 red - Color.parseColor("#E64A19"), // 1 orange - Color.parseColor("#F9A825"), // 2 yellow - Color.parseColor("#AFB42B"), // 3 light green - Color.parseColor("#388E3C"), // 4 dark green - Color.parseColor("#00897B"), // 5 teal - Color.parseColor("#00ACC1"), // 6 cyan - Color.parseColor("#039BE5"), // 7 blue - Color.parseColor("#5E35B1"), // 8 deep purple - Color.parseColor("#8E24AA"), // 9 purple - Color.parseColor("#D81B60"), // 10 pink - Color.parseColor("#303030"), // 11 dark grey - Color.parseColor("#aaaaaa") // 12 light grey - }; - - public static int colorToPaletteIndex(Context context, int color) - { - int[] palette = getPalette(context); - - for(int k = 0; k < palette.length; k++) - if(palette[k] == color) return k; - - return -1; - } - - public static int[] getPalette(Context context) - { - int resourceId = UIHelper.getStyleResource(context, R.attr.palette); - if(resourceId < 0) return CSV_PALETTE; - - return context.getResources().getIntArray(resourceId); - } - - public static int getColor(Context context, int paletteColor) - { - if(context == null) throw new IllegalArgumentException("Context is null"); - - int palette[] = getPalette(context); - if(paletteColor < 0 || paletteColor >= palette.length) - { - Log.w("ColorHelper", String.format("Invalid color: %d. Returning default.", paletteColor)); - paletteColor = 0; - } - - return palette[paletteColor]; - } - - public static int mixColors(int color1, int color2, float amount) - { - final byte ALPHA_CHANNEL = 24; - final byte RED_CHANNEL = 16; - final byte GREEN_CHANNEL = 8; - final byte BLUE_CHANNEL = 0; - - final float inverseAmount = 1.0f - amount; - - int a = ((int) (((float) (color1 >> ALPHA_CHANNEL & 0xff) * amount) + - ((float) (color2 >> ALPHA_CHANNEL & 0xff) * inverseAmount))) & 0xff; - int r = ((int) (((float) (color1 >> RED_CHANNEL & 0xff) * amount) + - ((float) (color2 >> RED_CHANNEL & 0xff) * inverseAmount))) & 0xff; - int g = ((int) (((float) (color1 >> GREEN_CHANNEL & 0xff) * amount) + - ((float) (color2 >> GREEN_CHANNEL & 0xff) * inverseAmount))) & 0xff; - int b = ((int) (((float) (color1 & 0xff) * amount) + - ((float) (color2 & 0xff) * inverseAmount))) & 0xff; - - return a << ALPHA_CHANNEL | r << RED_CHANNEL | g << GREEN_CHANNEL | b << BLUE_CHANNEL; - } - - public static int setHue(int color, float newHue) - { - return setHSVParameter(color, newHue, 0); - } - - public static int setSaturation(int color, float newSaturation) - { - return setHSVParameter(color, newSaturation, 1); - } - - public static int setValue(int color, float newValue) - { - return setHSVParameter(color, newValue, 2); - } - - public static int setAlpha(int color, float newAlpha) - { - int intAlpha = (int) (newAlpha * 255); - return Color.argb(intAlpha, Color.red(color), Color.green(color), Color.blue(color)); - } - - public static int setMinValue(int color, float newValue) - { - float hsv[] = new float[3]; - Color.colorToHSV(color, hsv); - hsv[2] = Math.max(hsv[2], newValue); - return Color.HSVToColor(hsv); - } - - private static int setHSVParameter(int color, float newValue, int index) - { - float hsv[] = new float[3]; - Color.colorToHSV(color, hsv); - hsv[index] = newValue; - return Color.HSVToColor(hsv); - } - - public static String toHTML(int color) - { - return String.format("#%06X", 0xFFFFFF & color); - } -} \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/helpers/HintManager.java b/app/src/main/java/org/isoron/uhabits/helpers/HintManager.java deleted file mode 100644 index 998939ed9..000000000 --- a/app/src/main/java/org/isoron/uhabits/helpers/HintManager.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (C) 2016 Á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.helpers; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.view.View; -import android.widget.TextView; - -import org.isoron.uhabits.R; - -public class HintManager -{ - private Context context; - private SharedPreferences prefs; - private View hintView; - - public HintManager(Context context, View hintView) - { - this.context = context; - this.hintView = hintView; - prefs = PreferenceManager.getDefaultSharedPreferences(context); - } - - public void dismissHint() - { - hintView.animate().alpha(0f).setDuration(500).setListener(new AnimatorListenerAdapter() - { - @Override - public void onAnimationEnd(Animator animation) - { - hintView.setVisibility(View.GONE); - } - }); - } - - public void showHintIfAppropriate() - { - Integer lastHintNumber = prefs.getInt("last_hint_number", -1); - Long lastHintTimestamp = prefs.getLong("last_hint_timestamp", -1); - - if (DateHelper.getStartOfToday() > lastHintTimestamp) showHint(lastHintNumber + 1); - } - - private void showHint(int hintNumber) - { - String[] hints = context.getResources().getStringArray(R.array.hints); - if (hintNumber >= hints.length) return; - - prefs.edit().putInt("last_hint_number", hintNumber).apply(); - prefs.edit().putLong("last_hint_timestamp", DateHelper.getStartOfToday()).apply(); - - TextView tvContent = (TextView) hintView.findViewById(R.id.hintContent); - tvContent.setText(hints[hintNumber]); - - hintView.setAlpha(0.0f); - hintView.setVisibility(View.VISIBLE); - hintView.animate().alpha(1f).setDuration(500); - } -} diff --git a/app/src/main/java/org/isoron/uhabits/helpers/ListHabitsHelper.java b/app/src/main/java/org/isoron/uhabits/helpers/ListHabitsHelper.java deleted file mode 100644 index 91a3a4aff..000000000 --- a/app/src/main/java/org/isoron/uhabits/helpers/ListHabitsHelper.java +++ /dev/null @@ -1,305 +0,0 @@ -/* - * Copyright (C) 2016 Á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.helpers; - -import android.content.Context; -import android.content.SharedPreferences; -import android.graphics.drawable.Drawable; -import android.os.Handler; -import android.preference.PreferenceManager; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.widget.LinearLayout; -import android.widget.TextView; - -import org.isoron.uhabits.R; -import org.isoron.uhabits.loaders.HabitListLoader; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.models.Score; -import org.isoron.uhabits.views.RingView; - -import java.util.GregorianCalendar; - -public class ListHabitsHelper -{ - private static final int CHECKMARK_LEFT_TO_RIGHT = 0; - private static final int CHECKMARK_RIGHT_TO_LEFT = 1; - - private final int lowContrastColor; - private final int mediumContrastColor; - - private final Context context; - private final HabitListLoader loader; - - public ListHabitsHelper(Context context, HabitListLoader loader) - { - this.context = context; - this.loader = loader; - - lowContrastColor = UIHelper.getStyledColor(context, R.attr.lowContrastTextColor); - mediumContrastColor = UIHelper.getStyledColor(context, R.attr.mediumContrastTextColor); - } - - public int getButtonCount() - { - float screenWidth = UIHelper.getScreenWidth(context); - float labelWidth = context.getResources().getDimension(R.dimen.habitNameWidth); - float buttonWidth = context.getResources().getDimension(R.dimen.checkmarkWidth); - return Math.max(0, (int) ((screenWidth - labelWidth) / buttonWidth)); - } - - public int getHabitNameWidth() - { - float screenWidth = UIHelper.getScreenWidth(context); - float buttonWidth = context.getResources().getDimension(R.dimen.checkmarkWidth); - float padding = UIHelper.dpToPixels(context, 15); - return (int) (screenWidth - padding - getButtonCount() * buttonWidth); - } - - public void updateCheckmarkButtons(Habit habit, LinearLayout llButtons) - { - int activeColor = getActiveColor(habit); - int m = llButtons.getChildCount(); - Long habitId = habit.getId(); - - int isChecked[] = loader.checkmarks.get(habitId); - - for (int i = 0; i < m; i++) - { - int position = i; - - if(getCheckmarkOrder() == CHECKMARK_RIGHT_TO_LEFT) - position = m - i - 1; - - TextView tvCheck = (TextView) llButtons.getChildAt(position); - tvCheck.setTag(R.string.habit_key, habitId); - tvCheck.setTag(R.string.offset_key, i); - if(isChecked.length > i) - updateCheckmark(activeColor, tvCheck, isChecked[i]); - } - } - - public int getActiveColor(Habit habit) - { - int activeColor = ColorHelper.getColor(context, habit.color); - if(habit.isArchived()) activeColor = mediumContrastColor; - - return activeColor; - } - - public void initializeLabelAndIcon(View itemView) - { - LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(getHabitNameWidth(), - LinearLayout.LayoutParams.WRAP_CONTENT, 1); - itemView.findViewById(R.id.label).setLayoutParams(params); - } - - public void updateNameAndIcon(Habit habit, RingView ring, TextView tvName) - { - int activeColor = getActiveColor(habit); - - tvName.setText(habit.name); - tvName.setTextColor(activeColor); - - int score = loader.scores.get(habit.getId()); - float percentage = (float) score / Score.MAX_VALUE; - - ring.setColor(activeColor); - ring.setPercentage(percentage); - ring.setPrecision(1.0f / 16); - } - - public void updateCheckmark(int activeColor, TextView tvCheck, int check) - { - switch (check) - { - case 2: - tvCheck.setText(R.string.fa_check); - tvCheck.setTextColor(activeColor); - tvCheck.setTag(R.string.toggle_key, 2); - break; - - case 1: - tvCheck.setText(R.string.fa_check); - tvCheck.setTextColor(lowContrastColor); - tvCheck.setTag(R.string.toggle_key, 1); - break; - - case 0: - tvCheck.setText(R.string.fa_times); - tvCheck.setTextColor(lowContrastColor); - tvCheck.setTag(R.string.toggle_key, 0); - break; - } - } - - public View inflateHabitCard(LayoutInflater inflater, - View.OnLongClickListener onCheckmarkLongClickListener, - View.OnClickListener onCheckmarkClickListener) - { - View view = inflater.inflate(R.layout.list_habits_item, null); - initializeLabelAndIcon(view); - inflateCheckmarkButtons(view, onCheckmarkLongClickListener, onCheckmarkClickListener, - inflater); - return view; - } - - public void updateHabitCard(View view, Habit habit, boolean selected) - { - RingView scoreRing = ((RingView) view.findViewById(R.id.scoreRing)); - TextView tvName = (TextView) view.findViewById(R.id.label); - LinearLayout llInner = (LinearLayout) view.findViewById(R.id.llInner); - LinearLayout llButtons = (LinearLayout) view.findViewById(R.id.llButtons); - - llInner.setTag(R.string.habit_key, habit.getId()); - llInner.setOnTouchListener(new HotspotTouchListener()); - - updateNameAndIcon(habit, scoreRing, tvName); - updateCheckmarkButtons(habit, llButtons); - updateHabitCardBackground(llInner, selected); - } - - - public void updateHabitCardBackground(View view, boolean isSelected) - { - if (android.os.Build.VERSION.SDK_INT >= 21) - { - if (isSelected) - view.setBackgroundResource(R.drawable.selected_box); - else - view.setBackgroundResource(R.drawable.ripple); - } - else - { - Drawable background; - - if (isSelected) - background = UIHelper.getStyledDrawable(context, R.attr.selectedBackground); - else - background = UIHelper.getStyledDrawable(context, R.attr.cardBackground); - - view.setBackgroundDrawable(background); - } - } - - public void inflateCheckmarkButtons(View view, View.OnLongClickListener onLongClickListener, - View.OnClickListener onClickListener, LayoutInflater inflater) - { - for (int i = 0; i < getButtonCount(); i++) - { - View check = inflater.inflate(R.layout.list_habits_item_check, null); - TextView btCheck = (TextView) check.findViewById(R.id.tvCheck); - btCheck.setTypeface(UIHelper.getFontAwesome(context)); - btCheck.setOnLongClickListener(onLongClickListener); - btCheck.setOnClickListener(onClickListener); - btCheck.setHapticFeedbackEnabled(false); - ((LinearLayout) view.findViewById(R.id.llButtons)).addView(check); - } - - view.setTag(R.id.timestamp_key, DateHelper.getStartOfToday()); - } - - public void updateHeader(ViewGroup header) - { - LayoutInflater inflater = LayoutInflater.from(context); - GregorianCalendar day = DateHelper.getStartOfTodayCalendar(); - header.removeAllViews(); - - for (int i = 0; i < getButtonCount(); i++) - { - int position = 0; - - if(getCheckmarkOrder() == CHECKMARK_LEFT_TO_RIGHT) - position = i; - - View tvDay = inflater.inflate(R.layout.list_habits_header_check, null); - TextView btCheck = (TextView) tvDay.findViewById(R.id.tvCheck); - btCheck.setText(DateHelper.formatHeaderDate(day)); - header.addView(tvDay, position); - day.add(GregorianCalendar.DAY_OF_MONTH, -1); - } - } - - public void updateEmptyMessage(View view) - { - if (loader.getLastLoadTimestamp() == null) view.setVisibility(View.GONE); - else view.setVisibility(loader.habits.size() > 0 ? View.GONE : View.VISIBLE); - } - - public void toggleCheckmarkView(View v, Habit habit) - { - int androidColor = ColorHelper.getColor(context, habit.color); - - if (v.getTag(R.string.toggle_key).equals(2)) - updateCheckmark(androidColor, (TextView) v, 0); - else - updateCheckmark(androidColor, (TextView) v, 2); - } - - public Long getHabitIdFromCheckmarkView(View v) - { - return (Long) v.getTag(R.string.habit_key); - } - - public long getTimestampFromCheckmarkView(View v) - { - Integer offset = (Integer) v.getTag(R.string.offset_key); - return DateHelper.getStartOfDay(DateHelper.getLocalTime() - - offset * DateHelper.millisecondsInOneDay); - } - - public void triggerRipple(View v, final float x, final float y) - { - final Drawable background = v.getBackground(); - if (android.os.Build.VERSION.SDK_INT >= 21) - background.setHotspot(x, y); - - background.setState(new int[]{android.R.attr.state_pressed, android.R.attr.state_enabled}); - - new Handler().postDelayed(new Runnable() - { - @Override - public void run() - { - background.setState(new int[]{}); - } - }, 25); - } - - private static class HotspotTouchListener implements View.OnTouchListener - { - @Override - public boolean onTouch(View v, MotionEvent event) - { - if (android.os.Build.VERSION.SDK_INT >= 21) - v.getBackground().setHotspot(event.getX(), event.getY()); - return false; - } - } - - public int getCheckmarkOrder() - { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - boolean reverse = prefs.getBoolean("pref_checkmark_reverse_order", false); - return reverse ? CHECKMARK_RIGHT_TO_LEFT : CHECKMARK_LEFT_TO_RIGHT; - } -} diff --git a/app/src/main/java/org/isoron/uhabits/io/AbstractImporter.java b/app/src/main/java/org/isoron/uhabits/io/AbstractImporter.java index 83cfddcb8..3f6da5f8b 100644 --- a/app/src/main/java/org/isoron/uhabits/io/AbstractImporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/AbstractImporter.java @@ -21,13 +21,30 @@ package org.isoron.uhabits.io; import android.support.annotation.NonNull; +import org.isoron.uhabits.HabitsApplication; +import org.isoron.uhabits.models.HabitList; + import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.Arrays; +import javax.inject.Inject; + +/** + * AbstractImporter is the base class for all classes that import data from + * files into the app. + */ public abstract class AbstractImporter { + @Inject + HabitList habitList; + + public AbstractImporter() + { + HabitsApplication.getComponent().inject(this); + } + public abstract boolean canHandle(@NonNull File file) throws IOException; public abstract void importHabitsFromFile(@NonNull File file) throws IOException; diff --git a/app/src/main/java/org/isoron/uhabits/io/GenericImporter.java b/app/src/main/java/org/isoron/uhabits/io/GenericImporter.java index c08a3a72f..6f8e2ef70 100644 --- a/app/src/main/java/org/isoron/uhabits/io/GenericImporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/GenericImporter.java @@ -26,6 +26,10 @@ import java.io.IOException; import java.util.LinkedList; import java.util.List; +/** + * A GenericImporter decides which implementation of AbstractImporter is able to + * handle a given file and delegates to it the task of importing the data. + */ public class GenericImporter extends AbstractImporter { List importers; @@ -42,8 +46,8 @@ public class GenericImporter extends AbstractImporter @Override public boolean canHandle(@NonNull File file) throws IOException { - for(AbstractImporter importer : importers) - if(importer.canHandle(file)) return true; + for (AbstractImporter importer : importers) + if (importer.canHandle(file)) return true; return false; } @@ -51,8 +55,7 @@ public class GenericImporter extends AbstractImporter @Override public void importHabitsFromFile(@NonNull File file) throws IOException { - for(AbstractImporter importer : importers) - if(importer.canHandle(file)) - importer.importHabitsFromFile(file); + for (AbstractImporter importer : importers) + if (importer.canHandle(file)) importer.importHabitsFromFile(file); } } diff --git a/app/src/main/java/org/isoron/uhabits/io/HabitBullCSVImporter.java b/app/src/main/java/org/isoron/uhabits/io/HabitBullCSVImporter.java index 46be626c9..9ce0aa005 100644 --- a/app/src/main/java/org/isoron/uhabits/io/HabitBullCSVImporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/HabitBullCSVImporter.java @@ -24,8 +24,8 @@ import android.support.annotation.NonNull; import com.activeandroid.ActiveAndroid; import com.opencsv.CSVReader; -import org.isoron.uhabits.helpers.DateHelper; -import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.DateUtils; import java.io.BufferedReader; import java.io.File; @@ -34,6 +34,9 @@ import java.io.IOException; import java.util.Calendar; import java.util.HashMap; +/** + * Class that imports data from HabitBull CSV files. + */ public class HabitBullCSVImporter extends AbstractImporter { @Override @@ -76,7 +79,7 @@ public class HabitBullCSVImporter extends AbstractImporter int month = Integer.parseInt(dateString[1]); int day = Integer.parseInt(dateString[2]); - Calendar date = DateHelper.getStartOfTodayCalendar(); + Calendar date = DateUtils.getStartOfTodayCalendar(); date.set(year, month - 1, day); long timestamp = date.getTimeInMillis(); @@ -89,16 +92,15 @@ public class HabitBullCSVImporter extends AbstractImporter if(h == null) { h = new Habit(); - h.name = name; - h.description = description; - h.freqNum = h.freqDen = 1; - h.save(); - + h.setName(name); + h.setDescription(description); + h.setFrequency(Frequency.DAILY); + habitList.add(h); habits.put(name, h); } - if(!h.repetitions.contains(timestamp)) - h.repetitions.toggle(timestamp); + if(!h.getRepetitions().containsTimestamp(timestamp)) + h.getRepetitions().toggleTimestamp(timestamp); } } } diff --git a/app/src/main/java/org/isoron/uhabits/io/HabitsCSVExporter.java b/app/src/main/java/org/isoron/uhabits/io/HabitsCSVExporter.java index b22da70e7..e53102f0a 100644 --- a/app/src/main/java/org/isoron/uhabits/io/HabitsCSVExporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/HabitsCSVExporter.java @@ -21,10 +21,12 @@ package org.isoron.uhabits.io; import android.support.annotation.NonNull; -import org.isoron.uhabits.helpers.DateHelper; +import org.isoron.uhabits.HabitsApplication; import org.isoron.uhabits.models.CheckmarkList; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.HabitList; import org.isoron.uhabits.models.ScoreList; +import org.isoron.uhabits.utils.DateUtils; import java.io.File; import java.io.FileInputStream; @@ -37,6 +39,11 @@ import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; +import javax.inject.Inject; + +/** + * Class that exports the application data to CSV files. + */ public class HabitsCSVExporter { private List habits; @@ -46,8 +53,13 @@ public class HabitsCSVExporter private String exportDirName; + @Inject + HabitList habitList; + public HabitsCSVExporter(List habits, File dir) { + HabitsApplication.getComponent().inject(this); + this.habits = habits; this.exportDirName = dir.getAbsolutePath() + "/"; @@ -61,20 +73,20 @@ public class HabitsCSVExporter new File(exportDirName).mkdirs(); FileWriter out = new FileWriter(exportDirName + filename); generateFilenames.add(filename); - Habit.writeCSV(habits, out); + habitList.writeCSV(out); out.close(); for(Habit h : habits) { - String sane = sanitizeFilename(h.name); - String habitDirName = String.format("%03d %s", h.position + 1, sane); + String sane = sanitizeFilename(h.getName()); + String habitDirName = String.format("%03d %s", habitList.indexOf(h) + 1, sane); habitDirName = habitDirName.trim() + "/"; new File(exportDirName + habitDirName).mkdirs(); generateDirs.add(habitDirName); - writeScores(habitDirName, h.scores); - writeCheckmarks(habitDirName, h.checkmarks); + writeScores(habitDirName, h.getScores()); + writeCheckmarks(habitDirName, h.getCheckmarks()); } } @@ -105,8 +117,8 @@ public class HabitsCSVExporter private String writeZipFile() throws IOException { - SimpleDateFormat dateFormat = DateHelper.getCSVDateFormat(); - String date = dateFormat.format(DateHelper.getStartOfToday()); + SimpleDateFormat dateFormat = DateUtils.getCSVDateFormat(); + String date = dateFormat.format(DateUtils.getStartOfToday()); String zipFilename = String.format("%s/Loop Habits CSV %s.zip", exportDirName, date); FileOutputStream fos = new FileOutputStream(zipFilename); diff --git a/app/src/main/java/org/isoron/uhabits/io/LoopDBImporter.java b/app/src/main/java/org/isoron/uhabits/io/LoopDBImporter.java index 27b7ecb15..d0037f9e3 100644 --- a/app/src/main/java/org/isoron/uhabits/io/LoopDBImporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/LoopDBImporter.java @@ -25,23 +25,28 @@ import android.support.annotation.NonNull; import com.activeandroid.ActiveAndroid; -import org.isoron.uhabits.helpers.DatabaseHelper; +import org.isoron.uhabits.utils.DatabaseUtils; +import org.isoron.uhabits.utils.FileUtils; import java.io.File; import java.io.IOException; +/** + * Class that imports data from database files exported by Loop Habit Tracker. + */ public class LoopDBImporter extends AbstractImporter { @Override public boolean canHandle(@NonNull File file) throws IOException { - if(!isSQLite3File(file)) return false; + if (!isSQLite3File(file)) return false; SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null, - SQLiteDatabase.OPEN_READONLY); + SQLiteDatabase.OPEN_READONLY); - Cursor c = db.rawQuery("select count(*) from SQLITE_MASTER where name=? or name=?", - new String[]{"Checkmarks", "Repetitions"}); + Cursor c = db.rawQuery( + "select count(*) from SQLITE_MASTER where name=? or name=?", + new String[]{"Checkmarks", "Repetitions"}); boolean result = (c.moveToFirst() && c.getInt(0) == 2); @@ -54,8 +59,8 @@ public class LoopDBImporter extends AbstractImporter public void importHabitsFromFile(@NonNull File file) throws IOException { ActiveAndroid.dispose(); - File originalDB = DatabaseHelper.getDatabaseFile(); - DatabaseHelper.copy(file, originalDB); - DatabaseHelper.initializeActiveAndroid(); + File originalDB = DatabaseUtils.getDatabaseFile(); + FileUtils.copy(file, originalDB); + DatabaseUtils.initializeActiveAndroid(); } } diff --git a/app/src/main/java/org/isoron/uhabits/io/RewireDBImporter.java b/app/src/main/java/org/isoron/uhabits/io/RewireDBImporter.java index 47fc92020..e9d1d4c83 100644 --- a/app/src/main/java/org/isoron/uhabits/io/RewireDBImporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/RewireDBImporter.java @@ -19,30 +19,33 @@ package org.isoron.uhabits.io; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.support.annotation.NonNull; +import android.database.*; +import android.database.sqlite.*; +import android.support.annotation.*; -import org.isoron.uhabits.helpers.DatabaseHelper; -import org.isoron.uhabits.helpers.DateHelper; -import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.DatabaseUtils; +import org.isoron.uhabits.utils.*; -import java.io.File; -import java.io.IOException; -import java.util.GregorianCalendar; +import java.io.*; +import java.util.*; +/** + * Class that imports database files exported by Rewire. + */ public class RewireDBImporter extends AbstractImporter { @Override public boolean canHandle(@NonNull File file) throws IOException { - if(!isSQLite3File(file)) return false; + if (!isSQLite3File(file)) return false; SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null, - SQLiteDatabase.OPEN_READONLY); + SQLiteDatabase.OPEN_READONLY); - Cursor c = db.rawQuery("select count(*) from SQLITE_MASTER where name=? or name=?", - new String[]{"CHECKINS", "UNIT"}); + Cursor c = db.rawQuery( + "select count(*) from SQLITE_MASTER where name=? or name=?", + new String[]{ "CHECKINS", "UNIT" }); boolean result = (c.moveToFirst() && c.getInt(0) == 2); @@ -54,10 +57,11 @@ public class RewireDBImporter extends AbstractImporter @Override public void importHabitsFromFile(@NonNull File file) throws IOException { - final SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null, + final SQLiteDatabase db = + SQLiteDatabase.openDatabase(file.getPath(), null, SQLiteDatabase.OPEN_READONLY); - DatabaseHelper.executeAsTransaction(new DatabaseHelper.Command() + DatabaseUtils.executeAsTransaction(new DatabaseUtils.Callback() { @Override public void execute() @@ -69,14 +73,48 @@ public class RewireDBImporter extends AbstractImporter db.close(); } + private void createCheckmarks(@NonNull SQLiteDatabase db, + @NonNull Habit habit, + int rewireHabitId) + { + Cursor c = null; + + try + { + String[] params = { Integer.toString(rewireHabitId) }; + c = db.rawQuery( + "select distinct date from checkins where habit_id=? and type=2", + params); + if (!c.moveToFirst()) return; + + do + { + String date = c.getString(0); + int year = Integer.parseInt(date.substring(0, 4)); + int month = Integer.parseInt(date.substring(4, 6)); + int day = Integer.parseInt(date.substring(6, 8)); + + GregorianCalendar cal = DateUtils.getStartOfTodayCalendar(); + cal.set(year, month - 1, day); + + habit.getRepetitions().toggleTimestamp(cal.getTimeInMillis()); + } while (c.moveToNext()); + } + finally + { + if (c != null) c.close(); + } + } + private void createHabits(SQLiteDatabase db) { Cursor c = null; try { - c = db.rawQuery("select _id, name, description, schedule, active_days, " + - "repeating_count, days, period from habits", new String[0]); + c = db.rawQuery( + "select _id, name, description, schedule, active_days, " + + "repeating_count, days, period from habits", new String[0]); if (!c.moveToFirst()) return; do @@ -91,36 +129,40 @@ public class RewireDBImporter extends AbstractImporter int periodIndex = c.getInt(7); Habit habit = new Habit(); - habit.name = name; - habit.description = description; + habit.setName(name); + habit.setDescription(description); int periods[] = { 7, 31, 365 }; + int numerator, denominator; switch (schedule) { case 0: - habit.freqNum = activeDays.split(",").length; - habit.freqDen = 7; + numerator = activeDays.split(",").length; + denominator = 7; break; case 1: - habit.freqNum = days; - habit.freqDen = periods[periodIndex]; + numerator = days; + denominator = (periods[periodIndex]); break; case 2: - habit.freqNum = 1; - habit.freqDen = repeatingCount; + numerator = 1; + denominator = repeatingCount; break; + + default: + throw new IllegalStateException(); } - habit.save(); + habit.setFrequency(new Frequency(numerator, denominator)); + habitList.add(habit); createReminder(db, habit, id); createCheckmarks(db, habit, id); - } - while (c.moveToNext()); + } while (c.moveToNext()); } finally { @@ -128,14 +170,18 @@ public class RewireDBImporter extends AbstractImporter } } - private void createReminder(SQLiteDatabase db, Habit habit, int rewireHabitId) + private void createReminder(SQLiteDatabase db, + Habit habit, + int rewireHabitId) { String[] params = { Integer.toString(rewireHabitId) }; Cursor c = null; try { - c = db.rawQuery("select time, active_days from reminders where habit_id=? limit 1", params); + c = db.rawQuery( + "select time, active_days from reminders where habit_id=? limit 1", + params); if (!c.moveToFirst()) return; int rewireReminder = Integer.parseInt(c.getString(0)); @@ -144,46 +190,19 @@ public class RewireDBImporter extends AbstractImporter boolean reminderDays[] = new boolean[7]; String activeDays[] = c.getString(1).split(","); - for(String d : activeDays) + for (String d : activeDays) { int idx = (Integer.parseInt(d) + 1) % 7; reminderDays[idx] = true; } - habit.reminderDays = DateHelper.packWeekdayList(reminderDays); - habit.reminderHour = rewireReminder / 60; - habit.reminderMin = rewireReminder % 60; - habit.save(); - } - finally - { - if(c != null) c.close(); - } - } + int hour = rewireReminder / 60; + int minute = rewireReminder % 60; + Integer days = DateUtils.packWeekdayList(reminderDays); - private void createCheckmarks(@NonNull SQLiteDatabase db, @NonNull Habit habit, int rewireHabitId) - { - Cursor c = null; - - try - { - String[] params = { Integer.toString(rewireHabitId) }; - c = db.rawQuery("select distinct date from checkins where habit_id=? and type=2", params); - if (!c.moveToFirst()) return; - - do - { - String date = c.getString(0); - int year = Integer.parseInt(date.substring(0, 4)); - int month = Integer.parseInt(date.substring(4, 6)); - int day = Integer.parseInt(date.substring(6, 8)); - - GregorianCalendar cal = DateHelper.getStartOfTodayCalendar(); - cal.set(year, month - 1, day); - - habit.repetitions.toggle(cal.getTimeInMillis()); - } - while (c.moveToNext()); + Reminder reminder = new Reminder(hour, minute, days); + habit.setReminder(reminder); + habitList.update(habit); } finally { diff --git a/app/src/main/java/org/isoron/uhabits/io/TickmateDBImporter.java b/app/src/main/java/org/isoron/uhabits/io/TickmateDBImporter.java index f0b6b9770..3494468ea 100644 --- a/app/src/main/java/org/isoron/uhabits/io/TickmateDBImporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/TickmateDBImporter.java @@ -23,26 +23,30 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.support.annotation.NonNull; -import org.isoron.uhabits.helpers.DatabaseHelper; -import org.isoron.uhabits.helpers.DateHelper; -import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.DatabaseUtils; +import org.isoron.uhabits.utils.DateUtils; import java.io.File; import java.io.IOException; import java.util.GregorianCalendar; +/** + * Class that imports data from database files exported by Tickmate. + */ public class TickmateDBImporter extends AbstractImporter { @Override public boolean canHandle(@NonNull File file) throws IOException { - if(!isSQLite3File(file)) return false; + if (!isSQLite3File(file)) return false; SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null, - SQLiteDatabase.OPEN_READONLY); + SQLiteDatabase.OPEN_READONLY); - Cursor c = db.rawQuery("select count(*) from SQLITE_MASTER where name=? or name=?", - new String[]{"tracks", "track2groups"}); + Cursor c = db.rawQuery( + "select count(*) from SQLITE_MASTER where name=? or name=?", + new String[]{"tracks", "track2groups"}); boolean result = (c.moveToFirst() && c.getInt(0) == 2); @@ -54,47 +58,39 @@ public class TickmateDBImporter extends AbstractImporter @Override public void importHabitsFromFile(@NonNull File file) throws IOException { - final SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null, + final SQLiteDatabase db = + SQLiteDatabase.openDatabase(file.getPath(), null, SQLiteDatabase.OPEN_READONLY); - DatabaseHelper.executeAsTransaction(new DatabaseHelper.Command() - { - @Override - public void execute() - { - createHabits(db); - } - }); - + DatabaseUtils.executeAsTransaction(() -> createHabits(db)); db.close(); } - private void createHabits(SQLiteDatabase db) + private void createCheckmarks(@NonNull SQLiteDatabase db, + @NonNull Habit habit, + int tickmateTrackId) { Cursor c = null; try { - c = db.rawQuery("select _id, name, description from tracks", new String[0]); + String[] params = {Integer.toString(tickmateTrackId)}; + c = db.rawQuery( + "select distinct year, month, day from ticks where _track_id=?", + params); if (!c.moveToFirst()) return; do { - int id = c.getInt(0); - String name = c.getString(1); - String description = c.getString(2); - - Habit habit = new Habit(); - habit.name = name; - habit.description = description; - habit.freqNum = 1; - habit.freqDen = 1; - habit.save(); + int year = c.getInt(0); + int month = c.getInt(1); + int day = c.getInt(2); - createCheckmarks(db, habit, id); + GregorianCalendar cal = DateUtils.getStartOfTodayCalendar(); + cal.set(year, month, day); - } - while (c.moveToNext()); + habit.getRepetitions().toggleTimestamp(cal.getTimeInMillis()); + } while (c.moveToNext()); } finally { @@ -102,28 +98,31 @@ public class TickmateDBImporter extends AbstractImporter } } - private void createCheckmarks(@NonNull SQLiteDatabase db, @NonNull Habit habit, int tickmateTrackId) + private void createHabits(SQLiteDatabase db) { Cursor c = null; try { - String[] params = { Integer.toString(tickmateTrackId) }; - c = db.rawQuery("select distinct year, month, day from ticks where _track_id=?", params); + c = db.rawQuery("select _id, name, description from tracks", + new String[0]); if (!c.moveToFirst()) return; do { - int year = c.getInt(0); - int month = c.getInt(1); - int day = c.getInt(2); + int id = c.getInt(0); + String name = c.getString(1); + String description = c.getString(2); - GregorianCalendar cal = DateHelper.getStartOfTodayCalendar(); - cal.set(year, month, day); + Habit habit = new Habit(); + habit.setName(name); + habit.setDescription(description); + habit.setFrequency(Frequency.DAILY); + habitList.add(habit); + + createCheckmarks(db, habit, id); - habit.repetitions.toggle(cal.getTimeInMillis()); - } - while (c.moveToNext()); + } while (c.moveToNext()); } finally { diff --git a/app/src/main/java/org/isoron/uhabits/io/package-info.java b/app/src/main/java/org/isoron/uhabits/io/package-info.java new file mode 100644 index 000000000..5cbd932fb --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/io/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2016 Á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 . + */ + +/** + * Provides classes that deal with importing from and exporting to files. + */ +package org.isoron.uhabits.io; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/loaders/HabitListLoader.java b/app/src/main/java/org/isoron/uhabits/loaders/HabitListLoader.java deleted file mode 100644 index 12a8e9e9f..000000000 --- a/app/src/main/java/org/isoron/uhabits/loaders/HabitListLoader.java +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Copyright (C) 2016 Á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.loaders; - -import org.isoron.uhabits.helpers.DateHelper; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.tasks.BaseTask; - -import java.util.HashMap; -import java.util.List; - -public class HabitListLoader -{ - public interface Listener - { - void onLoadFinished(); - } - - private BaseTask currentFetchTask; - private int checkmarkCount; - - private Listener listener; - private Long lastLoadTimestamp; - - public HashMap habits; - public List habitsList; - public HashMap checkmarks; - public HashMap scores; - - boolean includeArchived; - - public void setIncludeArchived(boolean includeArchived) - { - this.includeArchived = includeArchived; - } - - public void setCheckmarkCount(int checkmarkCount) - { - this.checkmarkCount = checkmarkCount; - } - - public void setListener(Listener listener) - { - this.listener = listener; - } - - public Long getLastLoadTimestamp() - { - return lastLoadTimestamp; - } - - public HabitListLoader() - { - habits = new HashMap<>(); - checkmarks = new HashMap<>(); - scores = new HashMap<>(); - } - - public void reorder(int from, int to) - { - Habit fromHabit = habitsList.get(from); - Habit toHabit = habitsList.get(to); - - habitsList.remove(from); - habitsList.add(to, fromHabit); - - Habit.reorder(fromHabit, toHabit); - } - - public void updateAllHabits(final boolean updateScoresAndCheckmarks) - { - if (currentFetchTask != null) currentFetchTask.cancel(true); - - currentFetchTask = new BaseTask() - { - public HashMap newHabits; - public HashMap newCheckmarks; - public HashMap newScores; - public List newHabitList; - - @Override - protected void doInBackground() - { - newHabits = new HashMap<>(); - newCheckmarks = new HashMap<>(); - newScores = new HashMap<>(); - newHabitList = Habit.getAll(includeArchived); - - long dateTo = DateHelper.getStartOfDay(DateHelper.getLocalTime()); - long dateFrom = dateTo - (checkmarkCount - 1) * DateHelper.millisecondsInOneDay; - int[] empty = new int[checkmarkCount]; - - for(Habit h : newHabitList) - { - Long id = h.getId(); - - newHabits.put(id, h); - - if(checkmarks.containsKey(id)) - newCheckmarks.put(id, checkmarks.get(id)); - else - newCheckmarks.put(id, empty); - - if(scores.containsKey(id)) - newScores.put(id, scores.get(id)); - else - newScores.put(id, 0); - } - - commit(); - - if(!updateScoresAndCheckmarks) return; - - int current = 0; - for (Habit h : newHabitList) - { - if (isCancelled()) return; - - Long id = h.getId(); - newScores.put(id, h.scores.getTodayValue()); - newCheckmarks.put(id, h.checkmarks.getValues(dateFrom, dateTo)); - - publishProgress(current++, newHabits.size()); - } - } - - private void commit() - { - habits = newHabits; - scores = newScores; - checkmarks = newCheckmarks; - habitsList = newHabitList; - } - - @Override - protected void onProgressUpdate(Integer... values) - { - if(listener != null) listener.onLoadFinished(); - } - - @Override - protected void onPostExecute(Void aVoid) - { - if (isCancelled()) return; - - lastLoadTimestamp = DateHelper.getStartOfToday(); - currentFetchTask = null; - - if(listener != null) listener.onLoadFinished(); - - super.onPostExecute(null); - } - - }; - - currentFetchTask.execute(); - } - - public void updateHabit(final Long id) - { - new BaseTask() - { - @Override - protected void doInBackground() - { - long dateTo = DateHelper.getStartOfDay(DateHelper.getLocalTime()); - long dateFrom = dateTo - (checkmarkCount - 1) * DateHelper.millisecondsInOneDay; - - Habit h = Habit.get(id); - if(h == null) return; - - habits.put(id, h); - scores.put(id, h.scores.getTodayValue()); - checkmarks.put(id, h.checkmarks.getValues(dateFrom, dateTo)); - } - - @Override - protected void onPostExecute(Void aVoid) - { - if(listener != null) - listener.onLoadFinished(); - - super.onPostExecute(null); - } - }.execute(); - } -} diff --git a/app/src/main/java/org/isoron/uhabits/models/Checkmark.java b/app/src/main/java/org/isoron/uhabits/models/Checkmark.java index a6c5ec06f..3ae1c4b1e 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Checkmark.java +++ b/app/src/main/java/org/isoron/uhabits/models/Checkmark.java @@ -19,48 +19,67 @@ package org.isoron.uhabits.models; -import com.activeandroid.Model; -import com.activeandroid.annotation.Column; -import com.activeandroid.annotation.Table; +import org.apache.commons.lang3.builder.*; -@Table(name = "Checkmarks") -public class Checkmark extends Model +/** + * A Checkmark represents the completion status of the habit for a given day. + *

+ * While repetitions simply record that the habit was performed at a given date, + * a checkmark provides more information, such as whether a repetition was + * expected at that day or not. + *

+ * Checkmarks are computed automatically from the list of repetitions. + */ +public final class Checkmark { /** - * Indicates that there was no repetition at the timestamp, even though a repetition was - * expected. + * Indicates that there was a repetition at the timestamp. */ - public static final int UNCHECKED = 0; + public static final int CHECKED_EXPLICITLY = 2; /** - * Indicates that there was no repetition at the timestamp, but one was not expected in any - * case, due to the frequency of the habit. + * Indicates that there was no repetition at the timestamp, but one was not + * expected in any case, due to the frequency of the habit. */ public static final int CHECKED_IMPLICITLY = 1; /** - * Indicates that there was a repetition at the timestamp. + * Indicates that there was no repetition at the timestamp, even though a + * repetition was expected. */ - public static final int CHECKED_EXPLICITLY = 2; + public static final int UNCHECKED = 0; - /** - * The habit to which this checkmark belongs. - */ - @Column(name = "habit") - public Habit habit; + private final long timestamp; - /** - * Timestamp of the day to which this checkmark corresponds. Time of the day must be midnight - * (UTC). - */ - @Column(name = "timestamp") - public Long timestamp; + private final int value; - /** - * Indicates whether there is a repetition at the given timestamp or not, and whether the - * repetition was expected. Assumes one of the values UNCHECKED, CHECKED_EXPLICITLY or - * CHECKED_IMPLICITLY. - */ - @Column(name = "value") - public Integer value; + public Checkmark(long timestamp, int value) + { + this.timestamp = timestamp; + this.value = value; + } + + public int compareNewer(Checkmark other) + { + return Long.signum(this.getTimestamp() - other.getTimestamp()); + } + + public long getTimestamp() + { + return timestamp; + } + + public int getValue() + { + return value; + } + + @Override + public String toString() + { + return new ToStringBuilder(this) + .append("timestamp", timestamp) + .append("value", value) + .toString(); + } } diff --git a/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java b/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java index 80b565c9c..701bedc64 100644 --- a/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java +++ b/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java @@ -19,28 +19,22 @@ package org.isoron.uhabits.models; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteStatement; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import com.activeandroid.Cache; -import com.activeandroid.query.Delete; -import com.activeandroid.query.Select; - -import org.isoron.uhabits.helpers.DateHelper; -import org.isoron.uhabits.helpers.UIHelper; - -import java.io.IOException; -import java.io.Writer; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.List; - -public class CheckmarkList +import android.support.annotation.*; + +import org.isoron.uhabits.utils.*; + +import java.io.*; +import java.text.*; +import java.util.*; + +/** + * The collection of {@link Checkmark}s belonging to a habit. + */ +public abstract class CheckmarkList { - private Habit habit; + protected Habit habit; + + public ModelObservable observable = new ModelObservable(); public CheckmarkList(Habit habit) { @@ -48,124 +42,158 @@ public class CheckmarkList } /** - * Deletes every checkmark that has timestamp either equal or newer than a given timestamp. - * These checkmarks will be recomputed at the next time they are queried. + * Adds all the given checkmarks to the list. + *

+ * This should never be called by the application, since the checkmarks are + * computed automatically from the list of repetitions. * - * @param timestamp the timestamp + * @param checkmarks the checkmarks to be added. */ - public void deleteNewerThan(long timestamp) - { - new Delete().from(Checkmark.class) - .where("habit = ?", habit.getId()) - .and("timestamp >= ?", timestamp) - .execute(); - } + public abstract void add(List checkmarks); /** - * Returns the values of the checkmarks that fall inside a certain interval of time. - * - * The values are returned in an array containing one integer value for each day of the - * interval. The first entry corresponds to the most recent day in the interval. Each subsequent - * entry corresponds to one day older than the previous entry. The boundaries of the time - * interval are included. + * Returns the values for all the checkmarks, since the oldest repetition of + * the habit until today. + *

+ * If there are no repetitions at all, returns an empty array. The values + * are returned in an array containing one integer value for each day since + * the first repetition of the habit until today. The first entry + * corresponds to today, the second entry corresponds to yesterday, and so + * on. * - * @param fromTimestamp timestamp for the oldest checkmark - * @param toTimestamp timestamp for the newest checkmark - * @return values for the checkmarks inside the given interval + * @return values for the checkmarks in the interval */ @NonNull - public int[] getValues(long fromTimestamp, long toTimestamp) + public final int[] getAllValues() { - compute(fromTimestamp, toTimestamp); - - if(fromTimestamp > toTimestamp) return new int[0]; - - String query = "select value, timestamp from Checkmarks where " + - "habit = ? and timestamp >= ? and timestamp <= ?"; + Repetition oldestRep = habit.getRepetitions().getOldest(); + if (oldestRep == null) return new int[0]; - SQLiteDatabase db = Cache.openDatabase(); - String args[] = { habit.getId().toString(), Long.toString(fromTimestamp), - Long.toString(toTimestamp) }; - Cursor cursor = db.rawQuery(query, args); + Long fromTimestamp = oldestRep.getTimestamp(); + Long toTimestamp = DateUtils.getStartOfToday(); - long day = DateHelper.millisecondsInOneDay; - int nDays = (int) ((toTimestamp - fromTimestamp) / day) + 1; - int[] checks = new int[nDays]; - - if (cursor.moveToFirst()) - { - do - { - long timestamp = cursor.getLong(1); - int offset = (int) ((timestamp - fromTimestamp) / day); - checks[nDays - offset - 1] = cursor.getInt(0); + return getValues(fromTimestamp, toTimestamp); + } - } while (cursor.moveToNext()); - } + /** + * Returns the list of checkmarks that fall within the given interval. + *

+ * There is exactly one checkmark per day in the interval. The endpoints of + * the interval are included. The list is ordered by timestamp (decreasing). + * That is, the first checkmark corresponds to the newest timestamp, and the + * last checkmark corresponds to the oldest timestamp. + * + * @param fromTimestamp timestamp of the beginning of the interval. + * @param toTimestamp timestamp of the end of the interval. + * @return the list of checkmarks within the interval. + */ + @NonNull + public abstract List getByInterval(long fromTimestamp, + long toTimestamp); - cursor.close(); - return checks; + /** + * Returns the checkmark for today. + * + * @return checkmark for today + */ + @Nullable + public final Checkmark getToday() + { + computeAll(); + return getNewestComputed(); } /** - * Returns the values for all the checkmarks, since the oldest repetition of the habit until - * today. If there are no repetitions at all, returns an empty array. + * Returns the value of today's checkmark. * - * The values are returned in an array containing one integer value for each day since the - * first repetition of the habit until today. The first entry corresponds to today, the second - * entry corresponds to yesterday, and so on. + * @return value of today's checkmark + */ + public final int getTodayValue() + { + Checkmark today = getToday(); + if (today != null) return today.getValue(); + else return Checkmark.UNCHECKED; + } + + /** + * Returns the values of the checkmarks that fall inside a certain interval + * of time. + *

+ * The values are returned in an array containing one integer value for each + * day of the interval. The first entry corresponds to the most recent day + * in the interval. Each subsequent entry corresponds to one day older than + * the previous entry. The boundaries of the time interval are included. * - * @return values for the checkmarks in the interval + * @param from timestamp for the oldest checkmark + * @param to timestamp for the newest checkmark + * @return values for the checkmarks inside the given interval */ - @NonNull - public int[] getAllValues() + public final int[] getValues(long from, long to) { - Repetition oldestRep = habit.repetitions.getOldest(); - if(oldestRep == null) return new int[0]; + List checkmarks = getByInterval(from, to); + int values[] = new int[checkmarks.size()]; - Long fromTimestamp = oldestRep.timestamp; - Long toTimestamp = DateHelper.getStartOfToday(); + int i = 0; + for (Checkmark c : checkmarks) + values[i++] = c.getValue(); - return getValues(fromTimestamp, toTimestamp); + return values; } /** - * Computes and stores one checkmark for each day, since the first repetition until today. - * Days that already have a corresponding checkmark are skipped. + * Marks as invalid every checkmark that has timestamp either equal or newer + * than a given timestamp. These checkmarks will be recomputed at the next + * time they are queried. + * + * @param timestamp the timestamp */ - protected void computeAll() + public abstract void invalidateNewerThan(long timestamp); + + /** + * Writes the entire list of checkmarks to the given writer, in CSV format. + * + * @param out the writer where the CSV will be output + * @throws IOException in case write operations fail + */ + public final void writeCSV(Writer out) throws IOException { - long fromTimestamp = habit.repetitions.getOldestTimestamp(); - if(fromTimestamp == 0) return; + computeAll(); - Long toTimestamp = DateHelper.getStartOfToday(); + int values[] = getAllValues(); + long timestamp = DateUtils.getStartOfToday(); + SimpleDateFormat dateFormat = DateUtils.getCSVDateFormat(); - compute(fromTimestamp, toTimestamp); + for (int value : values) + { + String date = dateFormat.format(new Date(timestamp)); + out.write(String.format("%s,%d\n", date, value)); + timestamp -= DateUtils.millisecondsInOneDay; + } } /** - * Computes and stores one checkmark for each day that falls inside the specified interval of - * time. Days that already have a corresponding checkmark are skipped. + * Computes and stores one checkmark for each day that falls inside the + * specified interval of time. Days that already have a corresponding + * checkmark are skipped. * * @param from timestamp for the beginning of the interval - * @param to timestamp for the end of the interval + * @param to timestamp for the end of the interval */ - protected void compute(long from, final long to) + protected final void compute(long from, final long to) { - UIHelper.throwIfMainThread(); + final long day = DateUtils.millisecondsInOneDay; - final long day = DateHelper.millisecondsInOneDay; + Checkmark newestCheckmark = getNewestComputed(); + if (newestCheckmark != null) + from = newestCheckmark.getTimestamp() + day; - Checkmark newestCheckmark = findNewest(); - if(newestCheckmark != null) - from = Math.max(from, newestCheckmark.timestamp + day); + if (from > to) return; - if(from > to) return; + Frequency freq = habit.getFrequency(); - long fromExtended = from - (long) (habit.freqDen) * day; - List reps = habit.repetitions - .selectFromTo(fromExtended, to) - .execute(); + long fromExtended = from - (long) (freq.getDenominator()) * day; + List reps = + habit.getRepetitions().getByInterval(fromExtended, to); final int nDays = (int) ((to - from) / day) + 1; int nDaysExtended = (int) ((to - fromExtended) / day) + 1; @@ -173,7 +201,7 @@ public class CheckmarkList for (Repetition rep : reps) { - int offset = (int) ((rep.timestamp - fromExtended) / day); + int offset = (int) ((rep.getTimestamp() - fromExtended) / day); checks[nDaysExtended - offset - 1] = Checkmark.CHECKED_EXPLICITLY; } @@ -181,122 +209,46 @@ public class CheckmarkList { int counter = 0; - for (int j = 0; j < habit.freqDen; j++) + for (int j = 0; j < freq.getDenominator(); j++) if (checks[i + j] == 2) counter++; - if (counter >= habit.freqNum) - if(checks[i] != Checkmark.CHECKED_EXPLICITLY) + if (counter >= freq.getNumerator()) + if (checks[i] != Checkmark.CHECKED_EXPLICITLY) checks[i] = Checkmark.CHECKED_IMPLICITLY; } + List checkmarks = new LinkedList<>(); - long timestamps[] = new long[nDays]; for (int i = 0; i < nDays; i++) - timestamps[i] = to - i * day; - - insert(timestamps, checks); - } - - private void insert(long timestamps[], int values[]) - { - String query = "insert into Checkmarks(habit, timestamp, value) values (?,?,?)"; - - SQLiteDatabase db = Cache.openDatabase(); - db.beginTransaction(); - - try { - SQLiteStatement statement = db.compileStatement(query); - - for (int i = 0; i < timestamps.length; i++) - { - statement.bindLong(1, habit.getId()); - statement.bindLong(2, timestamps[i]); - statement.bindLong(3, values[i]); - statement.execute(); - } - - db.setTransactionSuccessful(); + int value = checks[i]; + long timestamp = to - i * day; + checkmarks.add(new Checkmark(timestamp, value)); } - finally - { - db.endTransaction(); - } - } - /** - * Returns newest checkmark that has already been computed. Ignores any checkmark that has - * timestamp in the future. This does not update the cache. - * - * @return newest checkmark already computed - */ - @Nullable - protected Checkmark findNewest() - { - return new Select().from(Checkmark.class) - .where("habit = ?", habit.getId()) - .and("timestamp <= ?", DateHelper.getStartOfToday()) - .orderBy("timestamp desc") - .limit(1) - .executeSingle(); + add(checkmarks); } /** - * Returns the checkmark for today. - * - * @return checkmark for today + * Computes and stores one checkmark for each day, since the first + * repetition until today. Days that already have a corresponding checkmark + * are skipped. */ - @Nullable - public Checkmark getToday() + protected final void computeAll() { - long today = DateHelper.getStartOfToday(); - compute(today, today); - return findNewest(); - } + Repetition oldest = habit.getRepetitions().getOldest(); + if (oldest == null) return; - /** - * Returns the value of today's checkmark. - * - * @return value of today's checkmark - */ - public int getTodayValue() - { - Checkmark today = getToday(); - if(today != null) return today.value; - else return Checkmark.UNCHECKED; + Long today = DateUtils.getStartOfToday(); + compute(oldest.getTimestamp(), today); } /** - * Writes the entire list of checkmarks to the given writer, in CSV format. There is one - * line for each checkmark. Each line contains two fields: timestamp and value. + * Returns newest checkmark that has already been computed. + *

+ * Ignores any checkmark that has timestamp in the future. * - * @param out the writer where the CSV will be output - * @throws IOException in case write operations fail + * @return newest checkmark already computed */ - - public void writeCSV(Writer out) throws IOException - { - computeAll(); - - SimpleDateFormat dateFormat = DateHelper.getCSVDateFormat(); - - String query = "select timestamp, value from checkmarks where habit = ? order by timestamp"; - String params[] = { habit.getId().toString() }; - - SQLiteDatabase db = Cache.openDatabase(); - Cursor cursor = db.rawQuery(query, params); - - if(!cursor.moveToFirst()) return; - - do - { - String timestamp = dateFormat.format(new Date(cursor.getLong(0))); - Integer value = cursor.getInt(1); - out.write(String.format("%s,%d\n", timestamp, value)); - - } while(cursor.moveToNext()); - - cursor.close(); - out.close(); - } + protected abstract Checkmark getNewestComputed(); } diff --git a/app/src/main/java/org/isoron/uhabits/models/Frequency.java b/app/src/main/java/org/isoron/uhabits/models/Frequency.java new file mode 100644 index 000000000..5b893b5a1 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/Frequency.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2016 Á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.models; + +import org.apache.commons.lang3.builder.*; + +/** + * Represents how often is the habit repeated. + */ +public class Frequency +{ + public static final Frequency DAILY = new Frequency(1, 1); + + public static final Frequency FIVE_TIMES_PER_WEEK = new Frequency(5, 7); + + public static final Frequency THREE_TIMES_PER_WEEK = new Frequency(3, 7); + + public static final Frequency TWO_TIMES_PER_WEEK = new Frequency(2, 7); + + public static final Frequency WEEKLY = new Frequency(1, 7); + + private final int numerator; + + private final int denominator; + + public Frequency(int numerator, int denominator) + { + if (numerator == denominator) numerator = denominator = 1; + + this.numerator = numerator; + this.denominator = denominator; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Frequency frequency = (Frequency) o; + + return new EqualsBuilder() + .append(numerator, frequency.numerator) + .append(denominator, frequency.denominator) + .isEquals(); + } + + public int getDenominator() + { + return denominator; + } + + public int getNumerator() + { + return numerator; + } + + @Override + public int hashCode() + { + return new HashCodeBuilder(17, 37) + .append(numerator) + .append(denominator) + .toHashCode(); + } + + public double toDouble() + { + return (double) numerator / denominator; + } + + @Override + public String toString() + { + return new ToStringBuilder(this) + .append("numerator", numerator) + .append("denominator", denominator) + .toString(); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/Habit.java b/app/src/main/java/org/isoron/uhabits/models/Habit.java index 6973dcc55..4e5696e43 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Habit.java +++ b/app/src/main/java/org/isoron/uhabits/models/Habit.java @@ -19,133 +19,62 @@ package org.isoron.uhabits.models; -import android.annotation.SuppressLint; -import android.net.Uri; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import com.activeandroid.ActiveAndroid; -import com.activeandroid.Model; -import com.activeandroid.annotation.Column; -import com.activeandroid.annotation.Table; -import com.activeandroid.query.Delete; -import com.activeandroid.query.From; -import com.activeandroid.query.Select; -import com.activeandroid.query.Update; -import com.activeandroid.util.SQLiteUtils; -import com.opencsv.CSVWriter; - -import org.isoron.uhabits.helpers.ColorHelper; -import org.isoron.uhabits.helpers.DateHelper; - -import java.io.IOException; -import java.io.Writer; -import java.util.List; -import java.util.Locale; - -@Table(name = "Habits") -public class Habit extends Model -{ - /** - * Name of the habit - */ - @Column(name = "name") - public String name; - - /** - * Description of the habit - */ - @Column(name = "description") - public String description; +import android.net.*; +import android.support.annotation.*; - /** - * Frequency numerator. If a habit is performed 3 times in 7 days, this field equals 3. - */ - @Column(name = "freq_num") - public Integer freqNum; +import org.apache.commons.lang3.builder.*; +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.memory.*; - /** - * Frequency denominator. If a habit is performed 3 times in 7 days, this field equals 7. - */ - @Column(name = "freq_den") - public Integer freqDen; +import java.util.*; - /** - * Color of the habit. - * - * This number is not an android.graphics.Color, but an index to the activity color palette, - * which changes according to the theme. To convert this color into an android.graphics.Color, - * use ColorHelper.getColor(context, habit.color). - */ - @Column(name = "color") - public Integer color; +import javax.inject.*; - /** - * Position of the habit. Habits are usually sorted by this field. - */ - @Column(name = "position") - public Integer position; +/** + * The thing that the user wants to track. + */ +public class Habit +{ + public static final String HABIT_URI_FORMAT = + "content://org.isoron.uhabits/habit/%d"; - /** - * Hour of the day the reminder should be shown. If there is no reminder, this equals to null. - */ @Nullable - @Column(name = "reminder_hour") - public Integer reminderHour; + private Long id; - /** - * Minute the reminder should be shown. If there is no reminder, this equals to null. - */ - @Nullable - @Column(name = "reminder_min") - public Integer reminderMin; + @NonNull + private String name; - /** - * Days of the week the reminder should be shown. This field can be converted to a list of - * booleans using the method DateHelper.unpackWeekdayList and converted back to an integer by - * using the method DateHelper.packWeekdayList. If the habit has no reminders, this value - * should be ignored. - */ @NonNull - @Column(name = "reminder_days") - public Integer reminderDays; + private String description; - /** - * Not currently used. - */ - @Column(name = "highlight") - public Integer highlight; + @NonNull + private Frequency frequency; - /** - * Flag that indicates whether the habit is archived. Archived habits are usually omitted from - * listings, unless explicitly included. - */ - @Column(name = "archived") - public Integer archived; + @NonNull + private Integer color; - /** - * List of streaks belonging to this habit. - */ @NonNull - public StreakList streaks; + private boolean archived; - /** - * List of scores belonging to this habit. - */ @NonNull - public ScoreList scores; + private StreakList streaks; - /** - * List of repetitions belonging to this habit. - */ @NonNull - public RepetitionList repetitions; + private ScoreList scores; + + @NonNull + private RepetitionList repetitions; - /** - * List of checkmarks belonging to this habit. - */ @NonNull - public CheckmarkList checkmarks; + private CheckmarkList checkmarks; + + @Nullable + private Reminder reminder; + + private ModelObservable observable = new ModelObservable(); + + @Inject + ModelFactory factory; /** * Constructs a habit with the same attributes as the specified habit. @@ -154,361 +83,218 @@ public class Habit extends Model */ public Habit(Habit model) { - reminderDays = DateHelper.ALL_WEEK_DAYS; - - copyAttributes(model); - - checkmarks = new CheckmarkList(this); - streaks = new StreakList(this); - scores = new ScoreList(this); - repetitions = new RepetitionList(this); + copyFrom(model); + buildLists(); } /** - * Constructs a habit with default attributes. The habit is not archived, not highlighted, has - * no reminders and is placed in the last position of the list of habits. + * Constructs a habit with default attributes. + *

+ * The habit is not archived, not highlighted, has no reminders and is + * placed in the last position of the list of habits. */ public Habit() { this.color = 5; - this.position = Habit.countWithArchived(); - this.highlight = 0; - this.archived = 0; - this.freqDen = 7; - this.freqNum = 3; - this.reminderDays = DateHelper.ALL_WEEK_DAYS; - - checkmarks = new CheckmarkList(this); - streaks = new StreakList(this); - scores = new ScoreList(this); - repetitions = new RepetitionList(this); + this.archived = false; + this.frequency = new Frequency(3, 7); + + buildLists(); } - /** - * Returns the habit with specified id. - * - * @param id the id of the habit - * @return the habit, or null if none exist - */ - @Nullable - public static Habit get(long id) + private void buildLists() { - return Habit.load(Habit.class, id); + BaseComponent component = HabitsApplication.getComponent(); + if(component == null) factory = new MemoryModelFactory(); + else component.inject(this); + + checkmarks = factory.buildCheckmarkList(this); + streaks = factory.buildStreakList(this); + scores = factory.buildScoreList(this); + repetitions = factory.buildRepetitionList(this); } /** - * Returns a list of all habits, optionally including archived habits. - * - * @param includeArchive whether archived habits should be included the list - * @return list of all habits + * Clears the reminder for a habit. This sets all the related fields to + * null. */ - @NonNull - public static List getAll(boolean includeArchive) + public void clearReminder() { - if(includeArchive) return selectWithArchived().execute(); - else return select().execute(); + reminder = null; + observable.notifyListeners(); } /** - * Returns the habit that occupies a certain position. + * Copies all the attributes of the specified habit into this habit * - * @param position the position of the desired habit - * @return the habit at that position, or null if there is none + * @param model the model whose attributes should be copied from */ - @Nullable - public static Habit getByPosition(int position) + public void copyFrom(@NonNull Habit model) { - return selectWithArchived().where("position = ?", position).executeSingle(); + this.name = model.getName(); + this.description = model.getDescription(); + this.color = model.getColor(); + this.archived = model.isArchived(); + this.frequency = model.frequency; + this.reminder = model.reminder; + observable.notifyListeners(); } /** - * Changes the id of a habit on the database. - * - * @param oldId the original id - * @param newId the new id + * List of checkmarks belonging to this habit. */ - @SuppressLint("DefaultLocale") - public static void updateId(long oldId, long newId) + @NonNull + public CheckmarkList getCheckmarks() { - SQLiteUtils.execSql(String.format("update Habits set Id = %d where Id = %d", newId, oldId)); + return checkmarks; } - @NonNull - protected static From select() + /** + * Color of the habit. + *

+ * This number is not an android.graphics.Color, but an index to the + * activity color palette, which changes according to the theme. To convert + * this color into an android.graphics.Color, use ColorHelper.getColor(context, + * habit.color). + */ + public Integer getColor() { - return new Select().from(Habit.class).where("archived = 0").orderBy("position"); + return color; } - @NonNull - protected static From selectWithArchived() + public void setColor(Integer color) { - return new Select().from(Habit.class).orderBy("position"); + this.color = color; } - /** - * Returns the total number of unarchived habits. - * - * @return number of unarchived habits - */ - public static int count() + public String getDescription() { - return select().count(); + return description; } - /** - * Returns the total number of habits, including archived habits. - * - * @return number of habits, including archived - */ - public static int countWithArchived() + public void setDescription(String description) { - return selectWithArchived().count(); + this.description = description; } - /** - * Returns a list the habits that have a reminder. Does not include archived habits. - * - * @return list of habits with reminder - */ @NonNull - public static List getHabitsWithReminder() + public Frequency getFrequency() { - return select().where("reminder_hour is not null").execute(); + return frequency; } - /** - * Changes the position of a habit on the list. - * - * @param from the habit that should be moved - * @param to the habit that currently occupies the desired position - */ - public static void reorder(Habit from, Habit to) + public void setFrequency(@NonNull Frequency frequency) { - if(from == to) return; - - if (to.position < from.position) - { - new Update(Habit.class).set("position = position + 1") - .where("position >= ? and position < ?", to.position, from.position) - .execute(); - } - else - { - new Update(Habit.class).set("position = position - 1") - .where("position > ? and position <= ?", from.position, to.position) - .execute(); - } - - from.position = to.position; - from.save(); + this.frequency = frequency; } - /** - * Recomputes the position for every habit in the database. It should never be necessary - * to call this method. - */ - public static void rebuildOrder() + @Nullable + public Long getId() { - List habits = selectWithArchived().execute(); - - ActiveAndroid.beginTransaction(); - try - { - int i = 0; - for (Habit h : habits) - { - h.position = i++; - h.save(); - } - - ActiveAndroid.setTransactionSuccessful(); - } - finally - { - ActiveAndroid.endTransaction(); - } + return id; + } + public void setId(@Nullable Long id) + { + this.id = id; } - /** - * Copies all the attributes of the specified habit into this habit - * - * @param model the model whose attributes should be copied from - */ - public void copyAttributes(@NonNull Habit model) + @NonNull + public String getName() { - this.name = model.name; - this.description = model.description; - this.freqNum = model.freqNum; - this.freqDen = model.freqDen; - this.color = model.color; - this.position = model.position; - this.reminderHour = model.reminderHour; - this.reminderMin = model.reminderMin; - this.reminderDays = model.reminderDays; - this.highlight = model.highlight; - this.archived = model.archived; + return name; } - /** - * Saves the habit on the database, and assigns the specified id to it. - * - * @param id the id that the habit should receive - */ - public void save(long id) + public void setName(@NonNull String name) { - save(); - Habit.updateId(getId(), id); + this.name = name; } - /** - * Deletes the habit and all data associated to it, including checkmarks, repetitions and - * scores. - */ - public void cascadeDelete() + public ModelObservable getObservable() { - Long id = getId(); - - ActiveAndroid.beginTransaction(); - try - { - new Delete().from(Checkmark.class).where("habit = ?", id).execute(); - new Delete().from(Repetition.class).where("habit = ?", id).execute(); - new Delete().from(Score.class).where("habit = ?", id).execute(); - new Delete().from(Streak.class).where("habit = ?", id).execute(); - delete(); - - ActiveAndroid.setTransactionSuccessful(); - } - finally - { - ActiveAndroid.endTransaction(); - } + return observable; } /** - * Returns the public URI that identifies this habit - * @return the uri + * Returns the reminder for this habit. + *

+ * Before calling this method, you should call {@link #hasReminder()} to + * verify that a reminder does exist, otherwise an exception will be + * thrown. + * + * @return the reminder for this habit + * @throws IllegalStateException if habit has no reminder */ - public Uri getUri() + @NonNull + public Reminder getReminder() { - String s = String.format(Locale.US, "content://org.isoron.uhabits/habit/%d", getId()); - return Uri.parse(s); + if (reminder == null) throw new IllegalStateException(); + return reminder; } - /** - * Returns whether the habit is archived or not. - * @return true if archived - */ - public boolean isArchived() + public void setReminder(@Nullable Reminder reminder) { - return archived != 0; + this.reminder = reminder; } - private static void updateAttributes(@NonNull List habits, @Nullable Integer color, - @Nullable Integer archived) + @NonNull + public RepetitionList getRepetitions() { - ActiveAndroid.beginTransaction(); - - try - { - for (Habit h : habits) - { - if(color != null) h.color = color; - if(archived != null) h.archived = archived; - h.save(); - } - - ActiveAndroid.setTransactionSuccessful(); - } - finally - { - ActiveAndroid.endTransaction(); - } + return repetitions; } - /** - * Archives an entire list of habits - * - * @param habits the habits to be archived - */ - public static void archive(@NonNull List habits) + @NonNull + public ScoreList getScores() { - updateAttributes(habits, null, 1); + return scores; } - /** - * Unarchives an entire list of habits - * - * @param habits the habits to be unarchived - */ - public static void unarchive(@NonNull List habits) + @NonNull + public StreakList getStreaks() { - updateAttributes(habits, null, 0); + return streaks; } /** - * Sets the color for an entire list of habits. + * Returns the public URI that identifies this habit * - * @param habits the habits to be modified - * @param color the new color to be set + * @return the uri */ - public static void setColor(@NonNull List habits, int color) + public Uri getUri() { - updateAttributes(habits, color, null); + String s = String.format(Locale.US, HABIT_URI_FORMAT, getId()); + return Uri.parse(s); } /** - * Checks whether the habit has a reminder set. + * Returns whether the habit has a reminder. * - * @return true if habit has reminder + * @return true if habit has reminder, false otherwise */ public boolean hasReminder() { - return (reminderHour != null && reminderMin != null); + return reminder != null; } - /** - * Clears the reminder for a habit. This sets all the related fields to null. - */ - public void clearReminder() + public boolean isArchived() { - reminderHour = null; - reminderMin = null; - reminderDays = DateHelper.ALL_WEEK_DAYS; + return archived; } - /** - * Writes the list of habits to the given writer, in CSV format. There is one line for each - * habit, containing the fields name, description, frequency numerator, frequency denominator - * and color. The color is written in HTML format (#000000). - * - * @param habits the list of habits to write - * @param out the writer that will receive the result - * @throws IOException if write operations fail - */ - public static void writeCSV(List habits, Writer out) throws IOException + public void setArchived(boolean archived) + { + this.archived = archived; + } + + @Override + public String toString() { - String header[] = { "Position", "Name", "Description", "NumRepetitions", "Interval", "Color" }; - - CSVWriter csv = new CSVWriter(out); - csv.writeNext(header, false); - - for(Habit habit : habits) - { - String[] cols = - { - String.format("%03d", habit.position + 1), - habit.name, - habit.description, - Integer.toString(habit.freqNum), - Integer.toString(habit.freqDen), - ColorHelper.toHTML(ColorHelper.CSV_PALETTE[habit.color]) - }; - - csv.writeNext(cols, false); - } - - csv.close(); + return new ToStringBuilder(this) + .append("id", id) + .append("name", name) + .append("description", description) + .append("color", color) + .append("archived", archived) + .toString(); } } diff --git a/app/src/main/java/org/isoron/uhabits/models/HabitList.java b/app/src/main/java/org/isoron/uhabits/models/HabitList.java new file mode 100644 index 000000000..4bd211e7e --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/HabitList.java @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2016 Á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.models; + +import android.support.annotation.*; + +import com.opencsv.*; + +import org.isoron.uhabits.utils.*; + +import java.io.*; +import java.util.*; + +/** + * An ordered collection of {@link Habit}s. + */ +public abstract class HabitList +{ + private ModelObservable observable; + + /** + * Creates a new HabitList. + *

+ * Depending on the implementation, this list can either be empty or be + * populated by some pre-existing habits, for example, from a certain + * database. + */ + public HabitList() + { + observable = new ModelObservable(); + } + + /** + * Inserts a new habit in the list. + *

+ * If the id of the habit is null, the list will assign it a new id, which + * is guaranteed to be unique in the scope of the list. If id is not null, + * the caller should make sure that the list does not already contain + * another habit with same id, otherwise a RuntimeException will be thrown. + * + * @param habit the habit to be inserted + * @throws IllegalArgumentException if the habit is already on the list. + */ + public abstract void add(@NonNull Habit habit) + throws IllegalArgumentException; + + /** + * Returns the total number of active habits. + * + * @return number of active habits + */ + public abstract int countActive(); + + /** + * Returns the total number of habits, including archived habits. + * + * @return number of habits, including archived + */ + public abstract int countWithArchived(); + + /** + * Returns a list of all habits, optionally including archived habits. + * + * @param includeArchive whether archived habits should be included the + * list + * @return list of all habits + */ + @NonNull + public abstract List getAll(boolean includeArchive); + + /** + * Returns the habit with specified id. + * + * @param id the id of the habit + * @return the habit, or null if none exist + */ + @Nullable + public abstract Habit getById(long id); + + /** + * Returns the habit that occupies a certain position. + * + * @param position the position of the desired habit + * @return the habit at that position, or null if there is none + */ + @Nullable + public abstract Habit getByPosition(int position); + + /** + * Returns the list of habits that match a given condition. + * + * @param matcher the matcher that checks the condition + * @return the list of matching habits + */ + @NonNull + public List getFiltered(HabitMatcher matcher) + { + LinkedList habits = new LinkedList<>(); + for (Habit h : getAll(true)) if (matcher.matches(h)) habits.add(h); + return habits; + } + + public ModelObservable getObservable() + { + return observable; + } + + /** + * Returns a list the habits that have a reminder. Does not include archived + * habits. + * + * @return list of habits with reminder + */ + @NonNull + public List getWithReminder() + { + return getFiltered(habit -> habit.hasReminder()); + } + + /** + * Returns the index of the given habit in the list, or -1 if the list does + * not contain the habit. + * + * @param h the habit + * @return the index of the habit, or -1 if not in the list + */ + public abstract int indexOf(@NonNull Habit h); + + /** + * Removes the given habit from the list. + *

+ * If the given habit is not in the list, does nothing. + * + * @param h the habit to be removed. + */ + public abstract void remove(@NonNull Habit h); + + /** + * Changes the position of a habit in the list. + * + * @param from the habit that should be moved + * @param to the habit that currently occupies the desired position + */ + public abstract void reorder(Habit from, Habit to); + + /** + * Notifies the list that a certain list of habits has been modified. + *

+ * Depending on the implementation, this operation might trigger a write to + * disk, or do nothing at all. To make sure that the habits get persisted, + * this operation must be called. + * + * @param habits the list of habits that have been modified. + */ + public abstract void update(List habits); + + /** + * Notifies the list that a certain habit has been modified. + *

+ * See {@link #update(List)} for more details. + * + * @param habit the habit that has been modified. + */ + public void update(@NonNull Habit habit) + { + update(Collections.singletonList(habit)); + } + + /** + * Writes the list of habits to the given writer, in CSV format. There is + * one line for each habit, containing the fields name, description, + * frequency numerator, frequency denominator and color. The color is + * written in HTML format (#000000). + * + * @param out the writer that will receive the result + * @throws IOException if write operations fail + */ + public void writeCSV(@NonNull Writer out) throws IOException + { + String header[] = { + "Position", + "Name", + "Description", + "NumRepetitions", + "Interval", + "Color" + }; + + CSVWriter csv = new CSVWriter(out); + csv.writeNext(header, false); + + for (Habit habit : getAll(true)) + { + Frequency freq = habit.getFrequency(); + + String[] cols = { + String.format("%03d", indexOf(habit) + 1), + habit.getName(), + habit.getDescription(), + Integer.toString(freq.getNumerator()), + Integer.toString(freq.getDenominator()), + ColorUtils.CSV_PALETTE[habit.getColor()] + }; + + csv.writeNext(cols, false); + } + + csv.close(); + } + + /** + * A HabitMatcher decides whether habits match or not a certain condition. + * They can be used to produce filtered lists of habits. + */ + public interface HabitMatcher + { + /** + * Returns true if the given habit matches. + * + * @param habit the habit to be checked. + * @return true if matches, false otherwise. + */ + boolean matches(Habit habit); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/ModelFactory.java b/app/src/main/java/org/isoron/uhabits/models/ModelFactory.java new file mode 100644 index 000000000..6df0002c0 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/ModelFactory.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2016 Á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.models; + +/** + * Interface implemented by factories that provide concrete implementations of + * the core model classes. + */ +public interface ModelFactory +{ + RepetitionList buildRepetitionList(Habit habit); + + CheckmarkList buildCheckmarkList(Habit habit); + + HabitList buildHabitList(); + + ScoreList buildScoreList(Habit habit); + + StreakList buildStreakList(Habit habit); +} diff --git a/app/src/main/java/org/isoron/uhabits/models/ModelObservable.java b/app/src/main/java/org/isoron/uhabits/models/ModelObservable.java new file mode 100644 index 000000000..a762b5f8a --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/ModelObservable.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2016 Á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.models; + +import java.util.*; + +/** + * A ModelObservable allows objects to subscribe themselves to it and receive + * notifications whenever the model is changed. + */ +public class ModelObservable +{ + private List listeners; + + /** + * Creates a new ModelObservable with no listeners. + */ + public ModelObservable() + { + super(); + listeners = new LinkedList<>(); + } + + /** + * Adds the given listener to the observable. + * + * @param l the listener to be added. + */ + public void addListener(Listener l) + { + listeners.add(l); + } + + /** + * Notifies every listener that the model has changed. + *

+ * Only models should call this method. + */ + public void notifyListeners() + { + for (Listener l : listeners) l.onModelChange(); + } + + /** + * Removes the given listener. + *

+ * The listener will no longer be notified when the model changes. If the + * given listener is not subscribed to this observable, does nothing. + * + * @param l the listener to be removed + */ + public void removeListener(Listener l) + { + listeners.remove(l); + } + + /** + * Interface implemented by objects that want to be notified when the model + * changes. + */ + public interface Listener + { + /** + * Called whenever the model associated to this observable has been + * modified. + */ + void onModelChange(); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/Reminder.java b/app/src/main/java/org/isoron/uhabits/models/Reminder.java new file mode 100644 index 000000000..2377113c3 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/Reminder.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2016 Á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.models; + +public final class Reminder +{ + private final int hour; + + private final int minute; + + private final int days; + + public Reminder(int hour, int minute, int days) + { + this.hour = hour; + this.minute = minute; + this.days = days; + } + + /** + * Returns the days of the week the reminder should be shown. + *

+ * This field can be converted to a list of booleans using the method + * DateHelper.unpackWeekdayList and converted back to an integer by using + * the method DateHelper.packWeekdayList. + */ + public int getDays() + { + return days; + } + + public int getHour() + { + return hour; + } + + public int getMinute() + { + return minute; + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/Repetition.java b/app/src/main/java/org/isoron/uhabits/models/Repetition.java index f43243698..72e378205 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Repetition.java +++ b/app/src/main/java/org/isoron/uhabits/models/Repetition.java @@ -19,22 +19,40 @@ package org.isoron.uhabits.models; -import com.activeandroid.Model; -import com.activeandroid.annotation.Column; -import com.activeandroid.annotation.Table; +import org.apache.commons.lang3.builder.*; -@Table(name = "Repetitions") -public class Repetition extends Model +/** + * Represents a record that the user has performed a certain habit at a certain + * date. + */ +public final class Repetition { - /** - * Habit to which this repetition belong. - */ - @Column(name = "habit") - public Habit habit; + + private final long timestamp; /** - * Timestamp of the day this repetition occurred. Time of day should be midnight (UTC). + * Creates a new repetition with given parameters. + *

+ * The timestamp corresponds to the days this repetition occurred. Time of + * day must be midnight (UTC). + * + * @param timestamp the time this repetition occurred. */ - @Column(name = "timestamp") - public Long timestamp; + public Repetition(long timestamp) + { + this.timestamp = timestamp; + } + + public long getTimestamp() + { + return timestamp; + } + + @Override + public String toString() + { + return new ToStringBuilder(this) + .append("timestamp", timestamp) + .toString(); + } } diff --git a/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java b/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java index 5bfe22fba..23866b939 100644 --- a/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java +++ b/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java @@ -19,197 +19,176 @@ package org.isoron.uhabits.models; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; +import android.support.annotation.*; -import com.activeandroid.Cache; -import com.activeandroid.query.Delete; -import com.activeandroid.query.From; -import com.activeandroid.query.Select; -import com.activeandroid.util.SQLiteUtils; +import org.isoron.uhabits.utils.*; -import org.isoron.uhabits.helpers.DatabaseHelper; -import org.isoron.uhabits.helpers.DateHelper; +import java.util.*; -import java.util.Arrays; -import java.util.GregorianCalendar; -import java.util.HashMap; - -public class RepetitionList +/** + * The collection of {@link Repetition}s belonging to a habit. + */ +public abstract class RepetitionList { @NonNull - private Habit habit; - - public RepetitionList(@NonNull Habit habit) - { - this.habit = habit; - } + protected final Habit habit; @NonNull - protected From select() - { - return new Select().from(Repetition.class) - .where("habit = ?", habit.getId()) - .and("timestamp <= ?", DateHelper.getStartOfToday()) - .orderBy("timestamp"); - } + protected final ModelObservable observable; - @NonNull - protected From selectFromTo(long timeFrom, long timeTo) + public RepetitionList(@NonNull Habit habit) { - return select().and("timestamp >= ?", timeFrom).and("timestamp <= ?", timeTo); + this.habit = habit; + this.observable = new ModelObservable(); } /** - * Checks whether there is a repetition at a given timestamp. + * Adds a repetition to the list. + *

+ * Any implementation of this method must call observable.notifyListeners() + * after the repetition has been added. * - * @param timestamp the timestamp to check - * @return true if there is a repetition + * @param repetition the repetition to be added. */ - public boolean contains(long timestamp) - { - int count = select().where("timestamp = ?", timestamp).count(); - return (count > 0); - } + public abstract void add(Repetition repetition); /** - * Deletes the repetition at a given timestamp, if it exists. + * Returns true if the list contains a repetition that has the given + * timestamp. * - * @param timestamp the timestamp of the repetition to delete + * @param timestamp the timestamp to find. + * @return true if list contains repetition with given timestamp, false + * otherwise. */ - public void delete(long timestamp) + public boolean containsTimestamp(long timestamp) { - new Delete().from(Repetition.class) - .where("habit = ?", habit.getId()) - .and("timestamp = ?", timestamp) - .execute(); + return (getByTimestamp(timestamp) != null); } /** - * Toggles the repetition at a certain timestamp. That is, deletes the repetition if it exists - * or creates one if it does not. + * Returns the list of repetitions that happened within the given time + * interval. + *

+ * The list is sorted by timestamp in increasing order. That is, the first + * element corresponds to oldest timestamp, while the last element + * corresponds to the newest. The endpoints of the interval are included. * - * @param timestamp the timestamp of the repetition to toggle + * @param fromTimestamp timestamp of the beginning of the interval + * @param toTimestamp timestamp of the end of the interval + * @return list of repetitions within given time interval */ - public void toggle(long timestamp) - { - timestamp = DateHelper.getStartOfDay(timestamp); - - if (contains(timestamp)) - delete(timestamp); - else - insert(timestamp); - - habit.scores.invalidateNewerThan(timestamp); - habit.checkmarks.deleteNewerThan(timestamp); - habit.streaks.deleteNewerThan(timestamp); - } - - private void insert(long timestamp) - { - String[] args = { habit.getId().toString(), Long.toString(timestamp) }; - SQLiteUtils.execSql("insert into Repetitions(habit, timestamp) values (?,?)", args); - } + // TODO: Change order timestamp desc + public abstract List getByInterval(long fromTimestamp, + long toTimestamp); /** - * Returns the oldest repetition for the habit. If there is no repetition, returns null. - * Repetitions in the future are discarded. + * Returns the repetition that has the given timestamp, or null if none + * exists. * - * @return oldest repetition for the habit + * @param timestamp the repetition timestamp. + * @return the repetition that has the given timestamp. */ @Nullable - public Repetition getOldest() + public abstract Repetition getByTimestamp(long timestamp); + + @NonNull + public ModelObservable getObservable() { - return (Repetition) select().limit(1).executeSingle(); + return observable; } /** - * Returns the timestamp of the oldest repetition. If there are no repetitions, returns zero. - * Repetitions in the future are discarded. + * Returns the oldest repetition in the list. + *

+ * If the list is empty, returns null. Repetitions in the future are + * discarded. * - * @return timestamp of the oldest repetition + * @return oldest repetition in the list, or null if list is empty. */ - public long getOldestTimestamp() - { - String[] args = { habit.getId().toString(), Long.toString(DateHelper.getStartOfToday()) }; - String query = "select timestamp from Repetitions where habit = ? and timestamp <= ? " + - "order by timestamp limit 1"; - - return DatabaseHelper.longQuery(query, args); - } + @Nullable + public abstract Repetition getOldest(); /** - * Returns the total number of repetitions for each month, from the first repetition until - * today, grouped by day of week. The repetitions are returned in a HashMap. The key is the - * timestamp for the first day of the month, at midnight (00:00). The value is an integer - * array with 7 entries. The first entry contains the total number of repetitions during - * the specified month that occurred on a Saturday. The second entry corresponds to Sunday, - * and so on. If there are no repetitions during a certain month, the value is null. + * Returns the total number of repetitions for each month, from the first + * repetition until today, grouped by day of week. + *

+ * The repetitions are returned in a HashMap. The key is the timestamp for + * the first day of the month, at midnight (00:00). The value is an integer + * array with 7 entries. The first entry contains the total number of + * repetitions during the specified month that occurred on a Saturday. The + * second entry corresponds to Sunday, and so on. If there are no + * repetitions during a certain month, the value is null. * * @return total number of repetitions by month versus day of week */ @NonNull public HashMap getWeekdayFrequency() { - Repetition oldestRep = getOldest(); - if(oldestRep == null) return new HashMap<>(); - - String query = "select strftime('%Y', timestamp / 1000, 'unixepoch') as year," + - "strftime('%m', timestamp / 1000, 'unixepoch') as month," + - "strftime('%w', timestamp / 1000, 'unixepoch') as weekday, " + - "count(*) from repetitions " + - "where habit = ? and timestamp <= ? " + - "group by year, month, weekday"; - - String[] params = { habit.getId().toString(), - Long.toString(DateHelper.getStartOfToday()) }; - - SQLiteDatabase db = Cache.openDatabase(); - Cursor cursor = db.rawQuery(query, params); - - if(!cursor.moveToFirst()) return new HashMap<>(); - - HashMap map = new HashMap<>(); - GregorianCalendar date = DateHelper.getStartOfTodayCalendar(); + List reps = getByInterval(0, DateUtils.getStartOfToday()); + HashMap map = new HashMap<>(); - do + for (Repetition r : reps) { - int year = Integer.parseInt(cursor.getString(0)); - int month = Integer.parseInt(cursor.getString(1)); - int weekday = (Integer.parseInt(cursor.getString(2)) + 1) % 7; - int count = cursor.getInt(3); + Calendar date = DateUtils.getCalendar(r.getTimestamp()); + int weekday = date.get(Calendar.DAY_OF_WEEK) % 7; + date.set(Calendar.DAY_OF_MONTH, 1); - date.set(year, month - 1, 1); long timestamp = date.getTimeInMillis(); - Integer[] list = map.get(timestamp); - if(list == null) + if (list == null) { list = new Integer[7]; Arrays.fill(list, 0); map.put(timestamp, list); } - list[weekday] = count; + list[weekday]++; } - while (cursor.moveToNext()); - cursor.close(); return map; } /** - * Returns the total number of repetitions that happened within the specified interval of time. + * Removes a given repetition from the list. + *

+ * If the list does not contain the repetition, it is unchanged. + *

+ * Any implementation of this method must call observable.notifyListeners() + * after the repetition has been added. + * + * @param repetition the repetition to be removed + */ + public abstract void remove(@NonNull Repetition repetition); + + /** + * Adds or remove a repetition at a certain timestamp. + *

+ * If there exists a repetition on the list with the given timestamp, the + * method removes this repetition from the list and returns it. If there are + * no repetitions with the given timestamp, creates and adds one to the + * list, then returns it. * - * @param from beginning of the interval - * @param to end of the interval - * @return number of repetition in the given interval + * @param timestamp the timestamp for the timestamp that should be added or + * removed. + * @return the repetition that has been added or removed. */ - public int count(long from, long to) + @NonNull + public Repetition toggleTimestamp(long timestamp) { - return selectFromTo(from, to).count(); + timestamp = DateUtils.getStartOfDay(timestamp); + Repetition rep = getByTimestamp(timestamp); + + if (rep != null) remove(rep); + else + { + rep = new Repetition(timestamp); + add(rep); + } + + habit.getScores().invalidateNewerThan(timestamp); + habit.getCheckmarks().invalidateNewerThan(timestamp); + habit.getStreaks().invalidateNewerThan(timestamp); + return rep; } } diff --git a/app/src/main/java/org/isoron/uhabits/models/Score.java b/app/src/main/java/org/isoron/uhabits/models/Score.java index 5eba480e9..c9ebdea14 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Score.java +++ b/app/src/main/java/org/isoron/uhabits/models/Score.java @@ -19,78 +19,54 @@ package org.isoron.uhabits.models; -import com.activeandroid.Model; -import com.activeandroid.annotation.Column; -import com.activeandroid.annotation.Table; +import org.apache.commons.lang3.builder.*; -@Table(name = "Score") -public class Score extends Model +/** + * Represents how strong a habit is at a certain date. + */ +public final class Score { - /** - * Minimum score value required to earn half a star. - */ - public static final int HALF_STAR_CUTOFF = 9629750; - - /** - * Minimum score value required to earn a full star. - */ - public static final int FULL_STAR_CUTOFF = 15407600; - /** * Maximum score value attainable by any habit. */ public static final int MAX_VALUE = 19259478; /** - * Status indicating that the habit has not earned any star. - */ - public static final int EMPTY_STAR = 0; - - /** - * Status indicating that the habit has earned half a star. - */ - public static final int HALF_STAR = 1; - - /** - * Status indicating that the habit has earned a full star. - */ - public static final int FULL_STAR = 2; - - /** - * Habit to which this score belongs to. - */ - @Column(name = "habit") - public Habit habit; - - /** - * Timestamp of the day to which this score applies. Time of day should be midnight (UTC). + * Timestamp of the day to which this score applies. Time of day should be + * midnight (UTC). */ - @Column(name = "timestamp") - public Long timestamp; + private final Long timestamp; /** * Value of the score. */ - @Column(name = "score") - public Integer score; + private final Integer value; + + public Score(Long timestamp, Integer value) + { + this.timestamp = timestamp; + this.value = value; + } /** - * Given the frequency of the habit, the previous score, and the value of the current checkmark, - * computes the current score for the habit. - * - * The frequency of the habit is the number of repetitions divided by the length of the - * interval. For example, a habit that should be repeated 3 times in 8 days has frequency 3.0 / - * 8.0 = 0.375. - * - * The checkmarkValue should be UNCHECKED, CHECKED_IMPLICITLY or CHECK_EXPLICITLY. + * Given the frequency of the habit, the previous score, and the value of + * the current checkmark, computes the current score for the habit. + *

+ * The frequency of the habit is the number of repetitions divided by the + * length of the interval. For example, a habit that should be repeated 3 + * times in 8 days has frequency 3.0 / 8.0 = 0.375. + *

+ * The checkmarkValue should be UNCHECKED, CHECKED_IMPLICITLY or + * CHECK_EXPLICITLY. * - * @param frequency the frequency of the habit - * @param previousScore the previous score of the habit + * @param frequency the frequency of the habit + * @param previousScore the previous score of the habit * @param checkmarkValue the value of the current checkmark - * * @return the current score */ - public static int compute(double frequency, int previousScore, int checkmarkValue) + public static int compute(double frequency, + int previousScore, + int checkmarkValue) { double multiplier = Math.pow(0.5, 1.0 / (14.0 / frequency - 1)); int score = (int) (previousScore * multiplier); @@ -104,16 +80,27 @@ public class Score extends Model return score; } - /** - * Return the current star status for the habit, which can one of EMPTY_STAR, HALF_STAR or - * FULL_STAR. - * - * @return current star status - */ - public int getStarStatus() + public int compareNewer(Score other) + { + return Long.signum(this.getTimestamp() - other.getTimestamp()); + } + + public Long getTimestamp() + { + return timestamp; + } + + public Integer getValue() + { + return value; + } + + @Override + public String toString() { - if(score >= Score.FULL_STAR_CUTOFF) return Score.FULL_STAR; - if(score >= Score.HALF_STAR_CUTOFF) return Score.HALF_STAR; - return Score.EMPTY_STAR; + return new ToStringBuilder(this) + .append("timestamp", timestamp) + .append("value", value) + .toString(); } } diff --git a/app/src/main/java/org/isoron/uhabits/models/ScoreList.java b/app/src/main/java/org/isoron/uhabits/models/ScoreList.java index eca89b9e0..a8d797233 100644 --- a/app/src/main/java/org/isoron/uhabits/models/ScoreList.java +++ b/app/src/main/java/org/isoron/uhabits/models/ScoreList.java @@ -19,327 +19,231 @@ package org.isoron.uhabits.models; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteStatement; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import com.activeandroid.Cache; -import com.activeandroid.query.Delete; -import com.activeandroid.query.From; -import com.activeandroid.query.Select; -import com.activeandroid.util.SQLiteUtils; - -import org.isoron.uhabits.helpers.DatabaseHelper; -import org.isoron.uhabits.helpers.DateHelper; -import org.isoron.uhabits.helpers.UIHelper; - -import java.io.IOException; -import java.io.Writer; -import java.text.SimpleDateFormat; -import java.util.Date; - -public class ScoreList +import android.support.annotation.*; + +import org.isoron.uhabits.utils.*; + +import java.io.*; +import java.text.*; +import java.util.*; + +public abstract class ScoreList implements Iterable { - @NonNull - private Habit habit; + protected final Habit habit; + + protected ModelObservable observable; /** - * Constructs a new ScoreList associated with the given habit. + * Creates a new ScoreList for the given habit. + *

+ * The list is populated automatically according to the repetitions that the + * habit has. * - * @param habit the habit this list should be associated with + * @param habit the habit to which the scores belong. */ - public ScoreList(@NonNull Habit habit) + public ScoreList(Habit habit) { this.habit = habit; - } - - protected From select() - { - return new Select() - .from(Score.class) - .where("habit = ?", habit.getId()) - .orderBy("timestamp desc"); + observable = new ModelObservable(); } /** - * Marks all scores that have timestamp equal to or newer than the given timestamp as invalid. - * Any following getValue calls will trigger the scores to be recomputed. + * Adds the given scores to the list. + *

+ * This method should not be called by the application, since the scores are + * computed automatically from the list of repetitions. * - * @param timestamp the oldest timestamp that should be invalidated + * @param scores the scores to add. */ - public void invalidateNewerThan(long timestamp) - { - new Delete().from(Score.class) - .where("habit = ?", habit.getId()) - .and("timestamp >= ?", timestamp) - .execute(); - } + public abstract void add(List scores); + + public abstract List getAll(); /** - * Computes and saves the scores that are missing since the first repetition of the habit. + * Returns the score that has the given timestamp. + *

+ * If no such score exists, returns null. + * + * @param timestamp the timestamp to find. + * @return the score with given timestamp, or null if none exists. */ - private void computeAll() - { - long fromTimestamp = habit.repetitions.getOldestTimestamp(); - if(fromTimestamp == 0) return; + @Nullable + public abstract Score getByTimestamp(long timestamp); - long toTimestamp = DateHelper.getStartOfToday(); - compute(fromTimestamp, toTimestamp); + public ModelObservable getObservable() + { + return observable; } /** - * Computes and saves the scores that are missing inside a given time interval. Scores that - * have already been computed are skipped, therefore there is no harm in calling this function - * more times, or with larger intervals, than strictly needed. The endpoints of the interval are - * included. - * - * This function assumes that there are no gaps on the scores. That is, if the newest score has - * timestamp t, then every score with timestamp lower than t has already been computed. + * Returns the value of the score for today. * - * @param from timestamp of the beginning of the interval - * @param to timestamp of the end of the time interval + * @return value of today's score */ - protected void compute(long from, long to) + public int getTodayValue() { - UIHelper.throwIfMainThread(); - - final long day = DateHelper.millisecondsInOneDay; - final double freq = ((double) habit.freqNum) / habit.freqDen; - - int newestScoreValue = findNewestValue(); - long newestTimestamp = findNewestTimestamp(); - - if(newestTimestamp > 0) - from = newestTimestamp + day; - - final int checkmarkValues[] = habit.checkmarks.getValues(from, to); - final long beginning = from; - - int lastScore = newestScoreValue; - int size = checkmarkValues.length; - - long timestamps[] = new long[size]; - long values[] = new long[size]; - - for (int i = 0; i < checkmarkValues.length; i++) - { - int checkmarkValue = checkmarkValues[checkmarkValues.length - i - 1]; - lastScore = Score.compute(freq, lastScore, checkmarkValue); - timestamps[i] = beginning + day * i; - values[i] = lastScore; - } - - insert(timestamps, values); + return getValue(DateUtils.getStartOfToday()); } /** - * Returns the value of the most recent score that was already computed. If no score has been - * computed yet, returns zero. + * Returns the value of the score for a given day. + *

+ * If there is no score at the given timestamp (for example, if the + * timestamp given happens before the first repetition of the habit) then + * returns zero. * - * @return value of newest score, or zero if none exist + * @param timestamp the timestamp of a day + * @return score value for that day */ - protected int findNewestValue() + public final int getValue(long timestamp) { - String args[] = { habit.getId().toString() }; - String query = "select score from Score where habit = ? order by timestamp desc limit 1"; - return SQLiteUtils.intQuery(query, args); + Score s = getByTimestamp(timestamp); + if (s != null) return s.getValue(); + return 0; } - private long findNewestTimestamp() + public List groupBy(DateUtils.TruncateField field) { - String args[] = { habit.getId().toString() }; - String query = "select timestamp from Score where habit = ? order by timestamp desc limit 1"; - return DatabaseHelper.longQuery(query, args); - } - - private void insert(long timestamps[], long values[]) - { - String query = "insert into Score(habit, timestamp, score) values (?,?,?)"; - - SQLiteDatabase db = Cache.openDatabase(); - db.beginTransaction(); - - try - { - SQLiteStatement statement = db.compileStatement(query); - - for (int i = 0; i < timestamps.length; i++) - { - statement.bindLong(1, habit.getId()); - statement.bindLong(2, timestamps[i]); - statement.bindLong(3, values[i]); - statement.execute(); - } - - db.setTransactionSuccessful(); - } - finally - { - db.endTransaction(); - } + HashMap> groups = getGroupedValues(field); + List scores = groupsToAvgScores(groups); + Collections.sort(scores, (s1, s2) -> s2.compareNewer(s1)); + return scores; } /** - * Returns the score for a certain day. + * Marks all scores that have timestamp equal to or newer than the given + * timestamp as invalid. Any following getValue calls will trigger the + * scores to be recomputed. * - * @param timestamp the timestamp for the day - * @return the score for the day + * @param timestamp the oldest timestamp that should be invalidated */ - @Nullable - protected Score get(long timestamp) - { - Repetition oldestRep = habit.repetitions.getOldest(); - if(oldestRep == null) return null; - - compute(oldestRep.timestamp, timestamp); - - return select().where("timestamp = ?", timestamp).executeSingle(); - } + public abstract void invalidateNewerThan(long timestamp); - /** - * Returns the value of the score for a given day. - * - * @param timestamp the timestamp of a day - * @return score for that day - */ - public int getValue(long timestamp) + @Override + public Iterator iterator() { - computeAll(); - String[] args = { habit.getId().toString(), Long.toString(timestamp) }; - return SQLiteUtils.intQuery("select score from Score where habit = ? and timestamp = ?", args); + return getAll().iterator(); } - /** - * Returns the values of all the scores, from day of the first repetition until today, grouped - * in chunks of specified size. - * - * If the group size is one, then the value of each score is returned individually. If the group - * is, for example, seven, then the days are grouped in groups of seven consecutive days. - * - * The values are returned in an array of integers, with one entry for each group of days in the - * interval. This value corresponds to the average of the scores for the days inside the group. - * The first entry corresponds to the ending of the interval (that is, the most recent group of - * days). The last entry corresponds to the beginning of the interval. As usual, the time of the - * day for the timestamps should be midnight (UTC). The endpoints of the interval are included. - * - * The values are returned in an integer array. There is one entry for each day inside the - * interval. The first entry corresponds to today, while the last entry corresponds to the - * day of the oldest repetition. - * - * @param divisor the size of the groups - * @return array of values, with one entry for each group of days - */ - @NonNull - public int[] getAllValues(long divisor) + public void writeCSV(Writer out) throws IOException { - Repetition oldestRep = habit.repetitions.getOldest(); - if(oldestRep == null) return new int[0]; + computeAll(); + SimpleDateFormat dateFormat = DateUtils.getCSVDateFormat(); - long fromTimestamp = oldestRep.timestamp; - long toTimestamp = DateHelper.getStartOfToday(); - return getValues(fromTimestamp, toTimestamp, divisor); + for (Score s : this) + { + String timestamp = dateFormat.format(s.getTimestamp()); + String score = + String.format("%.4f", ((float) s.getValue()) / Score.MAX_VALUE); + out.write(String.format("%s,%s\n", timestamp, score)); + } } /** - * Same as getAllValues(long), but using a specified interval. + * Computes and saves the scores that are missing inside a given time + * interval. + *

+ * Scores that have already been computed are skipped, therefore there is no + * harm in calling this function more times, or with larger intervals, than + * strictly needed. The endpoints of the interval are included. + *

+ * This function assumes that there are no gaps on the scores. That is, if + * the newest score has timestamp t, then every score with timestamp lower + * than t has already been computed. * - * @param from beginning of the interval (included) - * @param to end of the interval (included) - * @param divisor size of the groups - * @return array of values, with one entry for each group of days + * @param from timestamp of the beginning of the interval + * @param to timestamp of the end of the time interval */ - @NonNull - protected int[] getValues(long from, long to, long divisor) + protected void compute(long from, long to) { - compute(from, to); + final long day = DateUtils.millisecondsInOneDay; + final double freq = habit.getFrequency().toDouble(); - divisor *= DateHelper.millisecondsInOneDay; - Long offset = to + divisor; + int newestValue = 0; + long newestTimestamp = 0; - String query = "select ((timestamp - ?) / ?) as time, avg(score) from Score " + - "where habit = ? and timestamp >= ? and timestamp <= ? " + - "group by time order by time desc"; - - String params[] = { offset.toString(), Long.toString(divisor), habit.getId().toString(), - Long.toString(from), Long.toString(to) }; + Score newest = getNewestComputed(); + if (newest != null) + { + newestValue = newest.getValue(); + newestTimestamp = newest.getTimestamp(); + } - SQLiteDatabase db = Cache.openDatabase(); - Cursor cursor = db.rawQuery(query, params); + if (newestTimestamp > 0) from = newestTimestamp + day; - if(!cursor.moveToFirst()) return new int[0]; + final int checkmarkValues[] = habit.getCheckmarks().getValues(from, to); + final long beginning = from; - int k = 0; - int[] scores = new int[cursor.getCount()]; + int lastScore = newestValue; + List scores = new LinkedList<>(); - do + for (int i = 0; i < checkmarkValues.length; i++) { - scores[k++] = (int) cursor.getFloat(1); + int value = checkmarkValues[checkmarkValues.length - i - 1]; + lastScore = Score.compute(freq, lastScore, value); + scores.add(new Score(beginning + day * i, lastScore)); } - while (cursor.moveToNext()); - cursor.close(); - return scores; + add(scores); } /** - * Returns the score for today. - * - * @return score for today + * Computes and saves the scores that are missing since the first repetition + * of the habit. */ - @Nullable - protected Score getToday() + protected void computeAll() { - return get(DateHelper.getStartOfToday()); - } + Repetition oldestRep = habit.getRepetitions().getOldest(); + if (oldestRep == null) return; - /** - * Returns the value of the score for today. - * - * @return value of today's score - */ - public int getTodayValue() - { - return getValue(DateHelper.getStartOfToday()); + long toTimestamp = DateUtils.getStartOfToday(); + compute(oldestRep.getTimestamp(), toTimestamp); } /** - * Returns the star status for today. The returned value is either Score.EMPTY_STAR, - * Score.HALF_STAR or Score.FULL_STAR. + * Returns the most recent score that has already been computed. + *

+ * If no score has been computed yet, returns null. * - * @return star status for today + * @return the newest score computed, or null if none exist */ - public int getTodayStarStatus() - { - Score score = getToday(); - if(score != null) return score.getStarStatus(); - else return Score.EMPTY_STAR; - } + @Nullable + protected abstract Score getNewestComputed(); - public void writeCSV(Writer out) throws IOException + @NonNull + private HashMap> getGroupedValues(DateUtils.TruncateField field) { - computeAll(); + HashMap> groups = new HashMap<>(); - SimpleDateFormat dateFormat = DateHelper.getCSVDateFormat(); + for (Score s : this) + { + long groupTimestamp = DateUtils.truncate(field, s.getTimestamp()); - String query = "select timestamp, score from score where habit = ? order by timestamp"; - String params[] = { habit.getId().toString() }; + if (!groups.containsKey(groupTimestamp)) + groups.put(groupTimestamp, new ArrayList<>()); - SQLiteDatabase db = Cache.openDatabase(); - Cursor cursor = db.rawQuery(query, params); + groups.get(groupTimestamp).add((long) s.getValue()); + } - if(!cursor.moveToFirst()) return; + return groups; + } - do + @NonNull + private List groupsToAvgScores(HashMap> groups) + { + List scores = new LinkedList<>(); + + for (Long timestamp : groups.keySet()) { - String timestamp = dateFormat.format(new Date(cursor.getLong(0))); - String score = String.format("%.4f", ((float) cursor.getInt(1)) / Score.MAX_VALUE); - out.write(String.format("%s,%s\n", timestamp, score)); + long meanValue = 0L; + ArrayList groupValues = groups.get(timestamp); - } while(cursor.moveToNext()); + for (Long v : groupValues) meanValue += v; + meanValue /= groupValues.size(); - cursor.close(); - out.close(); + scores.add(new Score(timestamp, (int) meanValue)); + } + + return scores; } } diff --git a/app/src/main/java/org/isoron/uhabits/models/Streak.java b/app/src/main/java/org/isoron/uhabits/models/Streak.java index 35a20f444..5a7a59f3d 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Streak.java +++ b/app/src/main/java/org/isoron/uhabits/models/Streak.java @@ -19,20 +19,55 @@ package org.isoron.uhabits.models; -import com.activeandroid.Model; -import com.activeandroid.annotation.Column; +import org.apache.commons.lang3.builder.*; +import org.isoron.uhabits.utils.*; -public class Streak extends Model +public final class Streak { - @Column(name = "habit") - public Habit habit; + private final long start; - @Column(name = "start") - public Long start; + private final long end; - @Column(name = "end") - public Long end; + public Streak(long start, long end) + { + this.start = start; + this.end = end; + } - @Column(name = "length") - public Long length; + public int compareLonger(Streak other) + { + if (this.getLength() != other.getLength()) + return Long.signum(this.getLength() - other.getLength()); + + return Long.signum(this.getEnd() - other.getEnd()); + } + + public int compareNewer(Streak other) + { + return Long.signum(this.getEnd() - other.getEnd()); + } + + public long getEnd() + { + return end; + } + + public long getLength() + { + return (end - start) / DateUtils.millisecondsInOneDay + 1; + } + + public long getStart() + { + return start; + } + + @Override + public String toString() + { + return new ToStringBuilder(this) + .append("start", start) + .append("end", end) + .toString(); + } } diff --git a/app/src/main/java/org/isoron/uhabits/models/StreakList.java b/app/src/main/java/org/isoron/uhabits/models/StreakList.java index 691403d25..4ae6a2b55 100644 --- a/app/src/main/java/org/isoron/uhabits/models/StreakList.java +++ b/app/src/main/java/org/isoron/uhabits/models/StreakList.java @@ -19,99 +19,122 @@ package org.isoron.uhabits.models; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; +import android.support.annotation.*; -import com.activeandroid.ActiveAndroid; -import com.activeandroid.Cache; -import com.activeandroid.query.Delete; -import com.activeandroid.query.Select; +import org.isoron.uhabits.utils.*; -import org.isoron.uhabits.helpers.DateHelper; -import org.isoron.uhabits.helpers.UIHelper; +import java.util.*; -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; - -public class StreakList +/** + * The collection of {@link Streak}s that belong to a habit. + *

+ * This list is populated automatically from the list of repetitions. + */ +public abstract class StreakList { - private Habit habit; + protected final Habit habit; + + protected ModelObservable observable; - public StreakList(Habit habit) + protected StreakList(Habit habit) { this.habit = habit; + observable = new ModelObservable(); } - public List getAll(int limit) - { - rebuild(); - - String query = "select * from (select * from streak where habit=? " + - "order by end <> ?, length desc, end desc limit ?) order by end desc"; + public abstract List getAll(); - String params[] = {habit.getId().toString(), Long.toString(DateHelper.getStartOfToday()), - Integer.toString(limit)}; + @NonNull + public List getBest(int limit) + { + List streaks = getAll(); + Collections.sort(streaks, (s1, s2) -> s2.compareLonger(s1)); + streaks = streaks.subList(0, Math.min(streaks.size(), limit)); + Collections.sort(streaks, (s1, s2) -> s2.compareNewer(s1)); + return streaks; + } - SQLiteDatabase db = Cache.openDatabase(); - Cursor cursor = db.rawQuery(query, params); + @Nullable + public abstract Streak getNewestComputed(); - if(!cursor.moveToFirst()) - { - cursor.close(); - return new LinkedList<>(); - } + @NonNull + public ModelObservable getObservable() + { + return observable; + } - List streaks = new LinkedList<>(); + public abstract void invalidateNewerThan(long timestamp); - do - { - Streak s = Streak.load(Streak.class, cursor.getInt(0)); - streaks.add(s); - } - while (cursor.moveToNext()); + public void rebuild() + { + long today = DateUtils.getStartOfToday(); - cursor.close(); - return streaks; + Long beginning = findBeginning(); + if (beginning == null || beginning > today) return; - } + int checks[] = habit.getCheckmarks().getValues(beginning, today); + List streaks = checkmarksToStreaks(beginning, checks); - public Streak getNewest() - { - return new Select().from(Streak.class) - .where("habit = ?", habit.getId()) - .orderBy("end desc") - .limit(1) - .executeSingle(); + removeNewestComputed(); + add(streaks); } - public void rebuild() + /** + * Converts a list of checkmark values to a list of streaks. + * + * @param beginning the timestamp corresponding to the first checkmark + * value. + * @param checks the checkmarks values, ordered by decreasing timestamp. + * @return the list of streaks. + */ + @NonNull + protected List checkmarksToStreaks(long beginning, int[] checks) { - UIHelper.throwIfMainThread(); + ArrayList transitions = getTransitions(beginning, checks); - long beginning; - long today = DateHelper.getStartOfToday(); - long day = DateHelper.millisecondsInOneDay; - - Streak newestStreak = getNewest(); - if (newestStreak != null) + List streaks = new LinkedList<>(); + for (int i = 0; i < transitions.size(); i += 2) { - beginning = newestStreak.start; + long start = transitions.get(i); + long end = transitions.get(i + 1); + streaks.add(new Streak(start, end)); } - else - { - Repetition oldestRep = habit.repetitions.getOldest(); - if (oldestRep == null) return; - beginning = oldestRep.timestamp; - } + return streaks; + } - if (beginning > today) return; + /** + * Finds the place where we should start when recomputing the streaks. + * + * @return + */ + @Nullable + protected Long findBeginning() + { + Streak newestStreak = getNewestComputed(); + if (newestStreak != null) return newestStreak.getStart(); - int checks[] = habit.checkmarks.getValues(beginning, today); - ArrayList list = new ArrayList<>(); + Repetition oldestRep = habit.getRepetitions().getOldest(); + if (oldestRep != null) return oldestRep.getTimestamp(); + return null; + } + + /** + * Returns the timestamps where there was a transition from performing a + * habit to not performing a habit, and vice-versa. + * + * @param beginning the timestamp for the first checkmark + * @param checks the checkmarks, ordered by decresing timestamp + * @return the list of transitions + */ + @NonNull + protected ArrayList getTransitions(long beginning, int[] checks) + { + long day = DateUtils.millisecondsInOneDay; long current = beginning; + + ArrayList list = new ArrayList<>(); list.add(current); for (int i = 1; i < checks.length; i++) @@ -125,36 +148,10 @@ public class StreakList if (list.size() % 2 == 1) list.add(current); - ActiveAndroid.beginTransaction(); - - if(newestStreak != null) newestStreak.delete(); - - try - { - for (int i = 0; i < list.size(); i += 2) - { - Streak streak = new Streak(); - streak.habit = habit; - streak.start = list.get(i); - streak.end = list.get(i + 1); - streak.length = (streak.end - streak.start) / day + 1; - streak.save(); - } - - ActiveAndroid.setTransactionSuccessful(); - } - finally - { - ActiveAndroid.endTransaction(); - } + return list; } + protected abstract void add(@NonNull List streaks); - public void deleteNewerThan(long timestamp) - { - new Delete().from(Streak.class) - .where("habit = ?", habit.getId()) - .and("end >= ?", timestamp - DateHelper.millisecondsInOneDay) - .execute(); - } + protected abstract void removeNewestComputed(); } diff --git a/app/src/main/java/org/isoron/uhabits/models/memory/MemoryCheckmarkList.java b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryCheckmarkList.java new file mode 100644 index 000000000..d8d45fb2a --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryCheckmarkList.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2016 Á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.models.memory; + +import android.support.annotation.*; + +import org.isoron.uhabits.models.*; + +import java.util.*; + +/** + * In-memory implementation of {@link CheckmarkList}. + */ +public class MemoryCheckmarkList extends CheckmarkList +{ + LinkedList list; + + public MemoryCheckmarkList(Habit habit) + { + super(habit); + list = new LinkedList<>(); + } + + @Override + public void add(List checkmarks) + { + list.addAll(checkmarks); + Collections.sort(list, (c1, c2) -> c2.compareNewer(c1)); + } + + @NonNull + @Override + public List getByInterval(long fromTimestamp, long toTimestamp) + { + compute(fromTimestamp, toTimestamp); + + List filtered = new LinkedList<>(); + + for (Checkmark c : list) + if (c.getTimestamp() >= fromTimestamp && + c.getTimestamp() <= toTimestamp) filtered.add(c); + + return filtered; + } + + @Override + public void invalidateNewerThan(long timestamp) + { + LinkedList invalid = new LinkedList<>(); + + for (Checkmark c : list) + if (c.getTimestamp() >= timestamp) invalid.add(c); + + list.removeAll(invalid); + } + + @Override + protected Checkmark getNewestComputed() + { + long newestTimestamp = 0; + Checkmark newestCheck = null; + + for (Checkmark c : list) + { + if (c.getTimestamp() > newestTimestamp) + { + newestCheck = c; + newestTimestamp = c.getTimestamp(); + } + } + + return newestCheck; + } + +} diff --git a/app/src/main/java/org/isoron/uhabits/models/memory/MemoryHabitList.java b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryHabitList.java new file mode 100644 index 000000000..d0f76dbce --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryHabitList.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2016 Á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.models.memory; + +import android.support.annotation.*; + +import org.isoron.uhabits.models.*; + +import java.util.*; + +/** + * In-memory implementation of {@link HabitList}. + */ +public class MemoryHabitList extends HabitList +{ + @NonNull + private LinkedList list; + + public MemoryHabitList() + { + list = new LinkedList<>(); + } + + @Override + public void add(@NonNull Habit habit) throws IllegalArgumentException + { + if (list.contains(habit)) + throw new IllegalArgumentException("habit already added"); + + Long id = habit.getId(); + if (id != null && getById(id) != null) + throw new RuntimeException("duplicate id"); + + if (id == null) habit.setId((long) list.size()); + list.addLast(habit); + } + + @Override + public int countActive() + { + int count = 0; + for (Habit h : list) if (!h.isArchived()) count++; + return count; + } + + @Override + public int countWithArchived() + { + return list.size(); + } + + @NonNull + @Override + public List getAll(boolean includeArchive) + { + if (includeArchive) return new LinkedList<>(list); + return getFiltered(habit -> !habit.isArchived()); + } + + @Override + public Habit getById(long id) + { + for (Habit h : list) if (h.getId() == id) return h; + return null; + } + + @Nullable + @Override + public Habit getByPosition(int position) + { + return list.get(position); + } + + @Override + public int indexOf(@NonNull Habit h) + { + return list.indexOf(h); + } + + @Override + public void remove(@NonNull Habit habit) + { + list.remove(habit); + } + + @Override + public void reorder(Habit from, Habit to) + { + int toPos = indexOf(to); + list.remove(from); + list.add(toPos, from); + } + + @Override + public void update(List habits) + { + // NOP + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/memory/MemoryModelFactory.java b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryModelFactory.java new file mode 100644 index 000000000..86e0eb9fe --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryModelFactory.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2016 Á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.models.memory; + +import org.isoron.uhabits.models.*; + +public class MemoryModelFactory implements ModelFactory +{ + @Override + public RepetitionList buildRepetitionList(Habit habit) + { + return new MemoryRepetitionList(habit); + } + + @Override + public HabitList buildHabitList() + { + return new MemoryHabitList(); + } + + @Override + public CheckmarkList buildCheckmarkList(Habit habit) + { + return new MemoryCheckmarkList(habit); + } + + @Override + public ScoreList buildScoreList(Habit habit) + { + return new MemoryScoreList(habit); + } + + @Override + public StreakList buildStreakList(Habit habit) + { + return new MemoryStreakList(habit); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/memory/MemoryRepetitionList.java b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryRepetitionList.java new file mode 100644 index 000000000..461de8021 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryRepetitionList.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2016 Á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.models.memory; + +import android.support.annotation.*; + +import org.isoron.uhabits.models.*; + +import java.util.*; + +/** + * In-memory implementation of {@link RepetitionList}. + */ +public class MemoryRepetitionList extends RepetitionList +{ + LinkedList list; + + public MemoryRepetitionList(Habit habit) + { + super(habit); + list = new LinkedList<>(); + } + + @Override + public void add(Repetition repetition) + { + list.add(repetition); + observable.notifyListeners(); + } + + @Override + public List getByInterval(long fromTimestamp, long toTimestamp) + { + LinkedList filtered = new LinkedList<>(); + + for (Repetition r : list) + { + long t = r.getTimestamp(); + if (t >= fromTimestamp && t <= toTimestamp) filtered.add(r); + } + + Collections.sort(filtered, + (r1, r2) -> (int) (r1.getTimestamp() - r2.getTimestamp())); + + return filtered; + } + + @Nullable + @Override + public Repetition getByTimestamp(long timestamp) + { + for (Repetition r : list) + if (r.getTimestamp() == timestamp) return r; + + return null; + } + + @Nullable + @Override + public Repetition getOldest() + { + long oldestTime = Long.MAX_VALUE; + Repetition oldestRep = null; + + for (Repetition rep : list) + { + if (rep.getTimestamp() < oldestTime) + { + oldestRep = rep; + oldestTime = rep.getTimestamp(); + } + + } + + return oldestRep; + } + + @Override + public void remove(@NonNull Repetition repetition) + { + list.remove(repetition); + observable.notifyListeners(); + } + +} diff --git a/app/src/main/java/org/isoron/uhabits/models/memory/MemoryScoreList.java b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryScoreList.java new file mode 100644 index 000000000..2705d407d --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryScoreList.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2016 Á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.models.memory; + +import android.support.annotation.*; + +import org.isoron.uhabits.models.*; + +import java.util.*; + +public class MemoryScoreList extends ScoreList +{ + List list; + + public MemoryScoreList(Habit habit) + { + super(habit); + list = new LinkedList<>(); + } + + @Override + public void invalidateNewerThan(long timestamp) + { + List discard = new LinkedList<>(); + + for (Score s : list) + if (s.getTimestamp() >= timestamp) discard.add(s); + + list.removeAll(discard); + getObservable().notifyListeners(); + } + + @Override + @NonNull + public List getAll() + { + computeAll(); + return new LinkedList<>(list); + } + + @Nullable + @Override + public Score getByTimestamp(long timestamp) + { + computeAll(); + for (Score s : list) + if (s.getTimestamp() == timestamp) return s; + + return null; + } + + @Override + public void add(List scores) + { + list.addAll(scores); + Collections.sort(list, + (s1, s2) -> Long.signum(s2.getTimestamp() - s1.getTimestamp())); + } + + @Nullable + @Override + protected Score getNewestComputed() + { + if (list.isEmpty()) return null; + return list.get(0); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/memory/MemoryStreakList.java b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryStreakList.java new file mode 100644 index 000000000..31fd3de8d --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryStreakList.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2016 Á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.models.memory; + +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; + +import java.util.*; + +public class MemoryStreakList extends StreakList +{ + LinkedList list; + + public MemoryStreakList(Habit habit) + { + super(habit); + list = new LinkedList<>(); + } + + @Override + public Streak getNewestComputed() + { + Streak newest = null; + + for (Streak s : list) + if (newest == null || s.getEnd() > newest.getEnd()) newest = s; + + return newest; + } + + @Override + public void invalidateNewerThan(long timestamp) + { + LinkedList discard = new LinkedList<>(); + + for (Streak s : list) + if (s.getEnd() >= timestamp - DateUtils.millisecondsInOneDay) + discard.add(s); + + list.removeAll(discard); + observable.notifyListeners(); + } + + @Override + protected void add(List streaks) + { + list.addAll(streaks); + Collections.sort(list, (s1, s2) -> s2.compareNewer(s1)); + } + + @Override + protected void removeNewestComputed() + { + Streak newest = getNewestComputed(); + if (newest != null) list.remove(newest); + } + + @Override + public List getAll() + { + rebuild(); + return new LinkedList<>(list); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/memory/package-info.java b/app/src/main/java/org/isoron/uhabits/models/memory/package-info.java new file mode 100644 index 000000000..f8272f334 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/memory/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2016 Á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 . + */ + +/** + * Provides in-memory implementation of core models. + */ +package org.isoron.uhabits.models.memory; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/models/package-info.java b/app/src/main/java/org/isoron/uhabits/models/package-info.java new file mode 100644 index 000000000..9cb029b90 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2016 Á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 Licenses along + * with this program. If not, see . + */ + +/** + * Provides core models classes, such as {@link org.isoron.uhabits.models.Habit} + * and {@link org.isoron.uhabits.models.Repetition}. + */ +package org.isoron.uhabits.models; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLModelFactory.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLModelFactory.java new file mode 100644 index 000000000..904391e36 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLModelFactory.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2016 Á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.models.sqlite; + +import org.isoron.uhabits.models.*; + +/** + * Factory that provides models backed by an SQLite database. + */ +public class SQLModelFactory implements ModelFactory +{ + @Override + public RepetitionList buildRepetitionList(Habit habit) + { + return new SQLiteRepetitionList(habit); + } + + @Override + public CheckmarkList buildCheckmarkList(Habit habit) + { + return new SQLiteCheckmarkList(habit); + } + + @Override + public HabitList buildHabitList() + { + return SQLiteHabitList.getInstance(); + } + + @Override + public ScoreList buildScoreList(Habit habit) + { + return new SQLiteScoreList(habit); + } + + @Override + public StreakList buildStreakList(Habit habit) + { + return new SQLiteStreakList(habit); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteCheckmarkList.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteCheckmarkList.java new file mode 100644 index 000000000..f33a3b277 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteCheckmarkList.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2016 Á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.models.sqlite; + +import android.database.sqlite.*; +import android.support.annotation.*; +import android.support.annotation.Nullable; + +import com.activeandroid.*; +import com.activeandroid.query.*; + +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.models.sqlite.records.*; +import org.jetbrains.annotations.*; + +import java.util.*; + +/** + * Implementation of a {@link CheckmarkList} that is backed by SQLite. + */ +public class SQLiteCheckmarkList extends CheckmarkList +{ + @Nullable + private HabitRecord habitRecord; + + @NonNull + private final SQLiteUtils sqlite; + + public SQLiteCheckmarkList(Habit habit) + { + super(habit); + sqlite = new SQLiteUtils<>(CheckmarkRecord.class); + } + + @Override + public void add(List checkmarks) + { + check(habit.getId()); + + String query = + "insert into Checkmarks(habit, timestamp, value) values (?,?,?)"; + + SQLiteDatabase db = Cache.openDatabase(); + db.beginTransaction(); + try + { + SQLiteStatement statement = db.compileStatement(query); + + for (Checkmark c : checkmarks) + { + statement.bindLong(1, habit.getId()); + statement.bindLong(2, c.getTimestamp()); + statement.bindLong(3, c.getValue()); + statement.execute(); + } + + db.setTransactionSuccessful(); + } + finally + { + db.endTransaction(); + } + } + + @NonNull + @Override + public List getByInterval(long fromTimestamp, long toTimestamp) + { + check(habit.getId()); + computeAll(); + + String query = "select habit, timestamp, value " + + "from checkmarks " + + "where habit = ? and timestamp >= ? and timestamp <= ? " + + "order by timestamp desc"; + + String params[] = { + Long.toString(habit.getId()), + Long.toString(fromTimestamp), + Long.toString(toTimestamp) + }; + + List records = sqlite.query(query, params); + for (CheckmarkRecord record : records) record.habit = habitRecord; + return toCheckmarks(records); + } + + @Override + public void invalidateNewerThan(long timestamp) + { + new Delete() + .from(CheckmarkRecord.class) + .where("habit = ?", habit.getId()) + .and("timestamp >= ?", timestamp) + .execute(); + + observable.notifyListeners(); + } + + @Override + @Nullable + protected Checkmark getNewestComputed() + { + check(habit.getId()); + + String query = "select habit, timestamp, value " + + "from checkmarks " + + "where habit = ? " + + "order by timestamp desc " + + "limit 1"; + + String params[] = { Long.toString(habit.getId()) }; + + CheckmarkRecord record = sqlite.querySingle(query, params); + if (record == null) return null; + record.habit = habitRecord; + return record.toCheckmark(); + } + + @Contract("null -> fail") + private void check(Long id) + { + if (id == null) throw new RuntimeException("habit is not saved"); + + if (habitRecord != null) return; + + habitRecord = HabitRecord.get(id); + if (habitRecord == null) throw new RuntimeException("habit not found"); + } + + @NonNull + private List toCheckmarks(@NonNull List records) + { + List checkmarks = new LinkedList<>(); + for (CheckmarkRecord r : records) checkmarks.add(r.toCheckmark()); + return checkmarks; + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteHabitList.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteHabitList.java new file mode 100644 index 000000000..4a366dac4 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteHabitList.java @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2016 Á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.models.sqlite; + +import android.database.sqlite.*; +import android.support.annotation.*; + +import com.activeandroid.*; +import com.activeandroid.query.*; + +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.models.sqlite.records.*; + +import java.util.*; + +/** + * Implementation of a {@link HabitList} that is backed by SQLite. + */ +public class SQLiteHabitList extends HabitList +{ + private static SQLiteHabitList instance; + + private HashMap cache; + + private final SQLiteUtils sqlite; + + + private SQLiteHabitList() + { + cache = new HashMap<>(); + sqlite = new SQLiteUtils<>(HabitRecord.class); + } + + /** + * Returns the global list of habits. + *

+ * There is only one list of habit per application, corresponding to the + * habits table of the SQLite database. + * + * @return the global list of habits. + */ + public static SQLiteHabitList getInstance() + { + if (instance == null) instance = new SQLiteHabitList(); + return instance; + } + + @Override + public void add(@NonNull Habit habit) + { + if (cache.containsValue(habit)) + throw new IllegalArgumentException("habit already added"); + + HabitRecord record = new HabitRecord(); + record.copyFrom(habit); + record.position = countWithArchived(); + + Long id = habit.getId(); + if (id == null) id = record.save(); + else record.save(id); + + habit.setId(id); + cache.put(id, habit); + } + + @Override + public int countActive() + { + SQLiteDatabase db = Cache.openDatabase(); + SQLiteStatement st = db.compileStatement( + "select count(*) from habits where archived = 0"); + return (int) st.simpleQueryForLong(); + } + + @Override + public int countWithArchived() + { + SQLiteDatabase db = Cache.openDatabase(); + SQLiteStatement st = db.compileStatement("select count(*) from habits"); + return (int) st.simpleQueryForLong(); + } + + @Override + @NonNull + public List getAll(boolean includeArchive) + { + List recordList; + if (includeArchive) + { + String query = HabitRecord.SELECT + "order by position"; + recordList = sqlite.query(query, null); + } + else + { + String query = HabitRecord.SELECT + "where archived = 0 " + + "order by position"; + recordList = sqlite.query(query, null); + } + + List habits = new LinkedList<>(); + for (HabitRecord record : recordList) + { + Habit habit = getById(record.getId()); + if (habit == null) + throw new RuntimeException("habit not in database"); + habits.add(habit); + } + + return habits; + } + + @Override + @Nullable + public Habit getById(long id) + { + if (!cache.containsKey(id)) + { + HabitRecord record = HabitRecord.get(id); + if (record == null) return null; + + Habit habit = new Habit(); + record.copyTo(habit); + cache.put(id, habit); + } + + return cache.get(id); + } + + @Override + @Nullable + public Habit getByPosition(int position) + { + String query = HabitRecord.SELECT + "where position = ? limit 1"; + String params[] = { Integer.toString(position) }; + HabitRecord record = sqlite.querySingle(query, params); + if (record != null) return getById(record.getId()); + return null; + } + + @Override + public int indexOf(@NonNull Habit h) + { + if (h.getId() == null) return -1; + HabitRecord record = HabitRecord.get(h.getId()); + if (record == null) return -1; + return record.position; + } + + @Deprecated + public void rebuildOrder() + { + List habits = getAll(true); + + int i = 0; + for (Habit h : habits) + { + HabitRecord record = HabitRecord.get(h.getId()); + if (record == null) + throw new RuntimeException("habit not in database"); + + record.position = i++; + record.save(); + } + + update(habits); + } + + @Override + public void remove(@NonNull Habit habit) + { + if (!cache.containsKey(habit.getId())) + throw new RuntimeException("habit not in cache"); + + cache.remove(habit.getId()); + HabitRecord record = HabitRecord.get(habit.getId()); + if (record == null) throw new RuntimeException("habit not in database"); + record.cascadeDelete(); + rebuildOrder(); + } + + @Override + public void reorder(Habit from, Habit to) + { + if (from == to) return; + + Integer toPos = indexOf(to); + Integer fromPos = indexOf(from); + + if (toPos < fromPos) + { + new Update(HabitRecord.class) + .set("position = position + 1") + .where("position >= ? and position < ?", toPos, fromPos) + .execute(); + } + else + { + new Update(HabitRecord.class) + .set("position = position - 1") + .where("position > ? and position <= ?", fromPos, toPos) + .execute(); + } + + HabitRecord record = HabitRecord.get(from.getId()); + if (record == null) throw new RuntimeException("habit not in database"); + record.position = toPos; + record.save(); + + update(from); + + getObservable().notifyListeners(); + } + + @Override + public void update(List habits) + { + for (Habit h : habits) + { + HabitRecord record = HabitRecord.get(h.getId()); + if (record == null) + throw new RuntimeException("habit not in database"); + record.copyFrom(h); + record.save(); + } + } + +} diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteRepetitionList.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteRepetitionList.java new file mode 100644 index 000000000..2ce44ceb4 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteRepetitionList.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2016 Á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.models.sqlite; + +import android.support.annotation.*; +import android.support.annotation.Nullable; + +import com.activeandroid.query.*; + +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.models.sqlite.records.*; +import org.jetbrains.annotations.*; + +import java.util.*; + +/** + * Implementation of a {@link RepetitionList} that is backed by SQLite. + */ +public class SQLiteRepetitionList extends RepetitionList +{ + private final SQLiteUtils sqlite; + + @Nullable + private HabitRecord habitRecord; + + public SQLiteRepetitionList(@NonNull Habit habit) + { + super(habit); + sqlite = new SQLiteUtils<>(RepetitionRecord.class); + } + + /** + * Adds a repetition to the global SQLite database. + *

+ * Given a repetition, this creates and saves the corresponding + * RepetitionRecord to the database. + * + * @param rep the repetition to be added + */ + @Override + public void add(Repetition rep) + { + check(habit.getId()); + + RepetitionRecord record = new RepetitionRecord(); + record.copyFrom(rep); + record.habit = habitRecord; + record.save(); + observable.notifyListeners(); + } + + @Override + public List getByInterval(long timeFrom, long timeTo) + { + check(habit.getId()); + String query = "select habit, timestamp " + + "from Repetitions " + + "where habit = ? and timestamp >= ? and timestamp <= ? " + + "order by timestamp"; + + String params[] = { + Long.toString(habit.getId()), + Long.toString(timeFrom), + Long.toString(timeTo) + }; + + List records = sqlite.query(query, params); + return toRepetitions(records); + } + + @Override + @Nullable + public Repetition getByTimestamp(long timestamp) + { + check(habit.getId()); + String query = "select habit, timestamp " + + "from Repetitions " + + "where habit = ? and timestamp = ? " + + "limit 1"; + + String params[] = + { Long.toString(habit.getId()), Long.toString(timestamp) }; + + RepetitionRecord record = sqlite.querySingle(query, params); + if (record == null) return null; + record.habit = habitRecord; + return record.toRepetition(); + } + + @Override + public Repetition getOldest() + { + check(habit.getId()); + String query = "select habit, timestamp " + + "from Repetitions " + + "where habit = ? " + + "order by timestamp asc " + + "limit 1"; + + String params[] = { Long.toString(habit.getId()) }; + + RepetitionRecord record = sqlite.querySingle(query, params); + if (record == null) return null; + record.habit = habitRecord; + return record.toRepetition(); + } + + @Override + public void remove(@NonNull Repetition repetition) + { + new Delete() + .from(RepetitionRecord.class) + .where("habit = ?", habit.getId()) + .and("timestamp = ?", repetition.getTimestamp()) + .execute(); + + observable.notifyListeners(); + } + + @Contract("null -> fail") + private void check(Long id) + { + if (id == null) throw new RuntimeException("habit is not saved"); + + if (habitRecord != null) return; + + habitRecord = HabitRecord.get(id); + if (habitRecord == null) throw new RuntimeException("habit not found"); + } + + @NonNull + private List toRepetitions( + @NonNull List records) + { + check(habit.getId()); + + List reps = new LinkedList<>(); + for (RepetitionRecord record : records) + { + record.habit = habitRecord; + reps.add(record.toRepetition()); + } + + return reps; + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteScoreList.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteScoreList.java new file mode 100644 index 000000000..ecafd2cec --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteScoreList.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2016 Á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.models.sqlite; + +import android.database.sqlite.*; +import android.support.annotation.*; +import android.support.annotation.Nullable; + +import com.activeandroid.*; +import com.activeandroid.query.*; + +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.models.sqlite.records.*; +import org.jetbrains.annotations.*; + +import java.util.*; + +/** + * Implementation of a ScoreList that is backed by SQLite. + */ +public class SQLiteScoreList extends ScoreList +{ + @Nullable + private HabitRecord habitRecord; + + @NonNull + private final SQLiteUtils sqlite; + + /** + * Constructs a new ScoreList associated with the given habit. + * + * @param habit the habit this list should be associated with + */ + public SQLiteScoreList(@NonNull Habit habit) + { + super(habit); + sqlite = new SQLiteUtils<>(ScoreRecord.class); + } + + @Override + public void add(List scores) + { + check(habit.getId()); + String query = + "insert into Score(habit, timestamp, score) values (?,?,?)"; + + SQLiteDatabase db = Cache.openDatabase(); + db.beginTransaction(); + + try + { + SQLiteStatement statement = db.compileStatement(query); + + for (Score s : scores) + { + statement.bindLong(1, habit.getId()); + statement.bindLong(2, s.getTimestamp()); + statement.bindLong(3, s.getValue()); + statement.execute(); + } + + db.setTransactionSuccessful(); + } + finally + { + db.endTransaction(); + } + } + + @Override + @NonNull + public List getAll() + { + check(habit.getId()); + computeAll(); + + String query = "select habit, timestamp, score from Score " + + "where habit = ? order by timestamp desc"; + + String params[] = {Long.toString(habit.getId())}; + + List records = sqlite.query(query, params); + for (ScoreRecord record : records) record.habit = habitRecord; + + List scores = new LinkedList<>(); + for (ScoreRecord rec : records) + scores.add(rec.toScore()); + + return scores; + } + + @Override + @Nullable + public Score getByTimestamp(long timestamp) + { + check(habit.getId()); + computeAll(); + + String query = "select habit, timestamp, score from Score " + + "where habit = ? and timestamp = ? " + + "order by timestamp desc"; + + String params[] = + {Long.toString(habit.getId()), Long.toString(timestamp)}; + + ScoreRecord record = sqlite.querySingle(query, params); + if (record == null) return null; + record.habit = habitRecord; + return record.toScore(); + } + + @Override + public void invalidateNewerThan(long timestamp) + { + new Delete() + .from(ScoreRecord.class) + .where("habit = ?", habit.getId()) + .and("timestamp >= ?", timestamp) + .execute(); + + getObservable().notifyListeners(); + } + + @Nullable + @Override + protected Score getNewestComputed() + { + check(habit.getId()); + String query = "select habit, timestamp, score from Score " + + "where habit = ? order by timestamp desc " + + "limit 1"; + + String params[] = {Long.toString(habit.getId())}; + + ScoreRecord record = sqlite.querySingle(query, params); + if (record == null) return null; + record.habit = habitRecord; + return record.toScore(); + } + + @Contract("null -> fail") + private void check(Long id) + { + if (id == null) throw new RuntimeException("habit is not saved"); + + if(habitRecord != null) return; + + habitRecord = HabitRecord.get(id); + if (habitRecord == null) throw new RuntimeException("habit not found"); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteStreakList.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteStreakList.java new file mode 100644 index 000000000..8cf10b49f --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteStreakList.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2016 Á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.models.sqlite; + +import android.support.annotation.*; +import android.support.annotation.Nullable; + +import com.activeandroid.query.*; + +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.models.sqlite.records.*; +import org.isoron.uhabits.utils.*; +import org.jetbrains.annotations.*; + +import java.util.*; + +/** + * Implementation of a StreakList that is backed by SQLite. + */ +public class SQLiteStreakList extends StreakList +{ + private HabitRecord habitRecord; + + @NonNull + private final SQLiteUtils sqlite; + + public SQLiteStreakList(Habit habit) + { + super(habit); + sqlite = new SQLiteUtils<>(StreakRecord.class); + } + + @Override + public List getAll() + { + check(habit.getId()); + rebuild(); + + String query = StreakRecord.SELECT + "where habit = ? " + + "order by end desc"; + + String params[] = { Long.toString(habit.getId())}; + + List records = sqlite.query(query, params); + return recordsToStreaks(records); + } + + @Override + public Streak getNewestComputed() + { + StreakRecord newestRecord = getNewestRecord(); + if (newestRecord == null) return null; + return newestRecord.toStreak(); + } + + @Override + public void invalidateNewerThan(long timestamp) + { + new Delete() + .from(StreakRecord.class) + .where("habit = ?", habit.getId()) + .and("end >= ?", timestamp - DateUtils.millisecondsInOneDay) + .execute(); + + observable.notifyListeners(); + } + + @Override + protected void add(@NonNull List streaks) + { + check(habit.getId()); + + DatabaseUtils.executeAsTransaction(() -> { + for (Streak streak : streaks) + { + StreakRecord record = new StreakRecord(); + record.copyFrom(streak); + record.habit = habitRecord; + record.save(); + } + }); + } + + @Override + protected void removeNewestComputed() + { + StreakRecord newestStreak = getNewestRecord(); + if (newestStreak != null) newestStreak.delete(); + } + + @Nullable + private StreakRecord getNewestRecord() + { + check(habit.getId()); + String query = StreakRecord.SELECT + "where habit = ? " + + "order by end desc " + + "limit 1 "; + String params[] = { habit.getId().toString() }; + StreakRecord record = sqlite.querySingle(query, params); + if (record != null) record.habit = habitRecord; + return record; + + } + + @NonNull + private List recordsToStreaks(List records) + { + LinkedList streaks = new LinkedList<>(); + + for (StreakRecord record : records) + { + record.habit = habitRecord; + streaks.add(record.toStreak()); + } + + return streaks; + } + + @Contract("null -> fail") + private void check(Long id) + { + if (id == null) throw new RuntimeException("habit is not saved"); + + if(habitRecord != null) return; + + habitRecord = HabitRecord.get(id); + if (habitRecord == null) throw new RuntimeException("habit not found"); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteUtils.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteUtils.java new file mode 100644 index 000000000..616b0d12f --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteUtils.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2016 Á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.models.sqlite; + +import android.database.*; +import android.database.sqlite.*; +import android.support.annotation.*; + +import com.activeandroid.*; + +import org.isoron.uhabits.models.sqlite.records.*; + +import java.util.*; + +public class SQLiteUtils +{ + private Class klass; + + public SQLiteUtils(Class klass) + { + this.klass = klass; + } + + @NonNull + public List query(String query, String params[]) + { + SQLiteDatabase db = Cache.openDatabase(); + try (Cursor c = db.rawQuery(query, params)) + { + return cursorToMultipleRecords(c); + } + } + + @Nullable + public T querySingle(String query, String params[]) + { + SQLiteDatabase db = Cache.openDatabase(); + try(Cursor c = db.rawQuery(query, params)) + { + if (!c.moveToNext()) return null; + return cursorToSingleRecord(c); + } + } + + @NonNull + private List cursorToMultipleRecords(Cursor c) + { + List records = new LinkedList<>(); + while (c.moveToNext()) records.add(cursorToSingleRecord(c)); + return records; + } + + @NonNull + private T cursorToSingleRecord(Cursor c) + { + try + { + T record = (T) klass.newInstance(); + record.copyFrom(c); + return record; + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/package-info.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/package-info.java new file mode 100644 index 000000000..469894365 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2016 Á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 . + */ + +/** + * Provides SQLite implementations of the core models. + */ +package org.isoron.uhabits.models.sqlite; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/records/CheckmarkRecord.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/records/CheckmarkRecord.java new file mode 100644 index 000000000..718aef00b --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/records/CheckmarkRecord.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2016 Á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.models.sqlite.records; + +import android.database.*; + +import com.activeandroid.*; +import com.activeandroid.annotation.*; + +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.models.sqlite.*; + +/** + * The SQLite database record corresponding to a {@link Checkmark}. + */ +@Table(name = "Checkmarks") +public class CheckmarkRecord extends Model implements SQLiteRecord +{ + /** + * The habit to which this checkmark belongs. + */ + @Column(name = "habit") + public HabitRecord habit; + + /** + * Timestamp of the day to which this checkmark corresponds. Time of the day + * must be midnight (UTC). + */ + @Column(name = "timestamp") + public Long timestamp; + + /** + * Indicates whether there is a repetition at the given timestamp or not, + * and whether the repetition was expected. Assumes one of the values + * UNCHECKED, CHECKED_EXPLICITLY or CHECKED_IMPLICITLY. + */ + @Column(name = "value") + public Integer value; + + @Override + public void copyFrom(Cursor c) + { + timestamp = c.getLong(1); + value = c.getInt(2); + } + + public Checkmark toCheckmark() + { + SQLiteHabitList habitList = SQLiteHabitList.getInstance(); + Habit h = habitList.getById(habit.getId()); + return new Checkmark(timestamp, value); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/records/HabitRecord.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/records/HabitRecord.java new file mode 100644 index 000000000..154312264 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/records/HabitRecord.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2016 Á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.models.sqlite.records; + +import android.annotation.*; +import android.database.*; +import android.support.annotation.*; + +import com.activeandroid.*; +import com.activeandroid.annotation.*; +import com.activeandroid.query.*; +import com.activeandroid.util.*; + +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.DatabaseUtils; + +import java.lang.reflect.*; + +/** + * The SQLite database record corresponding to a {@link Habit}. + */ +@Table(name = "Habits") +public class HabitRecord extends Model implements SQLiteRecord +{ + public static String SELECT = + "select id, color, description, freq_den, freq_num, " + + "name, position, reminder_hour, reminder_min, " + + "highlight, archived, reminder_days from habits "; + + @Column(name = "name") + public String name; + + @Column(name = "description") + public String description; + + @Column(name = "freq_num") + public Integer freqNum; + + @Column(name = "freq_den") + public Integer freqDen; + + @Column(name = "color") + public Integer color; + + @Column(name = "position") + public Integer position; + + @Nullable + @Column(name = "reminder_hour") + public Integer reminderHour; + + @Nullable + @Column(name = "reminder_min") + public Integer reminderMin; + + @NonNull + @Column(name = "reminder_days") + public Integer reminderDays; + + @Column(name = "highlight") + public Integer highlight; + + @Column(name = "archived") + public Integer archived; + + public HabitRecord() + { + + } + + @Nullable + public static HabitRecord get(long id) + { + return HabitRecord.load(HabitRecord.class, id); + } + + /** + * Changes the id of a habit on the database. + * + * @param oldId the original id + * @param newId the new id + */ + @SuppressLint("DefaultLocale") + public static void updateId(long oldId, long newId) + { + SQLiteUtils.execSql( + String.format("update Habits set Id = %d where Id = %d", newId, + oldId)); + } + + /** + * Deletes the habit and all data associated to it, including checkmarks, + * repetitions and scores. + */ + public void cascadeDelete() + { + Long id = getId(); + + DatabaseUtils.executeAsTransaction(() -> { + new Delete() + .from(CheckmarkRecord.class) + .where("habit = ?", id) + .execute(); + + new Delete() + .from(RepetitionRecord.class) + .where("habit = ?", id) + .execute(); + + new Delete() + .from(ScoreRecord.class) + .where("habit = ?", id) + .execute(); + + new Delete() + .from(StreakRecord.class) + .where("habit = ?", id) + .execute(); + + delete(); + }); + } + + public void copyFrom(Habit model) + { + this.name = model.getName(); + this.description = model.getDescription(); + this.highlight = 0; + this.color = model.getColor(); + this.archived = model.isArchived() ? 1 : 0; + Frequency freq = model.getFrequency(); + this.freqNum = freq.getNumerator(); + this.freqDen = freq.getDenominator(); + + if(model.hasReminder()) + { + Reminder reminder = model.getReminder(); + this.reminderHour = reminder.getHour(); + this.reminderMin = reminder.getMinute(); + this.reminderDays = reminder.getDays(); + } + } + + @Override + public void copyFrom(Cursor c) + { + setId(c.getLong(0)); + color = c.getInt(1); + description = c.getString(2); + freqDen = c.getInt(3); + freqNum = c.getInt(4); + name = c.getString(5); + position = c.getInt(6); + reminderHour = c.getInt(7); + reminderMin = c.getInt(8); + highlight = c.getInt(9); + archived = c.getInt(10); + reminderDays = c.getInt(11); + } + + public void copyTo(Habit habit) + { + habit.setName(this.name); + habit.setDescription(this.description); + habit.setFrequency(new Frequency(this.freqNum, this.freqDen)); + habit.setColor(this.color); + habit.setArchived(this.archived != 0); + habit.setId(this.getId()); + + if (reminderHour != null && reminderMin != null) + { + habit.setReminder( + new Reminder(reminderHour, reminderMin, reminderDays)); + } + } + + /** + * Saves the habit on the database, and assigns the specified id to it. + * + * @param id the id that the habit should receive + */ + public void save(long id) + { + save(); + updateId(getId(), id); + } + + private void setId(Long id) + { + try + { + Field f = (Model.class).getDeclaredField("mId"); + f.setAccessible(true); + f.set(this, id); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/records/RepetitionRecord.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/records/RepetitionRecord.java new file mode 100644 index 000000000..eec93777f --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/records/RepetitionRecord.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2016 Á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.models.sqlite.records; + +import android.database.*; + +import com.activeandroid.*; +import com.activeandroid.annotation.*; + +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.models.sqlite.*; + +/** + * The SQLite database record corresponding to a {@link Repetition}. + */ +@Table(name = "Repetitions") +public class RepetitionRecord extends Model implements SQLiteRecord +{ + @Column(name = "habit") + public HabitRecord habit; + + @Column(name = "timestamp") + public Long timestamp; + + public static RepetitionRecord get(Long id) + { + return RepetitionRecord.load(RepetitionRecord.class, id); + } + + public void copyFrom(Repetition repetition) + { + timestamp = repetition.getTimestamp(); + } + + @Override + public void copyFrom(Cursor c) + { + timestamp = c.getLong(1); + } + + public Repetition toRepetition() + { + SQLiteHabitList habitList = SQLiteHabitList.getInstance(); + Habit h = habitList.getById(habit.getId()); + return new Repetition(timestamp); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/records/SQLiteRecord.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/records/SQLiteRecord.java new file mode 100644 index 000000000..1991b1276 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/records/SQLiteRecord.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2016 Á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.models.sqlite.records; + +import android.database.*; + +public interface SQLiteRecord +{ + void copyFrom(Cursor c); +} diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/records/ScoreRecord.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/records/ScoreRecord.java new file mode 100644 index 000000000..09d8602a2 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/records/ScoreRecord.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2016 Á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.models.sqlite.records; + +import android.database.*; + +import com.activeandroid.*; +import com.activeandroid.annotation.*; + +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.models.sqlite.*; + +/** + * The SQLite database record corresponding to a Score. + */ +@Table(name = "Score") +public class ScoreRecord extends Model implements SQLiteRecord +{ + @Column(name = "habit") + public HabitRecord habit; + + /** + * Timestamp of the day to which this score applies. Time of day should be + * midnight (UTC). + */ + @Column(name = "timestamp") + public Long timestamp; + + /** + * Value of the score. + */ + @Column(name = "score") + public Integer score; + + @Override + public void copyFrom(Cursor c) + { + timestamp = c.getLong(1); + score = c.getInt(2); + } + + /** + * Constructs and returns a {@link Score} based on this record's data. + * + * @return a {@link Score} with this record's data + */ + public Score toScore() + { + SQLiteHabitList habitList = SQLiteHabitList.getInstance(); + Habit h = habitList.getById(habit.getId()); + return new Score(timestamp, score); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/records/StreakRecord.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/records/StreakRecord.java new file mode 100644 index 000000000..90b4362f2 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/records/StreakRecord.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2016 Á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.models.sqlite.records; + +import android.database.*; + +import com.activeandroid.*; +import com.activeandroid.annotation.*; + +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.models.sqlite.*; + +import java.lang.reflect.*; + +/** + * The SQLite database record corresponding to a Streak. + */ +@Table(name = "Streak") +public class StreakRecord extends Model implements SQLiteRecord +{ + public static final String SELECT = "select id, start, end, length from Streak "; + + @Column(name = "habit") + public HabitRecord habit; + + @Column(name = "start") + public Long start; + + @Column(name = "end") + public Long end; + + @Column(name = "length") + public Long length; + + public static StreakRecord get(Long id) + { + return StreakRecord.load(StreakRecord.class, id); + } + + public void copyFrom(Streak streak) + { + start = streak.getStart(); + end = streak.getEnd(); + length = streak.getLength(); + } + + @Override + public void copyFrom(Cursor c) + { + setId(c.getLong(0)); + start = c.getLong(1); + end = c.getLong(2); + length = c.getLong(3); + } + + private void setId(long id) + { + try + { + Field f = (Model.class).getDeclaredField("mId"); + f.setAccessible(true); + f.set(this, id); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + + public Streak toStreak() + { + SQLiteHabitList habitList = SQLiteHabitList.getInstance(); + Habit h = habitList.getById(habit.getId()); + return new Streak(start, end); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/records/package-info.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/records/package-info.java new file mode 100644 index 000000000..379d6a6e0 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/records/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2016 Á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 . + */ + +/** + * Provides classes that represent rows in the SQLite database. + */ +package org.isoron.uhabits.models.sqlite.records; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/package-info.java b/app/src/main/java/org/isoron/uhabits/package-info.java new file mode 100644 index 000000000..b080842fc --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2016 Á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 . + */ + +/** + * Provides classes for the Loop Habit Tracker app. + */ +package org.isoron.uhabits; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/tasks/BaseTask.java b/app/src/main/java/org/isoron/uhabits/tasks/BaseTask.java index d9542c84b..8bf2a8b23 100644 --- a/app/src/main/java/org/isoron/uhabits/tasks/BaseTask.java +++ b/app/src/main/java/org/isoron/uhabits/tasks/BaseTask.java @@ -21,6 +21,7 @@ package org.isoron.uhabits.tasks; import android.os.AsyncTask; import android.os.Build; +import android.support.annotation.*; import java.util.concurrent.TimeoutException; @@ -28,6 +29,7 @@ public abstract class BaseTask extends AsyncTask { private static int activeTaskCount; + @CallSuper @Override protected void onPreExecute() { @@ -35,6 +37,7 @@ public abstract class BaseTask extends AsyncTask activeTaskCount++; } + @CallSuper @Override protected void onPostExecute(Void aVoid) { diff --git a/app/src/main/java/org/isoron/uhabits/tasks/ExportCSVTask.java b/app/src/main/java/org/isoron/uhabits/tasks/ExportCSVTask.java index 5c85bc89e..1eb23a12a 100644 --- a/app/src/main/java/org/isoron/uhabits/tasks/ExportCSVTask.java +++ b/app/src/main/java/org/isoron/uhabits/tasks/ExportCSVTask.java @@ -20,12 +20,10 @@ package org.isoron.uhabits.tasks; import android.support.annotation.Nullable; -import android.view.View; -import android.widget.ProgressBar; -import org.isoron.uhabits.helpers.DatabaseHelper; import org.isoron.uhabits.io.HabitsCSVExporter; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.utils.FileUtils; import java.io.File; import java.io.IOException; @@ -58,12 +56,7 @@ public class ExportCSVTask extends BaseTask protected void onPreExecute() { super.onPreExecute(); - - if(progressBar != null) - { - progressBar.setIndeterminate(true); - progressBar.setVisibility(View.VISIBLE); - } + if(progressBar != null) progressBar.show(); } @Override @@ -72,9 +65,7 @@ public class ExportCSVTask extends BaseTask if(listener != null) listener.onExportCSVFinished(archiveFilename); - if(progressBar != null) - progressBar.setVisibility(View.GONE); - + if(progressBar != null) progressBar.hide(); super.onPostExecute(null); } @@ -83,7 +74,7 @@ public class ExportCSVTask extends BaseTask { try { - File dir = DatabaseHelper.getFilesDir("CSV"); + File dir = FileUtils.getFilesDir("CSV"); if(dir == null) return; HabitsCSVExporter exporter = new HabitsCSVExporter(selectedHabits, dir); diff --git a/app/src/main/java/org/isoron/uhabits/tasks/ExportDBTask.java b/app/src/main/java/org/isoron/uhabits/tasks/ExportDBTask.java index 4e184335f..541d3519f 100644 --- a/app/src/main/java/org/isoron/uhabits/tasks/ExportDBTask.java +++ b/app/src/main/java/org/isoron/uhabits/tasks/ExportDBTask.java @@ -20,10 +20,9 @@ package org.isoron.uhabits.tasks; import android.support.annotation.Nullable; -import android.view.View; -import android.widget.ProgressBar; -import org.isoron.uhabits.helpers.DatabaseHelper; +import org.isoron.uhabits.utils.DatabaseUtils; +import org.isoron.uhabits.utils.FileUtils; import java.io.File; import java.io.IOException; @@ -53,23 +52,14 @@ public class ExportDBTask extends BaseTask protected void onPreExecute() { super.onPreExecute(); - - if(progressBar != null) - { - progressBar.setIndeterminate(true); - progressBar.setVisibility(View.VISIBLE); - } + if(progressBar != null) progressBar.show(); } @Override protected void onPostExecute(Void aVoid) { - if(listener != null) - listener.onExportDBFinished(filename); - - if(progressBar != null) - progressBar.setVisibility(View.GONE); - + if(listener != null) listener.onExportDBFinished(filename); + if(progressBar != null) progressBar.hide(); super.onPostExecute(null); } @@ -80,10 +70,10 @@ public class ExportDBTask extends BaseTask try { - File dir = DatabaseHelper.getFilesDir("Backups"); + File dir = FileUtils.getFilesDir("Backups"); if(dir == null) return; - filename = DatabaseHelper.saveDatabaseCopy(dir); + filename = DatabaseUtils.saveDatabaseCopy(dir); } catch(IOException e) { diff --git a/app/src/main/java/org/isoron/uhabits/tasks/ImportDataTask.java b/app/src/main/java/org/isoron/uhabits/tasks/ImportDataTask.java index 477f1f51b..cdc6c538a 100644 --- a/app/src/main/java/org/isoron/uhabits/tasks/ImportDataTask.java +++ b/app/src/main/java/org/isoron/uhabits/tasks/ImportDataTask.java @@ -21,8 +21,6 @@ package org.isoron.uhabits.tasks; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.view.View; -import android.widget.ProgressBar; import org.isoron.uhabits.io.GenericImporter; @@ -36,7 +34,7 @@ public class ImportDataTask extends BaseTask public interface Listener { - void onImportFinished(int result); + void onImportDataFinished(int result); } @Nullable @@ -66,21 +64,14 @@ public class ImportDataTask extends BaseTask { super.onPreExecute(); - if(progressBar != null) - { - progressBar.setIndeterminate(true); - progressBar.setVisibility(View.VISIBLE); - } + if(progressBar != null) progressBar.show(); } @Override protected void onPostExecute(Void aVoid) { - if(progressBar != null) - progressBar.setVisibility(View.GONE); - - if(listener != null) listener.onImportFinished(result); - + if(progressBar != null) progressBar.hide(); + if(listener != null) listener.onImportDataFinished(result); super.onPostExecute(null); } diff --git a/app/src/main/java/org/isoron/uhabits/tasks/ProgressBar.java b/app/src/main/java/org/isoron/uhabits/tasks/ProgressBar.java new file mode 100644 index 000000000..afbfdac7a --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/tasks/ProgressBar.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2016 Á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.tasks; + +/** + * Simple progress bar, used to indicate the progress of a task. + */ +public interface ProgressBar +{ + /** + * Shows the progress bar. + */ + void show(); + + /** + * Hides the progress bar. + */ + void hide(); +} diff --git a/app/src/main/java/org/isoron/uhabits/tasks/SimpleTask.java b/app/src/main/java/org/isoron/uhabits/tasks/SimpleTask.java new file mode 100644 index 000000000..a85d0fb78 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/tasks/SimpleTask.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2016 Á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.tasks; + +public class SimpleTask +{ + private final BaseTask baseTask; + + public SimpleTask(Runnable runnable) + { + this.baseTask = new BaseTask() + { + @Override + protected void doInBackground() + { + runnable.run(); + } + }; + } + + public void execute() + { + baseTask.execute(); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/tasks/ToggleRepetitionTask.java b/app/src/main/java/org/isoron/uhabits/tasks/ToggleRepetitionTask.java index 3b46ec95c..56c6e170b 100644 --- a/app/src/main/java/org/isoron/uhabits/tasks/ToggleRepetitionTask.java +++ b/app/src/main/java/org/isoron/uhabits/tasks/ToggleRepetitionTask.java @@ -19,10 +19,18 @@ package org.isoron.uhabits.tasks; +import org.isoron.uhabits.HabitsApplication; +import org.isoron.uhabits.commands.CommandRunner; +import org.isoron.uhabits.commands.ToggleRepetitionCommand; import org.isoron.uhabits.models.Habit; +import javax.inject.Inject; + public class ToggleRepetitionTask extends BaseTask -{; +{ + @Inject + CommandRunner commandRunner; + public interface Listener { void onToggleRepetitionFinished(); } @@ -35,12 +43,14 @@ public class ToggleRepetitionTask extends BaseTask { this.timestamp = timestamp; this.habit = habit; + HabitsApplication.getComponent().inject(this); } @Override protected void doInBackground() { - habit.repetitions.toggle(timestamp); + ToggleRepetitionCommand command = new ToggleRepetitionCommand(habit, timestamp); + commandRunner.execute(command, habit.getId()); } @Override diff --git a/app/src/main/java/org/isoron/uhabits/tasks/package-info.java b/app/src/main/java/org/isoron/uhabits/tasks/package-info.java new file mode 100644 index 000000000..cc837895e --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/tasks/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2016 Á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 . + */ + +/** + * Provides async tasks for useful operations such as {@link + * org.isoron.uhabits.tasks.ExportCSVTask}. + */ +package org.isoron.uhabits.tasks; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/ui/AndroidProgressBar.java b/app/src/main/java/org/isoron/uhabits/ui/AndroidProgressBar.java new file mode 100644 index 000000000..22604c44c --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/AndroidProgressBar.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2016 Á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.ui; + +import android.view.*; + +import org.isoron.uhabits.tasks.*; + +/** + * Android implementation of {@link ProgressBar}. + */ +public class AndroidProgressBar implements ProgressBar +{ + private final android.widget.ProgressBar progressBar; + + public AndroidProgressBar(android.widget.ProgressBar progressBar) + { + this.progressBar = progressBar; + } + + @Override + public void hide() + { + progressBar.setVisibility(View.GONE); + } + + @Override + public void show() + { + progressBar.setIndeterminate(true); + progressBar.setVisibility(View.VISIBLE); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/BaseActivity.java b/app/src/main/java/org/isoron/uhabits/ui/BaseActivity.java new file mode 100644 index 000000000..d286e2fe0 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/BaseActivity.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2016 Á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.ui; + +import android.content.*; +import android.os.*; +import android.support.annotation.*; +import android.support.v7.app.*; +import android.view.*; + +import org.isoron.uhabits.utils.*; + +/** + * Base class for all activities in the application. + *

+ * This class delegates the responsibilities of an Android activity to other + * classes. For example, callbacks related to menus are forwarded to a {@link + * BaseMenu}, while callbacks related to activity results are forwarded to a + * {@link BaseScreen}. + *

+ * A BaseActivity also installs an {@link java.lang.Thread.UncaughtExceptionHandler} + * to the main thread that logs the exception to the disk before the application + * crashes. + */ +abstract public class BaseActivity extends AppCompatActivity + implements Thread.UncaughtExceptionHandler +{ + @Nullable + private BaseMenu baseMenu; + + @Nullable + private Thread.UncaughtExceptionHandler androidExceptionHandler; + + @Nullable + private BaseScreen screen; + + @Override + public boolean onCreateOptionsMenu(@Nullable Menu menu) + { + if (menu == null) return true; + if (baseMenu == null) return true; + baseMenu.onCreate(getMenuInflater(), menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(@Nullable MenuItem item) + { + if (item == null) return false; + if (baseMenu == null) return false; + return baseMenu.onItemSelected(item); + } + + public void setBaseMenu(@Nullable BaseMenu baseMenu) + { + this.baseMenu = baseMenu; + } + + public void setScreen(@Nullable BaseScreen screen) + { + this.screen = screen; + } + + @Override + public void uncaughtException(@Nullable Thread thread, + @Nullable Throwable ex) + { + if (ex == null) return; + + try + { + ex.printStackTrace(); + new BaseSystem(this).dumpBugReportToFile(); + } + catch (Exception e) + { + // ignored + } + + if (androidExceptionHandler != null) + androidExceptionHandler.uncaughtException(thread, ex); + else System.exit(1); + } + + @Override + protected void onActivityResult(int request, int result, Intent data) + { + if (screen == null) return; + screen.onResult(request, result, data); + } + + @Override + protected void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + InterfaceUtils.applyCurrentTheme(this); + androidExceptionHandler = Thread.getDefaultUncaughtExceptionHandler(); + Thread.setDefaultUncaughtExceptionHandler(this); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/BaseMenu.java b/app/src/main/java/org/isoron/uhabits/ui/BaseMenu.java new file mode 100644 index 000000000..00ed8f6fe --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/BaseMenu.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2016 Á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.ui; + +import android.support.annotation.*; +import android.view.*; + +import javax.annotation.*; + +/** + * Base class for all the menus in the application. + *

+ * This class receives from BaseActivity all callbacks related to menus, such as + * menu creation and click events. It also handles some implementation details + * of creating menus in Android, such as inflating the resources. + */ +public abstract class BaseMenu +{ + @NonNull + private final BaseActivity activity; + + public BaseMenu(@NonNull BaseActivity activity) + { + this.activity = activity; + } + + /** + * Declare that the menu has changed, and should be recreated. + */ + public void invalidate() + { + activity.invalidateOptionsMenu(); + } + + /** + * Called when the menu is first displayed. + *

+ * The given menu is already inflated and ready to receive items. The + * application should override this method and add items to the menu here. + * + * @param menu the menu that is being created. + */ + public void onCreate(@NonNull Menu menu) + { + } + + /** + * Called when the menu is first displayed. + *

+ * This method cannot be overridden. The application should override the + * methods onCreate(Menu) and getMenuResourceId instead. + * + * @param inflater a menu inflater, for creating the menu + * @param menu the menu that is being created. + */ + public final void onCreate(@NonNull MenuInflater inflater, + @NonNull Menu menu) + { + menu.clear(); + inflater.inflate(getMenuResourceId(), menu); + onCreate(menu); + } + + /** + * Called whenever an item on the menu is selected. + * + * @param item the item that was selected. + * @return true if the event was consumed, or false otherwise + */ + public boolean onItemSelected(@NonNull MenuItem item) + { + return false; + } + + /** + * Returns the id of the resource that should be used to inflate this menu. + * + * @return id of the menu resource. + */ + @Resource + protected abstract int getMenuResourceId(); +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/BaseRootView.java b/app/src/main/java/org/isoron/uhabits/ui/BaseRootView.java new file mode 100644 index 000000000..fe89eacba --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/BaseRootView.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2016 Á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.ui; + +import android.content.*; +import android.os.*; +import android.support.annotation.*; +import android.support.v7.widget.Toolbar; +import android.view.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.utils.*; + +public abstract class BaseRootView extends FrameLayout +{ + private final Context context; + + public BaseRootView(Context context) + { + super(context); + this.context = context; + } + + public boolean getDisplayHomeAsUp() + { + return false; + } + + @Nullable + public ProgressBar getProgressBar() + { + return null; + } + + @NonNull + public abstract Toolbar getToolbar(); + + public int getToolbarColor() + { + if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) && + !InterfaceUtils.isNightMode()) + { + return getContext().getResources().getColor(R.color.grey_900); + } + + return InterfaceUtils.getStyledColor(getContext(), R.attr.colorPrimary); + } + + protected void initToolbar() + { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + { + getToolbar().setElevation(InterfaceUtils.dpToPixels(context, 2)); + View view = findViewById(R.id.toolbarShadow); + if (view != null) view.setVisibility(View.GONE); + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/BaseScreen.java b/app/src/main/java/org/isoron/uhabits/ui/BaseScreen.java new file mode 100644 index 000000000..08b30526b --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/BaseScreen.java @@ -0,0 +1,314 @@ +/* + * Copyright (C) 2016 Á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.ui; + +import android.content.*; +import android.graphics.*; +import android.graphics.drawable.*; +import android.net.*; +import android.os.*; +import android.support.annotation.*; +import android.support.v7.app.*; +import android.support.v7.view.ActionMode; +import android.support.v7.widget.Toolbar; +import android.view.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.tasks.ProgressBar; +import org.isoron.uhabits.utils.*; + +import java.io.*; + +/** + * Base class for all screens in the application. + *

+ * Screens are responsible for deciding what root views and what menus should be + * attached to the main window. They are also responsible for showing other + * screens and for receiving their results. + */ +public abstract class BaseScreen +{ + protected BaseActivity activity; + + private Toast toast; + + @Nullable + private BaseRootView rootView; + + @Nullable + private BaseSelectionMenu selectionMenu; + + public BaseScreen(@NonNull BaseActivity activity) + { + this.activity = activity; + } + + @Deprecated + public static void setupActionBarColor(@NonNull AppCompatActivity activity, + int color) + { + + Toolbar toolbar = (Toolbar) activity.findViewById(R.id.toolbar); + if (toolbar == null) return; + + activity.setSupportActionBar(toolbar); + + ActionBar actionBar = activity.getSupportActionBar(); + if (actionBar == null) return; + + actionBar.setDisplayHomeAsUpEnabled(true); + + ColorDrawable drawable = new ColorDrawable(color); + actionBar.setBackgroundDrawable(drawable); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + { + int darkerColor = ColorUtils.mixColors(color, Color.BLACK, 0.75f); + activity.getWindow().setStatusBarColor(darkerColor); + + toolbar.setElevation(InterfaceUtils.dpToPixels(activity, 2)); + + View view = activity.findViewById(R.id.toolbarShadow); + if (view != null) view.setVisibility(View.GONE); + +// view = activity.findViewById(R.id.headerShadow); +// if (view != null) view.setVisibility(View.GONE); + } + } + + /** + * Ends the current selection operation. + */ + public void finishSelection() + { + if (selectionMenu == null) return; + selectionMenu.finish(); + } + + /** + * Returns the progress bar that is currently visible on the screen. + *

+ * If the root view attached to the screen does not provide any progress + * bars, returns null. + * + * @return current progress bar, or null if there are none. + */ + @Nullable + public ProgressBar getProgressBar() + { + if (rootView == null) return null; + return new AndroidProgressBar(rootView.getProgressBar()); + } + + /** + * Notifies the screen that its contents should be updated. + */ + public void invalidate() + { + if (rootView == null) return; + rootView.invalidate(); + } + + /** + * Called when another Activity has finished, and has returned some result. + * + * @param requestCode the request code originally supplied to {@link + * android.app.Activity#startActivityForResult(Intent, + * int, Bundle)}. + * @param resultCode the result code sent by the other activity. + * @param data an Intent containing extra data sent by the other + * activity. + * @see {@link android.app.Activity#onActivityResult(int, int, Intent)} + */ + public void onResult(int requestCode, int resultCode, Intent data) + { + } + + /** + * Sets the menu to be shown by this screen. + *

+ * This menu will be visible if when there is no active selection operation. + * If the provided menu is null, then no menu will be shown. + * + * @param menu the menu to be shown. + */ + public void setMenu(@Nullable BaseMenu menu) + { + activity.setBaseMenu(menu); + } + + /** + * Sets the root view for this screen. + * + * @param rootView the root view for this screen. + */ + protected void setRootView(@Nullable BaseRootView rootView) + { + this.rootView = rootView; + activity.setContentView(rootView); + if (rootView == null) return; + + invalidateToolbar(); + } + + /** + * Sets the menu to be shown when a selection is active on the screen. + * + * @param menu the menu to be shown during a selection + */ + public void setSelectionMenu(@Nullable BaseSelectionMenu menu) + { + this.selectionMenu = menu; + } + + /** + * Shows a message on the screen. + * + * @param stringId the string resource id for this message. + */ + public void showMessage(@Nullable Integer stringId) + { + if (stringId == null) return; + if (toast == null) + toast = Toast.makeText(activity, stringId, Toast.LENGTH_SHORT); + else toast.setText(stringId); + toast.show(); + } + + public void showSendEmailScreen(String to, String subject, String content) + { + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_SEND); + intent.setType("message/rfc822"); + intent.putExtra(Intent.EXTRA_EMAIL, new String[]{to}); + intent.putExtra(Intent.EXTRA_SUBJECT, subject); + intent.putExtra(Intent.EXTRA_TEXT, content); + activity.startActivity(intent); + } + + public void showSendFileScreen(@NonNull String archiveFilename) + { + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_SEND); + intent.setType("application/zip"); + intent.putExtra(Intent.EXTRA_STREAM, + Uri.fromFile(new File(archiveFilename))); + activity.startActivity(intent); + } + + /** + * Instructs the screen to start a selection. + *

+ * If a selection menu was provided, this menu will be shown instead of the + * regular one. + */ + public void startSelection() + { + activity.startSupportActionMode(new ActionModeWrapper()); + } + + protected void showDialog(AppCompatDialogFragment dialog, String tag) + { + dialog.show(activity.getSupportFragmentManager(), tag); + } + + public void invalidateToolbar() + { + if (rootView == null) return; + + activity.runOnUiThread(() -> { + Toolbar toolbar = rootView.getToolbar(); + activity.setSupportActionBar(toolbar); + ActionBar actionBar = activity.getSupportActionBar(); + if (actionBar == null) return; + + actionBar.setDisplayHomeAsUpEnabled(rootView.getDisplayHomeAsUp()); + + int color = rootView.getToolbarColor(); + setActionBarColor(actionBar, color); + setStatusBarColor(color); + setupToolbarElevation(toolbar); + }); + } + + private void setActionBarColor(@NonNull ActionBar actionBar, int color) + { + ColorDrawable drawable = new ColorDrawable(color); + actionBar.setBackgroundDrawable(drawable); + } + + private void setStatusBarColor(int baseColor) + { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return; + + int darkerColor = ColorUtils.mixColors(baseColor, Color.BLACK, 0.75f); + activity.getWindow().setStatusBarColor(darkerColor); + } + + private void setupToolbarElevation(Toolbar toolbar) + { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return; + + toolbar.setElevation(InterfaceUtils.dpToPixels(activity, 2)); + + View view = activity.findViewById(R.id.toolbarShadow); + if (view != null) view.setVisibility(View.GONE); + +// view = activity.findViewById(R.id.headerShadow); +// if (view != null) view.setVisibility(View.GONE); + } + + private class ActionModeWrapper implements ActionMode.Callback + { + @Override + public boolean onActionItemClicked(@Nullable ActionMode mode, + @Nullable MenuItem item) + { + if (item == null || selectionMenu == null) return false; + return selectionMenu.onItemClicked(item); + } + + @Override + public boolean onCreateActionMode(@Nullable ActionMode mode, + @Nullable Menu menu) + { + if (selectionMenu == null) return false; + if (mode == null || menu == null) return false; + selectionMenu.onCreate(activity.getMenuInflater(), mode, menu); + return true; + } + + @Override + public void onDestroyActionMode(@Nullable ActionMode mode) + { + if (selectionMenu == null) return; + selectionMenu.onFinish(); + } + + @Override + public boolean onPrepareActionMode(@Nullable ActionMode mode, + @Nullable Menu menu) + { + if (selectionMenu == null || menu == null) return false; + return selectionMenu.onPrepare(menu); + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/BaseSelectionMenu.java b/app/src/main/java/org/isoron/uhabits/ui/BaseSelectionMenu.java new file mode 100644 index 000000000..eb5595afc --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/BaseSelectionMenu.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2016 Á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.ui; + +import android.support.annotation.*; +import android.support.v7.view.ActionMode; +import android.view.*; + +/** + * Base class for all the selection menus in the application. + *

+ * A selection menu is a menu that appears when the screen starts a selection + * operation. It contains actions that modify the selected items, such as delete + * or archive. Since it replaces the toolbar, it also has a title. + *

+ * This class hides many implementation details of creating such menus in + * Android. The interface is supposed to look very similar to {@link BaseMenu}, + * with a few additional methods, such as finishing the selection operation. + * Internally, it uses an {@link ActionMode}. + */ +public abstract class BaseSelectionMenu +{ + @Nullable + private ActionMode actionMode; + + /** + * Finishes the selection operation. + */ + public void finish() + { + if (actionMode != null) actionMode.finish(); + } + + /** + * Declare that the menu has changed, and should be recreated. + */ + public void invalidate() + { + if (actionMode != null) actionMode.invalidate(); + } + + /** + * Called when the menu is first displayed. + *

+ * This method cannot be overridden. The application should override the + * methods onCreate(Menu) and getMenuResourceId instead. + * + * @param inflater a menu inflater, for creating the menu + * @param mode the action mode associated with this menu. + * @param menu the menu that is being created. + */ + public final void onCreate(@NonNull MenuInflater inflater, + @NonNull ActionMode mode, + @NonNull Menu menu) + { + this.actionMode = mode; + inflater.inflate(getResourceId(), menu); + onCreate(menu); + } + + /** + * Called when the selection operation is about to finish. + */ + public void onFinish() + { + + } + + /** + * Called whenever an item on the menu is selected. + * + * @param item the item that was selected. + * @return true if the event was consumed, or false otherwise + */ + public boolean onItemClicked(@NonNull MenuItem item) + { + return false; + } + + + /** + * Called whenever the menu is invalidated. + * + * @param menu the menu to be refreshed + * @return true if the menu has changes, false otherwise + */ + public boolean onPrepare(@NonNull Menu menu) + { + return false; + } + + /** + * Sets the title of the selection menu. + * + * @param title the new title. + */ + public void setTitle(String title) + { + if (actionMode != null) actionMode.setTitle(title); + } + + protected abstract int getResourceId(); + + /** + * Called when the menu is first created. + * + * @param menu the menu being created + */ + protected void onCreate(@NonNull Menu menu) + { + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/BaseSystem.java b/app/src/main/java/org/isoron/uhabits/ui/BaseSystem.java new file mode 100644 index 000000000..ec39c0270 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/BaseSystem.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2016 Á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.ui; + +import android.content.*; +import android.os.*; +import android.support.annotation.*; +import android.view.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.tasks.*; +import org.isoron.uhabits.utils.*; + +import java.io.*; +import java.lang.Process; +import java.util.*; + +import javax.inject.*; + +/** + * Base class for all systems class in the application. + *

+ * Classes derived from BaseSystem are responsible for handling events and + * sending requests to the Android operating system. Examples include capturing + * a bug report, obtaining device information, or requesting runtime + * permissions. + */ +public class BaseSystem +{ + private Context context; + + @Inject + HabitList habitList; + + public BaseSystem(Context context) + { + this.context = context; + HabitsApplication.getComponent().inject(this); + } + + /** + * Captures a bug report and saves it to a file in the SD card. + *

+ * The contents of the file are generated by the method {@link + * #getBugReport()}. The file is saved in the apps's external private + * storage. + * + * @return the generated file. + * @throws IOException when I/O errors occur. + */ + @NonNull + public File dumpBugReportToFile() throws IOException + { + String date = + DateUtils.getBackupDateFormat().format(DateUtils.getLocalTime()); + + if (context == null) throw new RuntimeException( + "application context should not be null"); + File dir = FileUtils.getFilesDir("Logs"); + if (dir == null) throw new IOException("log dir should not be null"); + + File logFile = + new File(String.format("%s/Log %s.txt", dir.getPath(), date)); + FileWriter output = new FileWriter(logFile); + output.write(getBugReport()); + output.close(); + + return logFile; + } + + /** + * Captures and returns a bug report. + *

+ * The bug report contains some device information and the logcat. + * + * @return a String containing the bug report. + * @throws IOException when any I/O error occur. + */ + @NonNull + public String getBugReport() throws IOException + { + String logcat = getLogcat(); + String deviceInfo = getDeviceInfo(); + return deviceInfo + "\n" + logcat; + } + + /** + * Recreates all application reminders. + */ + public void scheduleReminders() + { + new BaseTask() + { + + @Override + protected void doInBackground() + { + ReminderUtils.createReminderAlarms(context, habitList); + } + }.execute(); + } + + private String getDeviceInfo() + { + if (context == null) return "null context\n"; + + WindowManager wm = + (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + + return + String.format("App Version Name: %s\n", BuildConfig.VERSION_NAME) + + String.format("App Version Code: %s\n", BuildConfig.VERSION_CODE) + + String.format("OS Version: %s (%s)\n", + System.getProperty("os.version"), Build.VERSION.INCREMENTAL) + + String.format("OS API Level: %s\n", Build.VERSION.SDK) + + String.format("Device: %s\n", Build.DEVICE) + + String.format("Model (Product): %s (%s)\n", Build.MODEL, + Build.PRODUCT) + + String.format("Manufacturer: %s\n", Build.MANUFACTURER) + + String.format("Other tags: %s\n", Build.TAGS) + + String.format("Screen Width: %s\n", + wm.getDefaultDisplay().getWidth()) + + String.format("Screen Height: %s\n", + wm.getDefaultDisplay().getHeight()) + + String.format("External storage state: %s\n\n", + Environment.getExternalStorageState()); + } + + public String getLogcat() throws IOException + { + int maxLineCount = 250; + StringBuilder builder = new StringBuilder(); + + String[] command = new String[]{"logcat", "-d"}; + Process process = Runtime.getRuntime().exec(command); + + InputStreamReader in = new InputStreamReader(process.getInputStream()); + BufferedReader bufferedReader = new BufferedReader(in); + + LinkedList log = new LinkedList<>(); + + String line; + while ((line = bufferedReader.readLine()) != null) + { + log.addLast(line); + if (log.size() > maxLineCount) log.removeFirst(); + } + + for (String l : log) + { + builder.append(l); + builder.append('\n'); + } + + return builder.toString(); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/AboutActivity.java b/app/src/main/java/org/isoron/uhabits/ui/about/AboutActivity.java similarity index 79% rename from app/src/main/java/org/isoron/uhabits/AboutActivity.java rename to app/src/main/java/org/isoron/uhabits/ui/about/AboutActivity.java index 279a96d45..8e1b49e14 100644 --- a/app/src/main/java/org/isoron/uhabits/AboutActivity.java +++ b/app/src/main/java/org/isoron/uhabits/ui/about/AboutActivity.java @@ -17,42 +17,24 @@ * with this program. If not, see . */ -package org.isoron.uhabits; +package org.isoron.uhabits.ui.about; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.view.View; -import android.widget.TextView; +import android.content.*; +import android.net.*; +import android.os.*; +import android.view.*; +import android.widget.*; -import org.isoron.uhabits.helpers.UIHelper; +import org.isoron.uhabits.*; +import org.isoron.uhabits.ui.*; +import org.isoron.uhabits.utils.*; +/** + * Activity that allows the user to see information about the app itself. + * Display current version, link to Google Play and list of contributors. + */ public class AboutActivity extends BaseActivity implements View.OnClickListener { - - @Override - protected void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - - setContentView(R.layout.about); - setupSupportActionBar(true); - - int color = UIHelper.getStyledColor(this, R.attr.aboutScreenColor); - setupActionBarColor(color); - - TextView tvVersion = (TextView) findViewById(R.id.tvVersion); - TextView tvRate = (TextView) findViewById(R.id.tvRate); - TextView tvFeedback = (TextView) findViewById(R.id.tvFeedback); - TextView tvSource = (TextView) findViewById(R.id.tvSource); - - tvVersion.setText(String.format(getResources().getString(R.string.version_n), - BuildConfig.VERSION_NAME)); - tvRate.setOnClickListener(this); - tvFeedback.setOnClickListener(this); - tvSource.setOnClickListener(this); - } - @Override public void onClick(View v) { @@ -86,4 +68,29 @@ public class AboutActivity extends BaseActivity implements View.OnClickListener } } } + + @Override + protected void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + setContentView(R.layout.about); + + int color = + InterfaceUtils.getStyledColor(this, R.attr.aboutScreenColor); + + BaseScreen.setupActionBarColor(this, color); + + TextView tvVersion = (TextView) findViewById(R.id.tvVersion); + TextView tvRate = (TextView) findViewById(R.id.tvRate); + TextView tvFeedback = (TextView) findViewById(R.id.tvFeedback); + TextView tvSource = (TextView) findViewById(R.id.tvSource); + + tvVersion.setText( + String.format(getResources().getString(R.string.version_n), + BuildConfig.VERSION_NAME)); + tvRate.setOnClickListener(this); + tvFeedback.setOnClickListener(this); + tvSource.setOnClickListener(this); + } } diff --git a/app/src/main/java/org/isoron/uhabits/ui/about/package-info.java b/app/src/main/java/org/isoron/uhabits/ui/about/package-info.java new file mode 100644 index 000000000..483cc1927 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/about/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2016 Á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 . + */ + +/** + * Provides activity that shows information about the app. + */ +package org.isoron.uhabits.ui.about; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitFrequencyView.java b/app/src/main/java/org/isoron/uhabits/ui/common/views/FrequencyChart.java similarity index 66% rename from app/src/main/java/org/isoron/uhabits/views/HabitFrequencyView.java rename to app/src/main/java/org/isoron/uhabits/ui/common/views/FrequencyChart.java index 7890757dd..8f34b7a26 100644 --- a/app/src/main/java/org/isoron/uhabits/views/HabitFrequencyView.java +++ b/app/src/main/java/org/isoron/uhabits/ui/common/views/FrequencyChart.java @@ -17,103 +17,89 @@ * with this program. If not, see . */ -package org.isoron.uhabits.views; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.RectF; -import android.util.AttributeSet; - -import org.isoron.uhabits.R; -import org.isoron.uhabits.helpers.ColorHelper; -import org.isoron.uhabits.helpers.DateHelper; -import org.isoron.uhabits.helpers.UIHelper; -import org.isoron.uhabits.models.Habit; - -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.HashMap; -import java.util.Random; - -public class HabitFrequencyView extends ScrollableDataView implements HabitDataView -{ +package org.isoron.uhabits.ui.common.views; + +import android.content.*; +import android.graphics.*; +import android.support.annotation.*; +import android.util.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.utils.*; +import java.text.*; +import java.util.*; + +public class FrequencyChart extends ScrollableChart +{ private Paint pGrid; + private float em; - private Habit habit; + private SimpleDateFormat dfMonth; + private SimpleDateFormat dfYear; private Paint pText, pGraph; + private RectF rect, prevRect; + private int baseSize; + private int paddingTop; private float columnWidth; + private int columnHeight; + private int nColumns; private int textColor; + private int gridColor; + private int[] colors; + private int primaryColor; + private boolean isBackgroundTransparent; + @NonNull private HashMap frequency; - public HabitFrequencyView(Context context) + public FrequencyChart(Context context) { super(context); init(); } - public HabitFrequencyView(Context context, AttributeSet attrs) + public FrequencyChart(Context context, AttributeSet attrs) { super(context, attrs); - this.primaryColor = ColorHelper.getColor(getContext(), 7); this.frequency = new HashMap<>(); init(); } - public void setHabit(Habit habit) + public void setColor(int color) { - this.habit = habit; - createColors(); + this.primaryColor = color; + initColors(); + postInvalidate(); } - private void init() + public void setFrequency(HashMap frequency) { - createPaints(); - createColors(); - - dfMonth = DateHelper.getDateFormat("MMM"); - dfYear = DateHelper.getDateFormat("yyyy"); - - rect = new RectF(); - prevRect = new RectF(); + this.frequency = frequency; + postInvalidate(); } - private void createColors() + public void setIsBackgroundTransparent(boolean isBackgroundTransparent) { - if(habit != null) - { - this.primaryColor = ColorHelper.getColor(getContext(), habit.color); - } - - textColor = UIHelper.getStyledColor(getContext(), R.attr.mediumContrastTextColor); - gridColor = UIHelper.getStyledColor(getContext(), R.attr.lowContrastTextColor); - - colors = new int[4]; - colors[0] = gridColor; - colors[3] = primaryColor; - colors[1] = ColorHelper.mixColors(colors[0], colors[3], 0.66f); - colors[2] = ColorHelper.mixColors(colors[0], colors[3], 0.33f); + this.isBackgroundTransparent = isBackgroundTransparent; + initColors(); } - protected void createPaints() + protected void initPaints() { pText = new Paint(); pText.setAntiAlias(true); @@ -126,79 +112,6 @@ public class HabitFrequencyView extends ScrollableDataView implements HabitDataV pGrid.setAntiAlias(true); } - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) - { - int width = MeasureSpec.getSize(widthMeasureSpec); - int height = MeasureSpec.getSize(heightMeasureSpec); - setMeasuredDimension(width, height); - } - - @Override - protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) - { - if(height < 9) height = 200; - - baseSize = height / 8; - setScrollerBucketSize(baseSize); - - pText.setTextSize(baseSize * 0.4f); - pGraph.setTextSize(baseSize * 0.4f); - pGraph.setStrokeWidth(baseSize * 0.1f); - pGrid.setStrokeWidth(baseSize * 0.05f); - em = pText.getFontSpacing(); - - columnWidth = baseSize; - columnWidth = Math.max(columnWidth, getMaxMonthWidth() * 1.2f); - - columnHeight = 8 * baseSize; - nColumns = (int) (width / columnWidth); - paddingTop = 0; - } - - private float getMaxMonthWidth() - { - float maxMonthWidth = 0; - GregorianCalendar day = DateHelper.getStartOfTodayCalendar(); - - for(int i = 0; i < 12; i++) - { - day.set(Calendar.MONTH, i); - float monthWidth = pText.measureText(dfMonth.format(day.getTime())); - maxMonthWidth = Math.max(maxMonthWidth, monthWidth); - } - - return maxMonthWidth; - } - - public void refreshData() - { - if(isInEditMode()) - generateRandomData(); - else if(habit != null) - frequency = habit.repetitions.getWeekdayFrequency(); - - postInvalidate(); - } - - private void generateRandomData() - { - GregorianCalendar date = DateHelper.getStartOfTodayCalendar(); - date.set(Calendar.DAY_OF_MONTH, 1); - Random rand = new Random(); - frequency.clear(); - - for(int i = 0; i < 40; i++) - { - Integer values[] = new Integer[7]; - for(int j = 0; j < 7; j++) - values[j] = rand.nextInt(5); - - frequency.put(date.getTimeInMillis(), values); - date.add(Calendar.MONTH, -1); - } - } - @Override protected void onDraw(Canvas canvas) { @@ -214,12 +127,12 @@ public class HabitFrequencyView extends ScrollableDataView implements HabitDataV pGraph.setColor(primaryColor); prevRect.setEmpty(); - GregorianCalendar currentDate = DateHelper.getStartOfTodayCalendar(); + GregorianCalendar currentDate = DateUtils.getStartOfTodayCalendar(); currentDate.set(Calendar.DAY_OF_MONTH, 1); currentDate.add(Calendar.MONTH, -nColumns + 2 - getDataOffset()); - for(int i = 0; i < nColumns - 1; i++) + for (int i = 0; i < nColumns - 1; i++) { rect.set(0, 0, columnWidth, columnHeight); rect.offset(i * columnWidth, 0); @@ -229,21 +142,53 @@ public class HabitFrequencyView extends ScrollableDataView implements HabitDataV } } + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) + { + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + setMeasuredDimension(width, height); + } + + @Override + protected void onSizeChanged(int width, + int height, + int oldWidth, + int oldHeight) + { + if (height < 9) height = 200; + + baseSize = height / 8; + setScrollerBucketSize(baseSize); + + pText.setTextSize(baseSize * 0.4f); + pGraph.setTextSize(baseSize * 0.4f); + pGraph.setStrokeWidth(baseSize * 0.1f); + pGrid.setStrokeWidth(baseSize * 0.05f); + em = pText.getFontSpacing(); + + columnWidth = baseSize; + columnWidth = Math.max(columnWidth, getMaxMonthWidth() * 1.2f); + + columnHeight = 8 * baseSize; + nColumns = (int) (width / columnWidth); + paddingTop = 0; + } + private void drawColumn(Canvas canvas, RectF rect, GregorianCalendar date) { Integer values[] = frequency.get(date.getTimeInMillis()); float rowHeight = rect.height() / 8.0f; prevRect.set(rect); - Integer[] localeWeekdayList = DateHelper.getLocaleWeekdayList(); + Integer[] localeWeekdayList = DateUtils.getLocaleWeekdayList(); for (int j = 0; j < localeWeekdayList.length; j++) { rect.set(0, 0, baseSize, baseSize); rect.offset(prevRect.left, prevRect.top + baseSize * j); - int i = DateHelper.javaWeekdayToLoopWeekday(localeWeekdayList[j]); - if(values != null) - drawMarker(canvas, rect, values[i]); + int i = DateUtils.javaWeekdayToLoopWeekday(localeWeekdayList[j]); + if (values != null) drawMarker(canvas, rect, values[i]); rect.offset(0, rowHeight); } @@ -255,19 +200,12 @@ public class HabitFrequencyView extends ScrollableDataView implements HabitDataV { Date time = date.getTime(); - canvas.drawText(dfMonth.format(time), rect.centerX(), rect.centerY() - 0.1f * em, pText); + canvas.drawText(dfMonth.format(time), rect.centerX(), + rect.centerY() - 0.1f * em, pText); - if(date.get(Calendar.MONTH) == 1) - canvas.drawText(dfYear.format(time), rect.centerX(), rect.centerY() + 0.9f * em, pText); - } - - private void drawMarker(Canvas canvas, RectF rect, Integer value) - { - float padding = rect.height() * 0.2f; - float radius = (rect.height() - 2 * padding) / 2.0f / 4.0f * Math.min(value, 4); - - pGraph.setColor(colors[Math.min(3, Math.max(0, value - 1))]); - canvas.drawCircle(rect.centerX(), rect.centerY(), radius, pGraph); + if (date.get(Calendar.MONTH) == 1) + canvas.drawText(dfYear.format(time), rect.centerX(), + rect.centerY() + 0.9f * em, pText); } private void drawGrid(Canvas canvas, RectF rGrid) @@ -279,12 +217,14 @@ public class HabitFrequencyView extends ScrollableDataView implements HabitDataV pText.setColor(textColor); pGrid.setColor(gridColor); - for (String day : DateHelper.getLocaleDayNames(Calendar.SHORT)) { + for (String day : DateUtils.getLocaleDayNames(Calendar.SHORT)) + { canvas.drawText(day, rGrid.right - columnWidth, - rGrid.top + rowHeight / 2 + 0.25f * em, pText); + rGrid.top + rowHeight / 2 + 0.25f * em, pText); pGrid.setStrokeWidth(1f); - canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid); + canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, + pGrid); rGrid.offset(0, rowHeight); } @@ -292,9 +232,80 @@ public class HabitFrequencyView extends ScrollableDataView implements HabitDataV canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid); } - public void setIsBackgroundTransparent(boolean isBackgroundTransparent) + private void drawMarker(Canvas canvas, RectF rect, Integer value) { - this.isBackgroundTransparent = isBackgroundTransparent; - createColors(); + float padding = rect.height() * 0.2f; + float radius = + (rect.height() - 2 * padding) / 2.0f / 4.0f * Math.min(value, 4); + + pGraph.setColor(colors[Math.min(3, Math.max(0, value - 1))]); + canvas.drawCircle(rect.centerX(), rect.centerY(), radius, pGraph); + } + + private float getMaxMonthWidth() + { + float maxMonthWidth = 0; + GregorianCalendar day = DateUtils.getStartOfTodayCalendar(); + + for (int i = 0; i < 12; i++) + { + day.set(Calendar.MONTH, i); + float monthWidth = pText.measureText(dfMonth.format(day.getTime())); + maxMonthWidth = Math.max(maxMonthWidth, monthWidth); + } + + return maxMonthWidth; + } + + private void init() + { + initPaints(); + initColors(); + initDateFormats(); + initRects(); + } + + private void initColors() + { + textColor = InterfaceUtils.getStyledColor(getContext(), + R.attr.mediumContrastTextColor); + gridColor = InterfaceUtils.getStyledColor(getContext(), + R.attr.lowContrastTextColor); + + colors = new int[4]; + colors[0] = gridColor; + colors[3] = primaryColor; + colors[1] = ColorUtils.mixColors(colors[0], colors[3], 0.66f); + colors[2] = ColorUtils.mixColors(colors[0], colors[3], 0.33f); + } + + private void initDateFormats() + { + dfMonth = DateUtils.getDateFormat("MMM"); + dfYear = DateUtils.getDateFormat("yyyy"); + } + + private void initRects() + { + rect = new RectF(); + prevRect = new RectF(); + } + + public void populateWithRandomData() + { + GregorianCalendar date = DateUtils.getStartOfTodayCalendar(); + date.set(Calendar.DAY_OF_MONTH, 1); + Random rand = new Random(); + frequency.clear(); + + for (int i = 0; i < 40; i++) + { + Integer values[] = new Integer[7]; + for (int j = 0; j < 7; j++) + values[j] = rand.nextInt(5); + + frequency.put(date.getTimeInMillis(), values); + date.add(Calendar.MONTH, -1); + } } } diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitDataView.java b/app/src/main/java/org/isoron/uhabits/ui/common/views/HabitChart.java similarity index 90% rename from app/src/main/java/org/isoron/uhabits/views/HabitDataView.java rename to app/src/main/java/org/isoron/uhabits/ui/common/views/HabitChart.java index b1e239d5e..1b31f4c38 100644 --- a/app/src/main/java/org/isoron/uhabits/views/HabitDataView.java +++ b/app/src/main/java/org/isoron/uhabits/ui/common/views/HabitChart.java @@ -17,15 +17,13 @@ * with this program. If not, see . */ -package org.isoron.uhabits.views; +package org.isoron.uhabits.ui.common.views; import org.isoron.uhabits.models.Habit; -public interface HabitDataView +public interface HabitChart { void setHabit(Habit habit); void refreshData(); - - void postInvalidate(); } diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitHistoryView.java b/app/src/main/java/org/isoron/uhabits/ui/common/views/HistoryChart.java similarity index 58% rename from app/src/main/java/org/isoron/uhabits/views/HabitHistoryView.java rename to app/src/main/java/org/isoron/uhabits/ui/common/views/HistoryChart.java index 96899585e..cbf8bb718 100644 --- a/app/src/main/java/org/isoron/uhabits/views/HabitHistoryView.java +++ b/app/src/main/java/org/isoron/uhabits/ui/common/views/HistoryChart.java @@ -17,185 +17,161 @@ * with this program. If not, see . */ -package org.isoron.uhabits.views; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Paint.Align; -import android.graphics.RectF; -import android.util.AttributeSet; -import android.view.HapticFeedbackConstants; -import android.view.MotionEvent; - -import org.isoron.uhabits.R; -import org.isoron.uhabits.helpers.ColorHelper; -import org.isoron.uhabits.helpers.DateHelper; -import org.isoron.uhabits.helpers.UIHelper; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.tasks.BaseTask; -import org.isoron.uhabits.tasks.ToggleRepetitionTask; - -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.GregorianCalendar; -import java.util.Random; - -public class HabitHistoryView extends ScrollableDataView implements HabitDataView, - ToggleRepetitionTask.Listener +package org.isoron.uhabits.ui.common.views; + +import android.content.*; +import android.graphics.*; +import android.graphics.Paint.*; +import android.support.annotation.*; +import android.util.*; +import android.view.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.utils.*; + +import java.text.*; +import java.util.*; + +public class HistoryChart extends ScrollableChart { - private Habit habit; private int[] checkmarks; + private Paint pSquareBg, pSquareFg, pTextHeader; + private float squareSpacing; private float squareTextOffset; + private float headerTextOffset; private float columnWidth; + private float columnHeight; + private int nColumns; private SimpleDateFormat dfMonth; + private SimpleDateFormat dfYear; private Calendar baseDate; + private int nDays; - /** 0-based-position of today in the column */ + + /** + * 0-based-position of today in the column + */ private int todayPositionInColumn; + private int colors[]; + private RectF baseLocation; + private int primaryColor; private boolean isBackgroundTransparent; + private int textColor; + private int reverseTextColor; + private boolean isEditable; - public HabitHistoryView(Context context) + private String previousMonth; + + private String previousYear; + + private float headerOverflow = 0; + + @NonNull + private Controller controller; + + public HistoryChart(Context context) { super(context); init(); } - public HabitHistoryView(Context context, AttributeSet attrs) + public HistoryChart(Context context, AttributeSet attrs) { super(context, attrs); init(); } - public void setHabit(Habit habit) + @Override + public void onLongPress(MotionEvent e) { - this.habit = habit; - createColors(); + onSingleTapUp(e); } - private void init() + @Override + public boolean onSingleTapUp(MotionEvent e) { - createColors(); - createPaints(); + if (!isEditable) return false; - isEditable = false; - checkmarks = new int[0]; - primaryColor = ColorHelper.getColor(getContext(), 7); - dfMonth = DateHelper.getDateFormat("MMM"); - dfYear = DateHelper.getDateFormat("yyyy"); + performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP); - baseLocation = new RectF(); - } + int pointerId = e.getPointerId(0); + float x = e.getX(pointerId); + float y = e.getY(pointerId); - private void updateDate() - { - baseDate = DateHelper.getStartOfTodayCalendar(); - baseDate.add(Calendar.DAY_OF_YEAR, -(getDataOffset() - 1) * 7); + final Long timestamp = positionToTimestamp(x, y); + if (timestamp == null) return false; - nDays = (nColumns - 1) * 7; - int realWeekday = DateHelper.getStartOfTodayCalendar().get(Calendar.DAY_OF_WEEK); - todayPositionInColumn = (7 + realWeekday - baseDate.getFirstDayOfWeek()) % 7; + controller.onToggleCheckmark(timestamp); - baseDate.add(Calendar.DAY_OF_YEAR, -nDays); - baseDate.add(Calendar.DAY_OF_YEAR, -todayPositionInColumn); + return true; } - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) - { - int width = MeasureSpec.getSize(widthMeasureSpec); - int height = MeasureSpec.getSize(heightMeasureSpec); - setMeasuredDimension(width, height); - } - @Override - protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) + public void populateWithRandomData() { - if(height < 8) height = 200; - float baseSize = height / 8.0f; - setScrollerBucketSize((int) baseSize); - - squareSpacing = UIHelper.dpToPixels(getContext(), 1.0f); - float maxTextSize = getResources().getDimension(R.dimen.regularTextSize); - float textSize = height * 0.06f; - textSize = Math.min(textSize, maxTextSize); - - pSquareFg.setTextSize(textSize); - pTextHeader.setTextSize(textSize); - squareTextOffset = pSquareFg.getFontSpacing() * 0.4f; - headerTextOffset = pTextHeader.getFontSpacing() * 0.3f; + Random random = new Random(); + checkmarks = new int[100]; - float rightLabelWidth = getWeekdayLabelWidth() + headerTextOffset; - float horizontalPadding = getPaddingRight() + getPaddingLeft(); + for (int i = 0; i < 100; i++) + if (random.nextFloat() < 0.3) checkmarks[i] = 2; - columnWidth = baseSize; - columnHeight = 8 * baseSize; - nColumns = (int)((width - rightLabelWidth - horizontalPadding) / baseSize) + 1; + for (int i = 0; i < 100 - 7; i++) + { + int count = 0; + for (int j = 0; j < 7; j++) + if (checkmarks[i + j] != 0) count++; - updateDate(); + if (count >= 3) checkmarks[i] = Math.max(checkmarks[i], 1); + } } - private float getWeekdayLabelWidth() + public void setCheckmarks(int[] checkmarks) { - float width = 0; - - for(String w : DateHelper.getLocaleDayNames(Calendar.SHORT)) - width = Math.max(width, pSquareFg.measureText(w)); - - return width; + this.checkmarks = checkmarks; + postInvalidate(); } - private void createColors() + public void setColor(int color) { - if(habit != null) - this.primaryColor = ColorHelper.getColor(getContext(), habit.color); + this.primaryColor = color; + initColors(); + postInvalidate(); + } - if(isBackgroundTransparent) - primaryColor = ColorHelper.setMinValue(primaryColor, 0.75f); + public void setController(@NonNull Controller controller) + { + this.controller = controller; + } - int red = Color.red(primaryColor); - int green = Color.green(primaryColor); - int blue = Color.blue(primaryColor); + public void setIsBackgroundTransparent(boolean isBackgroundTransparent) + { + this.isBackgroundTransparent = isBackgroundTransparent; + initColors(); + } - if(isBackgroundTransparent) - { - colors = new int[3]; - colors[0] = Color.argb(16, 255, 255, 255); - colors[1] = Color.argb(128, red, green, blue); - colors[2] = primaryColor; - textColor = Color.WHITE; - reverseTextColor = Color.WHITE; - } - else - { - colors = new int[3]; - colors[0] = UIHelper.getStyledColor(getContext(), R.attr.lowContrastTextColor); - colors[1] = Color.argb(127, red, green, blue); - colors[2] = primaryColor; - textColor = UIHelper.getStyledColor(getContext(), R.attr.mediumContrastTextColor); - reverseTextColor = UIHelper.getStyledColor(getContext(), R.attr.highContrastReverseTextColor); - } + public void setIsEditable(boolean isEditable) + { + this.isEditable = isEditable; } - protected void createPaints() + protected void initPaints() { pTextHeader = new Paint(); pTextHeader.setTextAlign(Align.LEFT); @@ -208,48 +184,13 @@ public class HabitHistoryView extends ScrollableDataView implements HabitDataVie pSquareFg.setTextAlign(Align.CENTER); } - public void refreshData() - { - if(isInEditMode()) - generateRandomData(); - else - { - if(habit == null) return; - checkmarks = habit.checkmarks.getAllValues(); - } - - updateDate(); - postInvalidate(); - } - - private void generateRandomData() - { - Random random = new Random(); - checkmarks = new int[100]; - - for(int i = 0; i < 100; i++) - if(random.nextFloat() < 0.3) checkmarks[i] = 2; - - for(int i = 0; i < 100 - 7; i++) - { - int count = 0; - for (int j = 0; j < 7; j++) - if(checkmarks[i + j] != 0) - count++; - - if(count >= 3) checkmarks[i] = Math.max(checkmarks[i], 1); - } - } - - private String previousMonth; - private String previousYear; - @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); - baseLocation.set(0, 0, columnWidth - squareSpacing, columnWidth - squareSpacing); + baseLocation.set(0, 0, columnWidth - squareSpacing, + columnWidth - squareSpacing); baseLocation.offset(getPaddingLeft(), getPaddingTop()); headerOverflow = 0; @@ -263,108 +204,189 @@ public class HabitHistoryView extends ScrollableDataView implements HabitDataVie for (int column = 0; column < nColumns - 1; column++) { drawColumn(canvas, baseLocation, currentDate, column); - baseLocation.offset(columnWidth, - columnHeight); + baseLocation.offset(columnWidth, -columnHeight); } drawAxis(canvas, baseLocation); } - private void drawColumn(Canvas canvas, RectF location, GregorianCalendar date, int column) + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - drawColumnHeader(canvas, location, date); - location.offset(0, columnWidth); - - for (int j = 0; j < 7; j++) - { - if (!(column == nColumns - 2 && getDataOffset() == 0 && j > todayPositionInColumn)) - { - int checkmarkOffset = getDataOffset() * 7 + nDays - 7 * (column + 1) + - todayPositionInColumn - j; - drawSquare(canvas, location, date, checkmarkOffset); - } - - date.add(Calendar.DAY_OF_MONTH, 1); - location.offset(0, columnWidth); - } + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + setMeasuredDimension(width, height); } - private void drawSquare(Canvas canvas, RectF location, GregorianCalendar date, - int checkmarkOffset) + @Override + protected void onSizeChanged(int width, + int height, + int oldWidth, + int oldHeight) { - if (checkmarkOffset >= checkmarks.length) pSquareBg.setColor(colors[0]); - else pSquareBg.setColor(colors[checkmarks[checkmarkOffset]]); + if (height < 8) height = 200; + float baseSize = height / 8.0f; + setScrollerBucketSize((int) baseSize); - pSquareFg.setColor(reverseTextColor); - canvas.drawRect(location, pSquareBg); - String text = Integer.toString(date.get(Calendar.DAY_OF_MONTH)); - canvas.drawText(text, location.centerX(), location.centerY() + squareTextOffset, pSquareFg); + squareSpacing = InterfaceUtils.dpToPixels(getContext(), 1.0f); + float maxTextSize = + getResources().getDimension(R.dimen.regularTextSize); + float textSize = height * 0.06f; + textSize = Math.min(textSize, maxTextSize); + + pSquareFg.setTextSize(textSize); + pTextHeader.setTextSize(textSize); + squareTextOffset = pSquareFg.getFontSpacing() * 0.4f; + headerTextOffset = pTextHeader.getFontSpacing() * 0.3f; + + float rightLabelWidth = getWeekdayLabelWidth() + headerTextOffset; + float horizontalPadding = getPaddingRight() + getPaddingLeft(); + + columnWidth = baseSize; + columnHeight = 8 * baseSize; + nColumns = + (int) ((width - rightLabelWidth - horizontalPadding) / baseSize) + + 1; + + updateDate(); } private void drawAxis(Canvas canvas, RectF location) { float verticalOffset = pTextHeader.getFontSpacing() * 0.4f; - for (String day : DateHelper.getLocaleDayNames(Calendar.SHORT)) + for (String day : DateUtils.getLocaleDayNames(Calendar.SHORT)) { location.offset(0, columnWidth); canvas.drawText(day, location.left + headerTextOffset, - location.centerY() + verticalOffset, pTextHeader); + location.centerY() + verticalOffset, pTextHeader); } } - private float headerOverflow = 0; + private void drawColumn(Canvas canvas, + RectF location, + GregorianCalendar date, + int column) + { + drawColumnHeader(canvas, location, date); + location.offset(0, columnWidth); - private void drawColumnHeader(Canvas canvas, RectF location, GregorianCalendar date) + for (int j = 0; j < 7; j++) + { + if (!(column == nColumns - 2 && getDataOffset() == 0 && + j > todayPositionInColumn)) + { + int checkmarkOffset = + getDataOffset() * 7 + nDays - 7 * (column + 1) + + todayPositionInColumn - j; + drawSquare(canvas, location, date, checkmarkOffset); + } + + date.add(Calendar.DAY_OF_MONTH, 1); + location.offset(0, columnWidth); + } + } + + private void drawColumnHeader(Canvas canvas, + RectF location, + GregorianCalendar date) { String month = dfMonth.format(date.getTime()); String year = dfYear.format(date.getTime()); String text = null; - if (!month.equals(previousMonth)) - text = previousMonth = month; - else if(!year.equals(previousYear)) - text = previousYear = year; + if (!month.equals(previousMonth)) text = previousMonth = month; + else if (!year.equals(previousYear)) text = previousYear = year; - if(text != null) + if (text != null) { - canvas.drawText(text, location.left + headerOverflow, location.bottom - headerTextOffset, pTextHeader); - headerOverflow += pTextHeader.measureText(text) + columnWidth * 0.2f; + canvas.drawText(text, location.left + headerOverflow, + location.bottom - headerTextOffset, pTextHeader); + headerOverflow += + pTextHeader.measureText(text) + columnWidth * 0.2f; } headerOverflow = Math.max(0, headerOverflow - columnWidth); } - public void setIsBackgroundTransparent(boolean isBackgroundTransparent) + private void drawSquare(Canvas canvas, + RectF location, + GregorianCalendar date, + int checkmarkOffset) { - this.isBackgroundTransparent = isBackgroundTransparent; - createColors(); + if (checkmarkOffset >= checkmarks.length) pSquareBg.setColor(colors[0]); + else pSquareBg.setColor(colors[checkmarks[checkmarkOffset]]); + + pSquareFg.setColor(reverseTextColor); + canvas.drawRect(location, pSquareBg); + String text = Integer.toString(date.get(Calendar.DAY_OF_MONTH)); + canvas.drawText(text, location.centerX(), + location.centerY() + squareTextOffset, pSquareFg); } - @Override - public void onLongPress(MotionEvent e) + private float getWeekdayLabelWidth() { - onSingleTapUp(e); + float width = 0; + + for (String w : DateUtils.getLocaleDayNames(Calendar.SHORT)) + width = Math.max(width, pSquareFg.measureText(w)); + + return width; } - @Override - public boolean onSingleTapUp(MotionEvent e) + private void init() { - if(!isEditable) return false; + isEditable = false; + checkmarks = new int[0]; + controller = new Controller() {}; - performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP); + initColors(); + initPaints(); + initDateFormats(); + initRects(); + } - int pointerId = e.getPointerId(0); - float x = e.getX(pointerId); - float y = e.getY(pointerId); + private void initColors() + { + if (isBackgroundTransparent) + primaryColor = ColorUtils.setMinValue(primaryColor, 0.75f); - final Long timestamp = positionToTimestamp(x, y); - if(timestamp == null) return false; + int red = Color.red(primaryColor); + int green = Color.green(primaryColor); + int blue = Color.blue(primaryColor); - ToggleRepetitionTask task = new ToggleRepetitionTask(habit, timestamp); - task.setListener(this); - task.execute(); + if (isBackgroundTransparent) + { + colors = new int[3]; + colors[0] = Color.argb(16, 255, 255, 255); + colors[1] = Color.argb(128, red, green, blue); + colors[2] = primaryColor; + textColor = Color.WHITE; + reverseTextColor = Color.WHITE; + } + else + { + colors = new int[3]; + colors[0] = InterfaceUtils.getStyledColor(getContext(), + R.attr.lowContrastTextColor); + colors[1] = Color.argb(127, red, green, blue); + colors[2] = primaryColor; + textColor = InterfaceUtils.getStyledColor(getContext(), + R.attr.mediumContrastTextColor); + reverseTextColor = InterfaceUtils.getStyledColor(getContext(), + R.attr.highContrastReverseTextColor); + } + } - return true; + private void initDateFormats() + { + dfMonth = DateUtils.getDateFormat("MMM"); + dfYear = DateUtils.getDateFormat("yyyy"); + } + + private void initRects() + { + baseLocation = new RectF(); } private Long positionToTimestamp(float x, float y) @@ -372,41 +394,36 @@ public class HabitHistoryView extends ScrollableDataView implements HabitDataVie int col = (int) (x / columnWidth); int row = (int) (y / columnWidth); - if(row == 0) return null; - if(col == nColumns - 1) return null; + if (row == 0) return null; + if (col == nColumns - 1) return null; int offset = col * 7 + (row - 1); Calendar date = (Calendar) baseDate.clone(); date.add(Calendar.DAY_OF_YEAR, offset); - if(DateHelper.getStartOfDay(date.getTimeInMillis()) > DateHelper.getStartOfToday()) - return null; + if (DateUtils.getStartOfDay(date.getTimeInMillis()) > + DateUtils.getStartOfToday()) return null; return date.getTimeInMillis(); } - public void setIsEditable(boolean isEditable) + private void updateDate() { - this.isEditable = isEditable; + baseDate = DateUtils.getStartOfTodayCalendar(); + baseDate.add(Calendar.DAY_OF_YEAR, -(getDataOffset() - 1) * 7); + + nDays = (nColumns - 1) * 7; + int realWeekday = + DateUtils.getStartOfTodayCalendar().get(Calendar.DAY_OF_WEEK); + todayPositionInColumn = + (7 + realWeekday - baseDate.getFirstDayOfWeek()) % 7; + + baseDate.add(Calendar.DAY_OF_YEAR, -nDays); + baseDate.add(Calendar.DAY_OF_YEAR, -todayPositionInColumn); } - @Override - public void onToggleRepetitionFinished() + public interface Controller { - new BaseTask() - { - @Override - protected void doInBackground() - { - refreshData(); - } - - @Override - protected void onPostExecute(Void aVoid) - { - invalidate(); - super.onPostExecute(null); - } - }.execute(); + default void onToggleCheckmark(long timestamp) {} } } diff --git a/app/src/main/java/org/isoron/uhabits/views/RingView.java b/app/src/main/java/org/isoron/uhabits/ui/common/views/RingView.java similarity index 61% rename from app/src/main/java/org/isoron/uhabits/views/RingView.java rename to app/src/main/java/org/isoron/uhabits/ui/common/views/RingView.java index cac641a22..4afde5b34 100644 --- a/app/src/main/java/org/isoron/uhabits/views/RingView.java +++ b/app/src/main/java/org/isoron/uhabits/ui/common/views/RingView.java @@ -17,49 +17,54 @@ * with this program. If not, see . */ -package org.isoron.uhabits.views; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffXfermode; -import android.graphics.RectF; -import android.support.annotation.Nullable; -import android.text.TextPaint; -import android.util.AttributeSet; -import android.view.View; - -import org.isoron.uhabits.R; -import org.isoron.uhabits.helpers.ColorHelper; -import org.isoron.uhabits.helpers.UIHelper; +package org.isoron.uhabits.ui.common.views; + +import android.content.*; +import android.graphics.*; +import android.support.annotation.*; +import android.text.*; +import android.util.*; +import android.view.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.utils.*; + +import static org.isoron.uhabits.utils.InterfaceUtils.*; public class RingView extends View { public static final PorterDuffXfermode XFERMODE_CLEAR = - new PorterDuffXfermode(PorterDuff.Mode.CLEAR); + new PorterDuffXfermode(PorterDuff.Mode.CLEAR); private int color; + private float precision; + private float percentage; + private int diameter; + private float thickness; private RectF rect; + private TextPaint pRing; private Integer backgroundColor; + private Integer inactiveColor; private float em; + private String text; + private float textSize; + private boolean enableFontAwesome; @Nullable private Bitmap drawingCache; + private Canvas cacheCanvas; private boolean isTransparencyEnabled; @@ -70,55 +75,57 @@ public class RingView extends View percentage = 0.0f; precision = 0.01f; - color = ColorHelper.CSV_PALETTE[0]; - thickness = UIHelper.dpToPixels(getContext(), 2); + color = ColorUtils.getAndroidTestColor(0); + thickness = dpToPixels(getContext(), 2); text = ""; textSize = context.getResources().getDimension(R.dimen.smallTextSize); init(); } - public RingView(Context context, AttributeSet attrs) + public RingView(Context ctx, AttributeSet attrs) { - super(context, attrs); - - percentage = UIHelper.getFloatAttribute(context, attrs, "percentage", 0); - precision = UIHelper.getFloatAttribute(context, attrs, "precision", 0.01f); + super(ctx, attrs); - color = UIHelper.getColorAttribute(context, attrs, "color", 0); - backgroundColor = UIHelper.getColorAttribute(context, attrs, "backgroundColor", null); - inactiveColor = UIHelper.getColorAttribute(context, attrs, "inactiveColor", null); + percentage = getFloatAttribute(ctx, attrs, "percentage", 0); + precision = getFloatAttribute(ctx, attrs, "precision", 0.01f); - thickness = UIHelper.getFloatAttribute(context, attrs, "thickness", 0); - thickness = UIHelper.dpToPixels(context, thickness); + color = getColorAttribute(ctx, attrs, "color", 0); + backgroundColor = + getColorAttribute(ctx, attrs, "backgroundColor", null); + inactiveColor = getColorAttribute(ctx, attrs, "inactiveColor", null); - float defaultTextSize = context.getResources().getDimension(R.dimen.smallTextSize); - textSize = UIHelper.getFloatAttribute(context, attrs, "textSize", defaultTextSize); - textSize = UIHelper.spToPixels(context, textSize); + thickness = getFloatAttribute(ctx, attrs, "thickness", 0); + thickness = dpToPixels(ctx, thickness); - text = UIHelper.getAttribute(context, attrs, "text", ""); + float defaultTextSize = + ctx.getResources().getDimension(R.dimen.smallTextSize); + textSize = getFloatAttribute(ctx, attrs, "textSize", defaultTextSize); + textSize = spToPixels(ctx, textSize); + text = getAttribute(ctx, attrs, "text", ""); - enableFontAwesome = UIHelper.getBooleanAttribute(context, attrs, "enableFontAwesome", false); + enableFontAwesome = + getBooleanAttribute(ctx, attrs, "enableFontAwesome", false); init(); } - public void setColor(int color) + @Override + public void setBackgroundColor(int backgroundColor) { - this.color = color; + this.backgroundColor = backgroundColor; postInvalidate(); } - public void setTextSize(float textSize) + public void setColor(int color) { - this.textSize = textSize; + this.color = color; + postInvalidate(); } - @Override - public void setBackgroundColor(int backgroundColor) + public void setIsTransparencyEnabled(boolean isTransparencyEnabled) { - this.backgroundColor = backgroundColor; - postInvalidate(); + this.isTransparencyEnabled = isTransparencyEnabled; } public void setPercentage(float percentage) @@ -133,64 +140,21 @@ public class RingView extends View postInvalidate(); } - public void setThickness(float thickness) - { - this.thickness = thickness; - postInvalidate(); - } - public void setText(String text) { this.text = text; postInvalidate(); } - private void init() - { - pRing = new TextPaint(); - pRing.setAntiAlias(true); - pRing.setColor(color); - pRing.setTextAlign(Paint.Align.CENTER); - - if(backgroundColor == null) - backgroundColor = UIHelper.getStyledColor(getContext(), R.attr.cardBackgroundColor); - - if(inactiveColor == null) - inactiveColor = UIHelper.getStyledColor(getContext(), R.attr.highContrastTextColor); - - inactiveColor = ColorHelper.setAlpha(inactiveColor, 0.1f); - - rect = new RectF(); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) - { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - - int width = MeasureSpec.getSize(widthMeasureSpec); - int height = MeasureSpec.getSize(heightMeasureSpec); - diameter = Math.min(height, width); - - pRing.setTextSize(textSize); - em = pRing.measureText("M"); - - setMeasuredDimension(diameter, diameter); - } - - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) + public void setTextSize(float textSize) { - super.onSizeChanged(w, h, oldw, oldh); - - if(isTransparencyEnabled) reallocateCache(); + this.textSize = textSize; } - private void reallocateCache() + public void setThickness(float thickness) { - if (drawingCache != null) drawingCache.recycle(); - drawingCache = Bitmap.createBitmap(diameter, diameter, Bitmap.Config.ARGB_8888); - cacheCanvas = new Canvas(drawingCache); + this.thickness = thickness; + postInvalidate(); } @Override @@ -199,9 +163,9 @@ public class RingView extends View super.onDraw(canvas); Canvas activeCanvas; - if(isTransparencyEnabled) + if (isTransparencyEnabled) { - if(drawingCache == null) reallocateCache(); + if (drawingCache == null) reallocateCache(); activeCanvas = cacheCanvas; drawingCache.eraseColor(Color.TRANSPARENT); } @@ -220,12 +184,10 @@ public class RingView extends View pRing.setColor(inactiveColor); activeCanvas.drawArc(rect, angle - 90, 360 - angle, true, pRing); - if(thickness > 0) + if (thickness > 0) { - if(isTransparencyEnabled) - pRing.setXfermode(XFERMODE_CLEAR); - else - pRing.setColor(backgroundColor); + if (isTransparencyEnabled) pRing.setXfermode(XFERMODE_CLEAR); + else pRing.setColor(backgroundColor); rect.inset(thickness, thickness); activeCanvas.drawArc(rect, 0, 360, true, pRing); @@ -233,16 +195,63 @@ public class RingView extends View pRing.setColor(color); pRing.setTextSize(textSize); - if(enableFontAwesome) pRing.setTypeface(UIHelper.getFontAwesome(getContext())); - activeCanvas.drawText(text, rect.centerX(), rect.centerY() + 0.4f * em, pRing); + if (enableFontAwesome) + pRing.setTypeface(getFontAwesome(getContext())); + activeCanvas.drawText(text, rect.centerX(), + rect.centerY() + 0.4f * em, pRing); } - if(activeCanvas != canvas) - canvas.drawBitmap(drawingCache, 0, 0, null); + if (activeCanvas != canvas) canvas.drawBitmap(drawingCache, 0, 0, null); } - public void setIsTransparencyEnabled(boolean isTransparencyEnabled) + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - this.isTransparencyEnabled = isTransparencyEnabled; + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + diameter = Math.min(height, width); + + pRing.setTextSize(textSize); + em = pRing.measureText("M"); + + setMeasuredDimension(diameter, diameter); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) + { + super.onSizeChanged(w, h, oldw, oldh); + + if (isTransparencyEnabled) reallocateCache(); + } + + private void init() + { + pRing = new TextPaint(); + pRing.setAntiAlias(true); + pRing.setColor(color); + pRing.setTextAlign(Paint.Align.CENTER); + + if (backgroundColor == null) backgroundColor = + InterfaceUtils.getStyledColor(getContext(), + R.attr.cardBackgroundColor); + + if (inactiveColor == null) inactiveColor = + InterfaceUtils.getStyledColor(getContext(), + R.attr.highContrastTextColor); + + inactiveColor = ColorUtils.setAlpha(inactiveColor, 0.1f); + + rect = new RectF(); + } + + private void reallocateCache() + { + if (drawingCache != null) drawingCache.recycle(); + drawingCache = + Bitmap.createBitmap(diameter, diameter, Bitmap.Config.ARGB_8888); + cacheCanvas = new Canvas(drawingCache); } } diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitScoreView.java b/app/src/main/java/org/isoron/uhabits/ui/common/views/ScoreChart.java similarity index 60% rename from app/src/main/java/org/isoron/uhabits/views/HabitScoreView.java rename to app/src/main/java/org/isoron/uhabits/ui/common/views/ScoreChart.java index 6258e7b13..969be04de 100644 --- a/app/src/main/java/org/isoron/uhabits/views/HabitScoreView.java +++ b/app/src/main/java/org/isoron/uhabits/ui/common/views/ScoreChart.java @@ -17,206 +17,136 @@ * with this program. If not, see . */ -package org.isoron.uhabits.views; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffXfermode; -import android.graphics.RectF; -import android.support.annotation.Nullable; -import android.util.AttributeSet; - -import org.isoron.uhabits.R; -import org.isoron.uhabits.helpers.ColorHelper; -import org.isoron.uhabits.helpers.DateHelper; -import org.isoron.uhabits.helpers.UIHelper; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.models.Score; - -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.GregorianCalendar; -import java.util.Random; - -public class HabitScoreView extends ScrollableDataView implements HabitDataView +package org.isoron.uhabits.ui.common.views; + +import android.content.*; +import android.graphics.*; +import android.support.annotation.*; +import android.util.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; + +import java.text.*; +import java.util.*; + +import static org.isoron.uhabits.utils.InterfaceUtils.*; + +public class ScoreChart extends ScrollableChart { - public static final PorterDuffXfermode XFERMODE_CLEAR = - new PorterDuffXfermode(PorterDuff.Mode.CLEAR); - public static final PorterDuffXfermode XFERMODE_SRC = - new PorterDuffXfermode(PorterDuff.Mode.SRC); + private static final PorterDuffXfermode XFERMODE_CLEAR = + new PorterDuffXfermode(PorterDuff.Mode.CLEAR); - public static int DEFAULT_BUCKET_SIZES[] = { 1, 7, 31, 92, 365 }; + private static final PorterDuffXfermode XFERMODE_SRC = + new PorterDuffXfermode(PorterDuff.Mode.SRC); private Paint pGrid; + private float em; - private Habit habit; private SimpleDateFormat dfMonth; + private SimpleDateFormat dfDay; + private SimpleDateFormat dfYear; private Paint pText, pGraph; + private RectF rect, prevRect; + private int baseSize; + private int paddingTop; private float columnWidth; + private int columnHeight; + private int nColumns; private int textColor; + private int gridColor; @Nullable - private int[] scores; + private List scores; private int primaryColor; + + @Deprecated private int bucketSize = 7; - private int footerHeight; + private int backgroundColor; private Bitmap drawingCache; + private Canvas cacheCanvas; + private boolean isTransparencyEnabled; - public HabitScoreView(Context context) + private int skipYear = 0; + + private String previousYearText; + + private String previousMonthText; + + public ScoreChart(Context context) { super(context); init(); } - public HabitScoreView(Context context, AttributeSet attrs) + public ScoreChart(Context context, AttributeSet attrs) { super(context, attrs); - this.primaryColor = ColorHelper.getColor(getContext(), 7); init(); } - public void setHabit(Habit habit) - { - this.habit = habit; - createColors(); - } - - private void init() - { - createPaints(); - createColors(); - - dfYear = DateHelper.getDateFormat("yyyy"); - dfMonth = DateHelper.getDateFormat("MMM"); - dfDay = DateHelper.getDateFormat("d"); - - rect = new RectF(); - prevRect = new RectF(); - } - - private void createColors() - { - if(habit != null) - this.primaryColor = ColorHelper.getColor(getContext(), habit.color); - - textColor = UIHelper.getStyledColor(getContext(), R.attr.mediumContrastTextColor); - gridColor = UIHelper.getStyledColor(getContext(), R.attr.lowContrastTextColor); - backgroundColor = UIHelper.getStyledColor(getContext(), R.attr.cardBackgroundColor); - } - - protected void createPaints() + public void populateWithRandomData() { - pText = new Paint(); - pText.setAntiAlias(true); + Random random = new Random(); + scores = new LinkedList<>(); - pGraph = new Paint(); - pGraph.setTextAlign(Paint.Align.CENTER); - pGraph.setAntiAlias(true); + int previous = Score.MAX_VALUE / 2; + long timestamp = DateUtils.getStartOfToday(); + long day = DateUtils.millisecondsInOneDay; - pGrid = new Paint(); - pGrid.setAntiAlias(true); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) - { - int width = MeasureSpec.getSize(widthMeasureSpec); - int height = MeasureSpec.getSize(heightMeasureSpec); - setMeasuredDimension(width, height); + for (int i = 1; i < 100; i++) + { + int step = Score.MAX_VALUE / 10; + int current = previous + random.nextInt(step * 2) - step; + current = Math.max(0, Math.min(Score.MAX_VALUE, current)); + scores.add(new Score(timestamp, current)); + previous = current; + timestamp -= day; + } } - @Override - protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) + @Deprecated + public void setBucketSize(int bucketSize) { - if(height < 9) height = 200; - - float maxTextSize = getResources().getDimension(R.dimen.tinyTextSize); - float textSize = height * 0.06f; - pText.setTextSize(Math.min(textSize, maxTextSize)); - em = pText.getFontSpacing(); - - footerHeight = (int)(3 * em); - paddingTop = (int) (em); - - baseSize = (height - footerHeight - paddingTop) / 8; - setScrollerBucketSize(baseSize); - - columnWidth = baseSize; - columnWidth = Math.max(columnWidth, getMaxDayWidth() * 1.5f); - columnWidth = Math.max(columnWidth, getMaxMonthWidth() * 1.2f); - - nColumns = (int) (width / columnWidth); - columnWidth = (float) width / nColumns; - - columnHeight = 8 * baseSize; - - float minStrokeWidth = UIHelper.dpToPixels(getContext(), 1); - pGraph.setTextSize(baseSize * 0.5f); - pGraph.setStrokeWidth(baseSize * 0.1f); - pGrid.setStrokeWidth(Math.min(minStrokeWidth, baseSize * 0.05f)); - - if(isTransparencyEnabled) - initCache(width, height); + this.bucketSize = bucketSize; + postInvalidate(); } - private void initCache(int width, int height) + public void setIsTransparencyEnabled(boolean enabled) { - if (drawingCache != null) drawingCache.recycle(); - drawingCache = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - cacheCanvas = new Canvas(drawingCache); + this.isTransparencyEnabled = enabled; + initColors(); + requestLayout(); } - public void refreshData() + public void setColor(int primaryColor) { - if(isInEditMode()) - generateRandomData(); - else - { - if (habit == null) return; - scores = habit.scores.getAllValues(bucketSize); - } - + this.primaryColor = primaryColor; postInvalidate(); } - public void setBucketSize(int bucketSize) - { - this.bucketSize = bucketSize; - } - - private void generateRandomData() + public void setScores(@NonNull List scores) { - Random random = new Random(); - scores = new int[100]; - scores[0] = Score.MAX_VALUE / 2; - - for(int i = 1; i < 100; i++) - { - int step = Score.MAX_VALUE / 10; - scores[i] = scores[i - 1] + random.nextInt(step * 2) - step; - scores[i] = Math.max(0, Math.min(Score.MAX_VALUE, scores[i])); - } + this.scores = scores; + postInvalidate(); } @Override @@ -225,9 +155,9 @@ public class HabitScoreView extends ScrollableDataView implements HabitDataView super.onDraw(canvas); Canvas activeCanvas; - if(isTransparencyEnabled) + if (isTransparencyEnabled) { - if(drawingCache == null) initCache(getWidth(), getHeight()); + if (drawingCache == null) initCache(getWidth(), getHeight()); activeCanvas = cacheCanvas; drawingCache.eraseColor(Color.TRANSPARENT); @@ -237,7 +167,7 @@ public class HabitScoreView extends ScrollableDataView implements HabitDataView activeCanvas = canvas; } - if (habit == null || scores == null) return; + if (scores == null) return; rect.set(0, 0, nColumns * columnWidth, columnHeight); rect.offset(0, paddingTop); @@ -252,23 +182,20 @@ public class HabitScoreView extends ScrollableDataView implements HabitDataView previousYearText = ""; skipYear = 0; - long currentDate = DateHelper.getStartOfToday(); - - for(int k = 0; k < nColumns + getDataOffset() - 1; k++) - currentDate -= bucketSize * DateHelper.millisecondsInOneDay; - for (int k = 0; k < nColumns; k++) { - int score = 0; int offset = nColumns - k - 1 + getDataOffset(); - if(offset < scores.length) score = scores[offset]; + if (offset >= scores.size()) continue; + + int score = scores.get(offset).getValue(); + long timestamp = scores.get(offset).getTimestamp(); double relativeScore = ((double) score) / Score.MAX_VALUE; int height = (int) (columnHeight * relativeScore); rect.set(0, 0, baseSize, baseSize); rect.offset(k * columnWidth + (columnWidth - baseSize) / 2, - paddingTop + columnHeight - height - baseSize / 2); + paddingTop + columnHeight - height - baseSize / 2); if (!prevRect.isEmpty()) { @@ -282,18 +209,54 @@ public class HabitScoreView extends ScrollableDataView implements HabitDataView rect.set(0, 0, columnWidth, columnHeight); rect.offset(k * columnWidth, paddingTop); - drawFooter(activeCanvas, rect, currentDate); - - currentDate += bucketSize * DateHelper.millisecondsInOneDay; + drawFooter(activeCanvas, rect, timestamp); } - if(activeCanvas != canvas) - canvas.drawBitmap(drawingCache, 0, 0, null); + if (activeCanvas != canvas) canvas.drawBitmap(drawingCache, 0, 0, null); } - private int skipYear = 0; - private String previousYearText; - private String previousMonthText; + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) + { + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + setMeasuredDimension(width, height); + } + + @Override + protected void onSizeChanged(int width, + int height, + int oldWidth, + int oldHeight) + { + if (height < 9) height = 200; + + float maxTextSize = getResources().getDimension(R.dimen.tinyTextSize); + float textSize = height * 0.06f; + pText.setTextSize(Math.min(textSize, maxTextSize)); + em = pText.getFontSpacing(); + + int footerHeight = (int) (3 * em); + paddingTop = (int) (em); + + baseSize = (height - footerHeight - paddingTop) / 8; + columnWidth = baseSize; + columnWidth = Math.max(columnWidth, getMaxDayWidth() * 1.5f); + columnWidth = Math.max(columnWidth, getMaxMonthWidth() * 1.2f); + + nColumns = (int) (width / columnWidth); + columnWidth = (float) width / nColumns; + setScrollerBucketSize((int) columnWidth); + + columnHeight = 8 * baseSize; + + float minStrokeWidth = dpToPixels(getContext(), 1); + pGraph.setTextSize(baseSize * 0.5f); + pGraph.setStrokeWidth(baseSize * 0.1f); + pGrid.setStrokeWidth(Math.min(minStrokeWidth, baseSize * 0.05f)); + + if (isTransparencyEnabled) initCache(width, height); + } private void drawFooter(Canvas canvas, RectF rect, long currentDate) { @@ -301,35 +264,36 @@ public class HabitScoreView extends ScrollableDataView implements HabitDataView String monthText = dfMonth.format(currentDate); String dayText = dfDay.format(currentDate); - GregorianCalendar calendar = DateHelper.getCalendar(currentDate); + GregorianCalendar calendar = DateUtils.getCalendar(currentDate); String text; int year = calendar.get(Calendar.YEAR); boolean shouldPrintYear = true; - if(yearText.equals(previousYearText)) shouldPrintYear = false; - if(bucketSize >= 365 && (year % 2) != 0) shouldPrintYear = false; + if (yearText.equals(previousYearText)) shouldPrintYear = false; + if (bucketSize >= 365 && (year % 2) != 0) shouldPrintYear = false; - if(skipYear > 0) + if (skipYear > 0) { skipYear--; shouldPrintYear = false; } - if(shouldPrintYear) + if (shouldPrintYear) { previousYearText = yearText; previousMonthText = ""; pText.setTextAlign(Paint.Align.CENTER); - canvas.drawText(yearText, rect.centerX(), rect.bottom + em * 2.2f, pText); + canvas.drawText(yearText, rect.centerX(), rect.bottom + em * 2.2f, + pText); skipYear = 1; } - if(bucketSize < 365) + if (bucketSize < 365) { - if(!monthText.equals(previousMonthText)) + if (!monthText.equals(previousMonthText)) { previousMonthText = monthText; text = monthText; @@ -340,11 +304,11 @@ public class HabitScoreView extends ScrollableDataView implements HabitDataView } pText.setTextAlign(Paint.Align.CENTER); - canvas.drawText(text, rect.centerX(), rect.bottom + em * 1.2f, pText); + canvas.drawText(text, rect.centerX(), rect.bottom + em * 1.2f, + pText); } } - private void drawGrid(Canvas canvas, RectF rGrid) { int nRows = 5; @@ -356,9 +320,10 @@ public class HabitScoreView extends ScrollableDataView implements HabitDataView for (int i = 0; i < nRows; i++) { - canvas.drawText(String.format("%d%%", (100 - i * 100 / nRows)), rGrid.left + 0.5f * em, - rGrid.top + 1f * em, pText); - canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid); + canvas.drawText(String.format("%d%%", (100 - i * 100 / nRows)), + rGrid.left + 0.5f * em, rGrid.top + 1f * em, pText); + canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, + pGrid); rGrid.offset(0, rowHeight); } @@ -368,13 +333,13 @@ public class HabitScoreView extends ScrollableDataView implements HabitDataView private void drawLine(Canvas canvas, RectF rectFrom, RectF rectTo) { pGraph.setColor(primaryColor); - canvas.drawLine(rectFrom.centerX(), rectFrom.centerY(), rectTo.centerX(), rectTo.centerY(), - pGraph); + canvas.drawLine(rectFrom.centerX(), rectFrom.centerY(), + rectTo.centerX(), rectTo.centerY(), pGraph); } private void drawMarker(Canvas canvas, RectF rect) { - rect.inset(baseSize * 0.15f, baseSize * 0.15f); + rect.inset(baseSize * 0.225f, baseSize * 0.225f); setModeOrColor(pGraph, XFERMODE_CLEAR, backgroundColor); canvas.drawOval(rect, pGraph); @@ -382,35 +347,34 @@ public class HabitScoreView extends ScrollableDataView implements HabitDataView setModeOrColor(pGraph, XFERMODE_SRC, primaryColor); canvas.drawOval(rect, pGraph); - rect.inset(baseSize * 0.1f, baseSize * 0.1f); - setModeOrColor(pGraph, XFERMODE_CLEAR, backgroundColor); - canvas.drawOval(rect, pGraph); +// rect.inset(baseSize * 0.1f, baseSize * 0.1f); +// setModeOrColor(pGraph, XFERMODE_CLEAR, backgroundColor); +// canvas.drawOval(rect, pGraph); - if(isTransparencyEnabled) - pGraph.setXfermode(XFERMODE_SRC); + if (isTransparencyEnabled) pGraph.setXfermode(XFERMODE_SRC); } - public void setIsTransparencyEnabled(boolean enabled) + private float getMaxDayWidth() { - this.isTransparencyEnabled = enabled; - createColors(); - requestLayout(); - } + float maxDayWidth = 0; + GregorianCalendar day = DateUtils.getStartOfTodayCalendar(); - private void setModeOrColor(Paint p, PorterDuffXfermode mode, int color) - { - if(isTransparencyEnabled) - p.setXfermode(mode); - else - p.setColor(color); + for (int i = 0; i < 28; i++) + { + day.set(Calendar.DAY_OF_MONTH, i); + float monthWidth = pText.measureText(dfMonth.format(day.getTime())); + maxDayWidth = Math.max(maxDayWidth, monthWidth); + } + + return maxDayWidth; } private float getMaxMonthWidth() { float maxMonthWidth = 0; - GregorianCalendar day = DateHelper.getStartOfTodayCalendar(); + GregorianCalendar day = DateUtils.getStartOfTodayCalendar(); - for(int i = 0; i < 12; i++) + for (int i = 0; i < 12; i++) { day.set(Calendar.MONTH, i); float monthWidth = pText.measureText(dfMonth.format(day.getTime())); @@ -420,18 +384,61 @@ public class HabitScoreView extends ScrollableDataView implements HabitDataView return maxMonthWidth; } - private float getMaxDayWidth() + private void init() { - float maxDayWidth = 0; - GregorianCalendar day = DateHelper.getStartOfTodayCalendar(); + initPaints(); + initColors(); + initDateFormats(); + initRects(); + } - for(int i = 0; i < 28; i++) - { - day.set(Calendar.DAY_OF_MONTH, i); - float monthWidth = pText.measureText(dfMonth.format(day.getTime())); - maxDayWidth = Math.max(maxDayWidth, monthWidth); - } + private void initCache(int width, int height) + { + if (drawingCache != null) drawingCache.recycle(); + drawingCache = + Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + cacheCanvas = new Canvas(drawingCache); + } - return maxDayWidth; + private void initColors() + { + Context context = getContext(); + + primaryColor = Color.BLACK; + textColor = getStyledColor(context, R.attr.mediumContrastTextColor); + gridColor = getStyledColor(context, R.attr.lowContrastTextColor); + backgroundColor = getStyledColor(context, R.attr.cardBackgroundColor); + } + + private void initDateFormats() + { + dfYear = DateUtils.getDateFormat("yyyy"); + dfMonth = DateUtils.getDateFormat("MMM"); + dfDay = DateUtils.getDateFormat("d"); + } + + private void initPaints() + { + pText = new Paint(); + pText.setAntiAlias(true); + + pGraph = new Paint(); + pGraph.setTextAlign(Paint.Align.CENTER); + pGraph.setAntiAlias(true); + + pGrid = new Paint(); + pGrid.setAntiAlias(true); + } + + private void initRects() + { + rect = new RectF(); + prevRect = new RectF(); + } + + private void setModeOrColor(Paint p, PorterDuffXfermode mode, int color) + { + if (isTransparencyEnabled) p.setXfermode(mode); + else p.setColor(color); } } diff --git a/app/src/main/java/org/isoron/uhabits/views/ScrollableDataView.java b/app/src/main/java/org/isoron/uhabits/ui/common/views/ScrollableChart.java similarity index 76% rename from app/src/main/java/org/isoron/uhabits/views/ScrollableDataView.java rename to app/src/main/java/org/isoron/uhabits/ui/common/views/ScrollableChart.java index fbae274b7..9678630e9 100644 --- a/app/src/main/java/org/isoron/uhabits/views/ScrollableDataView.java +++ b/app/src/main/java/org/isoron/uhabits/ui/common/views/ScrollableChart.java @@ -17,52 +17,59 @@ * with this program. If not, see . */ -package org.isoron.uhabits.views; - -import android.animation.ValueAnimator; -import android.content.Context; -import android.util.AttributeSet; -import android.view.GestureDetector; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewParent; -import android.widget.Scroller; - -public abstract class ScrollableDataView extends View implements GestureDetector.OnGestureListener, - ValueAnimator.AnimatorUpdateListener +package org.isoron.uhabits.ui.common.views; + +import android.animation.*; +import android.content.*; +import android.util.*; +import android.view.*; +import android.widget.*; + +public abstract class ScrollableChart extends View + implements GestureDetector.OnGestureListener, + ValueAnimator.AnimatorUpdateListener { private int dataOffset; + private int scrollerBucketSize; private GestureDetector detector; + private Scroller scroller; + private ValueAnimator scrollAnimator; - public ScrollableDataView(Context context) + public ScrollableChart(Context context) { super(context); init(context); } - public ScrollableDataView(Context context, AttributeSet attrs) + public ScrollableChart(Context context, AttributeSet attrs) { super(context, attrs); init(context); } - private void init(Context context) + public int getDataOffset() { - detector = new GestureDetector(context, this); - scroller = new Scroller(context, null, true); - scrollAnimator = ValueAnimator.ofFloat(0, 1); - scrollAnimator.addUpdateListener(this); + return dataOffset; } @Override - public boolean onTouchEvent(MotionEvent event) + public void onAnimationUpdate(ValueAnimator animation) { - return detector.onTouchEvent(event); + if (!scroller.isFinished()) + { + scroller.computeScrollOffset(); + dataOffset = Math.max(0, scroller.getCurrX() / scrollerBucketSize); + postInvalidate(); + } + else + { + scrollAnimator.cancel(); + } } @Override @@ -72,30 +79,40 @@ public abstract class ScrollableDataView extends View implements GestureDetector } @Override - public void onShowPress(MotionEvent e) + public boolean onFling(MotionEvent e1, + MotionEvent e2, + float velocityX, + float velocityY) { + scroller.fling(scroller.getCurrX(), scroller.getCurrY(), + (int) velocityX / 2, 0, 0, 100000, 0, 0); + invalidate(); + + scrollAnimator.setDuration(scroller.getDuration()); + scrollAnimator.start(); + return false; } @Override - public boolean onSingleTapUp(MotionEvent e) + public void onLongPress(MotionEvent e) { - return false; + } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float dx, float dy) { - if(scrollerBucketSize == 0) - return false; + if (scrollerBucketSize == 0) return false; - if(Math.abs(dx) > Math.abs(dy)) + if (Math.abs(dx) > Math.abs(dy)) { ViewParent parent = getParent(); - if(parent != null) parent.requestDisallowInterceptTouchEvent(true); + if (parent != null) parent.requestDisallowInterceptTouchEvent(true); } - scroller.startScroll(scroller.getCurrX(), scroller.getCurrY(), (int) -dx, (int) dy, 0); + scroller.startScroll(scroller.getCurrX(), scroller.getCurrY(), + (int) -dx, (int) dy, 0); scroller.computeScrollOffset(); dataOffset = Math.max(0, scroller.getCurrX() / scrollerBucketSize); postInvalidate(); @@ -104,46 +121,33 @@ public abstract class ScrollableDataView extends View implements GestureDetector } @Override - public void onLongPress(MotionEvent e) + public void onShowPress(MotionEvent e) { } @Override - public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) + public boolean onSingleTapUp(MotionEvent e) { - scroller.fling(scroller.getCurrX(), scroller.getCurrY(), (int) velocityX / 2, 0, 0, 100000, - 0, 0); - invalidate(); - - scrollAnimator.setDuration(scroller.getDuration()); - scrollAnimator.start(); - return false; } @Override - public void onAnimationUpdate(ValueAnimator animation) + public boolean onTouchEvent(MotionEvent event) { - if (!scroller.isFinished()) - { - scroller.computeScrollOffset(); - dataOffset = Math.max(0, scroller.getCurrX() / scrollerBucketSize); - postInvalidate(); - } - else - { - scrollAnimator.cancel(); - } + return detector.onTouchEvent(event); } - public int getDataOffset() + public void setScrollerBucketSize(int scrollerBucketSize) { - return dataOffset; + this.scrollerBucketSize = scrollerBucketSize; } - public void setScrollerBucketSize(int scrollerBucketSize) + private void init(Context context) { - this.scrollerBucketSize = scrollerBucketSize; + detector = new GestureDetector(context, this); + scroller = new Scroller(context, null, true); + scrollAnimator = ValueAnimator.ofFloat(0, 1); + scrollAnimator.addUpdateListener(this); } } diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitStreakView.java b/app/src/main/java/org/isoron/uhabits/ui/common/views/StreakChart.java similarity index 55% rename from app/src/main/java/org/isoron/uhabits/views/HabitStreakView.java rename to app/src/main/java/org/isoron/uhabits/ui/common/views/StreakChart.java index 74fc6c518..81e69b661 100644 --- a/app/src/main/java/org/isoron/uhabits/views/HabitStreakView.java +++ b/app/src/main/java/org/isoron/uhabits/ui/common/views/StreakChart.java @@ -17,141 +17,102 @@ * with this program. If not, see . */ -package org.isoron.uhabits.views; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.RectF; -import android.util.AttributeSet; -import android.view.View; - -import org.isoron.uhabits.R; -import org.isoron.uhabits.helpers.ColorHelper; -import org.isoron.uhabits.helpers.UIHelper; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.models.Streak; - -import java.text.DateFormat; -import java.util.Collections; -import java.util.Date; -import java.util.List; -import java.util.TimeZone; - -public class HabitStreakView extends View implements HabitDataView +package org.isoron.uhabits.ui.common.views; + +import android.content.*; +import android.graphics.*; +import android.util.*; +import android.view.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; + +import java.text.*; +import java.util.*; + +public class StreakChart extends View { - private Habit habit; private Paint paint; private long minLength; + private long maxLength; private int[] colors; + private RectF rect; + private int baseSize; + private int primaryColor; + private List streaks; private boolean isBackgroundTransparent; + private DateFormat dateFormat; + private int width; + private float em; + private float maxLabelWidth; + private float textMargin; + private boolean shouldShowLabels; - private int maxStreakCount; + private int textColor; + private int reverseTextColor; - public HabitStreakView(Context context) + public StreakChart(Context context) { super(context); init(); } - public HabitStreakView(Context context, AttributeSet attrs) + public StreakChart(Context context, AttributeSet attrs) { super(context, attrs); - this.primaryColor = ColorHelper.getColor(getContext(), 7); init(); } - public void setHabit(Habit habit) - { - this.habit = habit; - createColors(); - } - - private void init() - { - createPaints(); - createColors(); - - streaks = Collections.emptyList(); - - dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM); - dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); - rect = new RectF(); - maxStreakCount = 10; - baseSize = getResources().getDimensionPixelSize(R.dimen.baseSize); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) + public void setIsBackgroundTransparent(boolean isBackgroundTransparent) { - int width = MeasureSpec.getSize(widthMeasureSpec); - int height = MeasureSpec.getSize(heightMeasureSpec); - setMeasuredDimension(width, height); + this.isBackgroundTransparent = isBackgroundTransparent; + initColors(); } - @Override - protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) + public void setStreaks(List streaks) { - maxStreakCount = height / baseSize; - this.width = width; - - float minTextSize = getResources().getDimension(R.dimen.tinyTextSize); - float maxTextSize = getResources().getDimension(R.dimen.regularTextSize); - float textSize = baseSize * 0.5f; - - paint.setTextSize(Math.max(Math.min(textSize, maxTextSize), minTextSize)); - em = paint.getFontSpacing(); - textMargin = 0.5f * em; - - updateMaxMin(); + this.streaks = streaks; + initColors(); + updateMaxMinLengths(); + requestLayout(); } - private void createColors() + public void populateWithRandomData() { - if(habit != null) - this.primaryColor = ColorHelper.getColor(getContext(), habit.color); - - int red = Color.red(primaryColor); - int green = Color.green(primaryColor); - int blue = Color.blue(primaryColor); + long day = DateUtils.millisecondsInOneDay; + long start = DateUtils.getStartOfToday(); + LinkedList streaks = new LinkedList<>(); - colors = new int[4]; - colors[3] = primaryColor; - colors[2] = Color.argb(192, red, green, blue); - colors[1] = Color.argb(96, red, green, blue); - colors[0] = UIHelper.getStyledColor(getContext(), R.attr.lowContrastTextColor); - textColor = UIHelper.getStyledColor(getContext(), R.attr.mediumContrastTextColor); - reverseTextColor = UIHelper.getStyledColor(getContext(), R.attr.highContrastReverseTextColor); - } + for(int i = 0; i < 10; i++) + { + int length = new Random().nextInt(100); + long end = start + length * day; + streaks.add(new Streak(start, end)); + start = end + day; + } - protected void createPaints() - { - paint = new Paint(); - paint.setTextAlign(Paint.Align.CENTER); - paint.setAntiAlias(true); + setStreaks(streaks); } - public void refreshData() + public void setColor(int color) { - if(habit == null) return; - streaks = habit.streaks.getAll(maxStreakCount); - updateMaxMin(); + this.primaryColor = color; postInvalidate(); } @@ -159,50 +120,60 @@ public class HabitStreakView extends View implements HabitDataView protected void onDraw(Canvas canvas) { super.onDraw(canvas); - if(streaks.size() == 0) return; + if (streaks.size() == 0) return; rect.set(0, 0, width, baseSize); - for(Streak s : streaks) + for (Streak s : streaks) { drawRow(canvas, s, rect); rect.offset(0, baseSize); } } - private void updateMaxMin() + @Override + protected void onMeasure(int widthSpec, int heightSpec) { - maxLength = 0; - minLength = Long.MAX_VALUE; - shouldShowLabels = true; + int width = MeasureSpec.getSize(widthSpec); + int height = streaks.size() * baseSize; - for (Streak s : streaks) - { - maxLength = Math.max(maxLength, s.length); - minLength = Math.min(minLength, s.length); + heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); + widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); + setMeasuredDimension(widthSpec, heightSpec); + } - float lw1 = paint.measureText(dateFormat.format(new Date(s.start))); - float lw2 = paint.measureText(dateFormat.format(new Date(s.end))); - maxLabelWidth = Math.max(maxLabelWidth, Math.max(lw1, lw2)); - } + @Override + protected void onSizeChanged(int width, + int height, + int oldWidth, + int oldHeight) + { + this.width = width; - if(width - 2 * maxLabelWidth < width * 0.25f) - { - maxLabelWidth = 0; - shouldShowLabels = false; - } + float minTextSize = getResources().getDimension(R.dimen.tinyTextSize); + float maxTextSize = + getResources().getDimension(R.dimen.regularTextSize); + float textSize = baseSize * 0.5f; + + paint.setTextSize( + Math.max(Math.min(textSize, maxTextSize), minTextSize)); + em = paint.getFontSpacing(); + textMargin = 0.5f * em; + + updateMaxMinLengths(); } private void drawRow(Canvas canvas, Streak streak, RectF rect) { - if(maxLength == 0) return; + if (maxLength == 0) return; - float percentage = (float) streak.length / maxLength; + float percentage = (float) streak.getLength() / maxLength; float availableWidth = width - 2 * maxLabelWidth; - if(shouldShowLabels) availableWidth -= 2 * textMargin; + if (shouldShowLabels) availableWidth -= 2 * textMargin; float barWidth = percentage * availableWidth; - float minBarWidth = paint.measureText(streak.length.toString()) + em; + float minBarWidth = + paint.measureText(Long.toString(streak.getLength())) + em; barWidth = Math.max(barWidth, minBarWidth); float gap = (width - barWidth) / 2; @@ -210,19 +181,20 @@ public class HabitStreakView extends View implements HabitDataView paint.setColor(percentageToColor(percentage)); - canvas.drawRect(rect.left + gap, rect.top + paddingTopBottom, rect.right - gap, - rect.bottom - paddingTopBottom, paint); + canvas.drawRect(rect.left + gap, rect.top + paddingTopBottom, + rect.right - gap, rect.bottom - paddingTopBottom, paint); float yOffset = rect.centerY() + 0.3f * em; paint.setColor(reverseTextColor); paint.setTextAlign(Paint.Align.CENTER); - canvas.drawText(streak.length.toString(), rect.centerX(), yOffset, paint); + canvas.drawText(Long.toString(streak.getLength()), rect.centerX(), + yOffset, paint); - if(shouldShowLabels) + if (shouldShowLabels) { - String startLabel = dateFormat.format(new Date(streak.start)); - String endLabel = dateFormat.format(new Date(streak.end)); + String startLabel = dateFormat.format(new Date(streak.getStart())); + String endLabel = dateFormat.format(new Date(streak.getEnd())); paint.setColor(textColor); paint.setTextAlign(Paint.Align.RIGHT); @@ -233,17 +205,74 @@ public class HabitStreakView extends View implements HabitDataView } } + private void init() + { + initPaints(); + initColors(); + + streaks = Collections.emptyList(); + + dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM); + dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + rect = new RectF(); + baseSize = getResources().getDimensionPixelSize(R.dimen.baseSize); + } + + private void initColors() + { + int red = Color.red(primaryColor); + int green = Color.green(primaryColor); + int blue = Color.blue(primaryColor); + + colors = new int[4]; + colors[3] = primaryColor; + colors[2] = Color.argb(192, red, green, blue); + colors[1] = Color.argb(96, red, green, blue); + colors[0] = InterfaceUtils.getStyledColor(getContext(), + R.attr.lowContrastTextColor); + textColor = InterfaceUtils.getStyledColor(getContext(), + R.attr.mediumContrastTextColor); + reverseTextColor = InterfaceUtils.getStyledColor(getContext(), + R.attr.highContrastReverseTextColor); + } + + private void initPaints() + { + paint = new Paint(); + paint.setTextAlign(Paint.Align.CENTER); + paint.setAntiAlias(true); + } + private int percentageToColor(float percentage) { - if(percentage >= 1.0f) return colors[3]; - if(percentage >= 0.8f) return colors[2]; - if(percentage >= 0.5f) return colors[1]; + if (percentage >= 1.0f) return colors[3]; + if (percentage >= 0.8f) return colors[2]; + if (percentage >= 0.5f) return colors[1]; return colors[0]; } - public void setIsBackgroundTransparent(boolean isBackgroundTransparent) + private void updateMaxMinLengths() { - this.isBackgroundTransparent = isBackgroundTransparent; - createColors(); + maxLength = 0; + minLength = Long.MAX_VALUE; + shouldShowLabels = true; + + for (Streak s : streaks) + { + maxLength = Math.max(maxLength, s.getLength()); + minLength = Math.min(minLength, s.getLength()); + + float lw1 = + paint.measureText(dateFormat.format(new Date(s.getStart()))); + float lw2 = + paint.measureText(dateFormat.format(new Date(s.getEnd()))); + maxLabelWidth = Math.max(maxLabelWidth, Math.max(lw1, lw2)); + } + + if (width - 2 * maxLabelWidth < width * 0.25f) + { + maxLabelWidth = 0; + shouldShowLabels = false; + } } } diff --git a/app/src/main/java/org/isoron/uhabits/ui/common/views/package-info.java b/app/src/main/java/org/isoron/uhabits/ui/common/views/package-info.java new file mode 100644 index 000000000..4c5fb6c16 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/common/views/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2016 Á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 . + */ + +/** + * Provides views that are used across the app, such as RingView. + */ +package org.isoron.uhabits.ui.common.views; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/edit/BaseDialogFragment.java b/app/src/main/java/org/isoron/uhabits/ui/habits/edit/BaseDialogFragment.java new file mode 100644 index 000000000..8b597694f --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/edit/BaseDialogFragment.java @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.edit; + +import android.os.*; +import android.support.annotation.*; +import android.support.v7.app.*; +import android.text.format.*; +import android.view.*; + +import com.android.colorpicker.*; +import com.android.datetimepicker.time.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.commands.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; +import org.isoron.uhabits.utils.DateUtils; + +import java.util.*; + +import javax.inject.*; + +import butterknife.*; + +public abstract class BaseDialogFragment extends AppCompatDialogFragment +{ + @Nullable + protected Habit originalHabit; + + @Nullable + protected Habit modifiedHabit; + + @Nullable + protected BaseDialogHelper helper; + + @Inject + protected Preferences prefs; + + @Inject + protected CommandRunner commandRunner; + + @Inject + protected HabitList habitList; + + @Override + public View onCreateView(LayoutInflater inflater, + ViewGroup container, + Bundle savedInstanceState) + { + View view = inflater.inflate(R.layout.edit_habit, container, false); + HabitsApplication.getComponent().inject(this); + ButterKnife.bind(this, view); + + helper = new BaseDialogHelper(this, view); + getDialog().setTitle(getTitle()); + initializeHabits(); + restoreSavedInstance(savedInstanceState); + helper.populateForm(modifiedHabit); + return view; + } + + @OnItemSelected(R.id.sFrequency) + public void onFrequencySelected(int position) + { + if (position < 0 || position > 4) throw new IllegalArgumentException(); + int freqNums[] = { 1, 1, 2, 5, 3 }; + int freqDens[] = { 1, 7, 7, 7, 7 }; + modifiedHabit.setFrequency(new Frequency(freqNums[position], freqDens[position])); + helper.populateFrequencyFields(modifiedHabit); + } + + @Override + @SuppressWarnings("ConstantConditions") + public void onSaveInstanceState(Bundle outState) + { + super.onSaveInstanceState(outState); + outState.putInt("color", modifiedHabit.getColor()); + if (modifiedHabit.hasReminder()) + { + Reminder reminder = modifiedHabit.getReminder(); + outState.putInt("reminderMin", reminder.getMinute()); + outState.putInt("reminderHour", reminder.getHour()); + outState.putInt("reminderDays", reminder.getDays()); + } + } + + protected abstract int getTitle(); + + protected abstract void initializeHabits(); + + protected void restoreSavedInstance(@Nullable Bundle bundle) + { + if (bundle == null) return; + modifiedHabit.setColor( + bundle.getInt("color", modifiedHabit.getColor())); + + + modifiedHabit.setReminder(null); + + int hour = (bundle.getInt("reminderHour", -1)); + int minute = (bundle.getInt("reminderMin", -1)); + int days = (bundle.getInt("reminderDays", -1)); + + if (hour >= 0 && minute >= 0) + { + Reminder reminder = new Reminder(hour, minute, days); + modifiedHabit.setReminder(reminder); + } + } + + protected abstract void saveHabit(); + + @OnClick(R.id.buttonDiscard) + void onButtonDiscardClick() + { + dismiss(); + } + + @OnClick(R.id.tvReminderTime) + @SuppressWarnings("ConstantConditions") + void onDateSpinnerClick() + { + int defaultHour = 8; + int defaultMin = 0; + + if (modifiedHabit.hasReminder()) + { + Reminder reminder = modifiedHabit.getReminder(); + defaultHour = reminder.getHour(); + defaultMin = reminder.getMinute(); + } + + showTimePicker(defaultHour, defaultMin); + } + + @OnClick(R.id.buttonSave) + void onSaveButtonClick() + { + helper.parseFormIntoHabit(modifiedHabit); + if (!helper.validate(modifiedHabit)) return; + saveHabit(); + dismiss(); + } + + @OnClick(R.id.tvReminderDays) + @SuppressWarnings("ConstantConditions") + void onWeekdayClick() + { + if (!modifiedHabit.hasReminder()) return; + Reminder reminder = modifiedHabit.getReminder(); + + WeekdayPickerDialog dialog = new WeekdayPickerDialog(); + dialog.setListener(new OnWeekdaysPickedListener()); + dialog.setSelectedDays( + DateUtils.unpackWeekdayList(reminder.getDays())); + dialog.show(getFragmentManager(), "weekdayPicker"); + } + + @OnClick(R.id.buttonPickColor) + void showColorPicker() + { + int androidColor = + ColorUtils.getColor(getContext(), modifiedHabit.getColor()); + + ColorPickerDialog picker = + ColorPickerDialog.newInstance(R.string.color_picker_default_title, + ColorUtils.getPalette(getContext()), androidColor, 4, + ColorPickerDialog.SIZE_SMALL); + + picker.setOnColorSelectedListener(new OnColorSelectedListener()); + picker.show(getFragmentManager(), "picker"); + } + + private void showTimePicker(int defaultHour, int defaultMin) + { + boolean is24HourMode = DateFormat.is24HourFormat(getContext()); + TimePickerDialog timePicker = + TimePickerDialog.newInstance(new OnTimeSetListener(), defaultHour, + defaultMin, is24HourMode); + timePicker.show(getFragmentManager(), "timePicker"); + } + + private class OnColorSelectedListener + implements ColorPickerSwatch.OnColorSelectedListener + { + @Override + public void onColorSelected(int androidColor) + { + int paletteColor = + ColorUtils.colorToPaletteIndex(getActivity(), androidColor); + prefs.setDefaultHabitColor(paletteColor); + modifiedHabit.setColor(paletteColor); + helper.populateColor(paletteColor); + } + } + + private class OnTimeSetListener + implements TimePickerDialog.OnTimeSetListener + { + @Override + public void onTimeCleared(RadialPickerLayout view) + { + modifiedHabit.clearReminder(); + helper.populateReminderFields(modifiedHabit); + } + + @Override + public void onTimeSet(RadialPickerLayout view, int hour, int minute) + { + Reminder reminder = + new Reminder(hour, minute, DateUtils.ALL_WEEK_DAYS); + modifiedHabit.setReminder(reminder); + helper.populateReminderFields(modifiedHabit); + } + } + + private class OnWeekdaysPickedListener + implements WeekdayPickerDialog.OnWeekdaysPickedListener + { + @Override + public void onWeekdaysPicked(boolean[] selectedDays) + { + if (isSelectionEmpty(selectedDays)) Arrays.fill(selectedDays, true); + + Reminder oldReminder = modifiedHabit.getReminder(); + modifiedHabit.setReminder( + new Reminder(oldReminder.getHour(), oldReminder.getMinute(), + DateUtils.packWeekdayList(selectedDays))); + helper.populateReminderFields(modifiedHabit); + } + + private boolean isSelectionEmpty(boolean[] selectedDays) + { + for (boolean d : selectedDays) if (d) return false; + return true; + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/edit/BaseDialogHelper.java b/app/src/main/java/org/isoron/uhabits/ui/habits/edit/BaseDialogHelper.java new file mode 100644 index 000000000..3cad2ae8d --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/edit/BaseDialogHelper.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.edit; + +import android.annotation.*; +import android.support.v4.app.*; +import android.view.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; + +import butterknife.*; + +public class BaseDialogHelper +{ + private DialogFragment frag; + + @BindView(R.id.tvName) + TextView tvName; + + @BindView(R.id.tvDescription) + TextView tvDescription; + + @BindView(R.id.tvFreqNum) + TextView tvFreqNum; + + @BindView(R.id.tvFreqDen) + TextView tvFreqDen; + + @BindView(R.id.tvReminderTime) + TextView tvReminderTime; + + @BindView(R.id.tvReminderDays) + TextView tvReminderDays; + + @BindView(R.id.sFrequency) + Spinner sFrequency; + + @BindView(R.id.llCustomFrequency) + ViewGroup llCustomFrequency; + + @BindView(R.id.llReminderDays) + ViewGroup llReminderDays; + + public BaseDialogHelper(DialogFragment frag, View view) + { + this.frag = frag; + ButterKnife.bind(this, view); + } + + protected void populateForm(final Habit habit) + { + if (habit.getName() != null) tvName.setText(habit.getName()); + if (habit.getDescription() != null) + tvDescription.setText(habit.getDescription()); + + populateColor(habit.getColor()); + populateFrequencyFields(habit); + populateReminderFields(habit); + } + + void parseFormIntoHabit(Habit habit) + { + habit.setName(tvName.getText().toString().trim()); + habit.setDescription(tvDescription.getText().toString().trim()); + String freqNum = tvFreqNum.getText().toString(); + String freqDen = tvFreqDen.getText().toString(); + if (!freqNum.isEmpty() && !freqDen.isEmpty()) + { + int numerator = Integer.parseInt(freqNum); + int denominator = Integer.parseInt(freqDen); + habit.setFrequency(new Frequency(numerator, denominator)); + } + } + + void populateColor(int paletteColor) + { + tvName.setTextColor( + ColorUtils.getColor(frag.getContext(), paletteColor)); + } + + @SuppressLint("SetTextI18n") + void populateFrequencyFields(Habit habit) + { + int quickSelectPosition = -1; + + Frequency freq = habit.getFrequency(); + + if (freq.equals(Frequency.DAILY)) + quickSelectPosition = 0; + + else if (freq.equals(Frequency.WEEKLY)) + quickSelectPosition = 1; + + else if (freq.equals(Frequency.TWO_TIMES_PER_WEEK)) + quickSelectPosition = 2; + + else if (freq.equals(Frequency.FIVE_TIMES_PER_WEEK)) + quickSelectPosition = 3; + + if (quickSelectPosition >= 0) + showSimplifiedFrequency(quickSelectPosition); + + else showCustomFrequency(); + + tvFreqNum.setText(Integer.toString(freq.getNumerator())); + tvFreqDen.setText(Integer.toString(freq.getDenominator())); + } + + @SuppressWarnings("ConstantConditions") + void populateReminderFields(Habit habit) + { + if (!habit.hasReminder()) + { + tvReminderTime.setText(R.string.reminder_off); + llReminderDays.setVisibility(View.GONE); + return; + } + + Reminder reminder = habit.getReminder(); + + String time = + DateUtils.formatTime(frag.getContext(), reminder.getHour(), + reminder.getMinute()); + tvReminderTime.setText(time); + llReminderDays.setVisibility(View.VISIBLE); + + boolean weekdays[] = DateUtils.unpackWeekdayList(reminder.getDays()); + tvReminderDays.setText( + DateUtils.formatWeekdayList(frag.getContext(), weekdays)); + } + + private void showCustomFrequency() + { + sFrequency.setVisibility(View.GONE); + llCustomFrequency.setVisibility(View.VISIBLE); + } + + @SuppressLint("SetTextI18n") + private void showSimplifiedFrequency(int quickSelectPosition) + { + sFrequency.setVisibility(View.VISIBLE); + sFrequency.setSelection(quickSelectPosition); + llCustomFrequency.setVisibility(View.GONE); + } + + boolean validate(Habit habit) + { + Boolean valid = true; + + if (habit.getName().length() == 0) + { + tvName.setError( + frag.getString(R.string.validation_name_should_not_be_blank)); + valid = false; + } + + Frequency freq = habit.getFrequency(); + + if (freq.getNumerator() <= 0) + { + tvFreqNum.setError( + frag.getString(R.string.validation_number_should_be_positive)); + valid = false; + } + + if (freq.getNumerator() > freq.getDenominator()) + { + tvFreqNum.setError( + frag.getString(R.string.validation_at_most_one_rep_per_day)); + valid = false; + } + + return valid; + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/edit/CreateHabitDialogFragment.java b/app/src/main/java/org/isoron/uhabits/ui/habits/edit/CreateHabitDialogFragment.java new file mode 100644 index 000000000..28188964d --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/edit/CreateHabitDialogFragment.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.edit; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.commands.*; +import org.isoron.uhabits.models.*; + +public class CreateHabitDialogFragment extends BaseDialogFragment +{ + @Override + protected int getTitle() + { + return R.string.create_habit; + } + + @Override + protected void initializeHabits() + { + modifiedHabit = new Habit(); + modifiedHabit.setFrequency(Frequency.DAILY); + modifiedHabit.setColor( + prefs.getDefaultHabitColor(modifiedHabit.getColor())); + } + + protected void saveHabit() + { + Command command = new CreateHabitCommand(modifiedHabit); + commandRunner.execute(command, null); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/edit/EditHabitDialogFragment.java b/app/src/main/java/org/isoron/uhabits/ui/habits/edit/EditHabitDialogFragment.java new file mode 100644 index 000000000..d4e9beb76 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/edit/EditHabitDialogFragment.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.edit; + +import android.os.Bundle; + +import org.isoron.uhabits.R; +import org.isoron.uhabits.commands.Command; +import org.isoron.uhabits.commands.EditHabitCommand; +import org.isoron.uhabits.models.Habit; + +public class EditHabitDialogFragment extends BaseDialogFragment +{ + public static EditHabitDialogFragment newInstance(long habitId) + { + EditHabitDialogFragment frag = new EditHabitDialogFragment(); + Bundle args = new Bundle(); + args.putLong("habitId", habitId); + frag.setArguments(args); + return frag; + } + + @Override + protected int getTitle() + { + return R.string.edit_habit; + } + + @Override + protected void initializeHabits() + { + Long habitId = (Long) getArguments().get("habitId"); + if (habitId == null) + throw new IllegalArgumentException("habitId must be specified"); + + originalHabit = habitList.getById(habitId); + modifiedHabit = new Habit(); + modifiedHabit.copyFrom(originalHabit); + } + + @Override + protected void saveHabit() + { + Command command = new EditHabitCommand(originalHabit, modifiedHabit); + commandRunner.execute(command, originalHabit.getId()); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/edit/HistoryEditorDialog.java b/app/src/main/java/org/isoron/uhabits/ui/habits/edit/HistoryEditorDialog.java new file mode 100644 index 000000000..3aa60ef76 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/edit/HistoryEditorDialog.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.edit; + +import android.app.*; +import android.content.*; +import android.os.*; +import android.support.annotation.*; +import android.support.v7.app.AlertDialog; +import android.support.v7.app.*; +import android.util.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.tasks.*; +import org.isoron.uhabits.ui.common.views.*; +import org.isoron.uhabits.utils.*; + +import javax.inject.*; + +public class HistoryEditorDialog extends AppCompatDialogFragment + implements DialogInterface.OnClickListener, ModelObservable.Listener +{ + @Nullable + private Habit habit; + + @Nullable + HistoryChart historyChart; + + @Inject + HabitList habitList; + + @NonNull + private Controller controller; + + public HistoryEditorDialog() + { + this.controller = new Controller() {}; + } + + @Override + public void onClick(DialogInterface dialog, int which) + { + dismiss(); + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) + { + Context context = getActivity(); + HabitsApplication.getComponent().inject(this); + historyChart = new HistoryChart(context); + historyChart.setController(controller); + + if (savedInstanceState != null) + { + long id = savedInstanceState.getLong("habit", -1); + if (id > 0) this.habit = habitList.getById(id); + } + + int padding = + (int) getResources().getDimension(R.dimen.history_editor_padding); + + historyChart.setPadding(padding, 0, padding, 0); + historyChart.setIsEditable(true); + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder + .setTitle(R.string.history) + .setView(historyChart) + .setPositiveButton(android.R.string.ok, this); + + return builder.create(); + } + + @Override + public void onModelChange() + { + refreshData(); + } + + @Override + public void onResume() + { + super.onResume(); + + DisplayMetrics metrics = getResources().getDisplayMetrics(); + int maxHeight = getResources().getDimensionPixelSize( + R.dimen.history_editor_max_height); + int width = metrics.widthPixels; + int height = Math.min(metrics.heightPixels, maxHeight); + + getDialog().getWindow().setLayout(width, height); + + refreshData(); + habit.getCheckmarks().observable.addListener(this); + } + + @Override + public void onPause() + { + habit.getCheckmarks().observable.removeListener(this); + super.onPause(); + } + + @Override + public void onSaveInstanceState(Bundle outState) + { + outState.putLong("habit", habit.getId()); + } + + public void setController(@NonNull Controller controller) + { + this.controller = controller; + if (historyChart != null) historyChart.setController(controller); + } + + public void setHabit(@Nullable Habit habit) + { + this.habit = habit; + } + + private void refreshData() + { + if (habit == null) return; + new RefreshTask().execute(); + } + + public interface Controller extends HistoryChart.Controller {} + + private class RefreshTask extends BaseTask + { + public int[] checkmarks; + + @Override + protected void doInBackground() + { + checkmarks = habit.getCheckmarks().getAllValues(); + } + + @Override + protected void onPostExecute(Void aVoid) + { + int color = ColorUtils.getColor(getContext(), habit.getColor()); + historyChart.setColor(color); + historyChart.setCheckmarks(checkmarks); + super.onPostExecute(aVoid); + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/dialogs/WeekdayPickerDialog.java b/app/src/main/java/org/isoron/uhabits/ui/habits/edit/WeekdayPickerDialog.java similarity index 75% rename from app/src/main/java/org/isoron/uhabits/dialogs/WeekdayPickerDialog.java rename to app/src/main/java/org/isoron/uhabits/ui/habits/edit/WeekdayPickerDialog.java index 5297b80eb..477c736f0 100644 --- a/app/src/main/java/org/isoron/uhabits/dialogs/WeekdayPickerDialog.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/edit/WeekdayPickerDialog.java @@ -17,7 +17,7 @@ * with this program. If not, see . */ -package org.isoron.uhabits.dialogs; +package org.isoron.uhabits.ui.habits.edit; import android.app.Dialog; import android.content.DialogInterface; @@ -26,10 +26,11 @@ import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatDialogFragment; import org.isoron.uhabits.R; -import org.isoron.uhabits.helpers.DateHelper; +import org.isoron.uhabits.utils.DateUtils; -public class WeekdayPickerDialog extends AppCompatDialogFragment - implements DialogInterface.OnMultiChoiceClickListener, DialogInterface.OnClickListener +public class WeekdayPickerDialog extends AppCompatDialogFragment implements + DialogInterface.OnMultiChoiceClickListener, + DialogInterface.OnClickListener { public interface OnWeekdaysPickedListener @@ -38,6 +39,7 @@ public class WeekdayPickerDialog extends AppCompatDialogFragment } private boolean[] selectedDays; + private OnWeekdaysPickedListener listener; public void setListener(OnWeekdaysPickedListener listener) @@ -54,10 +56,13 @@ public class WeekdayPickerDialog extends AppCompatDialogFragment public Dialog onCreateDialog(Bundle savedInstanceState) { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setTitle(R.string.select_weekdays) - .setMultiChoiceItems(DateHelper.getLongDayNames(), selectedDays, this) - .setPositiveButton(android.R.string.yes, this) - .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() + builder + .setTitle(R.string.select_weekdays) + .setMultiChoiceItems(DateUtils.getLongDayNames(), selectedDays, + this) + .setPositiveButton(android.R.string.yes, this) + .setNegativeButton(android.R.string.cancel, + new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) @@ -78,6 +83,6 @@ public class WeekdayPickerDialog extends AppCompatDialogFragment @Override public void onClick(DialogInterface dialog, int which) { - if(listener != null) listener.onWeekdaysPicked(selectedDays); + if (listener != null) listener.onWeekdaysPicked(selectedDays); } } diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/edit/package-info.java b/app/src/main/java/org/isoron/uhabits/ui/habits/edit/package-info.java new file mode 100644 index 000000000..b5d29a4f4 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/edit/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2016 Á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 . + */ + +/** + * Provides dialogs for editing habits and related classes. + */ +package org.isoron.uhabits.ui.habits.edit; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsActivity.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsActivity.java new file mode 100644 index 000000000..2a93436bb --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsActivity.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.list; + +import android.os.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.ui.*; +import org.isoron.uhabits.ui.habits.list.model.*; + +import javax.inject.*; + +/** + * Activity that allows the user to see and modify the list of habits. + */ +public class ListHabitsActivity extends BaseActivity +{ + @Inject + HabitList habitList; + + private HabitCardListAdapter adapter; + + private ListHabitsRootView rootView; + + private ListHabitsScreen screen; + + private ListHabitsMenu menu; + + private ListHabitsSelectionMenu selectionMenu; + + private ListHabitsController controller; + + private BaseSystem system; + + @Override + protected void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + HabitsApplication.getComponent().inject(this); + + int checkmarkCount = ListHabitsRootView.MAX_CHECKMARK_COUNT; + + system = new BaseSystem(this); + adapter = new HabitCardListAdapter(checkmarkCount); + rootView = new ListHabitsRootView(this, adapter); + screen = new ListHabitsScreen(this, rootView); + menu = new ListHabitsMenu(this, screen, adapter); + selectionMenu = new ListHabitsSelectionMenu(screen, adapter); + controller = new ListHabitsController(screen, system, habitList); + + screen.setMenu(menu); + screen.setSelectionMenu(selectionMenu); + rootView.setController(controller, selectionMenu); + + setScreen(screen); + controller.onStartup(); + } + + @Override + protected void onPause() + { + adapter.cancelRefresh(); + super.onPause(); + } + + @Override + protected void onResume() + { + super.onResume(); + adapter.refresh(); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsController.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsController.java new file mode 100644 index 000000000..8e2c69352 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsController.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.list; + +import android.os.*; +import android.support.annotation.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.commands.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.tasks.*; +import org.isoron.uhabits.ui.*; +import org.isoron.uhabits.ui.habits.list.controllers.*; +import org.isoron.uhabits.utils.*; + +import java.io.*; + +import javax.inject.*; + +public class ListHabitsController + implements ImportDataTask.Listener, HabitCardListController.HabitListener +{ + @NonNull + private final ListHabitsScreen screen; + + @NonNull + private final BaseSystem system; + + @NonNull + private final HabitList habitList; + + @Inject + Preferences prefs; + + @Inject + CommandRunner commandRunner; + + public ListHabitsController(@NonNull ListHabitsScreen screen, + @NonNull BaseSystem system, + @NonNull HabitList habitList) + { + this.screen = screen; + this.system = system; + this.habitList = habitList; + HabitsApplication.getComponent().inject(this); + } + + public void onExportCSV() + { + ExportCSVTask task = + new ExportCSVTask(habitList.getAll(true), screen.getProgressBar()); + task.setListener(filename -> { + if (filename != null) screen.showSendFileScreen(filename); + else screen.showMessage(R.string.could_not_export); + }); + task.execute(); + } + + public void onExportDB() + { + ExportDBTask task = new ExportDBTask(screen.getProgressBar()); + task.setListener(filename -> { + if (filename != null) screen.showSendFileScreen(filename); + else screen.showMessage(R.string.could_not_export); + }); + task.execute(); + } + + @Override + public void onHabitClick(@NonNull Habit h) + { + screen.showHabitScreen(h); + } + + @Override + public void onHabitReorder(@NonNull Habit from, @NonNull Habit to) + { + habitList.reorder(from, to); + } + + public void onImportData(@NonNull File file) + { + ImportDataTask task = new ImportDataTask(file, screen.getProgressBar()); + task.setListener(this); + task.execute(); + } + + @Override + public void onImportDataFinished(int result) + { + switch (result) + { + case ImportDataTask.SUCCESS: + screen.invalidate(); + 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; + } + } + + @Override + public void onInvalidToggle() + { + screen.showMessage(R.string.long_press_to_toggle); + } + + public void onSendBugReport() + { + try + { + system.dumpBugReportToFile(); + } + catch (IOException e) + { + // ignored + } + + try + { + String log = "---------- BUG REPORT BEGINS ----------\n"; + log += system.getBugReport(); + log += "---------- BUG REPORT ENDS ------------\n"; + String to = "dev@loophabits.org"; + String subject = "Bug Report - Loop Habit Tracker"; + screen.showSendEmailScreen(log, to, subject); + } + catch (IOException e) + { + e.printStackTrace(); + screen.showMessage(R.string.bug_report_failed); + } + } + + public void onStartup() + { + prefs.initialize(); + prefs.incrementLaunchCount(); + prefs.updateLastAppVersion(); + if (prefs.isFirstRun()) onFirstRun(); + + new Handler().postDelayed(() -> { + system.scheduleReminders(); + HabitsApplication.getWidgetManager().updateWidgets(); + }, 1000); + } + + @Override + public void onToggle(@NonNull Habit habit, long timestamp) + { + commandRunner.execute(new ToggleRepetitionCommand(habit, timestamp), + null); + } + + private void onFirstRun() + { + prefs.setFirstRun(false); + prefs.updateLastHint(-1, DateUtils.getStartOfToday()); + screen.showIntroScreen(); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsMenu.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsMenu.java new file mode 100644 index 000000000..b518a33be --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsMenu.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.list; + +import android.support.annotation.NonNull; +import android.view.Menu; +import android.view.MenuItem; + +import org.isoron.uhabits.R; +import org.isoron.uhabits.ui.BaseActivity; +import org.isoron.uhabits.ui.BaseMenu; +import org.isoron.uhabits.ui.habits.list.model.*; +import org.isoron.uhabits.utils.InterfaceUtils; + +public class ListHabitsMenu extends BaseMenu +{ + @NonNull + private final ListHabitsScreen screen; + + private boolean showArchived; + + private final HabitCardListAdapter adapter; + + public ListHabitsMenu(@NonNull BaseActivity activity, + @NonNull ListHabitsScreen screen, + @NonNull HabitCardListAdapter adapter) + { + super(activity); + this.screen = screen; + this.adapter = adapter; + } + + @Override + public void onCreate(@NonNull Menu menu) + { + MenuItem nightModeItem = menu.findItem(R.id.action_night_mode); + nightModeItem.setChecked(InterfaceUtils.isNightMode()); + + MenuItem showArchivedItem = menu.findItem(R.id.action_show_archived); + showArchivedItem.setChecked(showArchived); + } + + @Override + public boolean onItemSelected(@NonNull MenuItem item) + { + switch (item.getItemId()) + { + case R.id.action_night_mode: + screen.toggleNightMode(); + return true; + + case R.id.action_add: + screen.showCreateHabitScreen(); + return true; + + case R.id.action_faq: + screen.showFAQScreen(); + return true; + + case R.id.action_about: + screen.showAboutScreen(); + return true; + + case R.id.action_settings: + screen.showSettingsScreen(); + return true; + + case R.id.action_show_archived: + showArchived = !showArchived; + adapter.setShowArchived(showArchived); + invalidate(); + return true; + + default: + return false; + } + } + + @Override + protected int getMenuResourceId() + { + return R.menu.main_activity; + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsRootView.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsRootView.java new file mode 100644 index 000000000..d534098f7 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsRootView.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.list; + +import android.content.*; +import android.content.res.*; +import android.support.annotation.*; +import android.support.v7.widget.Toolbar; +import android.view.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.ui.*; +import org.isoron.uhabits.ui.habits.list.controllers.*; +import org.isoron.uhabits.ui.habits.list.model.*; +import org.isoron.uhabits.ui.habits.list.views.*; +import org.isoron.uhabits.utils.*; + +import butterknife.*; + +public class ListHabitsRootView extends BaseRootView + implements ModelObservable.Listener +{ + public static final int MAX_CHECKMARK_COUNT = 21; + + @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; + + @NonNull + private final HabitCardListAdapter listAdapter; + + public ListHabitsRootView(@NonNull Context context, + @NonNull HabitCardListAdapter listAdapter) + { + super(context); + addView(inflate(getContext(), R.layout.list_habits, null)); + ButterKnife.bind(this); + + this.listAdapter = listAdapter; + listView.setAdapter(listAdapter); + listAdapter.setListView(listView); + + tvStarEmpty.setTypeface(InterfaceUtils.getFontAwesome(getContext())); + initToolbar(); + + String hints[] = + getContext().getResources().getStringArray(R.array.hints); + HintList hintList = new HintList(hints); + hintView.setHints(hintList); + } + + public static int getCheckmarkCount(View v) + { + Resources res = v.getResources(); + float labelWidth = res.getDimension(R.dimen.habitNameWidth); + float buttonWidth = res.getDimension(R.dimen.checkmarkWidth); + return Math.min(MAX_CHECKMARK_COUNT, Math.max(0, + (int) ((v.getMeasuredWidth() - labelWidth) / buttonWidth))); + } + + @Override + @NonNull + public ProgressBar getProgressBar() + { + return progressBar; + } + + @NonNull + @Override + public Toolbar getToolbar() + { + return toolbar; + } + + @Override + public void onModelChange() + { + updateEmptyView(); + } + + public void setController(@NonNull ListHabitsController controller, + @NonNull ListHabitsSelectionMenu menu) + { + HabitCardListController listController = + new HabitCardListController(listAdapter, listView); + + listController.setHabitListener(controller); + listController.setSelectionListener(menu); + listView.setController(listController); + menu.setListController(listController); + } + + @Override + protected void onAttachedToWindow() + { + super.onAttachedToWindow(); + listAdapter.getObservable().addListener(this); + } + + @Override + protected void onDetachedFromWindow() + { + listAdapter.getObservable().removeListener(this); + super.onDetachedFromWindow(); + } + + + private void updateEmptyView() + { + llEmpty.setVisibility( + listAdapter.getCount() > 0 ? View.GONE : View.VISIBLE); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsScreen.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsScreen.java new file mode 100644 index 000000000..8d22d5eb0 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsScreen.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.list; + +import android.content.*; +import android.net.*; +import android.os.*; +import android.support.annotation.*; +import android.support.v7.app.*; + +import com.android.colorpicker.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.ui.*; +import org.isoron.uhabits.ui.about.*; +import org.isoron.uhabits.ui.habits.edit.*; +import org.isoron.uhabits.ui.habits.show.*; +import org.isoron.uhabits.ui.intro.*; +import org.isoron.uhabits.ui.settings.*; +import org.isoron.uhabits.utils.*; + +import java.io.*; + +public class ListHabitsScreen extends BaseScreen +{ + @Nullable + ListHabitsController controller; + + public ListHabitsScreen(@NonNull BaseActivity activity, + ListHabitsRootView rootView) + { + super(activity); + setRootView(rootView); + } + + @Override + public void onResult(int requestCode, int resultCode, Intent data) + { + if (controller == null) return; + + switch (resultCode) + { + case HabitsApplication.RESULT_IMPORT_DATA: + showImportScreen(); + break; + + case HabitsApplication.RESULT_EXPORT_CSV: + controller.onExportCSV(); + break; + + case HabitsApplication.RESULT_EXPORT_DB: + controller.onExportDB(); + break; + + case HabitsApplication.RESULT_BUG_REPORT: + controller.onSendBugReport(); + break; + } + } + + public void showAboutScreen() + { + Intent intent = new Intent(activity, AboutActivity.class); + activity.startActivity(intent); + } + + public void showColorPicker(Habit habit, OnColorSelectedListener callback) + { + int color = ColorUtils.getColor(activity, habit.getColor()); + + ColorPickerDialog picker = + ColorPickerDialog.newInstance(R.string.color_picker_default_title, + ColorUtils.getPalette(activity), color, 4, + ColorPickerDialog.SIZE_SMALL); + + picker.setOnColorSelectedListener(c -> { + c = ColorUtils.colorToPaletteIndex(activity, c); + callback.onColorSelected(c); + }); + picker.show(activity.getSupportFragmentManager(), "picker"); + } + + public void showCreateHabitScreen() + { + showDialog(new CreateHabitDialogFragment(), "editHabit"); + } + + public void showDeleteConfirmationScreen(Callback callback) + { + new AlertDialog.Builder(activity) + .setTitle(R.string.delete_habits) + .setMessage(R.string.delete_habits_message) + .setPositiveButton(android.R.string.yes, + (dialog, which) -> callback.run()) + .setNegativeButton(android.R.string.no, null) + .show(); + } + + public void showEditHabitScreen(Habit habit) + { + BaseDialogFragment frag = + EditHabitDialogFragment.newInstance(habit.getId()); + frag.show(activity.getSupportFragmentManager(), "editHabit"); + } + + public void showFAQScreen() + { + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_VIEW); + intent.setData(Uri.parse(activity.getString(R.string.helpURL))); + activity.startActivity(intent); + } + + public void showHabitScreen(@NonNull Habit habit) + { + Intent intent = new Intent(activity, ShowHabitActivity.class); + intent.setData( + Uri.parse("content://org.isoron.uhabits/habit/" + habit.getId())); + activity.startActivity(intent); + } + + public void showImportScreen() + { + if (controller == null) return; + + File dir = FileUtils.getFilesDir(null); + if (dir == null) + { + showMessage(R.string.could_not_import); + return; + } + + FilePickerDialog picker = new FilePickerDialog(activity, dir); + picker.setListener(file -> controller.onImportData(file)); + picker.show(); + } + + public void showIntroScreen() + { + Intent intent = new Intent(activity, IntroActivity.class); + activity.startActivity(intent); + } + + public void showSettingsScreen() + { + Intent intent = new Intent(activity, SettingsActivity.class); + activity.startActivityForResult(intent, 0); + } + + public void toggleNightMode() + { + if (InterfaceUtils.isNightMode()) + InterfaceUtils.setCurrentTheme(InterfaceUtils.THEME_LIGHT); + else InterfaceUtils.setCurrentTheme(InterfaceUtils.THEME_DARK); + + refreshTheme(); + } + + private void refreshTheme() + { + new Handler().postDelayed(() -> { + Intent intent = new Intent(activity, MainActivity.class); + + activity.finish(); + activity.overridePendingTransition(android.R.anim.fade_in, + android.R.anim.fade_out); + activity.startActivity(intent); + + }, 500); // HACK: Let the menu disappear first + } + + interface Callback + { + void run(); + } + + public interface OnColorSelectedListener + { + void onColorSelected(int color); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsSelectionMenu.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsSelectionMenu.java new file mode 100644 index 000000000..07980edca --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsSelectionMenu.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.list; + +import android.support.annotation.*; +import android.view.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.commands.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.ui.*; +import org.isoron.uhabits.ui.habits.list.controllers.*; +import org.isoron.uhabits.ui.habits.list.model.*; + +import java.util.*; + +import javax.inject.*; + +public class ListHabitsSelectionMenu extends BaseSelectionMenu + implements HabitCardListController.SelectionListener +{ + @NonNull + private final ListHabitsScreen screen; + + @Inject + CommandRunner commandRunner; + + @NonNull + private final HabitCardListAdapter listAdapter; + + @Nullable + private HabitCardListController listController; + + public ListHabitsSelectionMenu(@NonNull ListHabitsScreen screen, + HabitCardListAdapter listAdapter) + { + this.screen = screen; + HabitsApplication.getComponent().inject(this); + this.listAdapter = listAdapter; + } + + @Override + public void onFinish() + { + if (listController != null) listController.onSelectionFinished(); + super.onFinish(); + } + + @Override + public boolean onItemClicked(@NonNull MenuItem item) + { + List selected = listAdapter.getSelected(); + if (selected.isEmpty()) return false; + + Habit firstHabit = selected.get(0); + + switch (item.getItemId()) + { + case R.id.action_edit_habit: + edit(firstHabit); + finish(); + return true; + + case R.id.action_archive_habit: + archive(selected); + finish(); + return true; + + case R.id.action_unarchive_habit: + unarchive(selected); + finish(); + return true; + + case R.id.action_delete: + delete(selected); + return true; + + case R.id.action_color: + showColorPicker(selected, firstHabit); + return true; + + default: + return false; + } + } + + @Override + public boolean onPrepare(@NonNull Menu menu) + { + List selected = listAdapter.getSelected(); + + boolean showEdit = (selected.size() == 1); + boolean showArchive = true; + boolean showUnarchive = true; + for (Habit h : selected) + { + if (h.isArchived()) showArchive = false; + else showUnarchive = false; + } + + 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(showEdit); + itemArchive.setVisible(showArchive); + itemUnarchive.setVisible(showUnarchive); + + setTitle(Integer.toString(selected.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; + } + + private void archive(@NonNull List selected) + { + commandRunner.execute(new ArchiveHabitsCommand(selected), null); + } + + private void delete(@NonNull List selected) + { + screen.showDeleteConfirmationScreen(() -> { + commandRunner.execute(new DeleteHabitsCommand(selected), null); + finish(); + }); + } + + private void edit(@NonNull Habit firstHabit) + { + screen.showEditHabitScreen(firstHabit); + } + + private void showColorPicker(@NonNull List selected, + @NonNull Habit firstHabit) + { + screen.showColorPicker(firstHabit, color -> { + commandRunner.execute(new ChangeHabitColorCommand(selected, color), + null); + finish(); + }); + } + + private void unarchive(@NonNull List selected) + { + commandRunner.execute(new UnarchiveHabitsCommand(selected), null); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/controllers/CheckmarkButtonController.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/controllers/CheckmarkButtonController.java new file mode 100644 index 000000000..d5c5dc078 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/controllers/CheckmarkButtonController.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.list.controllers; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.isoron.uhabits.HabitsApplication; +import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.ui.habits.list.views.CheckmarkButtonView; +import org.isoron.uhabits.utils.Preferences; + +import javax.inject.Inject; + +public class CheckmarkButtonController +{ + @Nullable + private CheckmarkButtonView view; + + @Nullable + private Listener listener; + + @Inject + Preferences prefs; + + @NonNull + private Habit habit; + + private long timestamp; + + public CheckmarkButtonController(@NonNull Habit habit, long timestamp) + { + this.habit = habit; + this.timestamp = timestamp; + HabitsApplication.getComponent().inject(this); + } + + public void onClick() + { + if (prefs.isShortToggleEnabled()) performToggle(); + else performInvalidToggle(); + } + + public boolean onLongClick() + { + performToggle(); + return true; + } + + public void performInvalidToggle() + { + if (listener != null) listener.onInvalidToggle(); + } + + public void performToggle() + { + if (view != null) view.toggle(); + if (listener != null) listener.onToggle(habit, timestamp); + } + + public void setListener(@Nullable Listener listener) + { + this.listener = listener; + } + + public void setView(@Nullable CheckmarkButtonView view) + { + this.view = view; + } + + public interface Listener + { + /** + * Called when the user's attempt to perform a toggle is rejected. + */ + void onInvalidToggle(); + + + void onToggle(@NonNull Habit habit, long timestamp); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/controllers/HabitCardController.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/controllers/HabitCardController.java new file mode 100644 index 000000000..f62fca8a2 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/controllers/HabitCardController.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.list.controllers; + +import android.support.annotation.*; + +import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.ui.habits.list.views.HabitCardView; + +public class HabitCardController implements HabitCardView.Controller +{ + @Nullable + private HabitCardView view; + + @Nullable + private Listener listener; + + @Override + public void onInvalidToggle() + { + if (listener != null) listener.onInvalidToggle(); + } + + @Override + public void onToggle(@NonNull Habit habit, long timestamp) + { + if (view != null) view.triggerRipple(timestamp); + if (listener != null) listener.onToggle(habit, timestamp); + } + + public void setListener(@Nullable Listener listener) + { + this.listener = listener; + } + + public void setView(@Nullable HabitCardView view) + { + this.view = view; + } + + public interface Listener extends CheckmarkButtonController.Listener + { + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/controllers/HabitCardListController.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/controllers/HabitCardListController.java new file mode 100644 index 000000000..b1137b6ad --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/controllers/HabitCardListController.java @@ -0,0 +1,324 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.list.controllers; + +import android.support.annotation.*; + +import com.mobeta.android.dslv.*; + +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.ui.habits.list.model.*; +import org.isoron.uhabits.ui.habits.list.views.*; + +/** + * Controller responsible for receiving and processing the events generated by a + * HabitListView. These include selecting and reordering items, toggling + * checkmarks and clicking habits. + */ +public class HabitCardListController implements DragSortListView.DropListener, + DragSortListView.DragListener, + HabitCardListView.Controller +{ + private final Mode NORMAL_MODE = new NormalMode(); + + private final Mode SELECTION_MODE = new SelectionMode(); + + @NonNull + private final HabitCardListAdapter adapter; + + @NonNull + private final HabitCardListView view; + + @Nullable + private HabitListener habitListener; + + @Nullable + private SelectionListener selectionListener; + + @NonNull + private Mode activeMode; + + public HabitCardListController(@NonNull HabitCardListAdapter adapter, + @NonNull HabitCardListView view) + { + this.adapter = adapter; + this.view = view; + this.activeMode = new NormalMode(); + } + + /** + * Called when the user is dragging a habit which was originally at position + * 'from' and is currently hovering over position 'to'. Note that the user + * has not yet finished the dragging operation. + * + * @param from the original position of the habit + * @param to the position where the habit is currently hovering + */ + @Override + public void drag(int from, int to) + { + // ignored + } + + /** + * 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.reorder(from, to); + + if (habitListener != null) + habitListener.onHabitReorder(habitFrom, habitTo); + } + + /** + * 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); + } + + /** + * 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(); + view.setDragEnabled(true); + activeMode = new NormalMode(); + + if (selectionListener != null) selectionListener.onSelectionFinish(); + } + + public interface HabitListener extends CheckmarkButtonController.Listener + { + /** + * 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); + } + + /** + * 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. + *

+ * 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(); + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/controllers/package-info.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/controllers/package-info.java new file mode 100644 index 000000000..46e9e09ce --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/controllers/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2016 Á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 . + */ + +/** + * Provides controllers that are specific for {@link org.isoron.uhabits.ui.habits.list.ListHabitsActivity}. + */ +package org.isoron.uhabits.ui.habits.list.controllers; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/model/HabitCardListAdapter.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/model/HabitCardListAdapter.java new file mode 100644 index 000000000..0f9dab743 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/model/HabitCardListAdapter.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.list.model; + +import android.support.annotation.*; +import android.view.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.ui.habits.list.views.*; + +import java.util.*; + +/** + * Provides data that backs a {@link HabitCardListView}. + *

+ * The data if fetched and cached by a {@link HabitCardListCache}. This adapter + * also holds a list of items that have been selected. + */ +public class HabitCardListAdapter extends BaseAdapter + implements HabitCardListCache.Listener +{ + @NonNull + private ModelObservable observable; + + @Nullable + private HabitCardListView listView; + + @NonNull + private final LinkedList selected; + + @NonNull + private final HabitCardListCache cache; + + public HabitCardListAdapter(int checkmarkCount) + { + this.selected = new LinkedList<>(); + this.observable = new ModelObservable(); + + HabitsApplication.getComponent().inject(this); + + cache = new HabitCardListCache(); + cache.setListener(this); + cache.setCheckmarkCount(checkmarkCount); + } + + public void cancelRefresh() + { + cache.cancelTasks(); + } + + /** + * Sets all items as not selected. + */ + public void clearSelection() + { + selected.clear(); + notifyDataSetChanged(); + } + + @Override + public int getCount() + { + return cache.getHabitCount(); + } + + /** + * Returns the item that occupies a certain position on the list + * + * @param position position of the item + * @return the item at given position + * @throws IndexOutOfBoundsException if position is not valid + */ + @Override + @NonNull + public Habit getItem(int position) + { + return cache.getHabitByPosition(position); + } + + @Override + public long getItemId(int position) + { + return getItem(position).getId(); + } + + @NonNull + public ModelObservable getObservable() + { + return observable; + } + + @NonNull + public List getSelected() + { + return new LinkedList<>(selected); + } + + @Override + public View getView(int position, + @Nullable View view, + @Nullable ViewGroup parent) + { + if (listView == null) return null; + + Habit habit = cache.getHabitByPosition(position); + int score = cache.getScore(habit.getId()); + int checkmarks[] = cache.getCheckmarks(habit.getId()); + boolean selected = this.selected.contains(habit); + + return listView.buildCardView((HabitCardView) view, habit, score, + checkmarks, selected); + } + + /** + * Returns whether list of selected items is empty. + * + * @return true if selection is empty, false otherwise + */ + public boolean isSelectionEmpty() + { + return selected.isEmpty(); + } + + /** + * Notify the adapter that it has been attached to a ListView. + */ + public void onAttached() + { + cache.onAttached(); + } + + @Override + public void onCacheRefresh() + { + notifyDataSetChanged(); + observable.notifyListeners(); + } + + /** + * Notify the adapter that it has been detached from a ListView. + */ + public void onDetached() + { + cache.onDetached(); + } + + public void refresh() + { + cache.refreshAllHabits(true); + } + + /** + * Changes the order of habits on the adapter. + *

+ * Note that this only has effect on the adapter cache. The database is not + * modified, and the change is lost when the cache is refreshed. This method + * is useful for making the ListView more responsive: while we wait for the + * database operation to finish, the cache can be modified to reflect the + * changes immediately. + * + * @param from the habit that should be moved + * @param to the habit that currently occupies the desired position + */ + public void reorder(int from, int to) + { + cache.reorder(from, to); + } + + /** + * Sets the HabitCardListView that this adapter will provide data for. + *

+ * This object will be used to generated new HabitCardViews, upon demand. + * + * @param listView the HabitCardListView associated with this adapter + */ + public void setListView(@Nullable HabitCardListView listView) + { + this.listView = listView; + } + + public void setShowArchived(boolean showArchived) + { + cache.setIncludeArchived(showArchived); + cache.refreshAllHabits(true); + } + + /** + * Selects or deselects the item at a given position. + * + * @param position position of the item to be toggled + */ + public void toggleSelection(int position) + { + Habit h = getItem(position); + int k = selected.indexOf(h); + if (k < 0) selected.add(h); + else selected.remove(h); + notifyDataSetChanged(); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/model/HabitCardListCache.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/model/HabitCardListCache.java new file mode 100644 index 000000000..65a3fb9ed --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/model/HabitCardListCache.java @@ -0,0 +1,352 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.list.model; + +import android.support.annotation.*; +import android.util.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.commands.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.tasks.*; +import org.isoron.uhabits.utils.*; + +import java.util.*; + +import javax.inject.*; + +/** + * A HabitCardListCache fetches and keeps a cache of all the data necessary to + * render a HabitCardListView. + *

+ * This is needed since performing database lookups during scrolling can make + * the ListView very slow. It also registers itself as an observer of the + * models, in order to update itself automatically. + */ +public class HabitCardListCache implements CommandRunner.Listener +{ + boolean includeArchived; + + private int checkmarkCount; + + private BaseTask currentFetchTask; + + @Nullable + private Listener listener; + + @Nullable + private Long lastLoadTimestamp; + + @NonNull + private CacheData data; + + @Inject + CommandRunner commandRunner; + + @Inject + HabitList allHabits; + + public HabitCardListCache() + { + data = new CacheData(); + HabitsApplication.getComponent().inject(this); + } + + public void cancelTasks() + { + if(currentFetchTask != null) + currentFetchTask.cancel(true); + } + + public int[] getCheckmarks(long habitId) + { + return data.checkmarks.get(habitId); + } + + /** + * Returns the habits that occupies a certain position on the list. + * + * @param position the position of the habit + * @return the habit at given position + * @throws IndexOutOfBoundsException if position is not valid + */ + @NonNull + public Habit getHabitByPosition(int position) + { + return data.habitsList.get(position); + } + + public int getHabitCount() + { + return data.habits.size(); + } + + @Nullable + public Long getLastLoadTimestamp() + { + return lastLoadTimestamp; + } + + public int getScore(long habitId) + { + return data.scores.get(habitId); + } + + public boolean getIncludeArchived() + { + return includeArchived; + } + + public void onAttached() + { + refreshAllHabits(true); + if (lastLoadTimestamp == null) refreshAllHabits(true); + commandRunner.addListener(this); + } + + @Override + public void onCommandExecuted(@NonNull Command command, + @Nullable Long refreshKey) + { + if (refreshKey == null) refreshAllHabits(true); + else refreshHabit(refreshKey); + } + + public void onDetached() + { + commandRunner.removeListener(this); + } + + public void refreshAllHabits(final boolean refreshScoresAndCheckmarks) + { + Log.d("HabitCardListCache", "Refreshing all habits"); + if (currentFetchTask != null) currentFetchTask.cancel(true); + currentFetchTask = new RefreshAllHabitsTask(refreshScoresAndCheckmarks); + currentFetchTask.execute(); + } + + public void refreshHabit(final Long id) + { + new RefreshHabitTask(id).execute(); + } + + public void reorder(int from, int to) + { + Habit fromHabit = data.habitsList.get(from); + data.habitsList.remove(from); + data.habitsList.add(to, fromHabit); + if(listener != null) listener.onCacheRefresh(); + } + + public void setCheckmarkCount(int checkmarkCount) + { + this.checkmarkCount = checkmarkCount; + } + + public void setIncludeArchived(boolean includeArchived) + { + this.includeArchived = includeArchived; + } + + public void setListener(@Nullable Listener listener) + { + this.listener = listener; + } + + /** + * Interface definition for a callback to be invoked when the data on the + * cache has been modified. + */ + public interface Listener + { + /** + * Called when the data on the cache has been modified. + */ + void onCacheRefresh(); + } + + private class CacheData + { + @NonNull + public HashMap habits; + + @NonNull + public List habitsList; + + @NonNull + public HashMap checkmarks; + + @NonNull + public HashMap scores; + + /** + * Creates a new CacheData without any content. + */ + public CacheData() + { + habits = new HashMap<>(); + habitsList = new LinkedList<>(); + checkmarks = new HashMap<>(); + scores = new HashMap<>(); + } + + public void copyCheckmarksFrom(@NonNull CacheData oldData) + { + int[] empty = new int[checkmarkCount]; + + for (Long id : habits.keySet()) + { + if (oldData.checkmarks.containsKey(id)) + checkmarks.put(id, oldData.checkmarks.get(id)); + else checkmarks.put(id, empty); + } + } + + public void copyScoresFrom(@NonNull CacheData oldData) + { + for (Long id : habits.keySet()) + { + if (oldData.scores.containsKey(id)) + scores.put(id, oldData.scores.get(id)); + else scores.put(id, 0); + } + } + + public void fetchHabits() + { + habitsList = allHabits.getAll(includeArchived); + for (Habit h : habitsList) + habits.put(h.getId(), h); + } + } + + private class RefreshAllHabitsTask extends BaseTask + { + @NonNull + private CacheData newData; + + private final boolean refreshScoresAndCheckmarks; + + public RefreshAllHabitsTask(boolean refreshScoresAndCheckmarks) + { + this.refreshScoresAndCheckmarks = refreshScoresAndCheckmarks; + newData = new CacheData(); + } + + private void commit() + { + data = newData; + } + + @Override + protected void doInBackground() + { + newData.fetchHabits(); + newData.copyScoresFrom(data); + newData.copyCheckmarksFrom(data); + +// sleep(1000); + commit(); + + if (!refreshScoresAndCheckmarks) return; + + long dateTo = DateUtils.getStartOfDay(DateUtils.getLocalTime()); + long dateFrom = + dateTo - (checkmarkCount - 1) * DateUtils.millisecondsInOneDay; + + int current = 0; + for (Habit h : newData.habitsList) + { + if (isCancelled()) return; + + Long id = h.getId(); + newData.scores.put(id, h.getScores().getTodayValue()); + newData.checkmarks.put(id, + h.getCheckmarks().getValues(dateFrom, dateTo)); + +// sleep(1000); + publishProgress(current++, newData.habits.size()); + } + } + + private void sleep(int time) + { + try + { + Thread.sleep(time); + } + catch (InterruptedException e) + { + e.printStackTrace(); + } + } + + @Override + protected void onPostExecute(Void aVoid) + { + if (isCancelled()) return; + + lastLoadTimestamp = DateUtils.getStartOfToday(); + currentFetchTask = null; + + if (listener != null) listener.onCacheRefresh(); + super.onPostExecute(null); + } + + @Override + protected void onProgressUpdate(Integer... values) + { + if (listener != null) listener.onCacheRefresh(); + } + + } + + private class RefreshHabitTask extends BaseTask + { + private final Long id; + + public RefreshHabitTask(Long id) + { + this.id = id; + } + + @Override + protected void doInBackground() + { + long dateTo = DateUtils.getStartOfDay(DateUtils.getLocalTime()); + long dateFrom = + dateTo - (checkmarkCount - 1) * DateUtils.millisecondsInOneDay; + + Habit h = allHabits.getById(id); + if (h == null) return; + + data.habits.put(id, h); + data.scores.put(id, h.getScores().getTodayValue()); + data.checkmarks.put(id, h.getCheckmarks().getValues(dateFrom, dateTo)); + } + + @Override + protected void onPostExecute(Void aVoid) + { + if (listener != null) listener.onCacheRefresh(); + super.onPostExecute(null); + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/model/HintList.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/model/HintList.java new file mode 100644 index 000000000..096ae3a16 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/model/HintList.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.list.model; + +import android.support.annotation.NonNull; + +import org.isoron.uhabits.HabitsApplication; +import org.isoron.uhabits.utils.DateUtils; +import org.isoron.uhabits.utils.Preferences; + +import javax.inject.Inject; + +/** + * Provides a list of hints to be shown at the application startup, and takes + * care of deciding when a new hint should be shown. + */ +public class HintList +{ + @Inject + Preferences prefs; + + @NonNull + private final String[] hints; + + /** + * Constructs a new list containing the provided hints. + * + * @param hints initial list of hints + */ + public HintList(@NonNull String hints[]) + { + this.hints = hints; + HabitsApplication.getComponent().inject(this); + } + + /** + * Returns a new hint to be shown to the user. + *

+ * The hint returned is marked as read on the list, and will not be returned + * again. In case all hints have already been read, and there is nothing + * left, returns null. + * + * @return the next hint to be shown, or null if none + */ + public String pop() + { + int next = prefs.getLastHintNumber() + 1; + if (next >= hints.length) return null; + + prefs.updateLastHint(next, DateUtils.getStartOfToday()); + return hints[next]; + } + + /** + * Returns whether it is time to show a new hint or not. + * + * @return true if hint should be shown, false otherwise + */ + public boolean shouldShow() + { + long lastHintTimestamp = prefs.getLastHintTimestamp(); + return (DateUtils.getStartOfToday() > lastHintTimestamp); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/model/package-info.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/model/package-info.java new file mode 100644 index 000000000..06b94b85b --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/model/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2016 Á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 . + */ + +/** + * Provides models that are specific for {@link org.isoron.uhabits.ui.habits.list.ListHabitsActivity}. + */ +package org.isoron.uhabits.ui.habits.list.model; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/package-info.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/package-info.java new file mode 100644 index 000000000..2d3cc559b --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2016 Á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 . + */ + +/** + * Provides acitivity for listing habits and related classes. + */ +package org.isoron.uhabits.ui.habits.list; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/views/CheckmarkButtonView.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/views/CheckmarkButtonView.java new file mode 100644 index 000000000..ef6f7f2ca --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/views/CheckmarkButtonView.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.list.views; + +import android.content.Context; +import android.graphics.Canvas; +import android.view.HapticFeedbackConstants; +import android.widget.FrameLayout; +import android.widget.TextView; + +import org.isoron.uhabits.R; +import org.isoron.uhabits.models.Checkmark; +import org.isoron.uhabits.ui.habits.list.controllers.CheckmarkButtonController; +import org.isoron.uhabits.utils.InterfaceUtils; + +import butterknife.BindView; +import butterknife.ButterKnife; + +public class CheckmarkButtonView extends FrameLayout +{ + private int color; + + private int value; + + @BindView(R.id.tvCheck) + TextView tvCheck; + + public CheckmarkButtonView(Context context) + { + super(context); + init(); + } + + public void setColor(int color) + { + this.color = color; + postInvalidate(); + } + + public void setController(final CheckmarkButtonController controller) + { + setOnClickListener(v -> controller.onClick()); + setOnLongClickListener(v -> controller.onLongClick()); + } + + public void setValue(int value) + { + this.value = value; + postInvalidate(); + } + + public void toggle() + { +// value = (value == Checkmark.CHECKED_EXPLICITLY ? Checkmark.UNCHECKED : +// Checkmark.CHECKED_EXPLICITLY); + + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + postInvalidate(); + } + + private void init() + { + addView( + inflate(getContext(), R.layout.list_habits_card_checkmark, null)); + ButterKnife.bind(this); + + setWillNotDraw(false); + setHapticFeedbackEnabled(false); + + tvCheck.setTypeface(InterfaceUtils.getFontAwesome(getContext())); + } + + @Override + protected void onDraw(Canvas canvas) + { + int lowContrastColor = InterfaceUtils.getStyledColor(getContext(), + R.attr.lowContrastTextColor); + + if (value == Checkmark.CHECKED_EXPLICITLY) + { + tvCheck.setText(R.string.fa_check); + tvCheck.setTextColor(color); + } + + if (value == Checkmark.CHECKED_IMPLICITLY) + { + tvCheck.setText(R.string.fa_check); + tvCheck.setTextColor(lowContrastColor); + } + + if (value == Checkmark.UNCHECKED) + { + tvCheck.setText(R.string.fa_times); + tvCheck.setTextColor(lowContrastColor); + } + + super.onDraw(canvas); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/views/CheckmarkPanelView.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/views/CheckmarkPanelView.java new file mode 100644 index 000000000..93e22b127 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/views/CheckmarkPanelView.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.list.views; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.widget.LinearLayout; + +import org.isoron.uhabits.HabitsApplication; +import org.isoron.uhabits.R; +import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.ui.habits.list.controllers.CheckmarkButtonController; +import org.isoron.uhabits.utils.DateUtils; +import org.isoron.uhabits.utils.Preferences; + +import javax.inject.Inject; + +public class CheckmarkPanelView extends LinearLayout +{ + private static final int CHECKMARK_LEFT_TO_RIGHT = 0; + + private static final int CHECKMARK_RIGHT_TO_LEFT = 1; + + @Inject + Preferences prefs; + + private int checkmarkValues[]; + + private int nButtons; + + private int color; + + private Controller controller; + + @NonNull + private Habit habit; + + public CheckmarkPanelView(Context context) + { + super(context); + init(); + } + + public CheckmarkPanelView(Context context, AttributeSet attrs) + { + super(context, attrs); + init(); + } + + public CheckmarkPanelView(Context context, + AttributeSet attrs, + int defStyleAttr) + { + super(context, attrs, defStyleAttr); + init(); + } + + public CheckmarkButtonView getButton(int position) + { + return (CheckmarkButtonView) getChildAt(position); + } + + public void setCheckmarkValues(int[] checkmarkValues) + { + this.checkmarkValues = checkmarkValues; + + if (this.nButtons != checkmarkValues.length) + { + this.nButtons = checkmarkValues.length; + addCheckmarkButtons(); + } + + setupCheckmarkButtons(); + } + + public void setColor(int color) + { + this.color = color; + setupCheckmarkButtons(); + } + + public void setController(Controller controller) + { + this.controller = controller; + } + + public void setHabit(@NonNull Habit habit) + { + this.habit = habit; + } + + private void addCheckmarkButtons() + { + removeAllViews(); + + for (int i = 0; i < nButtons; i++) + addView(new CheckmarkButtonView(getContext())); + } + + private int getCheckmarkOrder() + { + if (isInEditMode()) return CHECKMARK_LEFT_TO_RIGHT; + return prefs.shouldReverseCheckmarks() ? CHECKMARK_RIGHT_TO_LEFT : + CHECKMARK_LEFT_TO_RIGHT; + } + + private CheckmarkButtonView indexToButton(int i) + { + int position = i; + + if (getCheckmarkOrder() == CHECKMARK_RIGHT_TO_LEFT) + position = nButtons - i - 1; + + return (CheckmarkButtonView) getChildAt(position); + } + + private void init() + { + if (isInEditMode()) return; + HabitsApplication.getComponent().inject(this); + setWillNotDraw(false); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) + { + float buttonWidth = getResources().getDimension(R.dimen.checkmarkWidth); + float buttonHeight = + getResources().getDimension(R.dimen.checkmarkHeight); + + float width = buttonWidth * nButtons; + + widthMeasureSpec = + MeasureSpec.makeMeasureSpec((int) width, MeasureSpec.EXACTLY); + heightMeasureSpec = MeasureSpec.makeMeasureSpec((int) buttonHeight, + MeasureSpec.EXACTLY); + + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + private void setupButtonControllers(long timestamp, + CheckmarkButtonView buttonView) + { + if (controller == null) return; + + CheckmarkButtonController buttonController = + new CheckmarkButtonController(habit, timestamp); + + buttonController.setListener(controller); + buttonController.setView(buttonView); + buttonView.setController(buttonController); + } + + private void setupCheckmarkButtons() + { + long timestamp = DateUtils.getStartOfToday(); + long day = DateUtils.millisecondsInOneDay; + + for (int i = 0; i < nButtons; i++) + { + CheckmarkButtonView buttonView = indexToButton(i); + buttonView.setValue(checkmarkValues[i]); + buttonView.setColor(color); + setupButtonControllers(timestamp, buttonView); + timestamp -= day; + } + } + + public interface Controller extends CheckmarkButtonController.Listener + { + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/views/HabitCardListView.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/views/HabitCardListView.java new file mode 100644 index 000000000..52366774e --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/views/HabitCardListView.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.list.views; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ListAdapter; + +import com.mobeta.android.dslv.DragSortController; +import com.mobeta.android.dslv.DragSortListView; + +import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.ui.habits.list.controllers.CheckmarkButtonController; +import org.isoron.uhabits.ui.habits.list.controllers.HabitCardController; +import org.isoron.uhabits.ui.habits.list.model.HabitCardListAdapter; + +public class HabitCardListView extends DragSortListView +{ + @Nullable + private HabitCardListAdapter adapter; + + @Nullable + private Controller controller; + + public HabitCardListView(Context context, AttributeSet attrs) + { + super(context, attrs); + setFloatViewManager(new ViewManager()); + setDragEnabled(true); + setLongClickable(true); + } + + /** + * Builds a new HabitCardView to be eventually added to this list, + * containing the given data. + * + * @param cardView an old HabitCardView that should be reused if possible, + * possibly null + * @param habit the habit for this card + * @param score the current score for the habit + * @param checkmarks the list of checkmark values to be included in the + * card + * @param selected true if the card is selected, false otherwise + * @return the HabitCardView generated + */ + public View buildCardView(@Nullable HabitCardView cardView, + @NonNull Habit habit, + int score, + int[] checkmarks, + boolean selected) + { + if (cardView == null) cardView = new HabitCardView(getContext()); + + cardView.setHabit(habit); + cardView.setSelected(selected); + cardView.setCheckmarkValues(checkmarks); + cardView.setScore(score); + + if (controller != null) + { + HabitCardController cardController = new HabitCardController(); + cardController.setListener(controller); + cardView.setController(cardController); + cardController.setView(cardView); + } + + return cardView; + } + + @Override + public void setAdapter(ListAdapter adapter) + { + this.adapter = (HabitCardListAdapter) adapter; + super.setAdapter(adapter); + } + + public void setController(@Nullable Controller controller) + { + this.controller = controller; + setDropListener(controller); + setDragListener(controller); + setOnItemClickListener(null); + setOnLongClickListener(null); + + if (controller == null) return; + + setOnItemClickListener((p, v, pos, id) -> controller.onItemClick(pos)); + setOnItemLongClickListener((p, v, pos, id) -> { + controller.onItemLongClick(pos); + return true; + }); + } + + @Override + protected void onAttachedToWindow() + { + super.onAttachedToWindow(); + if (adapter != null) adapter.onAttached(); + } + + @Override + protected void onDetachedFromWindow() + { + if (adapter != null) adapter.onDetached(); + super.onDetachedFromWindow(); + } + + public interface Controller extends CheckmarkButtonController.Listener, + HabitCardController.Listener, + DropListener, + DragListener + { + void onItemClick(int pos); + + void onItemLongClick(int pos); + } + + private class ViewManager extends DragSortController + { + public ViewManager() + { + super(HabitCardListView.this); + setRemoveEnabled(false); + } + + @Override + public View onCreateFloatView(int position) + { + if (adapter == null) return null; + return adapter.getView(position, null, null); + } + + @Override + public void onDestroyFloatView(View floatView) + { + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/views/HabitCardView.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/views/HabitCardView.java new file mode 100644 index 000000000..6435d61af --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/views/HabitCardView.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.list.views; + +import android.annotation.*; +import android.content.*; +import android.graphics.drawable.*; +import android.os.*; +import android.util.*; +import android.view.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.ui.common.views.*; +import org.isoron.uhabits.ui.habits.list.*; +import org.isoron.uhabits.utils.*; + +import java.util.*; + +import butterknife.*; + +import static org.isoron.uhabits.utils.InterfaceUtils.*; + +public class HabitCardView extends FrameLayout +{ + private Habit habit; + + @BindView(R.id.checkmarkPanel) + CheckmarkPanelView checkmarkPanel; + + @BindView(R.id.innerFrame) + LinearLayout innerFrame; + + @BindView(R.id.label) + TextView label; + + @BindView(R.id.scoreRing) + RingView scoreRing; + + private final Context context = getContext(); + + public HabitCardView(Context context) + { + super(context); + init(); + } + + public HabitCardView(Context context, AttributeSet attrs) + { + super(context, attrs); + init(); + } + + public HabitCardView(Context context, AttributeSet attrs, int defStyleAttr) + { + super(context, attrs, defStyleAttr); + init(); + } + + public void setCheckmarkValues(int checkmarks[]) + { + int count = ListHabitsRootView.getCheckmarkCount(this); + int visibleCheckmarks[] = Arrays.copyOfRange(checkmarks, 0, count); + checkmarkPanel.setCheckmarkValues(visibleCheckmarks); + postInvalidate(); + } + + public void setController(Controller controller) + { + checkmarkPanel.setController(null); + if (controller == null) return; + + checkmarkPanel.setController(controller); + } + + public void setHabit(Habit habit) + { + this.habit = habit; + int color = getActiveColor(habit); + + label.setText(habit.getName()); + label.setTextColor(color); + scoreRing.setColor(color); + checkmarkPanel.setColor(color); + checkmarkPanel.setHabit(habit); + + postInvalidate(); + } + + public void setScore(int score) + { + float percentage = (float) score / Score.MAX_VALUE; + scoreRing.setPercentage(percentage); + scoreRing.setPrecision(1.0f / 16); + postInvalidate(); + } + + @Override + public void setSelected(boolean isSelected) + { + super.setSelected(isSelected); + updateBackground(isSelected); + } + + public void triggerRipple(long timestamp) + { + long today = DateUtils.getStartOfToday(); + long day = DateUtils.millisecondsInOneDay; + int offset = (int) ((today - timestamp) / day); + CheckmarkButtonView button = checkmarkPanel.getButton(offset); + + float y = button.getHeight() / 2.0f; + float x = checkmarkPanel.getX() + button.getX() + button.getWidth() / 2; + triggerRipple(x, y); + } + + private int getActiveColor(Habit habit) + { + int mediumContrastColor = + getStyledColor(context, R.attr.mediumContrastTextColor); + int activeColor = ColorUtils.getColor(context, habit.getColor()); + if (habit.isArchived()) activeColor = mediumContrastColor; + + return activeColor; + } + + private void init() + { + setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + + inflate(context, R.layout.list_habits_card, this); + ButterKnife.bind(this); + + innerFrame.setOnTouchListener((v, event) -> { + if (android.os.Build.VERSION.SDK_INT >= 21) + v.getBackground().setHotspot(event.getX(), event.getY()); + return false; + }); + + if (isInEditMode()) initEditMode(); + } + + @SuppressLint("SetTextI18n") + private void initEditMode() + { + String habits[] = { + "Wake up early", + "Wash dishes", + "Exercise", + "Meditate", + "Play guitar", + "Wash clothes", + "Get a haircut" + }; + + Random rand = new Random(); + int color = ColorUtils.getAndroidTestColor(rand.nextInt(10)); + int[] values = { + rand.nextInt(3), + rand.nextInt(3), + rand.nextInt(3), + rand.nextInt(3), + rand.nextInt(3) + }; + + label.setText(habits[rand.nextInt(habits.length)]); + label.setTextColor(color); + scoreRing.setColor(color); + scoreRing.setPercentage(rand.nextFloat()); + checkmarkPanel.setColor(color); + checkmarkPanel.setCheckmarkValues(values); + } + + private void triggerRipple(final float x, final float y) + { + final Drawable background = innerFrame.getBackground(); + if (android.os.Build.VERSION.SDK_INT >= 21) background.setHotspot(x, y); + background.setState(new int[]{ + android.R.attr.state_pressed, android.R.attr.state_enabled + }); + new Handler().postDelayed(() -> background.setState(new int[]{}), 25); + } + + private void updateBackground(boolean isSelected) + { + if (android.os.Build.VERSION.SDK_INT >= 21) + { + if (isSelected) + innerFrame.setBackgroundResource(R.drawable.selected_box); + else innerFrame.setBackgroundResource(R.drawable.ripple); + } + else + { + Drawable background; + + if (isSelected) background = + getStyledDrawable(context, R.attr.selectedBackground); + else background = getStyledDrawable(context, R.attr.cardBackground); + + innerFrame.setBackgroundDrawable(background); + } + } + + public interface Controller extends CheckmarkPanelView.Controller + { + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/views/HeaderView.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/views/HeaderView.java new file mode 100644 index 000000000..084560c04 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/views/HeaderView.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.list.views; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.util.AttributeSet; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.isoron.uhabits.R; +import org.isoron.uhabits.ui.habits.list.*; +import org.isoron.uhabits.utils.DateUtils; + +import java.util.GregorianCalendar; + +public class HeaderView extends LinearLayout +{ + private static final int CHECKMARK_LEFT_TO_RIGHT = 0; + + private static final int CHECKMARK_RIGHT_TO_LEFT = 1; + + private final Context context; + + public HeaderView(Context context, AttributeSet attrs) + { + super(context, attrs); + this.context = context; + } + + private void createButtons() + { + removeAllViews(); + GregorianCalendar day = DateUtils.getStartOfTodayCalendar(); + double count = ListHabitsRootView.getCheckmarkCount(this); + + for (int i = 0; i < count; i++) + { + int position = 0; + + if (getCheckmarkOrder() == CHECKMARK_LEFT_TO_RIGHT) position = i; + + View tvDay = + inflate(context, R.layout.list_habits_header_checkmark, null); + TextView btCheck = (TextView) tvDay.findViewById(R.id.tvCheck); + btCheck.setText(DateUtils.formatHeaderDate(day)); + addView(tvDay, position); + day.add(GregorianCalendar.DAY_OF_MONTH, -1); + } + } + + private int getCheckmarkOrder() + { + if (isInEditMode()) return CHECKMARK_LEFT_TO_RIGHT; + + SharedPreferences prefs = + PreferenceManager.getDefaultSharedPreferences(getContext()); + boolean reverse = + prefs.getBoolean("pref_checkmark_reverse_order", false); + return reverse ? CHECKMARK_RIGHT_TO_LEFT : CHECKMARK_LEFT_TO_RIGHT; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) + { + createButtons(); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/views/HintView.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/views/HintView.java new file mode 100644 index 000000000..fc3f7a570 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/views/HintView.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.list.views; + +import android.animation.AnimatorListenerAdapter; +import android.annotation.SuppressLint; +import android.content.Context; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.TextView; + +import org.isoron.uhabits.R; +import org.isoron.uhabits.ui.habits.list.model.HintList; + +import java.util.Random; + +import butterknife.BindView; +import butterknife.ButterKnife; + +public class HintView extends FrameLayout +{ + @BindView(R.id.hintContent) + TextView hintContent; + + @Nullable + private HintList hintList; + + public HintView(Context context) + { + super(context); + init(); + } + + public HintView(Context context, AttributeSet attrs) + { + super(context, attrs); + init(); + } + + @Override + public void onAttachedToWindow() + { + super.onAttachedToWindow(); + showNext(); + } + + /** + * Sets the list of hints to be shown + * + * @param hintList the list of hints to be shown + */ + public void setHints(@Nullable HintList hintList) + { + this.hintList = hintList; + } + + private void dismiss() + { + animate().alpha(0f).setDuration(500).setListener(new DismissAnimator()); + } + + private void init() + { + addView(inflate(getContext(), R.layout.list_habits_hint, null)); + ButterKnife.bind(this); + + setVisibility(GONE); + setClickable(true); + setOnClickListener(v -> dismiss()); + + if (isInEditMode()) initEditMode(); + } + + @SuppressLint("SetTextI18n") + private void initEditMode() + { + String hints[] = { + "Cats are the most popular pet in the United States: There " + + "are 88 million pet cats and 74 million dogs.", + "A cat has been mayor of Talkeetna, Alaska, for 15 years. " + + "His name is Stubbs.", + "Cats can’t taste sweetness." + }; + + int k = new Random().nextInt(hints.length); + hintContent.setText(hints[k]); + setVisibility(VISIBLE); + setAlpha(1.0f); + } + + protected void showNext() + { + if (hintList == null) return; + if (!hintList.shouldShow()) return; + + String hint = hintList.pop(); + if (hint == null) return; + + hintContent.setText(hint); + requestLayout(); + + setAlpha(0.0f); + setVisibility(View.VISIBLE); + animate().alpha(1f).setDuration(500); + } + + private class DismissAnimator extends AnimatorListenerAdapter + { + @Override + public void onAnimationEnd(android.animation.Animator animation) + { + setVisibility(View.GONE); + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/show/ShowHabitActivity.java b/app/src/main/java/org/isoron/uhabits/ui/habits/show/ShowHabitActivity.java new file mode 100644 index 000000000..55e95d4e8 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/show/ShowHabitActivity.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.show; + +import android.content.*; +import android.net.*; +import android.os.*; +import android.support.annotation.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.ui.*; + +import javax.inject.*; + +/** + * Activity that allows the user to see more information about a single habit. + *

+ * Shows all the metadata for the habit, in addition to several charts. + */ +public class ShowHabitActivity extends BaseActivity +{ + @Inject + HabitList habitList; + + private ShowHabitController controller; + + private ShowHabitRootView rootView; + + private ShowHabitScreen screen; + + private ShowHabitsMenu menu; + + @Override + protected void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + HabitsApplication.getComponent().inject(this); + + Habit habit = getHabitFromIntent(); + rootView = new ShowHabitRootView(this, habit); + screen = new ShowHabitScreen(this, habit, rootView); + setScreen(screen); + + menu = new ShowHabitsMenu(this, screen); + screen.setMenu(menu); + + controller = new ShowHabitController(screen, habit); + rootView.setController(controller); + } + + @NonNull + private Habit getHabitFromIntent() + { + Uri data = getIntent().getData(); + Habit habit = habitList.getById(ContentUris.parseId(data)); + if (habit == null) throw new RuntimeException("habit not found"); + return habit; + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/show/ShowHabitController.java b/app/src/main/java/org/isoron/uhabits/ui/habits/show/ShowHabitController.java new file mode 100644 index 000000000..d05909f67 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/show/ShowHabitController.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.show; + +import android.support.annotation.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.commands.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.tasks.*; +import org.isoron.uhabits.ui.habits.edit.*; + +import javax.inject.*; + +public class ShowHabitController implements ShowHabitRootView.Controller, + HistoryEditorDialog.Controller +{ + @NonNull + private final ShowHabitScreen screen; + + @NonNull + private final Habit habit; + + @Inject + CommandRunner commandRunner; + + public ShowHabitController(@NonNull ShowHabitScreen screen, + @NonNull Habit habit) + { + HabitsApplication.getComponent().inject(this); + this.screen = screen; + this.habit = habit; + } + + @Override + public void onToolbarChanged() + { + screen.invalidateToolbar(); + } + + @Override + public void onEditHistoryButtonClick() + { + screen.showEditHistoryDialog(this); + } + + @Override + public void onToggleCheckmark(long timestamp) + { + new SimpleTask(() -> { + ToggleRepetitionCommand command; + command = new ToggleRepetitionCommand(habit, timestamp); + commandRunner.execute(command, null); + }).execute(); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/show/ShowHabitRootView.java b/app/src/main/java/org/isoron/uhabits/ui/habits/show/ShowHabitRootView.java new file mode 100644 index 000000000..265bbe21f --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/show/ShowHabitRootView.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.show; + +import android.content.*; +import android.support.annotation.*; +import android.support.v7.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.ui.*; +import org.isoron.uhabits.ui.habits.show.views.*; +import org.isoron.uhabits.utils.*; + +import butterknife.*; + +import static org.isoron.uhabits.utils.InterfaceUtils.*; + +public class ShowHabitRootView extends BaseRootView + implements ModelObservable.Listener +{ + @NonNull + private Habit habit; + + @BindView(R.id.frequencyCard) + FrequencyCard frequencyCard; + + @BindView(R.id.streakCard) + StreakCard streakCard; + + @BindView(R.id.subtitleCard) + SubtitleCard subtitleCard; + + @BindView(R.id.overviewCard) + OverviewCard overviewCard; + + @BindView(R.id.strengthCard) + ScoreCard scoreCard; + + @BindView(R.id.historyCard) + HistoryCard historyCard; + + @BindView(R.id.toolbar) + Toolbar toolbar; + + @NonNull + private Controller controller; + + public ShowHabitRootView(@NonNull Context context, + @NonNull Habit habit) + { + super(context); + this.habit = habit; + + addView(inflate(getContext(), R.layout.show_habit, null)); + ButterKnife.bind(this); + + controller = new Controller() {}; + + initCards(); + initToolbar(); + } + + @Override + public boolean getDisplayHomeAsUp() + { + return true; + } + + @NonNull + @Override + public Toolbar getToolbar() + { + return toolbar; + } + + public void setController(@NonNull Controller controller) + { + this.controller = controller; + historyCard.setController(controller); + } + + @Override + public int getToolbarColor() + { + if (!getStyledBoolean(getContext(), R.attr.useHabitColorAsPrimary)) + return super.getToolbarColor(); + + return ColorUtils.getColor(getContext(), habit.getColor()); + } + + @Override + public void onModelChange() + { + controller.onToolbarChanged(); + } + + @Override + protected void initToolbar() + { + super.initToolbar(); + toolbar.setTitle(habit.getName()); + } + + @Override + protected void onAttachedToWindow() + { + super.onAttachedToWindow(); + habit.getObservable().addListener(this); + } + + @Override + protected void onDetachedFromWindow() + { + habit.getObservable().removeListener(this); + super.onDetachedFromWindow(); + } + + private void initCards() + { + subtitleCard.setHabit(habit); + overviewCard.setHabit(habit); + scoreCard.setHabit(habit); + historyCard.setHabit(habit); + streakCard.setHabit(habit); + frequencyCard.setHabit(habit); + } + + public interface Controller extends HistoryCard.Controller + { + default void onToolbarChanged(){} + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/show/ShowHabitScreen.java b/app/src/main/java/org/isoron/uhabits/ui/habits/show/ShowHabitScreen.java new file mode 100644 index 000000000..d5f7cf21b --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/show/ShowHabitScreen.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.show; + +import android.support.annotation.*; +import android.support.v4.app.*; + +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.ui.*; +import org.isoron.uhabits.ui.habits.edit.*; + +public class ShowHabitScreen extends BaseScreen +{ + @NonNull + private final Habit habit; + + public ShowHabitScreen(@NonNull BaseActivity activity, + @NonNull Habit habit, + ShowHabitRootView view) + { + super(activity); + this.habit = habit; + setRootView(view); + } + + public void showEditHabitDialog() + { + Long id = habit.getId(); + if (id == null) throw new RuntimeException("habit not saved"); + + FragmentManager manager = activity.getSupportFragmentManager(); + EditHabitDialogFragment.newInstance(id).show(manager, "editHabit"); + } + + public void showEditHistoryDialog( + @NonNull HistoryEditorDialog.Controller controller) + { + HistoryEditorDialog dialog = new HistoryEditorDialog(); + dialog.setHabit(habit); + dialog.setController(controller); + dialog.show(activity.getSupportFragmentManager(), "historyEditor"); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/show/ShowHabitsMenu.java b/app/src/main/java/org/isoron/uhabits/ui/habits/show/ShowHabitsMenu.java new file mode 100644 index 000000000..41b37e043 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/show/ShowHabitsMenu.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.show; + +import android.support.annotation.*; +import android.view.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.ui.*; + +public class ShowHabitsMenu extends BaseMenu +{ + @NonNull + private final ShowHabitScreen screen; + + public ShowHabitsMenu(@NonNull BaseActivity activity, + @NonNull ShowHabitScreen screen) + { + super(activity); + this.screen = screen; + } + + @Override + public boolean onItemSelected(@NonNull MenuItem item) + { + switch (item.getItemId()) + { + case R.id.action_edit_habit: + screen.showEditHabitDialog(); + return true; + + default: + return false; + } + } + + @Override + protected int getMenuResourceId() + { + return R.menu.show_habit; + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/show/package-info.java b/app/src/main/java/org/isoron/uhabits/ui/habits/show/package-info.java new file mode 100644 index 000000000..3cdcf42b8 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/show/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2016 Á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 . + */ + +/** + * Provides activity that displays detailed habit information and related + * classes. + */ +package org.isoron.uhabits.ui.habits.show; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/FrequencyCard.java b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/FrequencyCard.java new file mode 100644 index 000000000..9db4db19c --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/FrequencyCard.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.show.views; + +import android.content.*; +import android.util.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.tasks.*; +import org.isoron.uhabits.ui.common.views.*; +import org.isoron.uhabits.utils.*; + +import java.util.*; + +import butterknife.*; + +public class FrequencyCard extends HabitCard +{ + @BindView(R.id.title) + TextView title; + + @BindView(R.id.frequencyChart) + FrequencyChart chart; + + public FrequencyCard(Context context) + { + super(context); + init(); + } + + public FrequencyCard(Context context, AttributeSet attrs) + { + super(context, attrs); + init(); + } + + @Override + protected void refreshData() + { + new RefreshTask().execute(); + } + + private void init() + { + inflate(getContext(), R.layout.show_habit_frequency, this); + ButterKnife.bind(this); + + if (isInEditMode()) initEditMode(); + } + + private void initEditMode() + { + int color = ColorUtils.getAndroidTestColor(1); + title.setTextColor(color); + chart.setColor(color); + chart.populateWithRandomData(); + } + + private class RefreshTask extends BaseTask + { + @Override + protected void doInBackground() + { + RepetitionList reps = getHabit().getRepetitions(); + HashMap frequency = reps.getWeekdayFrequency(); + chart.setFrequency(frequency); + } + + @Override + protected void onPreExecute() + { + super.onPreExecute(); + int color = + ColorUtils.getColor(getContext(), getHabit().getColor()); + title.setTextColor(color); + chart.setColor(color); + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/HabitCard.java b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/HabitCard.java new file mode 100644 index 000000000..625175d79 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/HabitCard.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.show.views; + +import android.content.*; +import android.support.annotation.*; +import android.util.*; +import android.widget.*; + +import org.isoron.uhabits.models.*; + +public abstract class HabitCard extends LinearLayout + implements ModelObservable.Listener +{ + @NonNull + private Habit habit; + + public HabitCard(Context context) + { + super(context); + init(); + } + + public HabitCard(Context context, AttributeSet attrs) + { + super(context, attrs); + init(); + } + + @NonNull + public Habit getHabit() + { + return habit; + } + + public void setHabit(@NonNull Habit habit) + { + detachFrom(this.habit); + attachTo(habit); + + this.habit = habit; + } + + @Override + public void onModelChange() + { + post(() -> refreshData()); + } + + @Override + protected void onAttachedToWindow() + { + if(isInEditMode()) return; + + super.onAttachedToWindow(); + refreshData(); + attachTo(habit); + } + + @Override + protected void onDetachedFromWindow() + { + detachFrom(habit); + super.onDetachedFromWindow(); + } + + protected abstract void refreshData(); + + private void attachTo(Habit habit) + { + habit.getObservable().addListener(this); + habit.getRepetitions().getObservable().addListener(this); + } + + private void detachFrom(Habit habit) + { + habit.getRepetitions().getObservable().removeListener(this); + habit.getObservable().removeListener(this); + } + + private void init() + { + if(!isInEditMode()) habit = new Habit(); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/HistoryCard.java b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/HistoryCard.java new file mode 100644 index 000000000..c901595ed --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/HistoryCard.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.show.views; + +import android.content.*; +import android.support.annotation.*; +import android.util.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.tasks.*; +import org.isoron.uhabits.ui.common.views.*; +import org.isoron.uhabits.utils.*; + +import butterknife.*; + +public class HistoryCard extends HabitCard +{ + @BindView(R.id.historyChart) + HistoryChart chart; + + @BindView(R.id.title) + TextView title; + + @NonNull + private Controller controller; + + public HistoryCard(Context context) + { + super(context); + init(); + } + + public HistoryCard(Context context, AttributeSet attrs) + { + super(context, attrs); + init(); + } + + @OnClick(R.id.edit) + public void onClickEditButton() + { + controller.onEditHistoryButtonClick(); + } + + public void setController(@NonNull Controller controller) + { + this.controller = controller; + chart.setController(controller); + } + + @Override + protected void refreshData() + { + Habit habit = getHabit(); + + new BaseTask() + { + @Override + protected void doInBackground() + { + int checkmarks[] = habit.getCheckmarks().getAllValues(); + chart.setCheckmarks(checkmarks); + } + + @Override + protected void onPreExecute() + { + super.onPreExecute(); + int color = ColorUtils.getColor(getContext(), habit.getColor()); + title.setTextColor(color); + chart.setColor(color); + } + }.execute(); + } + + private void init() + { + inflate(getContext(), R.layout.show_habit_history, this); + ButterKnife.bind(this); + controller = new Controller() {}; + if (isInEditMode()) initEditMode(); + } + + private void initEditMode() + { + int color = ColorUtils.getAndroidTestColor(1); + title.setTextColor(color); + chart.setColor(color); + chart.populateWithRandomData(); + } + + public interface Controller extends HistoryChart.Controller + { + default void onEditHistoryButtonClick() {} + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/OverviewCard.java b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/OverviewCard.java new file mode 100644 index 000000000..378b19650 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/OverviewCard.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.show.views; + +import android.content.*; +import android.support.annotation.*; +import android.util.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.tasks.*; +import org.isoron.uhabits.ui.common.views.*; +import org.isoron.uhabits.utils.*; + +import butterknife.*; + +public class OverviewCard extends HabitCard +{ + @NonNull + private Cache cache; + + @BindView(R.id.scoreRing) + RingView scoreRing; + + @BindView(R.id.scoreLabel) + TextView scoreLabel; + + @BindView(R.id.monthDiffLabel) + TextView monthDiffLabel; + + @BindView(R.id.yearDiffLabel) + TextView yearDiffLabel; + + @BindView(R.id.title) + TextView title; + + private int color; + + public OverviewCard(Context context) + { + super(context); + init(); + } + + public OverviewCard(Context context, AttributeSet attrs) + { + super(context, attrs); + init(); + } + + @Override + protected void refreshData() + { + new RefreshTask().execute(); + } + + private String formatPercentageDiff(float percentageDiff) + { + return String.format("%s%.0f%%", (percentageDiff >= 0 ? "+" : "\u2212"), + Math.abs(percentageDiff) * 100); + } + + private void init() + { + inflate(getContext(), R.layout.show_habit_overview, this); + ButterKnife.bind(this); + cache = new Cache(); + + if (isInEditMode()) initEditMode(); + } + + private void initEditMode() + { + color = ColorUtils.getAndroidTestColor(1); + cache.todayScore = Score.MAX_VALUE * 0.6f; + cache.lastMonthScore = Score.MAX_VALUE * 0.42f; + cache.lastYearScore = Score.MAX_VALUE * 0.75f; + refreshColors(); + refreshScore(); + } + + private void refreshColors() + { + scoreRing.setColor(color); + scoreLabel.setTextColor(color); + title.setTextColor(color); + } + + private void refreshScore() + { + float todayPercentage = cache.todayScore / Score.MAX_VALUE; + float monthDiff = + todayPercentage - (cache.lastMonthScore / Score.MAX_VALUE); + float yearDiff = + todayPercentage - (cache.lastYearScore / Score.MAX_VALUE); + + scoreRing.setPercentage(todayPercentage); + scoreLabel.setText(String.format("%.0f%%", todayPercentage * 100)); + + monthDiffLabel.setText(formatPercentageDiff(monthDiff)); + yearDiffLabel.setText(formatPercentageDiff(yearDiff)); + + int inactiveColor = InterfaceUtils.getStyledColor(getContext(), + R.attr.mediumContrastTextColor); + + monthDiffLabel.setTextColor(monthDiff >= 0 ? color : inactiveColor); + yearDiffLabel.setTextColor(yearDiff >= 0 ? color : inactiveColor); + + postInvalidate(); + } + + private class Cache + { + public float todayScore; + + public float lastMonthScore; + + public float lastYearScore; + } + + private class RefreshTask extends BaseTask + { + @Override + protected void doInBackground() + { + ScoreList scores = getHabit().getScores(); + + long today = DateUtils.getStartOfToday(); + long lastMonth = today - 30 * DateUtils.millisecondsInOneDay; + long lastYear = today - 365 * DateUtils.millisecondsInOneDay; + + cache.todayScore = (float) scores.getTodayValue(); + cache.lastMonthScore = (float) scores.getValue(lastMonth); + cache.lastYearScore = (float) scores.getValue(lastYear); + } + + @Override + protected void onPostExecute(Void aVoid) + { + refreshScore(); + super.onPostExecute(aVoid); + } + + @Override + protected void onPreExecute() + { + super.onPreExecute(); + color = ColorUtils.getColor(getContext(), getHabit().getColor()); + refreshColors(); + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/ScoreCard.java b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/ScoreCard.java new file mode 100644 index 000000000..b11a58571 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/ScoreCard.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.show.views; + +import android.content.*; +import android.support.annotation.*; +import android.util.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.tasks.*; +import org.isoron.uhabits.ui.common.views.*; +import org.isoron.uhabits.utils.*; + +import java.util.*; + +import butterknife.*; + +public class ScoreCard extends HabitCard +{ + public static final int[] BUCKET_SIZES = { 1, 7, 31, 92, 365 }; + + @BindView(R.id.spinner) + Spinner spinner; + + @BindView(R.id.scoreView) + ScoreChart chart; + + @BindView(R.id.title) + TextView title; + + private int bucketSize; + + public ScoreCard(Context context) + { + super(context); + init(); + } + + public ScoreCard(Context context, AttributeSet attrs) + { + super(context, attrs); + init(); + } + + @NonNull + public static DateUtils.TruncateField getTruncateField(int bucketSize) + { + if (bucketSize == 7) return DateUtils.TruncateField.WEEK_NUMBER; + if (bucketSize == 31) return DateUtils.TruncateField.MONTH; + if (bucketSize == 92) return DateUtils.TruncateField.QUARTER; + if (bucketSize == 365) return DateUtils.TruncateField.YEAR; + + Log.e("ScoreCard", + String.format("Unknown bucket size: %d", bucketSize)); + + return DateUtils.TruncateField.MONTH; + } + + @OnItemSelected(R.id.spinner) + public void onItemSelected(int position) + { + setBucketSizeFromPosition(position); + HabitsApplication.getWidgetManager().updateWidgets(); + refreshData(); + } + + @Override + protected void refreshData() + { + new RefreshTask().execute(); + } + + private int getDefaultSpinnerPosition() + { + if (isInEditMode()) return 0; + return InterfaceUtils.getDefaultScoreSpinnerPosition(getContext()); + } + + private void init() + { + inflate(getContext(), R.layout.show_habit_score, this); + ButterKnife.bind(this); + + int defaultPosition = getDefaultSpinnerPosition(); + setBucketSizeFromPosition(defaultPosition); + spinner.setSelection(defaultPosition); + + if (isInEditMode()) + { + spinner.setVisibility(GONE); + title.setTextColor(ColorUtils.getAndroidTestColor(1)); + chart.setColor(ColorUtils.getAndroidTestColor(1)); + chart.populateWithRandomData(); + } + } + + private void setBucketSizeFromPosition(int position) + { + if (isInEditMode()) return; + + InterfaceUtils.setDefaultScoreSpinnerPosition(getContext(), position); + bucketSize = BUCKET_SIZES[position]; + } + + private class RefreshTask extends BaseTask + { + @Override + protected void doInBackground() + { + List scores; + ScoreList scoreList = getHabit().getScores(); + + if (bucketSize == 1) scores = scoreList.getAll(); + else scores = scoreList.groupBy(getTruncateField(bucketSize)); + + chart.setScores(scores); + chart.setBucketSize(bucketSize); + } + + @Override + protected void onPreExecute() + { + super.onPreExecute(); + int color = + ColorUtils.getColor(getContext(), getHabit().getColor()); + title.setTextColor(color); + chart.setColor(color); + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/StreakCard.java b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/StreakCard.java new file mode 100644 index 000000000..4a7a0b25b --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/StreakCard.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.show.views; + +import android.content.*; +import android.util.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.tasks.*; +import org.isoron.uhabits.ui.common.views.*; +import org.isoron.uhabits.utils.*; + +import java.util.*; + +import butterknife.*; + +public class StreakCard extends HabitCard +{ + public static final int NUM_STREAKS = 10; + + @BindView(R.id.title) + TextView title; + + @BindView(R.id.streakChart) + StreakChart streakChart; + + public StreakCard(Context context) + { + super(context); + init(); + } + + public StreakCard(Context context, AttributeSet attrs) + { + super(context, attrs); + init(); + } + + @Override + protected void refreshData() + { + new RefreshTask().execute(); + } + + private void init() + { + inflate(getContext(), R.layout.show_habit_streak, this); + ButterKnife.bind(this); + setOrientation(VERTICAL); + if (isInEditMode()) initEditMode(); + } + + private void initEditMode() + { + int color = ColorUtils.getAndroidTestColor(1); + title.setTextColor(color); + streakChart.setColor(color); + streakChart.populateWithRandomData(); + } + + private class RefreshTask extends BaseTask + { + public List bestStreaks; + + @Override + protected void doInBackground() + { + StreakList streaks = getHabit().getStreaks(); + bestStreaks = streaks.getBest(NUM_STREAKS); + } + + @Override + protected void onPostExecute(Void aVoid) + { + streakChart.setStreaks(bestStreaks); + super.onPostExecute(aVoid); + } + + @Override + protected void onPreExecute() + { + super.onPreExecute(); + int color = + ColorUtils.getColor(getContext(), getHabit().getColor()); + title.setTextColor(color); + streakChart.setColor(color); + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/SubtitleCard.java b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/SubtitleCard.java new file mode 100644 index 000000000..0f075b163 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/SubtitleCard.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2016 Á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.ui.habits.show.views; + +import android.annotation.*; +import android.content.*; +import android.content.res.*; +import android.util.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; + +import butterknife.*; + +public class SubtitleCard extends HabitCard +{ + @BindView(R.id.questionLabel) + TextView questionLabel; + + @BindView(R.id.frequencyLabel) + TextView frequencyLabel; + + @BindView(R.id.reminderLabel) + TextView reminderLabel; + + public SubtitleCard(Context context) + { + super(context); + init(); + } + + public SubtitleCard(Context context, AttributeSet attrs) + { + super(context, attrs); + init(); + } + + @Override + protected void refreshData() + { + Habit habit = getHabit(); + int color = ColorUtils.getColor(getContext(), habit.getColor()); + + reminderLabel.setText(getResources().getString(R.string.reminder_off)); + questionLabel.setVisibility(VISIBLE); + + questionLabel.setTextColor(color); + questionLabel.setText(habit.getDescription()); + frequencyLabel.setText(toText(habit.getFrequency())); + + if (habit.hasReminder()) updateReminderText(habit.getReminder()); + + if (habit.getDescription().isEmpty()) questionLabel.setVisibility(GONE); + + invalidate(); + } + + private void init() + { + Context context = getContext(); + inflate(context, R.layout.show_habit_subtitle, this); + ButterKnife.bind(this); + + if (isInEditMode()) initEditMode(); + } + + @SuppressLint("SetTextI18n") + private void initEditMode() + { + questionLabel.setTextColor(ColorUtils.getAndroidTestColor(1)); + questionLabel.setText("Have you meditated today?"); + reminderLabel.setText("08:00"); + } + + private String toText(Frequency freq) + { + Resources resources = getResources(); + Integer num = freq.getNumerator(); + Integer den = freq.getDenominator(); + + if (num.equals(den)) return resources.getString(R.string.every_day); + + if (num == 1) + { + if (den == 7) return resources.getString(R.string.every_week); + if (den % 7 == 0) + return resources.getString(R.string.every_x_weeks, den / 7); + return resources.getString(R.string.every_x_days, den); + } + + String times_every = resources.getString(R.string.times_every); + return String.format("%d %s %d %s", num, times_every, den, + resources.getString(R.string.days)); + } + + private void updateReminderText(Reminder reminder) + { + reminderLabel.setText( + DateUtils.formatTime(getContext(), reminder.getHour(), + reminder.getMinute())); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/package-info.java b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/package-info.java new file mode 100644 index 000000000..5442e7bcf --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2016 Á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 . + */ + +/** + * Provides custom views that are used primarily on {@link + * org.isoron.uhabits.ui.habits.show.ShowHabitActivity}. + */ +package org.isoron.uhabits.ui.habits.show.views; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/IntroActivity.java b/app/src/main/java/org/isoron/uhabits/ui/intro/IntroActivity.java similarity index 73% rename from app/src/main/java/org/isoron/uhabits/IntroActivity.java rename to app/src/main/java/org/isoron/uhabits/ui/intro/IntroActivity.java index e298d1c63..0e6bcdf45 100644 --- a/app/src/main/java/org/isoron/uhabits/IntroActivity.java +++ b/app/src/main/java/org/isoron/uhabits/ui/intro/IntroActivity.java @@ -17,7 +17,7 @@ * with this program. If not, see . */ -package org.isoron.uhabits; +package org.isoron.uhabits.ui.intro; import android.graphics.Color; import android.os.Bundle; @@ -25,6 +25,12 @@ import android.os.Bundle; import com.github.paolorotolo.appintro.AppIntro2; import com.github.paolorotolo.appintro.AppIntroFragment; +import org.isoron.uhabits.R; + +/** + * Activity that introduces the app to the user, shown only after the app is + * launched for the first time. + */ public class IntroActivity extends AppIntro2 { @Override @@ -33,16 +39,16 @@ public class IntroActivity extends AppIntro2 showStatusBar(false); addSlide(AppIntroFragment.newInstance(getString(R.string.intro_title_1), - getString(R.string.intro_description_1), R.drawable.intro_icon_1, - Color.parseColor("#194673"))); + getString(R.string.intro_description_1), R.drawable.intro_icon_1, + Color.parseColor("#194673"))); addSlide(AppIntroFragment.newInstance(getString(R.string.intro_title_2), - getString(R.string.intro_description_2), R.drawable.intro_icon_2, - Color.parseColor("#ffa726"))); + getString(R.string.intro_description_2), R.drawable.intro_icon_2, + Color.parseColor("#ffa726"))); addSlide(AppIntroFragment.newInstance(getString(R.string.intro_title_4), - getString(R.string.intro_description_4), R.drawable.intro_icon_4, - Color.parseColor("#9575cd"))); + getString(R.string.intro_description_4), R.drawable.intro_icon_4, + Color.parseColor("#9575cd"))); } @Override diff --git a/app/src/main/java/org/isoron/uhabits/ui/intro/package-info.java b/app/src/main/java/org/isoron/uhabits/ui/intro/package-info.java new file mode 100644 index 000000000..200ce960c --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/intro/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2016 Á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 . + */ + +/** + * Provides activity that introduces app to the user and related classes. + */ +package org.isoron.uhabits.ui.intro; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/ui/package-info.java b/app/src/main/java/org/isoron/uhabits/ui/package-info.java new file mode 100644 index 000000000..6eb8588d5 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2016 Á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 . + */ + +/** + * Provides classes for the Android user interface. + */ +package org.isoron.uhabits.ui; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/dialogs/FilePickerDialog.java b/app/src/main/java/org/isoron/uhabits/ui/settings/FilePickerDialog.java similarity index 87% rename from app/src/main/java/org/isoron/uhabits/dialogs/FilePickerDialog.java rename to app/src/main/java/org/isoron/uhabits/ui/settings/FilePickerDialog.java index 94c574fbd..8d7faa1c7 100644 --- a/app/src/main/java/org/isoron/uhabits/dialogs/FilePickerDialog.java +++ b/app/src/main/java/org/isoron/uhabits/ui/settings/FilePickerDialog.java @@ -17,7 +17,7 @@ * with this program. If not, see . */ -package org.isoron.uhabits.dialogs; +package org.isoron.uhabits.ui.settings; import android.app.Activity; import android.app.Dialog; @@ -39,8 +39,11 @@ public class FilePickerDialog implements AdapterView.OnItemClickListener private static final String PARENT_DIR = ".."; private final Activity activity; + private ListView list; + private Dialog dialog; + private File currentPath; public interface OnFileSelectedListener @@ -59,21 +62,24 @@ public class FilePickerDialog implements AdapterView.OnItemClickListener dialog = new Dialog(activity); dialog.setContentView(list); - dialog.getWindow().setLayout(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + dialog + .getWindow() + .setLayout(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); navigateTo(initialDirectory); } @Override - public void onItemClick(AdapterView parent, View view, int which, long id) + public void onItemClick(AdapterView parent, + View view, + int which, + long id) { String filename = (String) list.getItemAtPosition(which); File file; - if (filename.equals(PARENT_DIR)) - file = currentPath.getParentFile(); - else - file = new File(currentPath, filename); + if (filename.equals(PARENT_DIR)) file = currentPath.getParentFile(); + else file = new File(currentPath, filename); if (file.isDirectory()) { @@ -102,7 +108,7 @@ public class FilePickerDialog implements AdapterView.OnItemClickListener File[] dirs = path.listFiles(new ReadableDirFilter()); File[] files = path.listFiles(new RegularReadableFileFilter()); - if(dirs == null || files == null) return; + if (dirs == null || files == null) return; this.currentPath = path; dialog.setTitle(currentPath.getPath()); @@ -142,7 +148,8 @@ public class FilePickerDialog implements AdapterView.OnItemClickListener { public FilePickerAdapter(@NonNull String[] fileList) { - super(FilePickerDialog.this.activity, android.R.layout.simple_list_item_1, fileList); + super(FilePickerDialog.this.activity, + android.R.layout.simple_list_item_1, fileList); } @Override diff --git a/app/src/main/java/org/isoron/uhabits/SettingsActivity.java b/app/src/main/java/org/isoron/uhabits/ui/settings/SettingsActivity.java similarity index 68% rename from app/src/main/java/org/isoron/uhabits/SettingsActivity.java rename to app/src/main/java/org/isoron/uhabits/ui/settings/SettingsActivity.java index 579e7a7a6..24198dc7b 100644 --- a/app/src/main/java/org/isoron/uhabits/SettingsActivity.java +++ b/app/src/main/java/org/isoron/uhabits/ui/settings/SettingsActivity.java @@ -17,12 +17,20 @@ * with this program. If not, see . */ -package org.isoron.uhabits; +package org.isoron.uhabits.ui.settings; -import android.os.Bundle; +import android.os.*; +import android.support.annotation.*; +import android.support.v4.app.*; +import android.view.*; -import org.isoron.uhabits.helpers.UIHelper; +import org.isoron.uhabits.*; +import org.isoron.uhabits.ui.*; +import org.isoron.uhabits.utils.*; +/** + * Activity that allows the user to view and modify the app settings. + */ public class SettingsActivity extends BaseActivity { @Override @@ -30,9 +38,9 @@ public class SettingsActivity extends BaseActivity { super.onCreate(savedInstanceState); setContentView(R.layout.settings_activity); - setupSupportActionBar(true); - int color = UIHelper.getStyledColor(this, R.attr.aboutScreenColor); - setupActionBarColor(color); + int color = + InterfaceUtils.getStyledColor(this, R.attr.aboutScreenColor); + BaseScreen.setupActionBarColor(this, color); } } diff --git a/app/src/main/java/org/isoron/uhabits/fragments/SettingsFragment.java b/app/src/main/java/org/isoron/uhabits/ui/settings/SettingsFragment.java similarity index 66% rename from app/src/main/java/org/isoron/uhabits/fragments/SettingsFragment.java rename to app/src/main/java/org/isoron/uhabits/ui/settings/SettingsFragment.java index 29ab76a4c..40bcd8c96 100644 --- a/app/src/main/java/org/isoron/uhabits/fragments/SettingsFragment.java +++ b/app/src/main/java/org/isoron/uhabits/ui/settings/SettingsFragment.java @@ -17,7 +17,7 @@ * with this program. If not, see . */ -package org.isoron.uhabits.fragments; +package org.isoron.uhabits.ui.settings; import android.app.backup.BackupManager; import android.content.Intent; @@ -27,13 +27,13 @@ import android.support.v7.preference.Preference; import android.support.v7.preference.PreferenceCategory; import android.support.v7.preference.PreferenceFragmentCompat; -import org.isoron.uhabits.MainActivity; +import org.isoron.uhabits.HabitsApplication; import org.isoron.uhabits.R; -import org.isoron.uhabits.helpers.ReminderHelper; -import org.isoron.uhabits.helpers.UIHelper; +import org.isoron.uhabits.utils.InterfaceUtils; +import org.isoron.uhabits.utils.ReminderUtils; public class SettingsFragment extends PreferenceFragmentCompat - implements SharedPreferences.OnSharedPreferenceChangeListener + implements SharedPreferences.OnSharedPreferenceChangeListener { private static int RINGTONE_REQUEST_CODE = 1; @@ -43,14 +43,18 @@ public class SettingsFragment extends PreferenceFragmentCompat super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.preferences); - setResultOnPreferenceClick("importData", MainActivity.RESULT_IMPORT_DATA); - setResultOnPreferenceClick("exportCSV", MainActivity.RESULT_EXPORT_CSV); - setResultOnPreferenceClick("exportDB", MainActivity.RESULT_EXPORT_DB); - setResultOnPreferenceClick("bugReport", MainActivity.RESULT_BUG_REPORT); + setResultOnPreferenceClick("importData", + HabitsApplication.RESULT_IMPORT_DATA); + setResultOnPreferenceClick("exportCSV", + HabitsApplication.RESULT_EXPORT_CSV); + setResultOnPreferenceClick("exportDB", + HabitsApplication.RESULT_EXPORT_DB); + setResultOnPreferenceClick("bugReport", + HabitsApplication.RESULT_BUG_REPORT); updateRingtoneDescription(); - if(UIHelper.isLocaleFullyTranslated()) + if (InterfaceUtils.isLocaleFullyTranslated()) removePreference("translate", "linksCategory"); } @@ -62,7 +66,8 @@ public class SettingsFragment extends PreferenceFragmentCompat private void removePreference(String preferenceKey, String categoryKey) { - PreferenceCategory cat = (PreferenceCategory) findPreference(categoryKey); + PreferenceCategory cat = + (PreferenceCategory) findPreference(categoryKey); Preference pref = findPreference(preferenceKey); cat.removePreference(pref); } @@ -70,15 +75,10 @@ public class SettingsFragment extends PreferenceFragmentCompat private void setResultOnPreferenceClick(String key, final int result) { Preference pref = findPreference(key); - pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() - { - @Override - public boolean onPreferenceClick(Preference preference) - { - getActivity().setResult(result); - getActivity().finish(); - return true; - } + pref.setOnPreferenceClickListener(preference -> { + getActivity().setResult(result); + getActivity().finish(); + return true; }); } @@ -87,19 +87,20 @@ public class SettingsFragment extends PreferenceFragmentCompat { super.onResume(); getPreferenceManager().getSharedPreferences(). - registerOnSharedPreferenceChangeListener(this); + registerOnSharedPreferenceChangeListener(this); } @Override public void onPause() { getPreferenceManager().getSharedPreferences(). - unregisterOnSharedPreferenceChangeListener(this); + unregisterOnSharedPreferenceChangeListener(this); super.onPause(); } @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, + String key) { BackupManager.dataChanged("org.isoron.uhabits"); } @@ -107,11 +108,12 @@ public class SettingsFragment extends PreferenceFragmentCompat @Override public boolean onPreferenceTreeClick(Preference preference) { - if(preference.getKey() == null) return false; + if (preference.getKey() == null) return false; if (preference.getKey().equals("reminderSound")) { - ReminderHelper.startRingtonePickerActivity(this, RINGTONE_REQUEST_CODE); + ReminderUtils.startRingtonePickerActivity(this, + RINGTONE_REQUEST_CODE); return true; } @@ -121,9 +123,9 @@ public class SettingsFragment extends PreferenceFragmentCompat @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { - if(requestCode == RINGTONE_REQUEST_CODE) + if (requestCode == RINGTONE_REQUEST_CODE) { - ReminderHelper.parseRingtoneData(getContext(), data); + ReminderUtils.parseRingtoneData(getContext(), data); updateRingtoneDescription(); return; } @@ -133,7 +135,7 @@ public class SettingsFragment extends PreferenceFragmentCompat private void updateRingtoneDescription() { - String ringtoneName = ReminderHelper.getRingtoneName(getContext()); + String ringtoneName = ReminderUtils.getRingtoneName(getContext()); if(ringtoneName == null) return; Preference ringtonePreference = findPreference("reminderSound"); ringtonePreference.setSummary(ringtoneName); diff --git a/app/src/main/java/org/isoron/uhabits/ui/settings/package-info.java b/app/src/main/java/org/isoron/uhabits/ui/settings/package-info.java new file mode 100644 index 000000000..28a65b5ea --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/settings/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2016 Á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 . + */ + +/** + * Provides activity for changing the settings. + */ +package org.isoron.uhabits.ui.settings; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/ui/widgets/BaseWidget.java b/app/src/main/java/org/isoron/uhabits/ui/widgets/BaseWidget.java new file mode 100644 index 000000000..0f1de8c61 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/widgets/BaseWidget.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2016 Á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.ui.widgets; + +import android.app.*; +import android.content.*; +import android.graphics.*; +import android.support.annotation.*; +import android.view.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.utils.*; + +import javax.inject.*; + +import static android.os.Build.VERSION.*; +import static android.os.Build.VERSION_CODES.*; +import static android.view.View.MeasureSpec.*; + +public abstract class BaseWidget +{ + @Inject + WidgetPreferences preferences; + + private final int id; + + @NonNull + private WidgetDimensions dimensions; + + @NonNull + private final Context context; + + public BaseWidget(@NonNull Context context, int id) + { + this.id = id; + this.context = context; + HabitsApplication.getComponent().inject(this); + dimensions = new WidgetDimensions(0, 0, 0, 0); + } + + public void delete() + { + preferences.removeWidget(id); + } + + @NonNull + public Context getContext() + { + return context; + } + + public int getId() + { + return id; + } + + @NonNull + public RemoteViews getLandscapeRemoteViews() + { + return getRemoteViews(dimensions.getLandscapeWidth(), + dimensions.getLandscapeHeight()); + } + + public abstract PendingIntent getOnClickPendingIntent(Context context); + + @NonNull + public RemoteViews getPortraitRemoteViews() + { + return getRemoteViews(dimensions.getPortraitWidth(), + dimensions.getPortraitHeight()); + } + + public abstract void refreshData(View widgetView); + + public void setDimensions(@NonNull WidgetDimensions dimensions) + { + this.dimensions = dimensions; + } + + protected abstract View buildView(); + + protected abstract int getDefaultHeight(); + + protected abstract int getDefaultWidth(); + + private void adjustRemoteViewsPadding(RemoteViews remoteViews, + View view, + int width, + int height) + { + int imageWidth = view.getMeasuredWidth(); + int imageHeight = view.getMeasuredHeight(); + int p[] = calculatePadding(width, height, imageWidth, imageHeight); + remoteViews.setViewPadding(R.id.buttonOverlay, p[0], p[1], p[2], p[3]); + } + + private void buildRemoteViews(View view, + RemoteViews remoteViews, + int width, + int height) + { + Bitmap bitmap = getBitmapFromView(view); + remoteViews.setImageViewBitmap(R.id.imageView, bitmap); + + if (SDK_INT >= JELLY_BEAN) + adjustRemoteViewsPadding(remoteViews, view, width, height); + + PendingIntent onClickIntent = getOnClickPendingIntent(context); + if (onClickIntent != null) + remoteViews.setOnClickPendingIntent(R.id.button, onClickIntent); + } + + private int[] calculatePadding(int entireWidth, + int entireHeight, + int imageWidth, + int imageHeight) + { + int w = (int) (((float) entireWidth - imageWidth) / 2); + int h = (int) (((float) entireHeight - imageHeight) / 2); + + return new int[]{ w, h, w, h }; + } + + private Bitmap getBitmapFromView(View view) + { + view.invalidate(); + view.setDrawingCacheEnabled(true); + view.buildDrawingCache(true); + return view.getDrawingCache(); + } + + @NonNull + private RemoteViews getRemoteViews(int width, int height) + { + View view = buildView(); + measureView(view, width, height); + + refreshData(view); + + if (view.isLayoutRequested()) measureView(view, width, height); + + RemoteViews remoteViews = + new RemoteViews(context.getPackageName(), R.layout.widget_wrapper); + + buildRemoteViews(view, remoteViews, width, height); + + return remoteViews; + } + + private void measureView(View view, int width, int height) + { + LayoutInflater inflater = LayoutInflater.from(context); + View entireView = inflater.inflate(R.layout.widget_wrapper, null); + + int specWidth = makeMeasureSpec(width, View.MeasureSpec.EXACTLY); + int specHeight = makeMeasureSpec(height, View.MeasureSpec.EXACTLY); + + entireView.measure(specWidth, specHeight); + entireView.layout(0, 0, entireView.getMeasuredWidth(), + entireView.getMeasuredHeight()); + + View imageView = entireView.findViewById(R.id.imageView); + width = imageView.getMeasuredWidth(); + height = imageView.getMeasuredHeight(); + + specWidth = makeMeasureSpec(width, View.MeasureSpec.EXACTLY); + specHeight = makeMeasureSpec(height, View.MeasureSpec.EXACTLY); + + view.measure(specWidth, specHeight); + view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight()); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/widgets/CheckmarkWidget.java b/app/src/main/java/org/isoron/uhabits/ui/widgets/CheckmarkWidget.java new file mode 100644 index 000000000..5d7a73c23 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/widgets/CheckmarkWidget.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2016 Á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.ui.widgets; + +import android.app.*; +import android.content.*; +import android.support.annotation.*; +import android.view.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.ui.widgets.views.*; +import org.isoron.uhabits.utils.*; + +public class CheckmarkWidget extends BaseWidget +{ + @NonNull + private final Habit habit; + + public CheckmarkWidget(@NonNull Context context, + int widgetId, + @NonNull Habit habit) + { + super(context, widgetId); + this.habit = habit; + } + + @Override + public PendingIntent getOnClickPendingIntent(Context context) + { + return HabitPendingIntents.toggleCheckmark(context, habit, null, 2); + } + + @Override + public void refreshData(View v) + { + CheckmarkWidgetView view = (CheckmarkWidgetView) v; + int color = ColorUtils.getColor(getContext(), habit.getColor()); + int score = habit.getScores().getTodayValue(); + float percentage = (float) score / Score.MAX_VALUE; + int checkmark = habit.getCheckmarks().getTodayValue(); + + view.setPercentage(percentage); + view.setActiveColor(color); + view.setName(habit.getName()); + view.setCheckmarkValue(checkmark); + view.refresh(); + } + + @Override + protected View buildView() + { + return new CheckmarkWidgetView(getContext()); + } + + @Override + protected int getDefaultHeight() + { + return 125; + } + + @Override + protected int getDefaultWidth() + { + return 125; + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/widgets/FrequencyWidget.java b/app/src/main/java/org/isoron/uhabits/ui/widgets/FrequencyWidget.java new file mode 100644 index 000000000..0eb0c7dd1 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/widgets/FrequencyWidget.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2016 Á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.ui.widgets; + +import android.app.*; +import android.content.*; +import android.support.annotation.*; +import android.view.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.ui.common.views.*; +import org.isoron.uhabits.ui.widgets.views.*; +import org.isoron.uhabits.utils.*; + +public class FrequencyWidget extends BaseWidget +{ + @NonNull + private final Habit habit; + + public FrequencyWidget(@NonNull Context context, + int widgetId, + @NonNull Habit habit) + { + super(context, widgetId); + this.habit = habit; + } + + @Override + public PendingIntent getOnClickPendingIntent(Context context) + { + return HabitPendingIntents.viewHabit(context, habit); + } + + @Override + public void refreshData(View v) + { + GraphWidgetView widgetView = (GraphWidgetView) v; + FrequencyChart chart = (FrequencyChart) widgetView.getDataView(); + + widgetView.setTitle(habit.getName()); + + int color = ColorUtils.getColor(getContext(), habit.getColor()); + + chart.setColor(color); + chart.setFrequency(habit.getRepetitions().getWeekdayFrequency()); + } + + @Override + protected View buildView() + { + FrequencyChart chart = new FrequencyChart(getContext()); + return new GraphWidgetView(getContext(), chart); + } + + @Override + protected int getDefaultHeight() + { + return 200; + } + + @Override + protected int getDefaultWidth() + { + return 200; + } +} diff --git a/app/src/main/java/org/isoron/uhabits/widgets/HabitPickerDialog.java b/app/src/main/java/org/isoron/uhabits/ui/widgets/HabitPickerDialog.java similarity index 53% rename from app/src/main/java/org/isoron/uhabits/widgets/HabitPickerDialog.java rename to app/src/main/java/org/isoron/uhabits/ui/widgets/HabitPickerDialog.java index d09881acf..fe4b57de2 100644 --- a/app/src/main/java/org/isoron/uhabits/widgets/HabitPickerDialog.java +++ b/app/src/main/java/org/isoron/uhabits/ui/widgets/HabitPickerDialog.java @@ -17,77 +17,83 @@ * with this program. If not, see . */ -package org.isoron.uhabits.widgets; - -import android.app.Activity; -import android.appwidget.AppWidgetManager; -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.view.View; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.ListView; - -import org.isoron.uhabits.MainActivity; -import org.isoron.uhabits.R; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.widgets.BaseWidgetProvider; - -import java.util.ArrayList; -import java.util.List; - -public class HabitPickerDialog extends Activity implements AdapterView.OnItemClickListener +package org.isoron.uhabits.ui.widgets; + +import android.app.*; +import android.content.*; +import android.os.*; +import android.view.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; + +import java.util.*; + +import javax.inject.*; + +import static android.appwidget.AppWidgetManager.*; + +public class HabitPickerDialog extends Activity + implements AdapterView.OnItemClickListener { + @Inject + HabitList habitList; + + @Inject + WidgetPreferences preferences; private Integer widgetId; + private ArrayList habitIds; + @Override + public void onItemClick(AdapterView parent, + View view, + int position, + long id) + { + Long habitId = habitIds.get(position); + preferences.addWidget(widgetId, habitId); + HabitsApplication.getWidgetManager().updateWidgets(); + + Intent resultValue = new Intent(); + resultValue.putExtra(EXTRA_APPWIDGET_ID, widgetId); + setResult(RESULT_OK, resultValue); + finish(); + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.widget_configure_activity); + HabitsApplication.getComponent().inject(this); Intent intent = getIntent(); Bundle extras = intent.getExtras(); - if (extras != null) widgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, - AppWidgetManager.INVALID_APPWIDGET_ID); + if (extras != null) + widgetId = extras.getInt(EXTRA_APPWIDGET_ID, INVALID_APPWIDGET_ID); ListView listView = (ListView) findViewById(R.id.listView); habitIds = new ArrayList<>(); ArrayList habitNames = new ArrayList<>(); - List habits = Habit.getAll(false); - for(Habit h : habits) + List habits = habitList.getAll(false); + for (Habit h : habits) { habitIds.add(h.getId()); - habitNames.add(h.name); + habitNames.add(h.getName()); } ArrayAdapter adapter = - new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, habitNames); + new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, + habitNames); listView.setAdapter(adapter); listView.setOnItemClickListener(this); } - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) - { - Long habitId = habitIds.get(position); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences( - getApplicationContext()); - prefs.edit().putLong(BaseWidgetProvider.getHabitIdKey(widgetId), habitId).commit(); - - MainActivity.updateWidgets(this); - - Intent resultValue = new Intent(); - resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId); - setResult(RESULT_OK, resultValue); - finish(); - } - } diff --git a/app/src/main/java/org/isoron/uhabits/ui/widgets/HistoryWidget.java b/app/src/main/java/org/isoron/uhabits/ui/widgets/HistoryWidget.java new file mode 100644 index 000000000..0af19bc7f --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/widgets/HistoryWidget.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2016 Á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.ui.widgets; + +import android.app.*; +import android.content.*; +import android.support.annotation.*; +import android.view.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.ui.common.views.*; +import org.isoron.uhabits.ui.widgets.views.*; +import org.isoron.uhabits.utils.*; + +public class HistoryWidget extends BaseWidget +{ + @NonNull + private Habit habit; + + public HistoryWidget(@NonNull Context context, int id, @NonNull Habit habit) + { + super(context, id); + this.habit = habit; + } + + @Override + public PendingIntent getOnClickPendingIntent(Context context) + { + return HabitPendingIntents.viewHabit(context, habit); + } + + @Override + public void refreshData(View view) + { + GraphWidgetView widgetView = (GraphWidgetView) view; + HistoryChart chart = (HistoryChart) widgetView.getDataView(); + + int color = ColorUtils.getColor(getContext(), habit.getColor()); + int[] values = habit.getCheckmarks().getAllValues(); + + chart.setColor(color); + chart.setCheckmarks(values); + } + + @Override + protected View buildView() + { + HistoryChart dataView = new HistoryChart(getContext()); + GraphWidgetView widgetView = + new GraphWidgetView(getContext(), dataView); + widgetView.setTitle(habit.getName()); + return widgetView; + } + + @Override + protected int getDefaultHeight() + { + return 250; + } + + @Override + protected int getDefaultWidth() + { + return 250; + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/widgets/ScoreWidget.java b/app/src/main/java/org/isoron/uhabits/ui/widgets/ScoreWidget.java new file mode 100644 index 000000000..cafb7e854 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/widgets/ScoreWidget.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2016 Á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.ui.widgets; + +import android.app.*; +import android.content.*; +import android.support.annotation.*; +import android.view.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.ui.common.views.*; +import org.isoron.uhabits.ui.habits.show.views.*; +import org.isoron.uhabits.ui.widgets.views.*; +import org.isoron.uhabits.utils.*; + +import java.util.*; + +public class ScoreWidget extends BaseWidget +{ + @NonNull + private Habit habit; + + public ScoreWidget(@NonNull Context context, int id, @NonNull Habit habit) + { + super(context, id); + this.habit = habit; + } + + @Override + public PendingIntent getOnClickPendingIntent(Context context) + { + return HabitPendingIntents.viewHabit(context, habit); + } + + @Override + public void refreshData(View view) + { + int defaultScoreInterval = + InterfaceUtils.getDefaultScoreSpinnerPosition(getContext()); + int size = ScoreCard.BUCKET_SIZES[defaultScoreInterval]; + + GraphWidgetView widgetView = (GraphWidgetView) view; + ScoreChart chart = (ScoreChart) widgetView.getDataView(); + + List scores; + ScoreList scoreList = habit.getScores(); + + if (size == 1) scores = scoreList.getAll(); + else scores = scoreList.groupBy(ScoreCard.getTruncateField(size)); + + int color = ColorUtils.getColor(getContext(), habit.getColor()); + + chart.setIsTransparencyEnabled(true); + chart.setBucketSize(size); + chart.setColor(color); + chart.setScores(scores); + } + + @Override + protected View buildView() + { + ScoreChart dataView = new ScoreChart(getContext()); + GraphWidgetView view = new GraphWidgetView(getContext(), dataView); + view.setTitle(habit.getName()); + return view; + } + + @Override + protected int getDefaultHeight() + { + return 300; + } + + @Override + protected int getDefaultWidth() + { + return 300; + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/widgets/StreakWidget.java b/app/src/main/java/org/isoron/uhabits/ui/widgets/StreakWidget.java new file mode 100644 index 000000000..e3cf9330e --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/widgets/StreakWidget.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2016 Á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.ui.widgets; + +import android.app.*; +import android.content.*; +import android.support.annotation.*; +import android.view.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.ui.common.views.*; +import org.isoron.uhabits.ui.widgets.views.*; +import org.isoron.uhabits.utils.*; + +import java.util.*; + +public class StreakWidget extends BaseWidget +{ + @NonNull + private Habit habit; + + public StreakWidget(@NonNull Context context, int id, @NonNull Habit habit) + { + super(context, id); + this.habit = habit; + } + + @Override + public PendingIntent getOnClickPendingIntent(Context context) + { + return HabitPendingIntents.viewHabit(context, habit); + } + + @Override + public void refreshData(View view) + { + GraphWidgetView widgetView = (GraphWidgetView) view; + StreakChart chart = (StreakChart) widgetView.getDataView(); + + int color = ColorUtils.getColor(getContext(), habit.getColor()); + + // TODO: make this dynamic + List streaks = habit.getStreaks().getBest(10); + + chart.setColor(color); + chart.setStreaks(streaks); + } + + @Override + protected View buildView() + { + StreakChart dataView = new StreakChart(getContext()); + GraphWidgetView view = new GraphWidgetView(getContext(), dataView); + view.setTitle(habit.getName()); + return view; + } + + @Override + protected int getDefaultHeight() + { + return 200; + } + + @Override + protected int getDefaultWidth() + { + return 200; + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/widgets/WidgetDimensions.java b/app/src/main/java/org/isoron/uhabits/ui/widgets/WidgetDimensions.java new file mode 100644 index 000000000..28285e34b --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/widgets/WidgetDimensions.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2016 Á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.ui.widgets; + +public class WidgetDimensions +{ + private final int portraitWidth; + + private final int portraitHeight; + + private final int landscapeWidth; + + private final int landscapeHeight; + + public WidgetDimensions(int portraitWidth, + int portraitHeight, + int landscapeWidth, + int landscapeHeight) + { + this.portraitWidth = portraitWidth; + this.portraitHeight = portraitHeight; + this.landscapeWidth = landscapeWidth; + this.landscapeHeight = landscapeHeight; + } + + public int getLandscapeHeight() + { + return landscapeHeight; + } + + public int getLandscapeWidth() + { + return landscapeWidth; + } + + public int getPortraitHeight() + { + return portraitHeight; + } + + public int getPortraitWidth() + { + return portraitWidth; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/ui/widgets/WidgetUpdater.java b/app/src/main/java/org/isoron/uhabits/ui/widgets/WidgetUpdater.java new file mode 100644 index 000000000..069f57ea8 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/widgets/WidgetUpdater.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2016 Á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.ui.widgets; + +import android.appwidget.*; +import android.content.*; +import android.support.annotation.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.commands.*; +import org.isoron.uhabits.widgets.*; + +import javax.inject.*; + +/** + * A WidgetUpdater listens to the commands being executed by the application and + * updates the home-screen widgets accordingly. + *

+ * There should be only one instance of this class, created when the application + * starts. To access it, call HabitApplication.getWidgetUpdater(). + */ +public class WidgetUpdater implements CommandRunner.Listener +{ + @Inject + CommandRunner commandRunner; + + @NonNull + private final Context context; + + public WidgetUpdater(@NonNull Context context) + { + this.context = context; + HabitsApplication.getComponent().inject(this); + } + + @Override + public void onCommandExecuted(@NonNull Command command, + @Nullable Long refreshKey) + { + updateWidgets(); + } + + /** + * Instructs the updater to start listening to commands. If any relevant + * commands are executed after this method is called, the corresponding + * widgets will get updated. + */ + public void startListening() + { + commandRunner.addListener(this); + } + + /** + * Instructs the updater to stop listening to commands. Every command + * executed after this method is called will be ignored by the updater. + */ + public void stopListening() + { + commandRunner.removeListener(this); + } + + public void updateWidgets() + { + updateWidgets(CheckmarkWidgetProvider.class); + updateWidgets(HistoryWidgetProvider.class); + updateWidgets(ScoreWidgetProvider.class); + updateWidgets(StreakWidgetProvider.class); + updateWidgets(FrequencyWidgetProvider.class); + } + + public void updateWidgets(Class providerClass) + { + ComponentName provider = new ComponentName(context, providerClass); + Intent intent = new Intent(context, providerClass); + intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + int ids[] = + AppWidgetManager.getInstance(context).getAppWidgetIds(provider); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids); + context.sendBroadcast(intent); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/views/CheckmarkWidgetView.java b/app/src/main/java/org/isoron/uhabits/ui/widgets/views/CheckmarkWidgetView.java similarity index 62% rename from app/src/main/java/org/isoron/uhabits/views/CheckmarkWidgetView.java rename to app/src/main/java/org/isoron/uhabits/ui/widgets/views/CheckmarkWidgetView.java index 869c91afe..3d3bc3a8c 100644 --- a/app/src/main/java/org/isoron/uhabits/views/CheckmarkWidgetView.java +++ b/app/src/main/java/org/isoron/uhabits/ui/widgets/views/CheckmarkWidgetView.java @@ -17,32 +17,31 @@ * with this program. If not, see . */ -package org.isoron.uhabits.views; - -import android.content.Context; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.util.AttributeSet; -import android.util.TypedValue; -import android.widget.TextView; - -import org.isoron.uhabits.R; -import org.isoron.uhabits.helpers.ColorHelper; -import org.isoron.uhabits.helpers.UIHelper; -import org.isoron.uhabits.models.Checkmark; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.models.Score; - -public class CheckmarkWidgetView extends HabitWidgetView implements HabitDataView +package org.isoron.uhabits.ui.widgets.views; + +import android.content.*; +import android.support.annotation.*; +import android.util.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.ui.common.views.*; +import org.isoron.uhabits.utils.*; + +public class CheckmarkWidgetView extends HabitWidgetView { private int activeColor; + private float percentage; @Nullable private String name; private RingView ring; + private TextView label; + private int checkmarkValue; public CheckmarkWidgetView(Context context) @@ -57,32 +56,6 @@ public class CheckmarkWidgetView extends HabitWidgetView implements HabitDataVie init(); } - private void init() - { - ring = (RingView) findViewById(R.id.scoreRing); - label = (TextView) findViewById(R.id.label); - - if(ring != null) ring.setIsTransparencyEnabled(true); - - if(isInEditMode()) - { - percentage = 0.75f; - name = "Wake up early"; - activeColor = ColorHelper.CSV_PALETTE[6]; - checkmarkValue = Checkmark.CHECKED_EXPLICITLY; - refresh(); - } - } - - @Override - public void setHabit(@NonNull Habit habit) - { - super.setHabit(habit); - this.name = habit.name; - this.activeColor = ColorHelper.getColor(getContext(), habit.color); - refresh(); - } - public void refresh() { if (backgroundPaint == null || frame == null || ring == null) return; @@ -98,8 +71,8 @@ public class CheckmarkWidgetView extends HabitWidgetView implements HabitDataVie case Checkmark.CHECKED_EXPLICITLY: text = getResources().getString(R.string.fa_check); backgroundColor = activeColor; - foregroundColor = - UIHelper.getStyledColor(context, R.attr.highContrastReverseTextColor); + foregroundColor = InterfaceUtils.getStyledColor(context, + R.attr.highContrastReverseTextColor); setShadowAlpha(0x4f); rebuildBackground(); @@ -110,15 +83,27 @@ public class CheckmarkWidgetView extends HabitWidgetView implements HabitDataVie case Checkmark.CHECKED_IMPLICITLY: text = getResources().getString(R.string.fa_check); - backgroundColor = UIHelper.getStyledColor(context, R.attr.cardBackgroundColor); - foregroundColor = UIHelper.getStyledColor(context, R.attr.mediumContrastTextColor); + backgroundColor = InterfaceUtils.getStyledColor(context, + R.attr.cardBackgroundColor); + foregroundColor = InterfaceUtils.getStyledColor(context, + R.attr.mediumContrastTextColor); + + setShadowAlpha(0x00); + rebuildBackground(); + break; case Checkmark.UNCHECKED: default: text = getResources().getString(R.string.fa_times); - backgroundColor = UIHelper.getStyledColor(context, R.attr.cardBackgroundColor); - foregroundColor = UIHelper.getStyledColor(context, R.attr.mediumContrastTextColor); + backgroundColor = InterfaceUtils.getStyledColor(context, + R.attr.cardBackgroundColor); + foregroundColor = InterfaceUtils.getStyledColor(context, + R.attr.mediumContrastTextColor); + + setShadowAlpha(0x00); + rebuildBackground(); + break; } @@ -134,6 +119,33 @@ public class CheckmarkWidgetView extends HabitWidgetView implements HabitDataVie postInvalidate(); } + public void setCheckmarkValue(int checkmarkValue) + { + this.checkmarkValue = checkmarkValue; + } + + public void setName(@NonNull String name) + { + this.name = name; + } + + public void setPercentage(float percentage) + { + this.percentage = percentage; + } + + public void setActiveColor(int activeColor) + { + this.activeColor = activeColor; + } + + @Override + @NonNull + protected Integer getInnerLayoutId() + { + return R.layout.widget_checkmark; + } + @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { @@ -147,16 +159,18 @@ public class CheckmarkWidgetView extends HabitWidgetView implements HabitDataVie w *= scale; h *= scale; - if(h < getResources().getDimension(R.dimen.checkmarkWidget_heightBreakpoint)) - ring.setVisibility(GONE); - else - ring.setVisibility(VISIBLE); + if (h < getResources().getDimension( + R.dimen.checkmarkWidget_heightBreakpoint)) ring.setVisibility(GONE); + else ring.setVisibility(VISIBLE); - widthMeasureSpec = MeasureSpec.makeMeasureSpec((int) w, MeasureSpec.EXACTLY); - heightMeasureSpec = MeasureSpec.makeMeasureSpec((int) h, MeasureSpec.EXACTLY); + widthMeasureSpec = + MeasureSpec.makeMeasureSpec((int) w, MeasureSpec.EXACTLY); + heightMeasureSpec = + MeasureSpec.makeMeasureSpec((int) h, MeasureSpec.EXACTLY); float textSize = 0.15f * h; - float maxTextSize = getResources().getDimension(R.dimen.smallerTextSize); + float maxTextSize = + getResources().getDimension(R.dimen.smallerTextSize); textSize = Math.min(textSize, maxTextSize); label.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); @@ -166,18 +180,20 @@ public class CheckmarkWidgetView extends HabitWidgetView implements HabitDataVie super.onMeasure(widthMeasureSpec, heightMeasureSpec); } - @Override - public void refreshData() + private void init() { - if(habit == null) return; - this.percentage = (float) habit.scores.getTodayValue() / Score.MAX_VALUE; - this.checkmarkValue = habit.checkmarks.getTodayValue(); - refresh(); - } + ring = (RingView) findViewById(R.id.scoreRing); + label = (TextView) findViewById(R.id.label); - @NonNull - protected Integer getInnerLayoutId() - { - return R.layout.widget_checkmark; + if (ring != null) ring.setIsTransparencyEnabled(true); + + if (isInEditMode()) + { + percentage = 0.75f; + name = "Wake up early"; + activeColor = ColorUtils.getAndroidTestColor(6); + checkmarkValue = Checkmark.CHECKED_EXPLICITLY; + refresh(); + } } } diff --git a/app/src/main/java/org/isoron/uhabits/views/GraphWidgetView.java b/app/src/main/java/org/isoron/uhabits/ui/widgets/views/GraphWidgetView.java similarity index 57% rename from app/src/main/java/org/isoron/uhabits/views/GraphWidgetView.java rename to app/src/main/java/org/isoron/uhabits/ui/widgets/views/GraphWidgetView.java index bb142ca7d..56ab7b05b 100644 --- a/app/src/main/java/org/isoron/uhabits/views/GraphWidgetView.java +++ b/app/src/main/java/org/isoron/uhabits/ui/widgets/views/GraphWidgetView.java @@ -17,62 +17,57 @@ * with this program. If not, see . */ -package org.isoron.uhabits.views; +package org.isoron.uhabits.ui.widgets.views; -import android.content.Context; -import android.support.annotation.NonNull; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; +import android.content.*; +import android.support.annotation.*; +import android.view.*; +import android.widget.*; -import org.isoron.uhabits.R; -import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.*; -public class GraphWidgetView extends HabitWidgetView implements HabitDataView +public class GraphWidgetView extends HabitWidgetView { - private final HabitDataView dataView; + private final View dataView; + private TextView title; - public GraphWidgetView(Context context, HabitDataView dataView) + public GraphWidgetView(Context context, View dataView) { super(context); this.dataView = dataView; init(); } - private void init() + public View getDataView() { - ViewGroup.LayoutParams params = - new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT); - ((View) dataView).setLayoutParams(params); - - ViewGroup innerFrame = (ViewGroup) findViewById(R.id.innerFrame); - innerFrame.addView(((View) dataView)); - - title = (TextView) findViewById(R.id.title); - title.setVisibility(VISIBLE); + return dataView; } - @Override - public void setHabit(@NonNull Habit habit) + public void setTitle(String text) { - super.setHabit(habit); - dataView.setHabit(habit); - title.setText(habit.name); + title.setText(text); } @Override - public void refreshData() - { - if(habit == null) return; - dataView.refreshData(); - } - @NonNull protected Integer getInnerLayoutId() { return R.layout.widget_graph; } + + private void init() + { + ViewGroup.LayoutParams params = + new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT); + dataView.setLayoutParams(params); + + ViewGroup innerFrame = (ViewGroup) findViewById(R.id.innerFrame); + innerFrame.addView(dataView); + + title = (TextView) findViewById(R.id.title); + title.setVisibility(VISIBLE); + } } diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitWidgetView.java b/app/src/main/java/org/isoron/uhabits/ui/widgets/views/HabitWidgetView.java similarity index 56% rename from app/src/main/java/org/isoron/uhabits/views/HabitWidgetView.java rename to app/src/main/java/org/isoron/uhabits/ui/widgets/views/HabitWidgetView.java index bab859aaa..b4e6dafea 100644 --- a/app/src/main/java/org/isoron/uhabits/views/HabitWidgetView.java +++ b/app/src/main/java/org/isoron/uhabits/ui/widgets/views/HabitWidgetView.java @@ -17,27 +17,23 @@ * with this program. If not, see . */ -package org.isoron.uhabits.views; - -import android.content.Context; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.drawable.InsetDrawable; -import android.graphics.drawable.ShapeDrawable; -import android.graphics.drawable.shapes.RoundRectShape; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.util.AttributeSet; -import android.view.ViewGroup; -import android.widget.FrameLayout; - -import org.isoron.uhabits.R; -import org.isoron.uhabits.helpers.UIHelper; -import org.isoron.uhabits.models.Habit; - -import java.util.Arrays; - -public abstract class HabitWidgetView extends FrameLayout implements HabitDataView +package org.isoron.uhabits.ui.widgets.views; + +import android.content.*; +import android.graphics.*; +import android.graphics.drawable.*; +import android.graphics.drawable.shapes.*; +import android.support.annotation.*; +import android.util.*; +import android.view.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.utils.*; + +import java.util.*; + +public abstract class HabitWidgetView extends FrameLayout { @Nullable protected InsetDrawable background; @@ -45,15 +41,8 @@ public abstract class HabitWidgetView extends FrameLayout implements HabitDataV @Nullable protected Paint backgroundPaint; - @Nullable - protected Habit habit; protected ViewGroup frame; - public void setShadowAlpha(int shadowAlpha) - { - this.shadowAlpha = shadowAlpha; - } - private int shadowAlpha; public HabitWidgetView(Context context) @@ -68,27 +57,28 @@ public abstract class HabitWidgetView extends FrameLayout implements HabitDataV init(); } - private void init() + public void setShadowAlpha(int shadowAlpha) { - inflate(getContext(), getInnerLayoutId(), this); - shadowAlpha = (int) (255 * UIHelper.getStyledFloat(getContext(), R.attr.widgetShadowAlpha)); - rebuildBackground(); + this.shadowAlpha = shadowAlpha; } - protected abstract @NonNull Integer getInnerLayoutId(); + protected abstract + @NonNull + Integer getInnerLayoutId(); protected void rebuildBackground() { Context context = getContext(); - int backgroundAlpha = - (int) (255 * UIHelper.getStyledFloat(context, R.attr.widgetBackgroundAlpha)); + int backgroundAlpha = (int) (255 * + InterfaceUtils.getStyledFloat(context, + R.attr.widgetBackgroundAlpha)); - int shadowRadius = (int) UIHelper.dpToPixels(context, 2); - int shadowOffset = (int) UIHelper.dpToPixels(context, 1); + int shadowRadius = (int) InterfaceUtils.dpToPixels(context, 2); + int shadowOffset = (int) InterfaceUtils.dpToPixels(context, 1); int shadowColor = Color.argb(shadowAlpha, 0, 0, 0); - float cornerRadius = UIHelper.dpToPixels(context, 5); + float cornerRadius = InterfaceUtils.dpToPixels(context, 5); float[] radii = new float[8]; Arrays.fill(radii, cornerRadius); @@ -98,20 +88,25 @@ public abstract class HabitWidgetView extends FrameLayout implements HabitDataV int insetLeftTop = Math.max(shadowRadius - shadowOffset, 0); int insetRightBottom = shadowRadius + shadowOffset; - background = new InsetDrawable(innerDrawable, insetLeftTop, insetLeftTop, insetRightBottom, - insetRightBottom); + background = + new InsetDrawable(innerDrawable, insetLeftTop, insetLeftTop, + insetRightBottom, insetRightBottom); backgroundPaint = innerDrawable.getPaint(); - backgroundPaint.setShadowLayer(shadowRadius, shadowOffset, shadowOffset, shadowColor); - backgroundPaint.setColor(UIHelper.getStyledColor(context, R.attr.cardBackgroundColor)); + backgroundPaint.setShadowLayer(shadowRadius, shadowOffset, shadowOffset, + shadowColor); + backgroundPaint.setColor( + InterfaceUtils.getStyledColor(context, R.attr.cardBackgroundColor)); backgroundPaint.setAlpha(backgroundAlpha); frame = (ViewGroup) findViewById(R.id.frame); - if(frame != null) frame.setBackgroundDrawable(background); + if (frame != null) frame.setBackgroundDrawable(background); } - @Override - public void setHabit(@NonNull Habit habit) + private void init() { - this.habit = habit; + inflate(getContext(), getInnerLayoutId(), this); + shadowAlpha = (int) (255 * InterfaceUtils.getStyledFloat(getContext(), + R.attr.widgetShadowAlpha)); + rebuildBackground(); } } diff --git a/app/src/main/java/org/isoron/uhabits/ui/widgets/views/package-info.java b/app/src/main/java/org/isoron/uhabits/ui/widgets/views/package-info.java new file mode 100644 index 000000000..7e26dce73 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/widgets/views/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2016 Á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 . + */ + +/** + * Provides views that are specific for the home-screen widgets. + */ +package org.isoron.uhabits.ui.widgets.views; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/utils/ColorUtils.java b/app/src/main/java/org/isoron/uhabits/utils/ColorUtils.java new file mode 100644 index 000000000..9499bccbf --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/utils/ColorUtils.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2016 Á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.utils; + +import android.content.*; +import android.graphics.*; +import android.util.*; + +import org.isoron.uhabits.*; + +public abstract class ColorUtils +{ + public static String CSV_PALETTE[] = { + "#D32F2F", // 0 red + "#E64A19", // 1 orange + "#F9A825", // 2 yellow + "#AFB42B", // 3 light green + "#388E3C", // 4 dark green + "#00897B", // 5 teal + "#00ACC1", // 6 cyan + "#039BE5", // 7 blue + "#5E35B1", // 8 deep purple + "#8E24AA", // 9 purple + "#D81B60", // 10 pink + "#303030", // 11 dark grey + "#aaaaaa" // 12 light grey + }; + + public static int colorToPaletteIndex(Context context, int color) + { + int[] palette = getPalette(context); + + for (int k = 0; k < palette.length; k++) + if (palette[k] == color) return k; + + return -1; + } + + public static int getAndroidTestColor(int index) + { + int palette[] = { + Color.parseColor("#D32F2F"), // 0 red + Color.parseColor("#E64A19"), // 1 orange + Color.parseColor("#F9A825"), // 2 yellow + Color.parseColor("#AFB42B"), // 3 light green + Color.parseColor("#388E3C"), // 4 dark green + Color.parseColor("#00897B"), // 5 teal + Color.parseColor("#00ACC1"), // 6 cyan + Color.parseColor("#039BE5"), // 7 blue + Color.parseColor("#5E35B1"), // 8 deep purple + Color.parseColor("#8E24AA"), // 9 purple + Color.parseColor("#D81B60"), // 10 pink + Color.parseColor("#303030"), // 11 dark grey + Color.parseColor("#aaaaaa") // 12 light grey + }; + + return palette[index]; + } + + public static int getColor(Context context, int paletteColor) + { + if (context == null) + throw new IllegalArgumentException("Context is null"); + + int palette[] = getPalette(context); + if (paletteColor < 0 || paletteColor >= palette.length) + { + Log.w("ColorHelper", + String.format("Invalid color: %d. Returning default.", + paletteColor)); + paletteColor = 0; + } + + return palette[paletteColor]; + } + + public static int[] getPalette(Context context) + { + int resourceId = + InterfaceUtils.getStyleResource(context, R.attr.palette); + if (resourceId < 0) throw new RuntimeException("resource not found"); + + return context.getResources().getIntArray(resourceId); + } + + public static int mixColors(int color1, int color2, float amount) + { + final byte ALPHA_CHANNEL = 24; + final byte RED_CHANNEL = 16; + final byte GREEN_CHANNEL = 8; + final byte BLUE_CHANNEL = 0; + + final float inverseAmount = 1.0f - amount; + + int a = ((int) (((float) (color1 >> ALPHA_CHANNEL & 0xff) * amount) + + ((float) (color2 >> ALPHA_CHANNEL & 0xff) * + inverseAmount))) & 0xff; + int r = ((int) (((float) (color1 >> RED_CHANNEL & 0xff) * amount) + + ((float) (color2 >> RED_CHANNEL & 0xff) * + inverseAmount))) & 0xff; + int g = ((int) (((float) (color1 >> GREEN_CHANNEL & 0xff) * amount) + + ((float) (color2 >> GREEN_CHANNEL & 0xff) * + inverseAmount))) & 0xff; + int b = ((int) (((float) (color1 & 0xff) * amount) + + ((float) (color2 & 0xff) * inverseAmount))) & 0xff; + + return a << ALPHA_CHANNEL | r << RED_CHANNEL | g << GREEN_CHANNEL | + b << BLUE_CHANNEL; + } + + public static int setAlpha(int color, float newAlpha) + { + int intAlpha = (int) (newAlpha * 255); + return Color.argb(intAlpha, Color.red(color), Color.green(color), + Color.blue(color)); + } + + public static int setMinValue(int color, float newValue) + { + float hsv[] = new float[3]; + Color.colorToHSV(color, hsv); + hsv[2] = Math.max(hsv[2], newValue); + return Color.HSVToColor(hsv); + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/utils/DatabaseUtils.java b/app/src/main/java/org/isoron/uhabits/utils/DatabaseUtils.java new file mode 100644 index 000000000..4ef5ae48f --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/utils/DatabaseUtils.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2016 Á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.utils; + +import android.content.*; +import android.database.*; +import android.support.annotation.*; + +import com.activeandroid.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.sqlite.records.*; + +import java.io.*; +import java.text.*; + +public abstract class DatabaseUtils +{ + public static void executeAsTransaction(Callback callback) + { + ActiveAndroid.beginTransaction(); + try + { + callback.execute(); + ActiveAndroid.setTransactionSuccessful(); + } + finally + { + ActiveAndroid.endTransaction(); + } + } + + @NonNull + public static File getDatabaseFile() + { + Context context = HabitsApplication.getContext(); + if (context == null) + throw new RuntimeException("No application context found"); + + String databaseFilename = getDatabaseFilename(); + + return new File(String.format("%s/../databases/%s", + context.getApplicationContext().getFilesDir().getPath(), + databaseFilename)); + } + + @NonNull + public static String getDatabaseFilename() + { + String databaseFilename = BuildConfig.databaseFilename; + + if (HabitsApplication.isTestMode()) databaseFilename = "test.db"; + + return databaseFilename; + } + + @SuppressWarnings("unchecked") + public static void initializeActiveAndroid() + { + Context context = HabitsApplication.getContext(); + if (context == null) throw new RuntimeException( + "application context should not be null"); + + Configuration dbConfig = new Configuration.Builder(context) + .setDatabaseName(getDatabaseFilename()) + .setDatabaseVersion(BuildConfig.databaseVersion) + .addModelClasses(CheckmarkRecord.class, HabitRecord.class, + RepetitionRecord.class, ScoreRecord.class, StreakRecord.class) + .create(); + + ActiveAndroid.initialize(dbConfig); + } + + public static long longQuery(String query, String args[]) + { + Cursor c = null; + + try + { + c = Cache.openDatabase().rawQuery(query, args); + if (!c.moveToFirst()) return 0; + return c.getLong(0); + } + finally + { + if (c != null) c.close(); + } + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + public static String saveDatabaseCopy(File dir) throws IOException + { + File db = getDatabaseFile(); + + SimpleDateFormat dateFormat = DateUtils.getBackupDateFormat(); + String date = dateFormat.format(DateUtils.getLocalTime()); + File dbCopy = new File( + String.format("%s/Loop Habits Backup %s.db", dir.getAbsolutePath(), + date)); + + FileUtils.copy(db, dbCopy); + + return dbCopy.getAbsolutePath(); + } + + public interface Callback + { + void execute(); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/helpers/DateHelper.java b/app/src/main/java/org/isoron/uhabits/utils/DateUtils.java similarity index 66% rename from app/src/main/java/org/isoron/uhabits/helpers/DateHelper.java rename to app/src/main/java/org/isoron/uhabits/utils/DateUtils.java index 4d0c02c32..c28128387 100644 --- a/app/src/main/java/org/isoron/uhabits/helpers/DateHelper.java +++ b/app/src/main/java/org/isoron/uhabits/utils/DateUtils.java @@ -17,7 +17,7 @@ * with this program. If not, see . */ -package org.isoron.uhabits.helpers; +package org.isoron.uhabits.utils; import android.content.Context; import android.text.format.DateFormat; @@ -31,60 +31,25 @@ import java.util.GregorianCalendar; import java.util.Locale; import java.util.TimeZone; -public class DateHelper +public abstract class DateUtils { - public static long millisecondsInOneDay = 24 * 60 * 60 * 1000; public static int ALL_WEEK_DAYS = 127; private static Long fixedLocalTime = null; - public static long getLocalTime() - { - if(fixedLocalTime != null) return fixedLocalTime; - - TimeZone tz = TimeZone.getDefault(); - long now = new Date().getTime(); - return now + tz.getOffset(now); - } - - public static void setFixedLocalTime(Long timestamp) - { - fixedLocalTime = timestamp; - } - - public static long toLocalTime(long timestamp) - { - TimeZone tz = TimeZone.getDefault(); - long now = new Date(timestamp).getTime(); - return now + tz.getOffset(now); - } - - public static long getStartOfDay(long timestamp) - { - return (timestamp / millisecondsInOneDay) * millisecondsInOneDay; - } - - public static GregorianCalendar getStartOfTodayCalendar() - { - return getCalendar(getStartOfToday()); - } - - public static GregorianCalendar getCalendar(long timestamp) - { - GregorianCalendar day = new GregorianCalendar(TimeZone.getTimeZone("GMT")); - day.setTimeInMillis(timestamp); - return day; - } + /** + * Number of milliseconds in one day. + */ + public static long millisecondsInOneDay = 24 * 60 * 60 * 1000; - public static int getWeekday(long timestamp) + public static String formatHeaderDate(GregorianCalendar day) { - GregorianCalendar day = getCalendar(timestamp); - return day.get(GregorianCalendar.DAY_OF_WEEK) % 7; - } + String dayOfMonth = + Integer.toString(day.get(GregorianCalendar.DAY_OF_MONTH)); + String dayOfWeek = day.getDisplayName(GregorianCalendar.DAY_OF_WEEK, + GregorianCalendar.SHORT, Locale.getDefault()); - public static long getStartOfToday() - { - return getStartOfDay(DateHelper.getLocalTime()); + return dayOfWeek + "\n" + dayOfMonth; } public static String formatTime(Context context, int hours, int minutes) @@ -98,74 +63,77 @@ public class DateHelper return df.format(date); } - public static SimpleDateFormat getDateFormat(String skeleton) + public static String formatWeekdayList(Context context, boolean weekday[]) { - String pattern; - Locale locale = Locale.getDefault(); + String shortDayNames[] = getShortDayNames(); + String longDayNames[] = getLongDayNames(); + StringBuilder buffer = new StringBuilder(); - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR2) - pattern = DateFormat.getBestDateTimePattern(locale, skeleton); - else - pattern = skeleton; + int count = 0; + int first = 0; + boolean isFirst = true; + for (int i = 0; i < 7; i++) + { + if (weekday[i]) + { + if (isFirst) first = i; + else buffer.append(", "); - SimpleDateFormat format = new SimpleDateFormat(pattern, locale); - format.setTimeZone(TimeZone.getTimeZone("UTC")); + buffer.append(shortDayNames[i]); + isFirst = false; + count++; + } + } - return format; + if (count == 1) return longDayNames[first]; + if (count == 2 && weekday[0] && weekday[1]) + return context.getString(R.string.weekends); + if (count == 5 && !weekday[0] && !weekday[1]) + return context.getString(R.string.any_weekday); + if (count == 7) return context.getString(R.string.any_day); + return buffer.toString(); } - public static SimpleDateFormat getCSVDateFormat() + public static SimpleDateFormat getBackupDateFormat() { - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.US); + SimpleDateFormat dateFormat = + new SimpleDateFormat("yyyy-MM-dd HHmmss", Locale.US); dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); return dateFormat; } - public static SimpleDateFormat getBackupDateFormat() + public static SimpleDateFormat getCSVDateFormat() { - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HHmmss", Locale.US); + SimpleDateFormat dateFormat = + new SimpleDateFormat("yyyy-MM-dd", Locale.US); dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); return dateFormat; } - public static String formatHeaderDate(GregorianCalendar day) - { - String dayOfMonth = Integer.toString(day.get(GregorianCalendar.DAY_OF_MONTH)); - String dayOfWeek = day.getDisplayName(GregorianCalendar.DAY_OF_WEEK, - GregorianCalendar.SHORT, Locale.getDefault()); - - return dayOfWeek + "\n" + dayOfMonth; - } - - public static int differenceInDays(Date from, Date to) + public static GregorianCalendar getCalendar(long timestamp) { - long milliseconds = getStartOfDay(to.getTime()) - getStartOfDay(from.getTime()); - return (int) (milliseconds / millisecondsInOneDay); + GregorianCalendar day = + new GregorianCalendar(TimeZone.getTimeZone("GMT")); + day.setTimeInMillis(timestamp); + return day; } - public static String[] getShortDayNames() + public static SimpleDateFormat getDateFormat(String skeleton) { - return getDayNames(GregorianCalendar.SHORT); - } + String pattern; + Locale locale = Locale.getDefault(); - public static String[] getLongDayNames() - { - return getDayNames(GregorianCalendar.LONG); - } + if (android.os.Build.VERSION.SDK_INT >= + android.os.Build.VERSION_CODES.JELLY_BEAN_MR2) + pattern = DateFormat.getBestDateTimePattern(locale, skeleton); + else pattern = skeleton; + SimpleDateFormat format = new SimpleDateFormat(pattern, locale); + format.setTimeZone(TimeZone.getTimeZone("UTC")); - /** - * Throughout the code, it is assumed that the weekdays are numbered from 0 (Saturday) to 6 - * (Friday). In the Java Calendar they are numbered from 1 (Sunday) to 7 (Saturday). This - * function converts from Java to our internal representation. - * - * @return weekday number in the internal interpretation - */ - public static int javaWeekdayToLoopWeekday(int number) - { - return number % 7; + return format; } public static String[] getDayNames(int format) @@ -178,13 +146,22 @@ public class DateHelper for (int i = 0; i < wdays.length; i++) { wdays[i] = day.getDisplayName(GregorianCalendar.DAY_OF_WEEK, format, - Locale.getDefault()); + Locale.getDefault()); day.add(GregorianCalendar.DAY_OF_MONTH, 1); } return wdays; } + public static long getLocalTime() + { + if (fixedLocalTime != null) return fixedLocalTime; + + TimeZone tz = TimeZone.getDefault(); + long now = new Date().getTime(); + return now + tz.getOffset(now); + } + /** * @return array with weekday names starting according to locale settings, * e.g. [Mo,Di,Mi,Do,Fr,Sa,So] in Europe @@ -194,10 +171,12 @@ public class DateHelper String[] days = new String[7]; Calendar calendar = new GregorianCalendar(); - calendar.set(GregorianCalendar.DAY_OF_WEEK, calendar.getFirstDayOfWeek()); + calendar.set(GregorianCalendar.DAY_OF_WEEK, + calendar.getFirstDayOfWeek()); for (int i = 0; i < days.length; i++) { - days[i] = calendar.getDisplayName(GregorianCalendar.DAY_OF_WEEK, format, + days[i] = + calendar.getDisplayName(GregorianCalendar.DAY_OF_WEEK, format, Locale.getDefault()); calendar.add(GregorianCalendar.DAY_OF_MONTH, 1); } @@ -206,14 +185,15 @@ public class DateHelper } /** - * @return array with week days numbers starting according to locale settings, - * e.g. [2,3,4,5,6,7,1] in Europe + * @return array with week days numbers starting according to locale + * settings, e.g. [2,3,4,5,6,7,1] in Europe */ public static Integer[] getLocaleWeekdayList() { Integer[] dayNumbers = new Integer[7]; Calendar calendar = new GregorianCalendar(); - calendar.set(GregorianCalendar.DAY_OF_WEEK, calendar.getFirstDayOfWeek()); + calendar.set(GregorianCalendar.DAY_OF_WEEK, + calendar.getFirstDayOfWeek()); for (int i = 0; i < dayNumbers.length; i++) { dayNumbers[i] = calendar.get(GregorianCalendar.DAY_OF_WEEK); @@ -222,33 +202,48 @@ public class DateHelper return dayNumbers; } - public static String formatWeekdayList(Context context, boolean weekday[]) + public static String[] getLongDayNames() { - String shortDayNames[] = getShortDayNames(); - String longDayNames[] = getLongDayNames(); - StringBuilder buffer = new StringBuilder(); + return getDayNames(GregorianCalendar.LONG); + } - int count = 0; - int first = 0; - boolean isFirst = true; - for(int i = 0; i < 7; i++) - { - if(weekday[i]) - { - if(isFirst) first = i; - else buffer.append(", "); + public static String[] getShortDayNames() + { + return getDayNames(GregorianCalendar.SHORT); + } - buffer.append(shortDayNames[i]); - isFirst = false; - count++; - } - } + public static long getStartOfDay(long timestamp) + { + return (timestamp / millisecondsInOneDay) * millisecondsInOneDay; + } - if(count == 1) return longDayNames[first]; - if(count == 2 && weekday[0] && weekday[1]) return context.getString(R.string.weekends); - if(count == 5 && !weekday[0] && !weekday[1]) return context.getString(R.string.any_weekday); - if(count == 7) return context.getString(R.string.any_day); - return buffer.toString(); + public static long getStartOfToday() + { + return getStartOfDay(DateUtils.getLocalTime()); + } + + public static GregorianCalendar getStartOfTodayCalendar() + { + return getCalendar(getStartOfToday()); + } + + public static int getWeekday(long timestamp) + { + GregorianCalendar day = getCalendar(timestamp); + return day.get(GregorianCalendar.DAY_OF_WEEK) % 7; + } + + /** + * Throughout the code, it is assumed that the weekdays are numbered from 0 + * (Saturday) to 6 (Friday). In the Java Calendar they are numbered from 1 + * (Sunday) to 7 (Saturday). This function converts from Java to our + * internal representation. + * + * @return weekday number in the internal interpretation + */ + public static int javaWeekdayToLoopWeekday(int number) + { + return number % 7; } public static Integer packWeekdayList(boolean weekday[]) @@ -256,26 +251,77 @@ public class DateHelper int list = 0; int current = 1; - for(int i = 0; i < 7; i++) + for (int i = 0; i < 7; i++) { - if(weekday[i]) list |= current; + if (weekday[i]) list |= current; current = current << 1; } return list; } + public static void setFixedLocalTime(Long timestamp) + { + fixedLocalTime = timestamp; + } + + public static long toLocalTime(long timestamp) + { + TimeZone tz = TimeZone.getDefault(); + long now = new Date(timestamp).getTime(); + return now + tz.getOffset(now); + } + + public static Long truncate(TruncateField field, long timestamp) + { + GregorianCalendar cal = DateUtils.getCalendar(timestamp); + + switch (field) + { + case MONTH: + cal.set(Calendar.DAY_OF_MONTH, 1); + return cal.getTimeInMillis(); + + case WEEK_NUMBER: + int firstWeekday = cal.getFirstDayOfWeek(); + int weekday = cal.get(Calendar.DAY_OF_WEEK); + int delta = weekday - firstWeekday; + if (delta < 0) delta += 7; + cal.add(Calendar.DAY_OF_YEAR, -delta); + return cal.getTimeInMillis(); + + case QUARTER: + int quarter = cal.get(Calendar.MONTH) / 3; + cal.set(Calendar.DAY_OF_MONTH, 1); + cal.set(Calendar.MONTH, quarter * 3); + return cal.getTimeInMillis(); + + case YEAR: + cal.set(Calendar.MONTH, Calendar.JANUARY); + cal.set(Calendar.DAY_OF_MONTH, 1); + return cal.getTimeInMillis(); + + default: + throw new IllegalArgumentException(); + } + } + public static boolean[] unpackWeekdayList(int list) { boolean[] weekday = new boolean[7]; int current = 1; - for(int i = 0; i < 7; i++) + for (int i = 0; i < 7; i++) { - if((list & current) != 0) weekday[i] = true; + if ((list & current) != 0) weekday[i] = true; current = current << 1; } return weekday; } + + public enum TruncateField + { + MONTH, WEEK_NUMBER, YEAR, QUARTER + } } diff --git a/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java b/app/src/main/java/org/isoron/uhabits/utils/FileUtils.java similarity index 54% rename from app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java rename to app/src/main/java/org/isoron/uhabits/utils/FileUtils.java index d3c3d21e5..867dbbcf0 100644 --- a/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java +++ b/app/src/main/java/org/isoron/uhabits/utils/FileUtils.java @@ -17,27 +17,16 @@ * with this program. If not, see . */ -package org.isoron.uhabits.helpers; +package org.isoron.uhabits.utils; import android.content.Context; -import android.database.Cursor; import android.os.Environment; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.util.Log; -import com.activeandroid.ActiveAndroid; -import com.activeandroid.Cache; -import com.activeandroid.Configuration; - -import org.isoron.uhabits.BuildConfig; import org.isoron.uhabits.HabitsApplication; -import org.isoron.uhabits.models.Checkmark; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.models.Repetition; -import org.isoron.uhabits.models.Score; -import org.isoron.uhabits.models.Streak; import java.io.File; import java.io.FileInputStream; @@ -45,9 +34,8 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.text.SimpleDateFormat; -public class DatabaseHelper +public abstract class FileUtils { public static void copy(File src, File dst) throws IOException { @@ -71,62 +59,6 @@ public class DatabaseHelper out.write(buffer, 0, numBytes); } - public interface Command - { - void execute(); - } - - public static void executeAsTransaction(Command command) - { - ActiveAndroid.beginTransaction(); - try - { - command.execute(); - ActiveAndroid.setTransactionSuccessful(); - } - finally - { - ActiveAndroid.endTransaction(); - } - } - - @SuppressWarnings("ResultOfMethodCallIgnored") - public static String saveDatabaseCopy(File dir) throws IOException - { - File db = getDatabaseFile(); - - SimpleDateFormat dateFormat = DateHelper.getBackupDateFormat(); - String date = dateFormat.format(DateHelper.getLocalTime()); - File dbCopy = new File(String.format("%s/Loop Habits Backup %s.db", dir.getAbsolutePath(), date)); - - copy(db, dbCopy); - - return dbCopy.getAbsolutePath(); - } - - @NonNull - public static File getDatabaseFile() - { - Context context = HabitsApplication.getContext(); - if(context == null) throw new RuntimeException("No application context found"); - - String databaseFilename = getDatabaseFilename(); - - return new File(String.format("%s/../databases/%s", - context.getApplicationContext().getFilesDir().getPath(), databaseFilename)); - } - - @NonNull - public static String getDatabaseFilename() - { - String databaseFilename = BuildConfig.databaseFilename; - - if (HabitsApplication.isTestMode()) - databaseFilename = "test.db"; - - return databaseFilename; - } - @Nullable public static File getSDCardDir(@Nullable String relativePath) { @@ -184,35 +116,4 @@ public class DatabaseHelper return dir; } - @SuppressWarnings("unchecked") - public static void initializeActiveAndroid() - { - Context context = HabitsApplication.getContext(); - if(context == null) throw new RuntimeException("application context should not be null"); - - Configuration dbConfig = new Configuration.Builder(context) - .setDatabaseName(getDatabaseFilename()) - .setDatabaseVersion(BuildConfig.databaseVersion) - .addModelClasses(Checkmark.class, Habit.class, Repetition.class, Score.class, - Streak.class) - .create(); - - ActiveAndroid.initialize(dbConfig); - } - - public static long longQuery(String query, String args[]) - { - Cursor c = null; - - try - { - c = Cache.openDatabase().rawQuery(query, args); - if (!c.moveToFirst()) return 0; - return c.getLong(0); - } - finally - { - if(c != null) c.close(); - } - } } diff --git a/app/src/main/java/org/isoron/uhabits/helpers/UIHelper.java b/app/src/main/java/org/isoron/uhabits/utils/InterfaceUtils.java similarity index 85% rename from app/src/main/java/org/isoron/uhabits/helpers/UIHelper.java rename to app/src/main/java/org/isoron/uhabits/utils/InterfaceUtils.java index 9848ff016..9d5916d2d 100644 --- a/app/src/main/java/org/isoron/uhabits/helpers/UIHelper.java +++ b/app/src/main/java/org/isoron/uhabits/utils/InterfaceUtils.java @@ -17,7 +17,7 @@ * with this program. If not, see . */ -package org.isoron.uhabits.helpers; +package org.isoron.uhabits.utils; import android.app.Activity; import android.content.Context; @@ -33,17 +33,13 @@ import android.preference.PreferenceManager; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.TypedValue; -import android.view.View; -import android.view.inputmethod.InputMethodManager; -import org.isoron.uhabits.BuildConfig; import org.isoron.uhabits.HabitsApplication; import org.isoron.uhabits.R; -import org.isoron.uhabits.commands.Command; import java.util.Locale; -public abstract class UIHelper +public abstract class InterfaceUtils { public static final String ISORON_NAMESPACE = "http://isoron.org/android"; @@ -55,12 +51,7 @@ public abstract class UIHelper public static void setFixedTheme(Integer fixedTheme) { - UIHelper.fixedTheme = fixedTheme; - } - - public interface OnSavedListener - { - void onSaved(Command command, Object savedObject); + InterfaceUtils.fixedTheme = fixedTheme; } public static Typeface getFontAwesome(Context context) @@ -71,32 +62,6 @@ public abstract class UIHelper return fontAwesome; } - public static void showSoftKeyboard(View view) - { - InputMethodManager imm = (InputMethodManager) view.getContext() - .getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); - } - - public static void incrementLaunchCount(Context context) - { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - int count = prefs.getInt("launch_count", 0); - prefs.edit().putInt("launch_count", count + 1).apply(); - } - - public static void updateLastAppVersion(Context context) - { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - prefs.edit().putInt("last_version", BuildConfig.VERSION_CODE).apply(); - } - - public static int getLaunchCount(Context context) - { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - return prefs.getInt("launch_count", 0); - } - public static String getAttribute(Context context, AttributeSet attrs, String name, String defaultValue) { @@ -304,13 +269,13 @@ public abstract class UIHelper } - public static void setDefaultScoreInterval(Context context, int position) + public static void setDefaultScoreSpinnerPosition(Context context, int position) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); prefs.edit().putInt("pref_score_view_interval", position).apply(); } - public static int getDefaultScoreInterval(Context context) + public static int getDefaultScoreSpinnerPosition(Context context) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); int defaultScoreInterval = prefs.getInt("pref_score_view_interval", 1); diff --git a/app/src/main/java/org/isoron/uhabits/utils/Preferences.java b/app/src/main/java/org/isoron/uhabits/utils/Preferences.java new file mode 100644 index 000000000..73a9e729b --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/utils/Preferences.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2016 Á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.utils; + +import android.content.*; +import android.preference.*; + +import org.isoron.uhabits.*; + +public class Preferences +{ + private Context context; + + private SharedPreferences prefs; + + public Preferences() + { + this.context = HabitsApplication.getContext(); + prefs = PreferenceManager.getDefaultSharedPreferences(context); + } + + public Integer getDefaultHabitColor(int fallbackColor) + { + return prefs.getInt("pref_default_habit_palette_color", fallbackColor); + } + + /** + * Returns the number of the last hint shown to the user. + * + * @return number of last hint shown + */ + public int getLastHintNumber() + { + return prefs.getInt("last_hint_number", -1); + } + + /** + * Returns the time when the last hint was shown to the user. + * + * @return timestamp of the day the last hint was shown + */ + public long getLastHintTimestamp() + { + return prefs.getLong("last_hint_timestamp", -1); + } + + public void incrementLaunchCount() + { + int count = prefs.getInt("launch_count", 0); + prefs.edit().putInt("launch_count", count + 1).apply(); + } + + public void initialize() + { + PreferenceManager.setDefaultValues(context, R.xml.preferences, false); + } + + public boolean isFirstRun() + { + return prefs.getBoolean("pref_first_run", true); + } + + public void setFirstRun(boolean isFirstRun) + { + prefs.edit().putBoolean("pref_first_run", isFirstRun).apply(); + } + + public boolean isShortToggleEnabled() + { + return prefs.getBoolean("pref_short_toggle", false); + } + + public void setShortToggleEnabled(boolean enabled) + { + prefs.edit().putBoolean("pref_short_toggle", enabled).apply(); + } + + public void setDefaultHabitColor(int color) + { + prefs.edit().putInt("pref_default_habit_palette_color", color).apply(); + } + + public void setShouldReverseCheckmarks(boolean shouldReverse) + { + prefs + .edit() + .putBoolean("pref_checkmark_reverse_order", shouldReverse) + .apply(); + } + + public boolean shouldReverseCheckmarks() + { + return prefs.getBoolean("pref_checkmark_reverse_order", false); + } + + public void updateLastAppVersion() + { + prefs.edit().putInt("last_version", BuildConfig.VERSION_CODE).apply(); + } + + /** + * Sets the last hint shown to the user, and the time that it was shown. + * + * @param number number of the last hint shown + * @param timestamp timestamp for the day the last hint was shown + */ + public void updateLastHint(int number, long timestamp) + { + prefs + .edit() + .putInt("last_hint_number", number) + .putLong("last_hint_timestamp", timestamp) + .apply(); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/helpers/ReminderHelper.java b/app/src/main/java/org/isoron/uhabits/utils/ReminderUtils.java similarity index 53% rename from app/src/main/java/org/isoron/uhabits/helpers/ReminderHelper.java rename to app/src/main/java/org/isoron/uhabits/utils/ReminderUtils.java index 2956815a4..52ab9ed29 100644 --- a/app/src/main/java/org/isoron/uhabits/helpers/ReminderHelper.java +++ b/app/src/main/java/org/isoron/uhabits/utils/ReminderUtils.java @@ -17,51 +17,40 @@ * with this program. If not, see . */ -package org.isoron.uhabits.helpers; - -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.media.Ringtone; -import android.media.RingtoneManager; -import android.net.Uri; -import android.os.Build; -import android.preference.PreferenceManager; -import android.provider.Settings; -import android.support.annotation.Nullable; +package org.isoron.uhabits.utils; + +import android.app.*; +import android.content.*; +import android.media.*; +import android.net.*; +import android.os.*; +import android.preference.*; +import android.provider.*; +import android.support.annotation.*; import android.support.v4.app.Fragment; -import android.util.Log; +import android.util.*; -import org.isoron.uhabits.HabitBroadcastReceiver; -import org.isoron.uhabits.R; -import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; -import java.text.DateFormat; -import java.util.Calendar; -import java.util.Date; +import java.text.*; +import java.util.*; -public class ReminderHelper +public abstract class ReminderUtils { - public static void createReminderAlarms(Context context) + public static void createReminderAlarm(Context context, + Habit habit, + @Nullable Long reminderTime) { - for (Habit habit : Habit.getHabitsWithReminder()) - createReminderAlarm(context, habit, null); - } - - public static void createReminderAlarm(Context context, Habit habit, @Nullable Long reminderTime) - { - if(!habit.hasReminder()) return; + if (!habit.hasReminder()) return; + Reminder reminder = habit.getReminder(); if (reminderTime == null) { Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(System.currentTimeMillis()); - //noinspection ConstantConditions - calendar.set(Calendar.HOUR_OF_DAY, habit.reminderHour); - //noinspection ConstantConditions - calendar.set(Calendar.MINUTE, habit.reminderMin); + calendar.set(Calendar.HOUR_OF_DAY, reminder.getHour()); + calendar.set(Calendar.MINUTE, reminder.getMinute()); calendar.set(Calendar.SECOND, 0); reminderTime = calendar.getTimeInMillis(); @@ -70,7 +59,8 @@ public class ReminderHelper reminderTime += AlarmManager.INTERVAL_DAY; } - long timestamp = DateHelper.getStartOfDay(DateHelper.toLocalTime(reminderTime)); + long timestamp = + DateUtils.getStartOfDay(DateUtils.toLocalTime(reminderTime)); Uri uri = habit.getUri(); @@ -80,22 +70,61 @@ public class ReminderHelper alarmIntent.putExtra("timestamp", timestamp); alarmIntent.putExtra("reminderTime", reminderTime); - PendingIntent pendingIntent = - PendingIntent.getBroadcast(context, ((int) (habit.getId() % Integer.MAX_VALUE)) + 1, - alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT); + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, + ((int) (habit.getId() % Integer.MAX_VALUE)) + 1, alarmIntent, + PendingIntent.FLAG_UPDATE_CURRENT); - AlarmManager manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + AlarmManager manager = + (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); if (Build.VERSION.SDK_INT >= 23) - manager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, reminderTime, pendingIntent); + manager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, + reminderTime, pendingIntent); else if (Build.VERSION.SDK_INT >= 19) - manager.setExact(AlarmManager.RTC_WAKEUP, reminderTime, pendingIntent); - else - manager.set(AlarmManager.RTC_WAKEUP, reminderTime, pendingIntent); + manager.setExact(AlarmManager.RTC_WAKEUP, reminderTime, + pendingIntent); + else manager.set(AlarmManager.RTC_WAKEUP, reminderTime, pendingIntent); - String name = habit.name.substring(0, Math.min(3, habit.name.length())); + String name = habit.getName().substring(0, Math.min(3, habit.getName().length())); Log.d("ReminderHelper", String.format("Setting alarm (%s): %s", - DateFormat.getDateTimeInstance().format(new Date(reminderTime)), name)); + DateFormat.getDateTimeInstance().format(new Date(reminderTime)), + name)); + } + + public static void createReminderAlarms(Context context, + HabitList habitList) + { + for (Habit habit : habitList.getWithReminder()) + createReminderAlarm(context, habit, null); + } + + @Nullable + public static String getRingtoneName(Context context) + { + try + { + Uri ringtoneUri = getRingtoneUri(context); + String ringtoneName = + context.getResources().getString(R.string.none); + + if (ringtoneUri != null) + { + Ringtone ringtone = + RingtoneManager.getRingtone(context, ringtoneUri); + if (ringtone != null) + { + ringtoneName = ringtone.getTitle(context); + ringtone.stop(); + } + } + + return ringtoneName; + } + catch (RuntimeException e) + { + e.printStackTrace(); + return null; + } } @Nullable @@ -104,70 +133,57 @@ public class ReminderHelper Uri ringtoneUri = null; Uri defaultRingtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI; - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - String prefRingtoneUri = prefs.getString("pref_ringtone_uri", defaultRingtoneUri.toString()); - if (prefRingtoneUri.length() > 0) ringtoneUri = Uri.parse(prefRingtoneUri); + SharedPreferences prefs = + PreferenceManager.getDefaultSharedPreferences(context); + String prefRingtoneUri = + prefs.getString("pref_ringtone_uri", defaultRingtoneUri.toString()); + if (prefRingtoneUri.length() > 0) + ringtoneUri = Uri.parse(prefRingtoneUri); return ringtoneUri; } public static void parseRingtoneData(Context context, @Nullable Intent data) { - if(data == null) return; + if (data == null) return; - Uri ringtoneUri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI); + Uri ringtoneUri = + data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI); if (ringtoneUri != null) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - prefs.edit().putString("pref_ringtone_uri", ringtoneUri.toString()).apply(); + SharedPreferences prefs = + PreferenceManager.getDefaultSharedPreferences(context); + prefs + .edit() + .putString("pref_ringtone_uri", ringtoneUri.toString()) + .apply(); } else { String off = context.getResources().getString(R.string.none); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + SharedPreferences prefs = + PreferenceManager.getDefaultSharedPreferences(context); prefs.edit().putString("pref_ringtone_uri", "").apply(); } } - public static void startRingtonePickerActivity(Fragment fragment, int requestCode) + public static void startRingtonePickerActivity(Fragment fragment, + int requestCode) { - Uri existingRingtoneUri = ReminderHelper.getRingtoneUri(fragment.getContext()); + Uri existingRingtoneUri = + ReminderUtils.getRingtoneUri(fragment.getContext()); Uri defaultRingtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI; Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER); - intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, + RingtoneManager.TYPE_NOTIFICATION); intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true); intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true); - intent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, defaultRingtoneUri); - intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, existingRingtoneUri); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, + defaultRingtoneUri); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, + existingRingtoneUri); fragment.startActivityForResult(intent, requestCode); } - - @Nullable - public static String getRingtoneName(Context context) - { - try - { - Uri ringtoneUri = getRingtoneUri(context); - String ringtoneName = context.getResources().getString(R.string.none); - - if (ringtoneUri != null) - { - Ringtone ringtone = RingtoneManager.getRingtone(context, ringtoneUri); - if (ringtone != null) - { - ringtoneName = ringtone.getTitle(context); - ringtone.stop(); - } - } - - return ringtoneName; - } - catch (RuntimeException e) - { - e.printStackTrace(); - return null; - } - } } diff --git a/app/src/main/java/org/isoron/uhabits/utils/WidgetPreferences.java b/app/src/main/java/org/isoron/uhabits/utils/WidgetPreferences.java new file mode 100644 index 000000000..6fac955b5 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/utils/WidgetPreferences.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2016 Á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.utils; + +import android.content.*; +import android.preference.*; + +import org.isoron.uhabits.*; + +public class WidgetPreferences +{ + private Context context; + + private SharedPreferences prefs; + + public WidgetPreferences() + { + this.context = HabitsApplication.getContext(); + prefs = PreferenceManager.getDefaultSharedPreferences(context); + } + + public void addWidget(int widgetId, long habitId) + { + prefs + .edit() + .putLong(getHabitIdKey(widgetId), habitId) + .commit(); + } + + public long getHabitIdFromWidgetId(int widgetId) + { + Long habitId = prefs.getLong(getHabitIdKey(widgetId), -1); + if (habitId < 0) throw new RuntimeException("widget not found"); + + return habitId; + } + + public void removeWidget(int id) + { + String habitIdKey = getHabitIdKey(id); + prefs.edit().remove(habitIdKey).apply(); + } + + private String getHabitIdKey(int id) + { + return String.format("widget-%06d-habit", id); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/utils/WidgetUtils.java b/app/src/main/java/org/isoron/uhabits/utils/WidgetUtils.java new file mode 100644 index 000000000..44e8238e7 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/utils/WidgetUtils.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2016 Á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.utils; + +import android.appwidget.*; +import android.content.*; +import android.os.*; +import android.support.annotation.*; +import android.widget.*; + +import org.isoron.uhabits.ui.widgets.*; + +import static android.os.Build.VERSION.*; +import static android.os.Build.VERSION_CODES.*; + +public abstract class WidgetUtils +{ + @NonNull + public static WidgetDimensions getDimensionsFromOptions( + @NonNull Context context, @NonNull Bundle options) + { + if (SDK_INT < JELLY_BEAN) + throw new AssertionError("method requires jelly-bean"); + + int maxWidth = (int) InterfaceUtils.dpToPixels(context, + options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH)); + int maxHeight = (int) InterfaceUtils.dpToPixels(context, + options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT)); + int minWidth = (int) InterfaceUtils.dpToPixels(context, + options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)); + int minHeight = (int) InterfaceUtils.dpToPixels(context, + options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT)); + + return new WidgetDimensions(minWidth, maxHeight, maxWidth, minHeight); + } + + public static void updateAppWidget(@NonNull AppWidgetManager manager, + @NonNull BaseWidget widget) + { + if (SDK_INT < JELLY_BEAN) + { + RemoteViews portrait = widget.getPortraitRemoteViews(); + manager.updateAppWidget(widget.getId(), portrait); + } + else + { + RemoteViews landscape = widget.getLandscapeRemoteViews(); + RemoteViews portrait = widget.getPortraitRemoteViews(); + RemoteViews views = new RemoteViews(landscape, portrait); + manager.updateAppWidget(widget.getId(), views); + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/utils/package-info.java b/app/src/main/java/org/isoron/uhabits/utils/package-info.java new file mode 100644 index 000000000..51db3c7a3 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/utils/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2016 Á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 . + */ + +/** + * Provides various utilities classes, such as {@link org.isoron.uhabits.utils.ColorUtils}. + */ +package org.isoron.uhabits.utils; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/views/NumberView.java b/app/src/main/java/org/isoron/uhabits/views/NumberView.java deleted file mode 100644 index b5412a7a3..000000000 --- a/app/src/main/java/org/isoron/uhabits/views/NumberView.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright (C) 2016 Á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.views; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.RectF; -import android.text.Layout; -import android.text.StaticLayout; -import android.text.TextPaint; -import android.util.AttributeSet; -import android.view.View; - -import org.isoron.uhabits.R; -import org.isoron.uhabits.helpers.ColorHelper; -import org.isoron.uhabits.helpers.UIHelper; - -public class NumberView extends View -{ - private int color; - private int number; - private float labelMarginTop; - private TextPaint pText; - private String label; - private RectF rect; - private StaticLayout labelLayout; - - private int width; - private int height; - - private float textSize; - private float labelTextSize; - private float numberTextSize; - private StaticLayout numberLayout; - - public NumberView(Context context) - { - super(context); - this.textSize = getResources().getDimension(R.dimen.regularTextSize); - init(); - } - - public NumberView(Context context, AttributeSet attrs) - { - super(context, attrs); - - this.textSize = getResources().getDimension(R.dimen.regularTextSize); - - this.label = UIHelper.getAttribute(context, attrs, "label", "Number"); - this.number = UIHelper.getIntAttribute(context, attrs, "number", 0); - this.textSize = UIHelper.getFloatAttribute(context, attrs, "textSize", - getResources().getDimension(R.dimen.regularTextSize)); - - this.color = ColorHelper.getColor(getContext(), 7); - init(); - } - - public void setColor(int color) - { - this.color = color; - pText.setColor(color); - postInvalidate(); - } - - public void setLabel(String label) - { - this.label = label; - requestLayout(); - postInvalidate(); - } - - public void setNumber(int number) - { - this.number = number; - postInvalidate(); - } - - public void setTextSize(float textSize) - { - this.textSize = textSize; - requestLayout(); - postInvalidate(); - } - - private void init() - { - pText = new TextPaint(); - pText.setAntiAlias(true); - pText.setTextAlign(Paint.Align.CENTER); - - rect = new RectF(); - } - - @Override - @SuppressLint("DrawAllocation") - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) - { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - - width = MeasureSpec.getSize(widthMeasureSpec); - height = MeasureSpec.getSize(heightMeasureSpec); - - labelTextSize = textSize; - labelMarginTop = textSize * 0.35f; - numberTextSize = textSize * 2.85f; - - pText.setTextSize(numberTextSize); - numberLayout = new StaticLayout(Integer.toString(number), pText, width, - Layout.Alignment.ALIGN_NORMAL, 1.0f, 0f, false); - - int numberWidth = numberLayout.getWidth(); - int numberHeight = numberLayout.getHeight(); - - pText.setTextSize(labelTextSize); - labelLayout = new StaticLayout(label, pText, width, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0f, - false); - int labelWidth = labelLayout.getWidth(); - int labelHeight = labelLayout.getHeight(); - - width = Math.max(numberWidth, labelWidth); - height = (int) (numberHeight + labelHeight + labelMarginTop); - - setMeasuredDimension(width, height); - } - - @Override - protected void onDraw(Canvas canvas) - { - super.onDraw(canvas); - rect.set(0, 0, width, height); - - canvas.save(); - canvas.translate(rect.centerX(), 0); - pText.setColor(color); - pText.setTextSize(numberTextSize); - numberLayout.draw(canvas); - canvas.restore(); - - canvas.save(); - pText.setColor(Color.GRAY); - pText.setTextSize(labelTextSize); - canvas.translate(rect.centerX(), numberLayout.getHeight() + labelMarginTop); - labelLayout.draw(canvas); - canvas.restore(); - } -} diff --git a/app/src/main/java/org/isoron/uhabits/views/RepetitionCountView.java b/app/src/main/java/org/isoron/uhabits/views/RepetitionCountView.java deleted file mode 100644 index ee850ab2f..000000000 --- a/app/src/main/java/org/isoron/uhabits/views/RepetitionCountView.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (C) 2016 Á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.views; - -import android.content.Context; -import android.util.AttributeSet; - -import org.isoron.uhabits.R; -import org.isoron.uhabits.helpers.ColorHelper; -import org.isoron.uhabits.helpers.DateHelper; -import org.isoron.uhabits.helpers.UIHelper; -import org.isoron.uhabits.models.Habit; - -import java.util.Calendar; -import java.util.GregorianCalendar; - -public class RepetitionCountView extends NumberView implements HabitDataView -{ - private int interval; - private Habit habit; - - public RepetitionCountView(Context context, AttributeSet attrs) - { - super(context, attrs); - this.interval = UIHelper.getIntAttribute(context, attrs, "interval", 7); - int labelValue = UIHelper.getIntAttribute(context, attrs, "labelValue", 7); - String labelFormat = UIHelper.getAttribute(context, attrs, "labelFormat", - getResources().getString(R.string.last_x_days)); - - setLabel(String.format(labelFormat, labelValue)); - } - - @Override - public void refreshData() - { - if(isInEditMode()) - { - setNumber(interval); - return; - } - - long to = DateHelper.getStartOfToday(); - long from; - - if(interval == 0) - { - from = 0; - } - else - { - GregorianCalendar fromCalendar = DateHelper.getStartOfTodayCalendar(); - fromCalendar.add(Calendar.DAY_OF_YEAR, -interval + 1); - from = fromCalendar.getTimeInMillis(); - } - - if(habit != null) - setNumber(habit.repetitions.count(from, to)); - - postInvalidate(); - } - - @Override - public void setHabit(Habit habit) - { - this.habit = habit; - setColor(ColorHelper.getColor(getContext(), habit.color)); - } -} diff --git a/app/src/main/java/org/isoron/uhabits/widgets/BaseWidgetProvider.java b/app/src/main/java/org/isoron/uhabits/widgets/BaseWidgetProvider.java index 9c36fe398..89d91e2f4 100644 --- a/app/src/main/java/org/isoron/uhabits/widgets/BaseWidgetProvider.java +++ b/app/src/main/java/org/isoron/uhabits/widgets/BaseWidgetProvider.java @@ -19,300 +19,131 @@ package org.isoron.uhabits.widgets; -import android.app.PendingIntent; -import android.appwidget.AppWidgetManager; -import android.appwidget.AppWidgetProvider; -import android.content.Context; -import android.content.SharedPreferences; -import android.graphics.Bitmap; -import android.os.Build; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.ImageView; -import android.widget.RemoteViews; -import android.widget.TextView; +import android.appwidget.*; +import android.content.*; +import android.os.*; +import android.support.annotation.*; +import android.widget.*; -import org.isoron.uhabits.R; -import org.isoron.uhabits.helpers.UIHelper; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.tasks.BaseTask; +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.ui.widgets.*; +import org.isoron.uhabits.utils.*; -import java.io.FileOutputStream; -import java.io.IOException; +import javax.inject.*; + +import static android.os.Build.VERSION.*; +import static android.os.Build.VERSION_CODES.*; +import static org.isoron.uhabits.utils.WidgetUtils.*; public abstract class BaseWidgetProvider extends AppWidgetProvider { - private class WidgetDimensions - { - public int portraitWidth, portraitHeight; - public int landscapeWidth, landscapeHeight; - } - - protected abstract int getDefaultHeight(); - - protected abstract int getDefaultWidth(); - - protected abstract PendingIntent getOnClickPendingIntent(Context context, Habit habit); + @Inject + HabitList habitList; - protected abstract int getLayoutId(); + @Inject + WidgetPreferences widgetPrefs; - protected abstract View buildCustomView(Context context, Habit habit); - - public static String getHabitIdKey(long widgetId) + public BaseWidgetProvider() { - return String.format("widget-%06d-habit", widgetId); + HabitsApplication.getComponent().inject(this); } @Override - public void onDeleted(Context context, int[] appWidgetIds) + public void onAppWidgetOptionsChanged(@Nullable Context context, + @Nullable AppWidgetManager manager, + int widgetId, + @Nullable Bundle options) { - Context appContext = context.getApplicationContext(); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(appContext); - - for(Integer id : appWidgetIds) - prefs.edit().remove(getHabitIdKey(id)).apply(); - } - - @Override - public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, - int appWidgetId, Bundle newOptions) - { - updateWidget(context, appWidgetManager, appWidgetId, newOptions); - } - - @Override - public void onUpdate(Context context, AppWidgetManager manager, int[] appWidgetIds) - { - for(int id : appWidgetIds) + try { - Bundle options = null; - - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) - options = manager.getAppWidgetOptions(id); + if (context == null) throw new RuntimeException("context is null"); + if (manager == null) throw new RuntimeException("manager is null"); + if (options == null) throw new RuntimeException("options is null"); + context.setTheme(R.style.TransparentWidgetTheme); - updateWidget(context, manager, id, options); + BaseWidget widget = getWidgetFromId(context, widgetId); + WidgetDimensions dims = getDimensionsFromOptions(context, options); + widget.setDimensions(dims); + updateAppWidget(manager, widget); } - } - - private void updateWidget(Context context, AppWidgetManager manager, - int widgetId, Bundle options) - { - WidgetDimensions dim = getWidgetDimensions(context, options); - - Context appContext = context.getApplicationContext(); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(appContext); - - Long habitId = prefs.getLong(getHabitIdKey(widgetId), -1L); - if(habitId < 0) return; - - Habit habit = Habit.get(habitId); - if(habit == null) + catch (RuntimeException e) { drawErrorWidget(context, manager, widgetId); - return; + e.printStackTrace(); } - - new RenderWidgetTask(widgetId, context, habit, dim, manager).execute(); } - private void drawErrorWidget(Context context, AppWidgetManager manager, int widgetId) + @Override + public void onDeleted(@Nullable Context context, @Nullable int[] ids) { - RemoteViews errorView = new RemoteViews(context.getPackageName(), R.layout.widget_error); - manager.updateAppWidget(widgetId, errorView); - } - - protected abstract void refreshCustomViewData(View widgetView); + if (context == null) throw new RuntimeException("context is null"); + if (ids == null) throw new RuntimeException("ids is null"); - private void savePreview(Context context, int widgetId, Bitmap widgetCache, int width, - int height, String label) - { - try + for (int id : ids) { - LayoutInflater inflater = LayoutInflater.from(context); - View view = inflater.inflate(getLayoutId(), null); - - TextView tvLabel = (TextView) view.findViewById(R.id.label); - if(tvLabel != null) tvLabel.setText(label); - - ImageView iv = (ImageView) view.findViewById(R.id.imageView); - if(iv != null) iv.setImageBitmap(widgetCache); - - view.measure(width, height); - view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight()); - view.setDrawingCacheEnabled(true); - view.buildDrawingCache(); - Bitmap previewCache = view.getDrawingCache(); - - String filename = String.format("%s/%d_%d.png", context.getExternalCacheDir(), widgetId, width); - Log.d("BaseWidgetProvider", String.format("Writing %s", filename)); - FileOutputStream out = new FileOutputStream(filename); - - if(previewCache != null) - previewCache.compress(Bitmap.CompressFormat.PNG, 100, out); - - out.close(); - } - catch (IOException e) - { - e.printStackTrace(); + BaseWidget widget = getWidgetFromId(context, id); + widget.delete(); } } - private WidgetDimensions getWidgetDimensions(Context context, Bundle options) + @Override + public void onUpdate(@Nullable Context context, + @Nullable AppWidgetManager manager, + @Nullable int[] widgetIds) { - int maxWidth = getDefaultWidth(); - int minWidth = getDefaultWidth(); - int maxHeight = getDefaultHeight(); - int minHeight = getDefaultHeight(); - - if (options != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) - { - maxWidth = (int) UIHelper.dpToPixels(context, - options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH)); - maxHeight = (int) UIHelper.dpToPixels(context, - options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT)); - minWidth = (int) UIHelper.dpToPixels(context, - options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)); - minHeight = (int) UIHelper.dpToPixels(context, - options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT)); - } + if (context == null) throw new RuntimeException("context is null"); + if (manager == null) throw new RuntimeException("manager is null"); + if (widgetIds == null) throw new RuntimeException("widgetIds is null"); + context.setTheme(R.style.TransparentWidgetTheme); - WidgetDimensions ws = new WidgetDimensions(); - ws.portraitWidth = minWidth; - ws.portraitHeight = maxHeight; - ws.landscapeWidth = maxWidth; - ws.landscapeHeight = minHeight; - return ws; + for (int id : widgetIds) + update(context, manager, id); } - private void measureCustomView(Context context, int w, int h, View customView) + @NonNull + protected Habit getHabitFromWidgetId(int widgetId) { - LayoutInflater inflater = LayoutInflater.from(context); - View entireView = inflater.inflate(getLayoutId(), null); - - int specWidth = View.MeasureSpec.makeMeasureSpec(w, View.MeasureSpec.EXACTLY); - int specHeight = View.MeasureSpec.makeMeasureSpec(h, View.MeasureSpec.EXACTLY); - - entireView.measure(specWidth, specHeight); - entireView.layout(0, 0, entireView.getMeasuredWidth(), entireView.getMeasuredHeight()); + long habitId = widgetPrefs.getHabitIdFromWidgetId(widgetId); + Habit habit = habitList.getById(habitId); + if (habit == null) throw new RuntimeException("habit not found"); + return habit; + } - View imageView = entireView.findViewById(R.id.imageView); - w = imageView.getMeasuredWidth(); - h = imageView.getMeasuredHeight(); + @NonNull + protected abstract BaseWidget getWidgetFromId(@NonNull Context context, + int id); - specWidth = View.MeasureSpec.makeMeasureSpec(w, View.MeasureSpec.EXACTLY); - specHeight = View.MeasureSpec.makeMeasureSpec(h, View.MeasureSpec.EXACTLY); - customView.measure(specWidth, specHeight); - customView.layout(0, 0, customView.getMeasuredWidth(), customView.getMeasuredHeight()); + private void drawErrorWidget(Context context, + AppWidgetManager manager, + int widgetId) + { + RemoteViews errorView = + new RemoteViews(context.getPackageName(), R.layout.widget_error); + manager.updateAppWidget(widgetId, errorView); } - private class RenderWidgetTask extends BaseTask + private void update(@NonNull Context context, + @NonNull AppWidgetManager manager, + int widgetId) { - private final int widgetId; - private final Context context; - private final Habit habit; - private final AppWidgetManager manager; - private RemoteViews portraitRemoteViews, landscapeRemoteViews; - private View portraitWidgetView, landscapeWidgetView; - private WidgetDimensions dim; - - public RenderWidgetTask(int widgetId, Context context, Habit habit, WidgetDimensions ws, - AppWidgetManager manager) - { - this.widgetId = widgetId; - this.context = context; - this.habit = habit; - this.manager = manager; - this.dim = ws; - } - - @Override - protected void onPreExecute() - { - super.onPreExecute(); - context.setTheme(R.style.TransparentWidgetTheme); - - portraitRemoteViews = new RemoteViews(context.getPackageName(), getLayoutId()); - portraitWidgetView = buildCustomView(context, habit); - measureCustomView(context, dim.portraitWidth, dim.portraitHeight, portraitWidgetView); - - landscapeRemoteViews = new RemoteViews(context.getPackageName(), getLayoutId()); - landscapeWidgetView = buildCustomView(context, habit); - measureCustomView(context, dim.landscapeWidth, dim.landscapeHeight, - landscapeWidgetView); - } - - private void updateAppWidget() - { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) - manager.updateAppWidget(widgetId, new RemoteViews(landscapeRemoteViews, - portraitRemoteViews)); - else - manager.updateAppWidget(widgetId, portraitRemoteViews); - } - - @Override - protected void doInBackground() + try { - refreshCustomViewData(portraitWidgetView); - refreshCustomViewData(landscapeWidgetView); - } + BaseWidget widget = getWidgetFromId(context, widgetId); - @Override - protected void onPostExecute(Void aVoid) - { - try - { - buildRemoteViews(portraitWidgetView, portraitRemoteViews, - dim.portraitWidth, dim.portraitHeight); - buildRemoteViews(landscapeWidgetView, landscapeRemoteViews, - dim.landscapeWidth, dim.landscapeHeight); - updateAppWidget(); - } - catch (Exception e) + if (SDK_INT > JELLY_BEAN) { - drawErrorWidget(context, manager, widgetId); - e.printStackTrace(); + Bundle options = manager.getAppWidgetOptions(widgetId); + widget.setDimensions( + getDimensionsFromOptions(context, options)); } - super.onPostExecute(aVoid); + updateAppWidget(manager, widget); } - - private void buildRemoteViews(View widgetView, RemoteViews remoteViews, int width, - int height) + catch (RuntimeException e) { - widgetView.invalidate(); - widgetView.setDrawingCacheEnabled(true); - widgetView.buildDrawingCache(true); - Bitmap drawingCache = widgetView.getDrawingCache(); - remoteViews.setTextViewText(R.id.label, habit.name); - remoteViews.setImageViewBitmap(R.id.imageView, drawingCache); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) - { - int imageWidth = widgetView.getMeasuredWidth(); - int imageHeight = widgetView.getMeasuredHeight(); - int p[] = getPadding(width, height, imageWidth, imageHeight); - remoteViews.setViewPadding(R.id.buttonOverlay, p[0], p[1], p[2], p[3]); - } - - //savePreview(context, widgetId, drawingCache, width, height, habit.name); - - PendingIntent onClickIntent = getOnClickPendingIntent(context, habit); - if (onClickIntent != null) remoteViews.setOnClickPendingIntent(R.id.button, - onClickIntent); + drawErrorWidget(context, manager, widgetId); + e.printStackTrace(); } } - - private int[] getPadding(int entireWidth, int entireHeight, int imageWidth, - int imageHeight) - { - int w = (int) (((float) entireWidth - imageWidth) / 2); - int h = (int) (((float) entireHeight - imageHeight) / 2); - - return new int[]{ w, h, w, h }; - } } diff --git a/app/src/main/java/org/isoron/uhabits/widgets/CheckmarkWidgetProvider.java b/app/src/main/java/org/isoron/uhabits/widgets/CheckmarkWidgetProvider.java index 6f6e12dbc..c574d9c2e 100644 --- a/app/src/main/java/org/isoron/uhabits/widgets/CheckmarkWidgetProvider.java +++ b/app/src/main/java/org/isoron/uhabits/widgets/CheckmarkWidgetProvider.java @@ -18,55 +18,19 @@ */ package org.isoron.uhabits.widgets; -import android.app.PendingIntent; -import android.content.Context; -import android.view.View; +import android.content.*; +import android.support.annotation.*; -import org.isoron.uhabits.HabitBroadcastReceiver; -import org.isoron.uhabits.R; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.views.CheckmarkWidgetView; -import org.isoron.uhabits.views.HabitDataView; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.ui.widgets.*; public class CheckmarkWidgetProvider extends BaseWidgetProvider { + @NonNull @Override - protected View buildCustomView(Context context, Habit habit) + protected CheckmarkWidget getWidgetFromId(@NonNull Context context, int id) { - CheckmarkWidgetView view = new CheckmarkWidgetView(context); - view.setHabit(habit); - return view; + Habit habit = getHabitFromWidgetId(id); + return new CheckmarkWidget(context, id, habit); } - - @Override - protected void refreshCustomViewData(View view) - { - ((HabitDataView) view).refreshData(); - } - - @Override - protected PendingIntent getOnClickPendingIntent(Context context, Habit habit) - { - return HabitBroadcastReceiver.buildCheckIntent(context, habit, null, 2); - } - - @Override - protected int getDefaultHeight() - { - return 125; - } - - @Override - protected int getDefaultWidth() - { - return 125; - } - - @Override - protected int getLayoutId() - { - return R.layout.widget_wrapper; - } - - } diff --git a/app/src/main/java/org/isoron/uhabits/widgets/FrequencyWidgetProvider.java b/app/src/main/java/org/isoron/uhabits/widgets/FrequencyWidgetProvider.java index 2fdbedb71..ab7d1b8bc 100644 --- a/app/src/main/java/org/isoron/uhabits/widgets/FrequencyWidgetProvider.java +++ b/app/src/main/java/org/isoron/uhabits/widgets/FrequencyWidgetProvider.java @@ -19,55 +19,19 @@ package org.isoron.uhabits.widgets; -import android.app.PendingIntent; -import android.content.Context; -import android.view.View; +import android.content.*; +import android.support.annotation.*; -import org.isoron.uhabits.HabitBroadcastReceiver; -import org.isoron.uhabits.R; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.views.GraphWidgetView; -import org.isoron.uhabits.views.HabitDataView; -import org.isoron.uhabits.views.HabitFrequencyView; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.ui.widgets.*; public class FrequencyWidgetProvider extends BaseWidgetProvider { + @NonNull @Override - protected View buildCustomView(Context context, Habit habit) + protected BaseWidget getWidgetFromId(@NonNull Context context, int id) { - HabitFrequencyView dataView = new HabitFrequencyView(context); - GraphWidgetView view = new GraphWidgetView(context, dataView); - view.setHabit(habit); - return view; - } - - @Override - protected void refreshCustomViewData(View view) - { - ((HabitDataView) view).refreshData(); - } - - @Override - protected PendingIntent getOnClickPendingIntent(Context context, Habit habit) - { - return HabitBroadcastReceiver.buildViewHabitIntent(context, habit); - } - - @Override - protected int getDefaultHeight() - { - return 200; - } - - @Override - protected int getDefaultWidth() - { - return 200; - } - - @Override - protected int getLayoutId() - { - return R.layout.widget_wrapper; + Habit habit = getHabitFromWidgetId(id); + return new FrequencyWidget(context, id, habit); } } diff --git a/app/src/main/java/org/isoron/uhabits/widgets/HistoryWidgetProvider.java b/app/src/main/java/org/isoron/uhabits/widgets/HistoryWidgetProvider.java index bb8be7e25..23ad2340e 100644 --- a/app/src/main/java/org/isoron/uhabits/widgets/HistoryWidgetProvider.java +++ b/app/src/main/java/org/isoron/uhabits/widgets/HistoryWidgetProvider.java @@ -18,55 +18,19 @@ */ package org.isoron.uhabits.widgets; -import android.app.PendingIntent; -import android.content.Context; -import android.view.View; +import android.content.*; +import android.support.annotation.*; -import org.isoron.uhabits.HabitBroadcastReceiver; -import org.isoron.uhabits.R; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.views.GraphWidgetView; -import org.isoron.uhabits.views.HabitDataView; -import org.isoron.uhabits.views.HabitHistoryView; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.ui.widgets.*; -public class HistoryWidgetProvider extends BaseWidgetProvider +public class HistoryWidgetProvider extends BaseWidgetProvider { + @NonNull @Override - protected View buildCustomView(Context context, Habit habit) + protected BaseWidget getWidgetFromId(@NonNull Context context, int id) { - HabitHistoryView dataView = new HabitHistoryView(context); - GraphWidgetView view = new GraphWidgetView(context, dataView); - view.setHabit(habit); - return view; - } - - @Override - protected void refreshCustomViewData(View view) - { - ((HabitDataView) view).refreshData(); - } - - @Override - protected PendingIntent getOnClickPendingIntent(Context context, Habit habit) - { - return HabitBroadcastReceiver.buildViewHabitIntent(context, habit); - } - - @Override - protected int getDefaultHeight() - { - return 250; - } - - @Override - protected int getDefaultWidth() - { - return 250; - } - - @Override - protected int getLayoutId() - { - return R.layout.widget_wrapper; + Habit habit = getHabitFromWidgetId(id); + return new HistoryWidget(context, id, habit); } } diff --git a/app/src/main/java/org/isoron/uhabits/widgets/ScoreWidgetProvider.java b/app/src/main/java/org/isoron/uhabits/widgets/ScoreWidgetProvider.java index 2608887b4..97167bc85 100644 --- a/app/src/main/java/org/isoron/uhabits/widgets/ScoreWidgetProvider.java +++ b/app/src/main/java/org/isoron/uhabits/widgets/ScoreWidgetProvider.java @@ -18,62 +18,19 @@ */ package org.isoron.uhabits.widgets; -import android.app.PendingIntent; -import android.content.Context; -import android.view.View; +import android.content.*; +import android.support.annotation.*; -import org.isoron.uhabits.HabitBroadcastReceiver; -import org.isoron.uhabits.R; -import org.isoron.uhabits.helpers.UIHelper; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.views.GraphWidgetView; -import org.isoron.uhabits.views.HabitDataView; -import org.isoron.uhabits.views.HabitScoreView; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.ui.widgets.*; public class ScoreWidgetProvider extends BaseWidgetProvider { + @NonNull @Override - protected View buildCustomView(Context context, Habit habit) + protected BaseWidget getWidgetFromId(@NonNull Context context, int id) { - int defaultScoreInterval = UIHelper.getDefaultScoreInterval(context); - int size = HabitScoreView.DEFAULT_BUCKET_SIZES[defaultScoreInterval]; - - HabitScoreView dataView = new HabitScoreView(context); - dataView.setIsTransparencyEnabled(true); - dataView.setBucketSize(size); - - GraphWidgetView view = new GraphWidgetView(context, dataView); - view.setHabit(habit); - return view; - } - - @Override - protected void refreshCustomViewData(View view) - { - ((HabitDataView) view).refreshData(); - } - - @Override - protected PendingIntent getOnClickPendingIntent(Context context, Habit habit) - { - return HabitBroadcastReceiver.buildViewHabitIntent(context, habit); - } - - @Override - protected int getDefaultHeight() - { - return 300; - } - - @Override - protected int getDefaultWidth() - { - return 300; - } - - @Override - protected int getLayoutId() - { - return R.layout.widget_wrapper; + Habit habit = getHabitFromWidgetId(id); + return new ScoreWidget(context, id, habit); } } diff --git a/app/src/main/java/org/isoron/uhabits/widgets/StreakWidgetProvider.java b/app/src/main/java/org/isoron/uhabits/widgets/StreakWidgetProvider.java index f0455d00a..df0691598 100644 --- a/app/src/main/java/org/isoron/uhabits/widgets/StreakWidgetProvider.java +++ b/app/src/main/java/org/isoron/uhabits/widgets/StreakWidgetProvider.java @@ -18,55 +18,31 @@ */ package org.isoron.uhabits.widgets; -import android.app.PendingIntent; -import android.content.Context; -import android.view.View; +import android.content.*; +import android.support.annotation.*; -import org.isoron.uhabits.HabitBroadcastReceiver; -import org.isoron.uhabits.R; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.views.GraphWidgetView; -import org.isoron.uhabits.views.HabitDataView; -import org.isoron.uhabits.views.HabitStreakView; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.ui.widgets.*; -public class StreakWidgetProvider extends BaseWidgetProvider +public class StreakWidgetProvider extends BaseWidgetProvider { + @NonNull @Override - protected View buildCustomView(Context context, Habit habit) + protected BaseWidget getWidgetFromId(@NonNull Context context, int id) { - HabitStreakView dataView = new HabitStreakView(context); - GraphWidgetView view = new GraphWidgetView(context, dataView); - view.setHabit(habit); - return view; + Habit habit = getHabitFromWidgetId(id); + return new StreakWidget(context, id, habit); } - @Override - protected void refreshCustomViewData(View view) - { - ((HabitDataView) view).refreshData(); - } - - @Override - protected PendingIntent getOnClickPendingIntent(Context context, Habit habit) - { - return HabitBroadcastReceiver.buildViewHabitIntent(context, habit); - } - - @Override - protected int getDefaultHeight() - { - return 200; - } - - @Override - protected int getDefaultWidth() - { - return 200; - } - - @Override - protected int getLayoutId() - { - return R.layout.widget_wrapper; - } +// GraphWidgetView widgetView = (GraphWidgetView) view; +// StreakChart chart = (StreakChart) widgetView.getDataView(); +// +// int color = ColorUtils.getColor(context, habit.getColor()); +// +// // TODO: make this dynamic +// List streaks = habit.getStreaks().getBest(10); +// +// chart.setColor(color); +// chart.setStreaks(streaks); +// } } diff --git a/app/src/main/java/org/isoron/uhabits/widgets/package-info.java b/app/src/main/java/org/isoron/uhabits/widgets/package-info.java new file mode 100644 index 000000000..5616e64ad --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/widgets/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2016 Á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 . + */ + +/** + * Provides home-screen Android widgets and related classes. + */ +package org.isoron.uhabits.widgets; \ No newline at end of file diff --git a/app/src/main/res/layout/edit_habit.xml b/app/src/main/res/layout/edit_habit.xml index bc2f0a98c..e744f58e5 100644 --- a/app/src/main/res/layout/edit_habit.xml +++ b/app/src/main/res/layout/edit_habit.xml @@ -22,7 +22,7 @@ style="@style/dialogForm" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" - tools:context=".dialogs.EditHabitDialogFragment" + tools:context=".ui.habits.edit.BaseDialogFragment" tools:ignore="MergeRootFrame"> @@ -49,7 +49,7 @@ @@ -78,7 +78,7 @@ android:gravity="fill"> @@ -125,7 +125,7 @@ android:text=""/> diff --git a/app/src/main/res/layout/list_habits_fragment.xml b/app/src/main/res/layout/list_habits.xml similarity index 56% rename from app/src/main/res/layout/list_habits_fragment.xml rename to app/src/main/res/layout/list_habits.xml index bf97d95be..8e4781399 100644 --- a/app/src/main/res/layout/list_habits_fragment.xml +++ b/app/src/main/res/layout/list_habits.xml @@ -19,31 +19,44 @@ --> + android:background="?windowBackgroundColor"> - + + + + + app:drag_start_mode="onLongPress" + app:sort_enabled="true" + app:track_drag_sort="false" + android:layout_below="@id/header"/> + android:orientation="vertical" + android:visibility="gone"> - - - - - - - - - - - - + android:layout_alignParentBottom="true"/> - - - + \ No newline at end of file diff --git a/app/src/main/res/layout/list_habits_activity.xml b/app/src/main/res/layout/list_habits_activity.xml deleted file mode 100644 index 0bb0106cd..000000000 --- a/app/src/main/res/layout/list_habits_activity.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/layout/list_habits_item.xml b/app/src/main/res/layout/list_habits_card.xml similarity index 60% rename from app/src/main/res/layout/list_habits_item.xml rename to app/src/main/res/layout/list_habits_card.xml index 63a766d35..3d2ecc88e 100644 --- a/app/src/main/res/layout/list_habits_item.xml +++ b/app/src/main/res/layout/list_habits_card.xml @@ -18,31 +18,34 @@ ~ with this program. If not, see . --> - + + android:id="@+id/innerFrame" + style="@style/ListHabits.HabitCard" + android:layout_width="match_parent"> - + android:layout_marginTop="0dp" + habit:thickness="3"/> + style="@style/ListHabits.Label"/> + + - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/list_habits_item_check.xml b/app/src/main/res/layout/list_habits_card_checkmark.xml similarity index 100% rename from app/src/main/res/layout/list_habits_item_check.xml rename to app/src/main/res/layout/list_habits_card_checkmark.xml diff --git a/app/src/main/res/layout/list_habits_header_check.xml b/app/src/main/res/layout/list_habits_header_checkmark.xml similarity index 100% rename from app/src/main/res/layout/list_habits_header_check.xml rename to app/src/main/res/layout/list_habits_header_checkmark.xml diff --git a/app/src/main/res/layout/list_habits_hint.xml b/app/src/main/res/layout/list_habits_hint.xml new file mode 100644 index 000000000..11d86bd72 --- /dev/null +++ b/app/src/main/res/layout/list_habits_hint.xml @@ -0,0 +1,46 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_habits_preview.xml b/app/src/main/res/layout/list_habits_preview.xml new file mode 100644 index 000000000..87250848d --- /dev/null +++ b/app/src/main/res/layout/list_habits_preview.xml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/settings_activity.xml b/app/src/main/res/layout/settings_activity.xml index cf4cf8048..210bcf43c 100644 --- a/app/src/main/res/layout/settings_activity.xml +++ b/app/src/main/res/layout/settings_activity.xml @@ -23,7 +23,7 @@ android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context="org.isoron.uhabits.SettingsActivity" + tools:context=".ui.settings.SettingsActivity" tools:ignore="MergeRootFrame"> . --> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -