diff --git a/.gitignore b/.gitignore index b5365d8c1..655fdf419 100644 --- a/.gitignore +++ b/.gitignore @@ -1,36 +1,22 @@ -#built application files -*.apk *.ap_ - -# files for the dex VM -*.dex - -# Java class files +*.apk *.class - -# generated files -bin/ -gen/ - -# Local configuration file (sdk path, etc) -local.properties - -# Windows thumbnail db -Thumbs.db - -# OSX files +*.dex +*.iml +*.local.* +*.swp +*.trace +*~ .DS_Store - -# Eclipse project files .classpath -.project - -# Android Studio -.idea -#.idea/workspace.xml - remove # and delete .idea if it better suit your needs. .gradle -build/ -*.iml - +.idea +.project +Thumbs.db art/ -*.actual.png +bin/ +build/ +captures/ +docs/ +gen/ +local.properties diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 5117b1e5e..000000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "libs/drag-sort-listview"] - path = libs/drag-sort-listview - url = https://github.com/iSoron/drag-sort-listview.git diff --git a/CHANGELOG.md b/CHANGELOG.md index e30699b10..33d769281 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # Changelog +### 1.6.0 (Oct 10, 2016) + +* Add option to make notifications sticky +* Add option to hide completed habits +* Display total number of repetitions for each habit +* Pebble integration: check/snooze habits from the watch +* Tasker/Locale integration: allow third-party apps to add checkmarks +* Export an unified CSV file, with checkmarks for all the habits +* Increase width of name column according to screen size +* Stop showing reminders for archived habits +* Add Danish, Dutch, Greek, Hindi and Portuguese (PT) translations +* Other minor fixes and enhancements + +### 1.5.6 (Jun 19, 2016) + +* Fix bug that prevented checkmark widget from working + +### 1.5.5 (Jun 19, 2016) + +* Fix bug that prevented check button on notification to work sometimes +* Fix bug that caused back button to apparently erase some checkmarks +* Complete French translation +* Add Croatian and Slovenian translations + ### 1.5.4 (May 29, 2016) * Fix crash upon opening settings screen in some phones diff --git a/NOTICE.md b/NOTICE.md index cea565a0b..d28f35a31 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -59,28 +59,6 @@ under the SIL OFL 1.1. requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. - -### DragSortListView - - - - A subclass of the Android ListView component that enables drag - and drop re-ordering of list items. - - Copyright 2012 Carl Bauer - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - ### Material Design Icons @@ -107,4 +85,123 @@ Extended linear layout that wrap its content when there is no place in the curre distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations - under the License. \ No newline at end of file + under the License. + +### Dagger 2 + + + +A fast dependency injector for Android and Java. + + Copyright 2012 Square, Inc. + Copyright 2012 Google, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +### AutoFactory + + + +A source code generator for JSR-330-compatible factories. + + Copyright 2013 Google, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +### Retrolambda + + + +Backport of Java 8's lambda expressions to Java 7, 6 and 5 + + Copyright (c) 2013-2016 Esko Luontola and other Retrolambda contributors + This software is released under the Apache License 2.0. + The license text is at http://www.apache.org/licenses/LICENSE-2.0 + +### PebbleKit SDK + + + +Android PebbleKit SDK to talk to the Pebble via Bluetooth + + The MIT License (MIT) + Copyright (c) 2014 - 2015 Pebble Technology + +### AppIntro + + + +Make a cool intro for your Android app. + + Copyright 2015 Paolo Rotolo + Copyright 2016 Maximilian Narr + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +### ButterKnife + + + +Bind Android views and callbacks to fields and methods + + Copyright 2013 Jake Wharton + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +### opencsv + + + +Opencsv is a very simple csv (comma-separated values) parser library for Java. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index f108eca4e..244743f0e 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,13 @@ # Loop Habit Tracker + + Loop is a simple Android app that helps you create and maintain good habits, allowing you to achieve your long-term goals. Detailed graphs and statistics @@ -16,6 +19,15 @@ source. Git if on F-Droid

+## Screenshots + +[![Main screen][screen1th]][screen1] +[![Edit habit][screen2th]][screen2] +[![Habit strength][screen3th]][screen3] +[![Habit history and streaks][screen4th]][screen4] +[![Widgets][screen5th]][screen5] +[![Night mode][screen6th]][screen6] + ## Features * **Simple, beautiful and modern interface.** Loop has a minimalistic interface @@ -47,21 +59,12 @@ source. and there will never be. The complete source code is available under the GPLv3. -## Screenshots - -[![Main screen][screen1th]][screen1] -[![Edit habit][screen2th]][screen2] -[![Habit strength][screen3th]][screen3] -[![Habit history and streaks][screen4th]][screen4] -[![Widgets][screen5th]][screen5] -[![Night mode][screen6th]][screen6] - ## Installing The easiest way to install Loop is through the [Google Play Store][playstore] or [F-Droid][fdroid]. You may also download and install the APK from the [releases page][releases]; note, however, that the app will not be updated automatically. To build this -app from the source code, see [building instructions][build]. +app from the source code, see [build instructions][build]. ## Contributing @@ -89,18 +92,22 @@ contribute, even if you are not a software developer. ## License - This program 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. + + + Copyright (C) 2016 Álinson Santos Xavier + + 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. - This program 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. + 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 . + You should have received a copy of the GNU General Public License along + with this program. If not, see . [screen1]: screenshots/original/uhabits1.png [screen2]: screenshots/original/uhabits2.png diff --git a/app/build.gradle b/app/build.gradle index f8cefba08..2a5f2ae4b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,4 +1,7 @@ apply plugin: 'com.android.application' +apply plugin: 'com.neenbedankt.android-apt' +apply plugin: 'me.tatarka.retrolambda' +apply plugin: 'jacoco' android { compileSdkVersion 23 @@ -13,7 +16,7 @@ android { buildConfigField "String", "databaseFilename", "\"uhabits.db\"" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" - //testInstrumentationRunnerArgument "size", "small" + testInstrumentationRunnerArgument "size", "medium" } buildTypes { @@ -29,23 +32,59 @@ 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 { - compile 'com.android.support:support-v4:23.3.0' + + androidTestApt 'com.google.dagger:dagger-compiler:2.2' + + androidTestCompile 'com.android.support:support-annotations:23.3.0' + androidTestCompile 'com.android.support.test:rules:0.5' + androidTestCompile 'com.android.support.test:runner:0.5' + androidTestCompile 'com.google.auto.factory:auto-factory:1.0-beta3' + androidTestCompile "com.google.dexmaker:dexmaker:1.2" + androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2' + androidTestCompile 'org.mockito:mockito-core:1.10.19' + + apt 'com.google.dagger:dagger-compiler:2.2' + apt 'com.jakewharton:butterknife-compiler:8.0.1' + compile 'com.android.support:appcompat-v7:23.3.0' compile 'com.android.support:design:23.3.0' compile 'com.android.support:preference-v14:23.3.0' + compile 'com.android.support:support-v4:23.3.0' + compile 'com.getpebble:pebblekit:3.0.0' compile 'com.github.paolorotolo:appintro:3.4.0' - compile 'org.apmem.tools:layouts:1.10@aar' - compile 'com.opencsv:opencsv:3.7' + compile 'com.google.auto.factory:auto-factory:1.0-beta3' + compile 'com.google.dagger:dagger:2.2' + compile 'com.jakewharton:butterknife:8.0.1' compile 'com.michaelpardo:activeandroid:3.1.0-SNAPSHOT' + compile 'com.opencsv:opencsv:3.7' + compile 'org.apmem.tools:layouts:1.10@aar' + compile 'org.jetbrains:annotations-java5:15.0' - compile project(':libs:drag-sort-listview:library') + provided 'javax.annotation:jsr250-api:1.0' - 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' + testApt 'com.google.dagger:dagger-compiler:2.2' + + testCompile 'junit:junit:4.12' + testCompile 'org.hamcrest:hamcrest-library:1.3' + testCompile 'org.mockito:mockito-core:1.10.19' androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.1') { exclude group: 'com.android.support' @@ -60,13 +99,44 @@ dependencies { } } +retrolambda { + defaultMethods true +} -task grantAnimationPermission(type: Exec, dependsOn: 'installDebug') { - commandLine "adb shell pm grant org.isoron.uhabits android.permission.SET_ANIMATION_SCALE".split(' ') +jacoco { + toolVersion = "0.7.4.201502262128" } -tasks.whenTaskAdded { task -> - if (task.name.startsWith('connected')) { - task.dependsOn grantAnimationPermission +task coverageReport(type: JacocoReport, dependsOn: ['testDebugUnitTest']) { + + jacocoClasspath = configurations['androidJacocoAnt'] + + reports { + html.enabled = true } -} \ No newline at end of file + + def excludes = [ + '**/R.class', + '**/R$*.class', + '**/BuildConfig.*', + '**/Manifest*', + '**/*Test*.*', + '**/*$Lambda$*', + '**/*$ViewBinder*', + '**/*MembersInjector*', + '**/*_Provide*', + '**/com/android/**/*', + 'android/**/*', + '**/*Dagger*', + '**/*_Factory*' + ] + + def srcDir = "${project.projectDir}/src/main/java" + def classDir = "${buildDir}/intermediates/classes/debug" + def jvmExecData = "${buildDir}/jacoco/testDebugUnitTest.exec" + def connectedExecData = "${buildDir}/outputs/code-coverage/connected/coverage.ec" + + sourceDirectories = files(srcDir) + classDirectories = files(fileTree(dir: classDir, excludes: excludes)) + executionData = files(jvmExecData, connectedExecData) +} diff --git a/app/proguard-rules.txt b/app/proguard-rules.txt new file mode 100644 index 000000000..363b143b7 --- /dev/null +++ b/app/proguard-rules.txt @@ -0,0 +1,3 @@ +-dontwarn java.beans.** +-dontwarn java.lang.** +-dontobfuscate \ 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/HabitCardView/render.png b/app/src/androidTest/assets/views/habits/list/HabitCardView/render.png new file mode 100644 index 000000000..9144a5077 Binary files /dev/null and b/app/src/androidTest/assets/views/habits/list/HabitCardView/render.png differ diff --git a/app/src/androidTest/assets/views/habits/list/HabitCardView/render_changed.png b/app/src/androidTest/assets/views/habits/list/HabitCardView/render_changed.png new file mode 100644 index 000000000..4533ccec4 Binary files /dev/null and b/app/src/androidTest/assets/views/habits/list/HabitCardView/render_changed.png differ diff --git a/app/src/androidTest/assets/views/habits/list/HabitCardView/render_selected.png b/app/src/androidTest/assets/views/habits/list/HabitCardView/render_selected.png new file mode 100644 index 000000000..97b15373d Binary files /dev/null and b/app/src/androidTest/assets/views/habits/list/HabitCardView/render_selected.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/habits/show/FrequencyCard/render.png b/app/src/androidTest/assets/views/habits/show/FrequencyCard/render.png new file mode 100644 index 000000000..1e050904c Binary files /dev/null and b/app/src/androidTest/assets/views/habits/show/FrequencyCard/render.png differ diff --git a/app/src/androidTest/assets/views/habits/show/HistoryCard/render.png b/app/src/androidTest/assets/views/habits/show/HistoryCard/render.png new file mode 100644 index 000000000..7cde393d2 Binary files /dev/null and b/app/src/androidTest/assets/views/habits/show/HistoryCard/render.png differ diff --git a/app/src/androidTest/assets/views/habits/show/OverviewCard/render.png b/app/src/androidTest/assets/views/habits/show/OverviewCard/render.png new file mode 100644 index 000000000..d3f6cb13c Binary files /dev/null and b/app/src/androidTest/assets/views/habits/show/OverviewCard/render.png differ diff --git a/app/src/androidTest/assets/views/habits/show/ScoreCard/render.png b/app/src/androidTest/assets/views/habits/show/ScoreCard/render.png new file mode 100644 index 000000000..e604ba958 Binary files /dev/null and b/app/src/androidTest/assets/views/habits/show/ScoreCard/render.png differ diff --git a/app/src/androidTest/assets/views/habits/show/StreakCard/render.png b/app/src/androidTest/assets/views/habits/show/StreakCard/render.png new file mode 100644 index 000000000..eca454e71 Binary files /dev/null and b/app/src/androidTest/assets/views/habits/show/StreakCard/render.png differ diff --git a/app/src/androidTest/assets/views/habits/show/SubtitleCard/render.png b/app/src/androidTest/assets/views/habits/show/SubtitleCard/render.png new file mode 100644 index 000000000..7e17a10b3 Binary files /dev/null and b/app/src/androidTest/assets/views/habits/show/SubtitleCard/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..55a756d92 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/AndroidTestComponent.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; + + +import org.isoron.uhabits.models.sqlite.*; +import org.isoron.uhabits.tasks.*; + +import dagger.*; + +@AppScope +@Component(modules = { + AppModule.class, SingleThreadTaskRunner.class, SQLModelFactory.class +}) +public interface AndroidTestComponent extends AppComponent +{ + + +} 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..edb28d55d --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.java @@ -0,0 +1,133 @@ +/* + * 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.models.*; +import org.isoron.uhabits.preferences.*; +import org.isoron.uhabits.tasks.*; +import org.isoron.uhabits.utils.*; +import org.junit.*; + +import java.util.*; +import java.util.concurrent.*; + +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; + + protected Preferences prefs; + + protected HabitList habitList; + + protected TaskRunner taskRunner; + + protected HabitLogger logger; + + protected HabitFixtures fixtures; + + protected CountDownLatch latch; + + protected AndroidTestComponent component; + + @Before + public void setUp() + { + if (!isLooperPrepared) + { + Looper.prepare(); + isLooperPrepared = true; + } + + targetContext = InstrumentationRegistry.getTargetContext(); + testContext = InstrumentationRegistry.getContext(); + + DateUtils.setFixedLocalTime(FIXED_LOCAL_TIME); + setTheme(R.style.AppBaseTheme); + + component = DaggerAndroidTestComponent + .builder() + .appModule(new AppModule(targetContext.getApplicationContext())) + .build(); + + HabitsApplication.setComponent(component); + prefs = component.getPreferences(); + habitList = component.getHabitList(); + taskRunner = component.getTaskRunner(); + logger = component.getHabitsLogger(); + + ModelFactory modelFactory = component.getModelFactory(); + fixtures = new HabitFixtures(modelFactory, 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 awaitLatch() throws InterruptedException + { + assertTrue(latch.await(60, TimeUnit.SECONDS)); + } + + protected void setTheme(@StyleRes int themeId) + { + targetContext.setTheme(themeId); + StyledResources.setFixedTheme(themeId); + } + + protected void sleep(int time) + { + try + { + Thread.sleep(time); + } + catch (InterruptedException e) + { + fail(); + } + } +} 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 54% 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..689cafcaa 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.utils.*; +import org.isoron.uhabits.widgets.*; -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,128 @@ 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.setLayoutParams(new ViewGroup.LayoutParams(width, height)); + 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 +198,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..bd5928ecc --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/HabitFixtures.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; + +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 ModelFactory modelFactory; + + private final HabitList habitList; + + public HabitFixtures(ModelFactory modelFactory, HabitList habitList) + { + this.modelFactory = modelFactory; + this.habitList = habitList; + } + + public Habit createEmptyHabit() + { + Habit habit = modelFactory.buildHabit(); + 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 = modelFactory.buildHabit(); + 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) + habitList.remove(h); + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/HabitLoggerTest.java b/app/src/androidTest/java/org/isoron/uhabits/HabitLoggerTest.java new file mode 100644 index 000000000..05c2c85c6 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/HabitLoggerTest.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; + +import android.os.*; +import android.support.test.runner.*; +import android.test.suitebuilder.annotation.*; + +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.activities.*; +import org.junit.*; +import org.junit.runner.*; + +import java.io.*; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +@RunWith(AndroidJUnit4.class) +@MediumTest +public class HabitLoggerTest extends BaseAndroidTest +{ + @Test + public void testLogReminderScheduled() throws IOException + { + if (!isLogcatAvailable()) return; + + long time = 1422277200000L; // 13:00 jan 26, 2015 (UTC) + Habit habit = fixtures.createEmptyHabit(); + habit.setName("Write journal"); + + logger.logReminderScheduled(habit, time); + + String expectedMsg = "Setting alarm (2015-01-26 130000): Wri\n"; + assertLogcatContains(expectedMsg); + } + + protected void assertLogcatContains(String expectedMsg) throws IOException + { + BaseSystem system = new BaseSystem(targetContext); + String logcat = system.getLogcat(); + assertThat(logcat, containsString(expectedMsg)); + } + + protected boolean isLogcatAvailable() + { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; + } +} 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 69% 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..46b3c03c3 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.activities.*; +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 +@MediumTest +public class HabitsApplicationTest extends BaseAndroidTest { @Test public void test_getLogcat() throws IOException @@ -45,7 +45,8 @@ public class HabitsApplicationTest String msg = "LOGCAT TEST"; new RuntimeException(msg).printStackTrace(); - String log = HabitsApplication.getLogcat(); + BaseSystem system = new BaseSystem(targetContext); + String log = system.getLogcat(); assertThat(log, containsString(msg)); } } diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/views/HabitFrequencyViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/activities/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/activities/common/views/FrequencyChartTest.java index 590e2c9d2..2bdf05438 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/views/HabitFrequencyViewTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/activities/common/views/FrequencyChartTest.java @@ -17,64 +17,66 @@ * with this program. If not, see . */ -package org.isoron.uhabits.unit.views; +package org.isoron.uhabits.activities.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/activities/common/views/HistoryChartTest.java b/app/src/androidTest/java/org/isoron/uhabits/activities/common/views/HistoryChartTest.java new file mode 100644 index 000000000..05a2c5a06 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/activities/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.activities.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/activities/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/activities/common/views/RingViewTest.java index 9f831ba49..6dcf84081 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/views/RingViewTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/activities/common/views/RingViewTest.java @@ -17,35 +17,37 @@ * with this program. If not, see . */ -package org.isoron.uhabits.unit.views; +package org.isoron.uhabits.activities.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/activities/common/views/ScoreChartTest.java similarity index 51% rename from app/src/androidTest/java/org/isoron/uhabits/unit/views/HabitScoreViewTest.java rename to app/src/androidTest/java/org/isoron/uhabits/activities/common/views/ScoreChartTest.java index a7f1228b9..d747eb77a 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/views/HabitScoreViewTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/activities/common/views/ScoreChartTest.java @@ -17,88 +17,92 @@ * with this program. If not, see . */ -package org.isoron.uhabits.unit.views; +package org.isoron.uhabits.activities.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().toList()); + 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/activities/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/activities/common/views/StreakChartTest.java index ececee945..47b9e7c04 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/views/HabitStreakViewTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/activities/common/views/StreakChartTest.java @@ -17,58 +17,57 @@ * with this program. If not, see . */ -package org.isoron.uhabits.unit.views; +package org.isoron.uhabits.activities.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/activities/habits/list/views/CheckmarkButtonViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonViewTest.java new file mode 100644 index 000000000..b8bbc0bac --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonViewTest.java @@ -0,0 +1,187 @@ +/* + * 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.activities.habits.list.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.*; +import java.util.concurrent.*; + +@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/activities/habits/list/views/CheckmarkPanelViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelViewTest.java new file mode 100644 index 000000000..84954771b --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/activities/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.activities.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 = fixtures.createEmptyHabit(); + + latch = new CountDownLatch(1); + checkmarks = new int[]{ + Checkmark.CHECKED_EXPLICITLY, Checkmark.UNCHECKED, + Checkmark.CHECKED_IMPLICITLY, Checkmark.CHECKED_EXPLICITLY}; + + view = new CheckmarkPanelView(targetContext); + view.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/activities/habits/list/views/HabitCardViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HabitCardViewTest.java new file mode 100644 index 000000000..2ca386e18 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HabitCardViewTest.java @@ -0,0 +1,91 @@ +/* + * 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.activities.habits.list.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 static org.mockito.Mockito.mock; + +@RunWith(AndroidJUnit4.class) +@MediumTest +public class HabitCardViewTest extends BaseViewTest +{ + private HabitCardView view; + + public static final String PATH = "habits/list/HabitCardView/"; + + private HabitCardView.Controller controller; + + private Habit habit; + + @Override + public void setUp() + { + super.setUp(); + setTheme(R.style.AppBaseTheme); + + habit = fixtures.createLongHabit(); + CheckmarkList checkmarks = habit.getCheckmarks(); + + long today = DateUtils.getStartOfToday(); + long day = DateUtils.millisecondsInOneDay; + int[] values = checkmarks.getValues(today - 5 * day, today); + + controller = mock(HabitCardView.Controller.class); + + view = new HabitCardView(targetContext); + view.setHabit(habit); + view.setCheckmarkValues(values); + view.setSelected(false); + view.setScore(habit.getScores().getTodayValue()); + view.setController(controller); + measureView(view, dpToPixels(400), dpToPixels(50)); + } + + @Test + public void testRender() throws Exception + { + assertRenders(view, PATH + "render.png"); + } + + @Test + public void testRender_selected() throws Exception + { + view.setSelected(true); + measureView(view, dpToPixels(400), dpToPixels(50)); + assertRenders(view, PATH + "render_selected.png"); + } + + @Test + public void testChangeModel() throws Exception + { + habit.setName("Wake up early"); + habit.setColor(2); + habit.getObservable().notifyListeners(); + assertRenders(view, PATH + "render_changed.png"); + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HintViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/activities/habits/list/views/HintViewTest.java new file mode 100644 index 000000000..0427c07a0 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/activities/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.activities.habits.list.views; + +import android.support.test.runner.*; +import android.test.suitebuilder.annotation.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.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/activities/habits/show/views/FrequencyCardTest.java b/app/src/androidTest/java/org/isoron/uhabits/activities/habits/show/views/FrequencyCardTest.java new file mode 100644 index 000000000..2e37d1c10 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/activities/habits/show/views/FrequencyCardTest.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.activities.habits.show.views; + +import android.support.test.runner.*; +import android.test.suitebuilder.annotation.*; +import android.view.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.junit.*; +import org.junit.runner.*; + +@RunWith(AndroidJUnit4.class) +@MediumTest +public class FrequencyCardTest extends BaseViewTest +{ + public static final String PATH = "habits/show/FrequencyCard/"; + + private FrequencyCard view; + + private Habit habit; + + @Before + @Override + public void setUp() + { + super.setUp(); + habit = fixtures.createLongHabit(); + + view = (FrequencyCard) LayoutInflater + .from(targetContext) + .inflate(R.layout.show_habit, null) + .findViewById(R.id.frequencyCard); + + view.setHabit(habit); + view.refreshData(); + + measureView(view, 800, 600); + } + + @Test + public void testRender() throws Exception + { + assertRenders(view, PATH + "render.png"); + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/activities/habits/show/views/HistoryCardTest.java b/app/src/androidTest/java/org/isoron/uhabits/activities/habits/show/views/HistoryCardTest.java new file mode 100644 index 000000000..0a9d69291 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/activities/habits/show/views/HistoryCardTest.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.activities.habits.show.views; + +import android.support.test.runner.*; +import android.test.suitebuilder.annotation.*; +import android.view.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.junit.*; +import org.junit.runner.*; + +@RunWith(AndroidJUnit4.class) +@MediumTest +public class HistoryCardTest extends BaseViewTest +{ + public static final String PATH = "habits/show/HistoryCard/"; + + private HistoryCard view; + + private Habit habit; + + @Before + @Override + public void setUp() + { + super.setUp(); + habit = fixtures.createLongHabit(); + + view = (HistoryCard) LayoutInflater + .from(targetContext) + .inflate(R.layout.show_habit, null) + .findViewById(R.id.historyCard); + + view.setHabit(habit); + view.refreshData(); + + measureView(view, 800, 600); + } + + @Test + public void testRender() throws Exception + { + assertRenders(view, PATH + "render.png"); + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/activities/habits/show/views/OverviewCardTest.java b/app/src/androidTest/java/org/isoron/uhabits/activities/habits/show/views/OverviewCardTest.java new file mode 100644 index 000000000..0e54b7adf --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/activities/habits/show/views/OverviewCardTest.java @@ -0,0 +1,63 @@ +/* + * 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.activities.habits.show.views; + +import android.support.test.runner.*; +import android.test.suitebuilder.annotation.*; +import android.view.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.junit.*; +import org.junit.runner.*; + +@RunWith(AndroidJUnit4.class) +@MediumTest +public class OverviewCardTest extends BaseViewTest +{ + public static final String PATH = "habits/show/OverviewCard/"; + + private OverviewCard view; + + private Habit habit; + + @Before + @Override + public void setUp() + { + super.setUp(); + habit = fixtures.createLongHabit(); + + view = (OverviewCard) LayoutInflater + .from(targetContext) + .inflate(R.layout.show_habit, null) + .findViewById(R.id.overviewCard); + + view.setHabit(habit); + view.refreshData(); + measureView(view, 800, 300); + } + + @Test + public void testRender() throws Exception + { + assertRenders(view, PATH + "render.png"); + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/activities/habits/show/views/ScoreCardTest.java b/app/src/androidTest/java/org/isoron/uhabits/activities/habits/show/views/ScoreCardTest.java new file mode 100644 index 000000000..c93d0b1bd --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/activities/habits/show/views/ScoreCardTest.java @@ -0,0 +1,63 @@ +/* + * 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.activities.habits.show.views; + +import android.support.test.runner.*; +import android.test.suitebuilder.annotation.*; +import android.view.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.junit.*; +import org.junit.runner.*; + +@RunWith(AndroidJUnit4.class) +@MediumTest +public class ScoreCardTest extends BaseViewTest +{ + public static final String PATH = "habits/show/ScoreCard/"; + + private ScoreCard view; + + private Habit habit; + + @Before + @Override + public void setUp() + { + super.setUp(); + habit = fixtures.createLongHabit(); + + view = (ScoreCard) LayoutInflater + .from(targetContext) + .inflate(R.layout.show_habit, null) + .findViewById(R.id.scoreCard); + + view.setHabit(habit); + view.refreshData(); + measureView(view, 800, 600); + } + + @Test + public void testRender() throws Exception + { + assertRenders(view, PATH + "render.png"); + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/activities/habits/show/views/StreakCardTest.java b/app/src/androidTest/java/org/isoron/uhabits/activities/habits/show/views/StreakCardTest.java new file mode 100644 index 000000000..ae12e3819 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/activities/habits/show/views/StreakCardTest.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.activities.habits.show.views; + +import android.support.test.runner.*; +import android.test.suitebuilder.annotation.*; +import android.view.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.junit.*; +import org.junit.runner.*; + +@RunWith(AndroidJUnit4.class) +@MediumTest +public class StreakCardTest extends BaseViewTest +{ + public static final String PATH = "habits/show/StreakCard/"; + + private StreakCard view; + + private Habit habit; + + @Before + @Override + public void setUp() + { + super.setUp(); + habit = fixtures.createLongHabit(); + + view = (StreakCard) LayoutInflater + .from(targetContext) + .inflate(R.layout.show_habit, null) + .findViewById(R.id.streakCard); + + view.setHabit(habit); + view.refreshData(); + + measureView(view, 800, 600); + } + + @Test + public void testRender() throws Exception + { + assertRenders(view, PATH + "render.png"); + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/activities/habits/show/views/SubtitleCardTest.java b/app/src/androidTest/java/org/isoron/uhabits/activities/habits/show/views/SubtitleCardTest.java new file mode 100644 index 000000000..09cfb02b2 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/activities/habits/show/views/SubtitleCardTest.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.activities.habits.show.views; + +import android.support.test.runner.*; +import android.test.suitebuilder.annotation.*; +import android.view.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.junit.*; +import org.junit.runner.*; + +@RunWith(AndroidJUnit4.class) +@MediumTest +public class SubtitleCardTest extends BaseViewTest +{ + public static final String PATH = "habits/show/SubtitleCard/"; + + private SubtitleCard view; + + private Habit habit; + + @Before + @Override + public void setUp() + { + super.setUp(); + + habit = fixtures.createLongHabit(); + habit.setReminder(new Reminder(8, 30, WeekdayList.EVERY_DAY)); + + view = (SubtitleCard) LayoutInflater + .from(targetContext) + .inflate(R.layout.show_habit, null) + .findViewById(R.id.subtitleCard); + + view.setHabit(habit); + view.refreshData(); + + measureView(view, 800, 200); + } + + @Test + public void testRender() throws Exception + { + assertRenders(view, PATH + "render.png"); + } +} 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..c03e9c9db --- /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.actionAdd)).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..f9284b58e --- /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.R; +import org.isoron.uhabits.activities.habits.list.*; +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<>(ListHabitsActivity.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.actionAdd)).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 = + DateFormats.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 58% 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..4a82c1239 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,80 +17,52 @@ * 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.io.HabitsCSVExporter; -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.Enumeration; -import java.util.List; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; - -import static junit.framework.Assert.assertTrue; +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 java.util.zip.*; + +import static junit.framework.Assert.*; @RunWith(AndroidJUnit4.class) -@SmallTest -public class HabitsCSVExporterTest extends BaseTest +@MediumTest +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 selected = new LinkedList<>(); + for (Habit h : habitList) selected.add(h); - HabitsCSVExporter exporter = new HabitsCSVExporter(habits, baseDir); + HabitsCSVExporter exporter = + new HabitsCSVExporter(habitList, selected, baseDir); String filename = exporter.writeArchive(); assertAbsolutePathExists(filename); @@ -103,16 +75,45 @@ public class HabitsCSVExporterTest extends BaseTest assertPathExists("001 Wake up early/Scores.csv"); assertPathExists("002 Meditate/Checkmarks.csv"); assertPathExists("002 Meditate/Scores.csv"); + assertPathExists("Checkmarks.csv"); + assertPathExists("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/io/ImportTest.java b/app/src/androidTest/java/org/isoron/uhabits/io/ImportTest.java new file mode 100644 index 000000000..6dcdf434d --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/io/ImportTest.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.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) +@MediumTest +public class ImportTest extends BaseAndroidTest +{ + private File baseDir; + + private Context context; + + @Override + @Before + public void setUp() + { + super.setUp(); + DateUtils.setFixedLocalTime(null); + + fixtures.purgeHabits(habitList); + context = InstrumentationRegistry.getInstrumentation().getContext(); + baseDir = FileUtils.getFilesDir("Backups"); + if (baseDir == null) fail("baseDir should not be null"); + } + + @Test + public void testHabitBullCSV() throws IOException + { + importFromFile("habitbull.csv"); + + assertThat(habitList.size(), equalTo(4)); + + Habit habit = habitList.getByPosition(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 testLoopDB() throws IOException + { + importFromFile("loop.db"); + + assertThat(habitList.size(), equalTo(9)); + + Habit habit = habitList.getByPosition(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 + public void testRewireDB() throws IOException + { + importFromFile("rewire.db"); + + assertThat(habitList.size(), equalTo(3)); + + Habit habit = habitList.getByPosition(0); + 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)); + assertTrue(containsRepetition(habit, 2016, 1, 28)); + assertFalse(containsRepetition(habit, 2016, 3, 10)); + + habit = habitList.getByPosition(1); + 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().toArray(), equalTo(reminderDays)); + } + + @Test + public void testTickmateDB() throws IOException + { + importFromFile("tickmate.db"); + + assertThat(habitList.size(), equalTo(3)); + + Habit h = habitList.getByPosition(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)); + } + + private boolean containsRepetition(Habit h, int year, int month, int day) + { + GregorianCalendar date = DateUtils.getStartOfTodayCalendar(); + date.set(year, month - 1, day); + return h.getRepetitions().containsTimestamp(date.getTimeInMillis()); + } + + private void copyAssetToFile(String assetPath, File dst) throws IOException + { + InputStream in = context.getAssets().open(assetPath); + FileUtils.copy(in, dst); + } + + 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 = component.getGenericImporter(); + assertThat(importer.canHandle(file), is(true)); + + importer.importHabitsFromFile(file); + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/models/sqlite/HabitRecordTest.java b/app/src/androidTest/java/org/isoron/uhabits/models/sqlite/HabitRecordTest.java new file mode 100644 index 000000000..d701056df --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/models/sqlite/HabitRecordTest.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.models.sqlite; + +import android.support.test.runner.*; +import android.test.suitebuilder.annotation.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.models.sqlite.records.*; +import org.junit.*; +import org.junit.runner.*; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.core.IsEqual.*; + +@RunWith(AndroidJUnit4.class) +@MediumTest +public class HabitRecordTest extends BaseAndroidTest +{ + @Override + public void setUp() + { + super.setUp(); + + Habit h = component.getModelFactory().buildHabit(); + h.setName("Hello world"); + h.setId(1000L); + + HabitRecord record = new HabitRecord(); + record.copyFrom(h); + record.position = 0; + record.save(1000L); + } + + @Test + public void testCopyFrom() + { + Habit habit = component.getModelFactory().buildHabit(); + habit.setName("Hello world"); + habit.setDescription("Did you greet the world today?"); + habit.setColor(1); + habit.setArchived(true); + habit.setFrequency(Frequency.THREE_TIMES_PER_WEEK); + habit.setReminder(new Reminder(8, 30, WeekdayList.EVERY_DAY)); + habit.setId(1000L); + + HabitRecord rec = new HabitRecord(); + rec.copyFrom(habit); + + assertThat(rec.name, equalTo(habit.getName())); + assertThat(rec.description, equalTo(habit.getDescription())); + assertThat(rec.color, equalTo(habit.getColor())); + assertThat(rec.archived, equalTo(1)); + assertThat(rec.freqDen, equalTo(7)); + assertThat(rec.freqNum, equalTo(3)); + + Reminder reminder = habit.getReminder(); + assertThat(rec.reminderDays, equalTo(reminder.getDays().toInteger())); + assertThat(rec.reminderHour, equalTo(reminder.getHour())); + assertThat(rec.reminderMin, equalTo(reminder.getMinute())); + + habit.setReminder(null); + rec.copyFrom(habit); + + assertThat(rec.reminderMin, equalTo(null)); + assertThat(rec.reminderHour, equalTo(null)); + assertThat(rec.reminderDays, equalTo(0)); + } +} 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..32e6df853 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/models/sqlite/SQLiteCheckmarkListTest.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.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 testGetByInterval_withLongInterval() + { + long from = today - 200 * day; + long to = today; + + List list = checkmarks.getByInterval(from, to); + assertThat(list.size(), equalTo(201)); + } + + @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..ae51ed1f5 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/models/sqlite/SQLiteHabitListTest.java @@ -0,0 +1,227 @@ +/* + * 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(); + + private SQLiteHabitList habitList; + + private ModelFactory modelFactory; + + @Override + public void setUp() + { + super.setUp(); + this.habitList = (SQLiteHabitList) super.habitList; + fixtures.purgeHabits(habitList); + + modelFactory = component.getModelFactory(); + + for (int i = 0; i < 10; i++) + { + Habit h = modelFactory.buildHabit(); + 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 = modelFactory.buildHabit(); + habitList.add(habit); + exception.expect(IllegalArgumentException.class); + habitList.add(habit); + } + + @Test + public void testAdd_withId() + { + Habit habit = modelFactory.buildHabit(); + 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 = modelFactory.buildHabit(); + 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 testSize() + { + assertThat(habitList.size(), equalTo(10)); + } + + @Test + public void testGetAll_withArchived() + { + List habits = habitList.toList(); + assertThat(habits.size(), equalTo(10)); + assertThat(habits.get(3).getName(), equalTo("habit 3")); + } + +// @Test +// public void testGetAll_withoutArchived() +// { +// List habits = habitList.toList(); +// assertThat(habits.size(), equalTo(5)); +// assertThat(habits.get(3).getName(), equalTo("habit 7")); +// +// List another = habitList.toList(); +// 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")); + } + + @Test + public void testIndexOf() + { + Habit h1 = habitList.getByPosition(5); + assertNotNull(h1); + assertThat(habitList.indexOf(h1), equalTo(5)); + + Habit h2 = modelFactory.buildHabit(); + 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..db3f1aaa8 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/models/sqlite/SQLiteScoreListTest.java @@ -0,0 +1,136 @@ +/* + * 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.*; + +@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.toList(); + 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 testGetByInterval() + { + long from = today - 10 * day; + long to = today - 3 * day; + + List list = scores.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 testGetByInterval_withLongInterval() + { + long from = today - 200 * day; + long to = today; + + List list = scores.getByInterval(from, to); + assertThat(list.size(), equalTo(201)); + } + + 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/receivers/PebbleReceiverTest.java b/app/src/androidTest/java/org/isoron/uhabits/receivers/PebbleReceiverTest.java new file mode 100644 index 000000000..631f2d396 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/receivers/PebbleReceiverTest.java @@ -0,0 +1,173 @@ +/* + * 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.pebble; + +import android.content.*; +import android.support.annotation.*; +import android.support.test.runner.*; +import android.test.suitebuilder.annotation.*; + +import com.getpebble.android.kit.*; +import com.getpebble.android.kit.util.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.receivers.*; +import org.json.*; +import org.junit.*; +import org.junit.runner.*; + +import static com.getpebble.android.kit.Constants.*; +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.core.IsEqual.*; + +@RunWith(AndroidJUnit4.class) +@MediumTest +public class PebbleReceiverTest extends BaseAndroidTest +{ + + private Habit habit1; + + private Habit habit2; + + @Override + public void setUp() + { + super.setUp(); + + fixtures.purgeHabits(habitList); + + habit1 = fixtures.createEmptyHabit(); + habit1.setName("Exercise"); + + habit2 = fixtures.createEmptyHabit(); + habit2.setName("Meditate"); + } + + @Test + public void testCount() throws Exception + { + onPebbleReceived((dict) -> { + assertThat(dict.getString(0), equalTo("COUNT")); + assertThat(dict.getInteger(1), equalTo(2L)); + }); + + PebbleDictionary dict = buildCountRequest(); + sendFromPebbleToAndroid(dict); + awaitLatch(); + } + + @Test + public void testFetch() throws Exception + { + onPebbleReceived((dict) -> { + assertThat(dict.getString(0), equalTo("HABIT")); + assertThat(dict.getInteger(1), equalTo(habit2.getId())); + assertThat(dict.getString(2), equalTo(habit2.getName())); + assertThat(dict.getInteger(3), equalTo(0L)); + }); + + PebbleDictionary dict = buildFetchRequest(1); + sendFromPebbleToAndroid(dict); + awaitLatch(); + } + +// @Test +// public void testToggle() throws Exception +// { +// int v = habit1.getCheckmarks().getTodayValue(); +// assertThat(v, equalTo(Checkmark.UNCHECKED)); +// +// onPebbleReceived((dict) -> { +// assertThat(dict.getString(0), equalTo("OK")); +// int value = habit1.getCheckmarks().getTodayValue(); +// assertThat(value, equalTo(200)); //Checkmark.CHECKED_EXPLICITLY)); +// }); +// +// PebbleDictionary dict = buildToggleRequest(habit1.getId()); +// sendFromPebbleToAndroid(dict); +// awaitLatch(); +// } + + @NonNull + protected PebbleDictionary buildCountRequest() + { + PebbleDictionary dict = new PebbleDictionary(); + dict.addString(0, "COUNT"); + return dict; + } + + @NonNull + protected PebbleDictionary buildFetchRequest(int position) + { + PebbleDictionary dict = new PebbleDictionary(); + dict.addString(0, "FETCH"); + dict.addInt32(1, position); + return dict; + } + + protected void onPebbleReceived(PebbleProcessor processor) + { + BroadcastReceiver pebbleReceiver = new BroadcastReceiver() + { + @Override + public void onReceive(Context context, Intent intent) + { + try + { + String jsonData = intent.getStringExtra(MSG_DATA); + PebbleDictionary dict = PebbleDictionary.fromJson(jsonData); + processor.process(dict); + latch.countDown(); + targetContext.unregisterReceiver(this); + } + catch (JSONException e) + { + throw new RuntimeException(e); + } + } + }; + + IntentFilter filter = new IntentFilter(Constants.INTENT_APP_SEND); + targetContext.registerReceiver(pebbleReceiver, filter); + } + + protected void sendFromPebbleToAndroid(PebbleDictionary dict) + { + Intent intent = new Intent(Constants.INTENT_APP_RECEIVE); + intent.putExtra(Constants.APP_UUID, PebbleReceiver.WATCHAPP_UUID); + intent.putExtra(Constants.TRANSACTION_ID, 0); + intent.putExtra(Constants.MSG_DATA, dict.toJsonString()); + targetContext.sendBroadcast(intent); + } + + private PebbleDictionary buildToggleRequest(long habitId) + { + PebbleDictionary dict = new PebbleDictionary(); + dict.addString(0, "TOGGLE"); + dict.addInt32(1, (int) habitId); + return dict; + } + + interface PebbleProcessor + { + void process(PebbleDictionary dict); + } +} 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..86860f006 --- /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) +@MediumTest +public class ExportCSVTaskTest extends BaseAndroidTest +{ + @Before + @Override + public void setUp() + { + super.setUp(); + } + + @Test + public void testExportCSV() throws Throwable + { + fixtures.purgeHabits(habitList); + fixtures.createShortHabit(); + + List selected = new LinkedList<>(); + for (Habit h : habitList) selected.add(h); + + taskRunner.execute( + new ExportCSVTask(habitList, selected, archiveFilename -> { + assertThat(archiveFilename, is(not(nullValue()))); + File f = new File(archiveFilename); + assertTrue(f.exists()); + assertTrue(f.canRead()); + })); + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/tasks/ExportDBTaskTest.java b/app/src/androidTest/java/org/isoron/uhabits/tasks/ExportDBTaskTest.java new file mode 100644 index 000000000..ac3dce2ba --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/tasks/ExportDBTaskTest.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.tasks; + +import android.support.test.runner.*; +import android.test.suitebuilder.annotation.*; + +import org.isoron.uhabits.*; +import org.junit.*; +import org.junit.runner.*; + +import java.io.*; + +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) +@MediumTest +public class ExportDBTaskTest extends BaseAndroidTest +{ + @Before + public void setUp() + { + super.setUp(); + } + + @Test + public void testExportCSV() throws Throwable + { + ExportDBTask task = new ExportDBTask(filename -> { + assertThat(filename, is(not(nullValue()))); + + File f = new File(filename); + assertTrue(f.exists()); + assertTrue(f.canRead()); + }); + + taskRunner.execute(task); + } +} 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 89751372f..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_SENDTO)); - } -} 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/CreateHabitCommandTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/commands/CreateHabitCommandTest.java deleted file mode 100644 index d97fa2e07..000000000 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/commands/CreateHabitCommandTest.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.unit.commands; - -import android.support.test.runner.AndroidJUnit4; -import android.test.suitebuilder.annotation.SmallTest; - -import org.isoron.uhabits.BaseTest; -import org.isoron.uhabits.commands.CreateHabitCommand; -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.util.List; - -import static junit.framework.Assert.assertTrue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; - -@RunWith(AndroidJUnit4.class) -@SmallTest -public class CreateHabitCommandTest extends BaseTest -{ - - private CreateHabitCommand command; - private Habit model; - - @Before - public void setup() - { - super.setup(); - - model = new Habit(); - model.name = "New habit"; - command = new CreateHabitCommand(model); - - HabitFixtures.purgeHabits(); - } - - @Test - public void testExecuteUndoRedo() - { - assertTrue(Habit.getAll(true).isEmpty()); - - command.execute(); - - List allHabits = Habit.getAll(true); - assertThat(allHabits.size(), equalTo(1)); - - Habit habit = allHabits.get(0); - Long id = habit.getId(); - assertThat(habit.name, equalTo(model.name)); - - command.undo(); - assertTrue(Habit.getAll(true).isEmpty()); - - command.execute(); - allHabits = Habit.getAll(true); - assertThat(allHabits.size(), equalTo(1)); - - habit = allHabits.get(0); - Long newId = habit.getId(); - assertThat(id, equalTo(newId)); - assertThat(habit.name, equalTo(model.name)); - } -} diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/commands/DeleteHabitsCommandTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/commands/DeleteHabitsCommandTest.java deleted file mode 100644 index 36a97198a..000000000 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/commands/DeleteHabitsCommandTest.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.unit.commands; - -import android.support.test.runner.AndroidJUnit4; -import android.test.suitebuilder.annotation.SmallTest; - -import org.isoron.uhabits.BaseTest; -import org.isoron.uhabits.commands.DeleteHabitsCommand; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.unit.HabitFixtures; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.junit.runner.RunWith; - -import java.util.LinkedList; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; - -@RunWith(AndroidJUnit4.class) -@SmallTest -public class DeleteHabitsCommandTest extends BaseTest -{ - private DeleteHabitsCommand command; - private LinkedList habits; - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - @Before - public void setup() - { - super.setup(); - - HabitFixtures.purgeHabits(); - habits = new LinkedList<>(); - - // Habits that shuold be deleted - for(int i = 0; i < 3; i ++) - { - Habit habit = HabitFixtures.createShortHabit(); - habits.add(habit); - } - - // Extra habit that should not be deleted - Habit extraHabit = HabitFixtures.createShortHabit(); - extraHabit.name = "extra"; - extraHabit.save(); - - command = new DeleteHabitsCommand(habits); - } - - @Test - public void testExecuteUndoRedo() - { - assertThat(Habit.getAll(true).size(), equalTo(4)); - - command.execute(); - assertThat(Habit.getAll(true).size(), equalTo(1)); - assertThat(Habit.getAll(true).get(0).name, equalTo("extra")); - - thrown.expect(UnsupportedOperationException.class); - command.undo(); - } -} 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/io/ImportTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/io/ImportTest.java deleted file mode 100644 index 8f6f3fbe0..000000000 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/io/ImportTest.java +++ /dev/null @@ -1,173 +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.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; - -@RunWith(AndroidJUnit4.class) -@SmallTest -public class ImportTest extends BaseTest -{ - private File baseDir; - private Context context; - - @Before - public void setup() - { - super.setup(); - DateHelper.setFixedLocalTime(null); - - HabitFixtures.purgeHabits(); - context = InstrumentationRegistry.getInstrumentation().getContext(); - baseDir = DatabaseHelper.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 - { - 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); - } - - 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()); - } - - @Test - public void testTickmateDB() throws IOException - { - importFromFile("tickmate.db"); - - List habits = Habit.getAll(true); - assertThat(habits.size(), equalTo(3)); - - 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)); - } - - @Test - public void testRewireDB() throws IOException - { - importFromFile("rewire.db"); - - List habits = Habit.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)); - assertFalse(habit.hasReminder()); - assertFalse(containsRepetition(habit, 2015, 12, 31)); - assertTrue(containsRepetition(habit, 2016, 1, 18)); - assertTrue(containsRepetition(habit, 2016, 1, 28)); - 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))); - } - - @Test - public void testHabitBullCSV() throws IOException - { - importFromFile("habitbull.csv"); - - List habits = Habit.getAll(true); - assertThat(habits.size(), equalTo(4)); - - 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)); - } - - @Test - public void testLoopDB() throws IOException - { - importFromFile("loop.db"); - - List habits = Habit.getAll(true); - assertThat(habits.size(), equalTo(9)); - - 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)); - } -} 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 f09664031..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 = - "Name,Description,NumRepetitions,Interval,Color\n" + - "Meditate,Did you meditate this morning?,1,1,#AFB42B\n" + - "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/tasks/ExportDBTaskTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/tasks/ExportDBTaskTest.java deleted file mode 100644 index 26269e353..000000000 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/tasks/ExportDBTaskTest.java +++ /dev/null @@ -1,75 +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.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.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.io.File; - -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 ExportDBTaskTest extends BaseTest -{ - @Before - public void setup() - { - super.setup(); - } - - @Test - public void testExportCSV() throws Throwable - { - Context context = InstrumentationRegistry.getContext(); - - ProgressBar bar = new ProgressBar(context); - ExportDBTask task = new ExportDBTask(bar); - task.setListener(new ExportDBTask.Listener() - { - @Override - public void onExportDBFinished(String filename) - { - assertThat(filename, is(not(nullValue()))); - - File f = new File(filename); - assertTrue(f.exists()); - assertTrue(f.canRead()); - } - }); - - task.execute(); - waitForAsyncTasks(); - } -} diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/tasks/ImportDataTaskTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/tasks/ImportDataTaskTest.java deleted file mode 100644 index d6a3cabaa..000000000 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/tasks/ImportDataTaskTest.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.unit.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.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.junit.Assert.fail; - -@RunWith(AndroidJUnit4.class) -@SmallTest -public class ImportDataTaskTest extends BaseTest -{ - private File baseDir; - - @Before - public void setup() - { - super.setup(); - - baseDir = DatabaseHelper.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); - } - - private void assertTaskResult(final int expectedResult, String assetFilename) throws Throwable - { - ImportDataTask task = createTask(assetFilename); - - task.setListener(new ImportDataTask.Listener() - { - @Override - public void onImportFinished(int result) - { - assertThat(result, equalTo(expectedResult)); - } - }); - - task.execute(); - waitForAsyncTasks(); - } - - @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); - } - - @Test - public void testImportInvalidData() throws Throwable - { - assertTaskResult(ImportDataTask.NOT_RECOGNIZED, "icon.png"); - } - - @Test - public void testImportValidData() throws Throwable - { - assertTaskResult(ImportDataTask.SUCCESS, "loop.db"); - } -} 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/androidTest/java/org/isoron/uhabits/widgets/CheckmarkWidgetTest.java b/app/src/androidTest/java/org/isoron/uhabits/widgets/CheckmarkWidgetTest.java new file mode 100644 index 000000000..4d97a7d4e --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/widgets/CheckmarkWidgetTest.java @@ -0,0 +1,85 @@ +/* + * 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.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/widgets/FrequencyWidgetTest.java b/app/src/androidTest/java/org/isoron/uhabits/widgets/FrequencyWidgetTest.java new file mode 100644 index 000000000..ac361fd96 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/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.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/widgets/HistoryWidgetTest.java b/app/src/androidTest/java/org/isoron/uhabits/widgets/HistoryWidgetTest.java new file mode 100644 index 000000000..c1adc6882 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/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.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/widgets/ScoreWidgetTest.java b/app/src/androidTest/java/org/isoron/uhabits/widgets/ScoreWidgetTest.java new file mode 100644 index 000000000..f4d5074b6 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/widgets/ScoreWidgetTest.java @@ -0,0 +1,63 @@ +/* + * 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.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.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/widgets/StreakWidgetTest.java b/app/src/androidTest/java/org/isoron/uhabits/widgets/StreakWidgetTest.java new file mode 100644 index 000000000..1c7639bb2 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/widgets/StreakWidgetTest.java @@ -0,0 +1,63 @@ +/* + * 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.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.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/widgets/views/CheckmarkWidgetViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetViewTest.java new file mode 100644 index 000000000..bb151334b --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/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.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(); + setTheme(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/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 12e827336..d3443abd2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -21,20 +21,20 @@ + android:versionCode="23" + android:versionName="1.6.0"> + android:maxSdkVersion="18"/> + android:maxSdkVersion="18"/> - + + android:supportsRtl="true" + android:theme="@style/AppBaseTheme"> + + + android:label="@string/main_activity_title" + android:launchMode="singleTop" + android:targetActivity=".activities.habits.list.ListHabitsActivity"> - - + + android:value=".activities.habits.list.ListHabitsActivity"/> + android:value=".activities.habits.list.ListHabitsActivity"/> @@ -89,11 +96,11 @@ + android:value=".activities.habits.list.ListHabitsActivity"/> - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/android/datetimepicker/time/TimePickerDialog.java b/app/src/main/java/com/android/datetimepicker/time/TimePickerDialog.java index f36ad2780..8f22206f4 100644 --- a/app/src/main/java/com/android/datetimepicker/time/TimePickerDialog.java +++ b/app/src/main/java/com/android/datetimepicker/time/TimePickerDialog.java @@ -24,13 +24,14 @@ import org.isoron.uhabits.R; import android.animation.ObjectAnimator; import android.annotation.SuppressLint; +import android.app.*; +import android.app.ActionBar; import android.app.ActionBar.LayoutParams; -import android.app.DialogFragment; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Resources; import android.os.Bundle; -import android.support.v7.app.AppCompatDialogFragment; +import android.support.v7.app.*; import android.util.Log; import android.view.KeyCharacterMap; import android.view.KeyEvent; @@ -193,10 +194,16 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue } } + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) + { + return new AppCompatDialog(getActivity(), R.style.TimePickerDialog); + } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - getDialog().getWindow().requestFeature(Window.FEATURE_NO_TITLE); + getDialog().requestWindowFeature(Window.FEATURE_NO_TITLE); View view = inflater.inflate(R.layout.time_picker_dialog, null); KeyboardListener keyboardListener = new KeyboardListener(); diff --git a/app/src/main/java/org/isoron/uhabits/AboutActivity.java b/app/src/main/java/org/isoron/uhabits/AboutActivity.java deleted file mode 100644 index 279a96d45..000000000 --- a/app/src/main/java/org/isoron/uhabits/AboutActivity.java +++ /dev/null @@ -1,89 +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.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.view.View; -import android.widget.TextView; - -import org.isoron.uhabits.helpers.UIHelper; - -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) - { - switch (v.getId()) - { - case R.id.tvRate: - { - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_VIEW); - intent.setData(Uri.parse(getString(R.string.playStoreURL))); - startActivity(intent); - break; - } - - case R.id.tvFeedback: - { - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_SENDTO); - intent.setData(Uri.parse(getString(R.string.feedbackURL))); - startActivity(intent); - break; - } - - case R.id.tvSource: - { - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_VIEW); - intent.setData(Uri.parse(getString(R.string.sourceCodeURL))); - startActivity(intent); - break; - } - } - } -} diff --git a/app/src/main/java/org/isoron/uhabits/AppComponent.java b/app/src/main/java/org/isoron/uhabits/AppComponent.java new file mode 100644 index 000000000..8c07b93ea --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/AppComponent.java @@ -0,0 +1,82 @@ +/* + * 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.*; + +import org.isoron.uhabits.activities.habits.list.model.*; +import org.isoron.uhabits.commands.*; +import org.isoron.uhabits.intents.*; +import org.isoron.uhabits.io.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.models.sqlite.*; +import org.isoron.uhabits.notifications.*; +import org.isoron.uhabits.preferences.*; +import org.isoron.uhabits.tasks.*; +import org.isoron.uhabits.utils.*; +import org.isoron.uhabits.widgets.*; + +import dagger.*; + +@AppScope +@Component(modules = { + AppModule.class, AndroidTaskRunner.class, SQLModelFactory.class +}) +public interface AppComponent +{ + CommandRunner getCommandRunner(); + + @AppContext + Context getContext(); + + CreateHabitCommandFactory getCreateHabitCommandFactory(); + + DirFinder getDirFinder(); + + EditHabitCommandFactory getEditHabitCommandFactory(); + + GenericImporter getGenericImporter(); + + HabitCardListCache getHabitCardListCache(); + + HabitList getHabitList(); + + HabitLogger getHabitsLogger(); + + IntentFactory getIntentFactory(); + + IntentParser getIntentParser(); + + ModelFactory getModelFactory(); + + NotificationTray getNotificationTray(); + + PendingIntentFactory getPendingIntentFactory(); + + Preferences getPreferences(); + + ReminderScheduler getReminderScheduler(); + + TaskRunner getTaskRunner(); + + WidgetPreferences getWidgetPreferences(); + + WidgetUpdater getWidgetUpdater(); +} diff --git a/app/src/main/java/org/isoron/uhabits/AppContext.java b/app/src/main/java/org/isoron/uhabits/AppContext.java new file mode 100644 index 000000000..ebd0aab10 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/AppContext.java @@ -0,0 +1,31 @@ +/* + * 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 java.lang.annotation.*; + +import javax.inject.*; + +@Qualifier +@Documented +@Retention(RetentionPolicy.RUNTIME) +public @interface AppContext +{ +} diff --git a/app/src/main/java/org/isoron/uhabits/SettingsActivity.java b/app/src/main/java/org/isoron/uhabits/AppModule.java similarity index 65% rename from app/src/main/java/org/isoron/uhabits/SettingsActivity.java rename to app/src/main/java/org/isoron/uhabits/AppModule.java index 579e7a7a6..c5aa66c70 100644 --- a/app/src/main/java/org/isoron/uhabits/SettingsActivity.java +++ b/app/src/main/java/org/isoron/uhabits/AppModule.java @@ -19,20 +19,24 @@ package org.isoron.uhabits; -import android.os.Bundle; +import android.content.*; -import org.isoron.uhabits.helpers.UIHelper; +import dagger.*; -public class SettingsActivity extends BaseActivity +@Module +public class AppModule { - @Override - protected void onCreate(Bundle savedInstanceState) + private final Context context; + + public AppModule(@AppContext Context context) { - super.onCreate(savedInstanceState); - setContentView(R.layout.settings_activity); - setupSupportActionBar(true); + this.context = context; + } - int color = UIHelper.getStyledColor(this, R.attr.aboutScreenColor); - setupActionBarColor(color); + @Provides + @AppContext + Context getContext() + { + return context; } } diff --git a/app/src/main/java/org/isoron/uhabits/AppScope.java b/app/src/main/java/org/isoron/uhabits/AppScope.java new file mode 100644 index 000000000..75c59577a --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/AppScope.java @@ -0,0 +1,25 @@ +/* + * 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.*; + +@Scope +public @interface AppScope {} 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/HabitBroadcastReceiver.java b/app/src/main/java/org/isoron/uhabits/HabitBroadcastReceiver.java deleted file mode 100644 index 695f935b4..000000000 --- a/app/src/main/java/org/isoron/uhabits/HabitBroadcastReceiver.java +++ /dev/null @@ -1,280 +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.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; - -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"; - - @Override - public void onReceive(final Context context, Intent intent) - { - switch (intent.getAction()) - { - case ACTION_SHOW_REMINDER: - createNotification(context, intent); - createReminderAlarmsDelayed(context); - break; - - case ACTION_DISMISS: - dismissAllHabits(); - break; - - case ACTION_CHECK: - checkHabit(context, intent); - break; - - case ACTION_SNOOZE: - snoozeHabit(context, intent); - break; - - case Intent.ACTION_BOOT_COMPLETED: - ReminderHelper.createReminderAlarms(context); - 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 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); - - MainActivity.updateWidgets(context); - } - - private void dismissAllHabits() - { - - } - - private void dismissNotification(Context context, Long habitId) - { - NotificationManager notificationManager = - (NotificationManager) context.getSystemService(Activity.NOTIFICATION_SERVICE); - - int notificationId = (int) (habitId % Integer.MAX_VALUE); - notificationManager.cancel(notificationId); - } - - - 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()); - - if (habit == null) return; - - new BaseTask() - { - int todayValue; - - @Override - protected void doInBackground() - { - todayValue = habit.checkmarks.getTodayValue(); - } - - @Override - protected void onPostExecute(Void aVoid) - { - if (todayValue != Checkmark.UNCHECKED) return; - if (!checkWeekday(intent, habit)) return; - if (!habit.hasReminder()) return; - - Intent contentIntent = new Intent(context, MainActivity.class); - contentIntent.setData(data); - PendingIntent contentPendingIntent = - PendingIntent.getActivity(context, 0, contentIntent, - PendingIntent.FLAG_UPDATE_CURRENT); - - PendingIntent dismissPendingIntent = buildDismissIntent(context); - PendingIntent checkIntentPending = buildCheckIntent(context, - habit, timestamp, 1); - PendingIntent snoozeIntentPending = buildSnoozeIntent(context, habit); - - Uri ringtoneUri = ReminderHelper.getRingtoneUri(context); - - NotificationCompat.WearableExtender wearableExtender = - 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(); - - notification.flags |= Notification.FLAG_AUTO_CANCEL; - - NotificationManager notificationManager = - (NotificationManager) context.getSystemService( - Activity.NOTIFICATION_SERVICE); - - int notificationId = (int) (habit.getId() % Integer.MAX_VALUE); - notificationManager.notify(notificationId, notification); - - super.onPostExecute(aVoid); - } - }.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) - { - Intent deleteIntent = new Intent(context, HabitBroadcastReceiver.class); - deleteIntent.setAction(ACTION_DISMISS); - return PendingIntent.getBroadcast(context, 0, deleteIntent, - PendingIntent.FLAG_UPDATE_CURRENT); - } - - public static PendingIntent buildViewHabitIntent(Context context, Habit habit) - { - 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) - { - Long timestamp = intent.getLongExtra("timestamp", DateHelper.getStartOfToday()); - - boolean reminderDays[] = DateHelper.unpackWeekdayList(habit.reminderDays); - int weekday = DateHelper.getWeekday(timestamp); - - return reminderDays[weekday]; - } - - 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); - } -} diff --git a/app/src/main/java/org/isoron/uhabits/HabitLogger.java b/app/src/main/java/org/isoron/uhabits/HabitLogger.java new file mode 100644 index 000000000..fb781c273 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/HabitLogger.java @@ -0,0 +1,54 @@ +/* + * 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.support.annotation.*; +import android.util.*; + +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; + +import java.text.*; +import java.util.*; + +import javax.inject.*; + +@AppScope +public class HabitLogger +{ + @Inject + public HabitLogger() + { + + } + + public void logReminderScheduled(@NonNull Habit habit, + @NonNull Long reminderTime) + { + int min = Math.min(3, habit.getName().length()); + String name = habit.getName().substring(0, min); + + DateFormat df = DateFormats.getBackupDateFormat(); + String time = df.format(new Date(reminderTime)); + + Log.i("ReminderHelper", + String.format("Setting alarm (%s): %s", time, name)); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/HabitsApplication.java b/app/src/main/java/org/isoron/uhabits/HabitsApplication.java index dee4ca17d..692cd0402 100644 --- a/app/src/main/java/org/isoron/uhabits/HabitsApplication.java +++ b/app/src/main/java/org/isoron/uhabits/HabitsApplication.java @@ -19,36 +19,62 @@ 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.notifications.*; +import org.isoron.uhabits.preferences.*; +import org.isoron.uhabits.tasks.*; +import org.isoron.uhabits.utils.*; +import org.isoron.uhabits.widgets.*; -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 { - @Nullable private static Context context; + private static AppComponent component; + + private WidgetUpdater widgetUpdater; + + private ReminderScheduler reminderScheduler; + + private NotificationTray notificationTray; + + public AppComponent getComponent() + { + return component; + } + + public static void setComponent(AppComponent component) + { + HabitsApplication.component = component; + } + + @NonNull + @Deprecated + public static Context getContext() + { + if (context == null) throw new RuntimeException("context is null"); + return context; + } + public static boolean isTestMode() { try { - if(context != null) - context.getClassLoader().loadClass("org.isoron.uhabits.unit.models.HabitTest"); + if (context != null) + { + String testClass = "org.isoron.uhabits.BaseAndroidTest"; + context.getClassLoader().loadClass(testClass); + } return true; } catch (final Exception e) @@ -57,110 +83,54 @@ public class HabitsApplication extends Application } } - @Nullable - public static Context getContext() - { - return context; - } - @Override public void onCreate() { super.onCreate(); HabitsApplication.context = this; + component = DaggerAppComponent + .builder() + .appModule(new AppModule(context)) + .build(); + if (isTestMode()) { - File db = DatabaseHelper.getDatabaseFile(); - if(db.exists()) db.delete(); + File db = DatabaseUtils.getDatabaseFile(); + if (db.exists()) db.delete(); } - DatabaseHelper.initializeActiveAndroid(); - } - - @Override - public void onTerminate() - { - HabitsApplication.context = null; - ActiveAndroid.dispose(); - 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); + DatabaseUtils.initializeActiveAndroid(); - InputStreamReader in = new InputStreamReader(process.getInputStream()); - BufferedReader bufferedReader = new BufferedReader(in); + widgetUpdater = component.getWidgetUpdater(); + widgetUpdater.startListening(); - LinkedList log = new LinkedList<>(); + reminderScheduler = component.getReminderScheduler(); + reminderScheduler.startListening(); - String line; - while ((line = bufferedReader.readLine()) != null) - { - log.addLast(line); - if(log.size() > maxNLines) log.removeFirst(); - } + notificationTray = component.getNotificationTray(); + notificationTray.startListening(); - for(String l : log) - { - builder.append(l); - builder.append('\n'); - } + Preferences prefs = component.getPreferences(); + prefs.initialize(); + prefs.updateLastAppVersion(); - return builder.toString(); + TaskRunner taskRunner = component.getTaskRunner(); + taskRunner.execute(() -> { + reminderScheduler.scheduleAll(); + widgetUpdater.updateWidgets(); + }); } - 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 + @Override + public void onTerminate() { - 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; - } + HabitsApplication.context = null; + ActiveAndroid.dispose(); - @NonNull - public static String generateBugReport() throws IOException - { - String logcat = getLogcat(); - String deviceInfo = getDeviceInfo(); - return deviceInfo + "\n" + logcat; + reminderScheduler.stopListening(); + widgetUpdater.stopListening(); + notificationTray.stopListening(); + super.onTerminate(); } } 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 deleted file mode 100644 index 77c8625a1..000000000 --- a/app/src/main/java/org/isoron/uhabits/MainActivity.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; - -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.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 -{ - 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(); - } -} diff --git a/app/src/main/java/org/isoron/uhabits/activities/ActivityComponent.java b/app/src/main/java/org/isoron/uhabits/activities/ActivityComponent.java new file mode 100644 index 000000000..e80a9f540 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/ActivityComponent.java @@ -0,0 +1,35 @@ +/* + * 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.activities; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.common.dialogs.*; + +import dagger.*; + +@ActivityScope +@Component(modules = { ActivityModule.class }, + dependencies = { AppComponent.class }) +public interface ActivityComponent +{ + ColorPickerDialogFactory getColorPickerDialogFactory(); + + ThemeSwitcher getThemeSwitcher(); +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/ActivityContext.java b/app/src/main/java/org/isoron/uhabits/activities/ActivityContext.java new file mode 100644 index 000000000..c75c28dae --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/ActivityContext.java @@ -0,0 +1,31 @@ +/* + * 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.activities; + +import java.lang.annotation.*; + +import javax.inject.*; + +@Qualifier +@Documented +@Retention(RetentionPolicy.RUNTIME) +public @interface ActivityContext +{ +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/ActivityModule.java b/app/src/main/java/org/isoron/uhabits/activities/ActivityModule.java new file mode 100644 index 000000000..dcf743241 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/ActivityModule.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.activities; + +import android.content.*; + +import dagger.*; + +@Module +public class ActivityModule +{ + private BaseActivity activity; + + public ActivityModule(BaseActivity activity) + { + this.activity = activity; + } + + @Provides + public BaseActivity getActivity() + { + return activity; + } + + @Provides + @ActivityContext + public Context getContext() + { + return activity; + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/ActivityScope.java b/app/src/main/java/org/isoron/uhabits/activities/ActivityScope.java new file mode 100644 index 000000000..3d02e80ae --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/ActivityScope.java @@ -0,0 +1,28 @@ +/* + * 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.activities; + +import javax.inject.*; + +/** + * Scope used by objects that live as long as the activity is alive. + */ +@Scope +public @interface ActivityScope { } diff --git a/app/src/main/java/org/isoron/uhabits/activities/BaseActivity.java b/app/src/main/java/org/isoron/uhabits/activities/BaseActivity.java new file mode 100644 index 000000000..311749159 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/BaseActivity.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.activities; + +import android.content.*; +import android.os.*; +import android.support.annotation.*; +import android.support.v7.app.*; +import android.view.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.habits.list.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.models.sqlite.*; + +import static android.R.anim.*; + +/** + * 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; + + private ActivityComponent component; + + public ActivityComponent getComponent() + { + return component; + } + + @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 restartWithFade() + { + new Handler().postDelayed(() -> { + Intent intent = new Intent(this, ListHabitsActivity.class); + finish(); + overridePendingTransition(fade_in, fade_out); + startActivity(intent); + + }, 500); // HACK: Let the menu disappear first + } + + public void setBaseMenu(@Nullable BaseMenu baseMenu) + { + this.baseMenu = baseMenu; + } + + public void setScreen(@Nullable BaseScreen screen) + { + this.screen = screen; + } + + public void showDialog(AppCompatDialogFragment dialog, String tag) + { + dialog.show(getSupportFragmentManager(), tag); + } + + public void showDialog(AppCompatDialog dialog) + { + dialog.show(); + } + + @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 (ex.getCause() instanceof InconsistentDatabaseException) + { + HabitsApplication app = (HabitsApplication) getApplication(); + HabitList habits = app.getComponent().getHabitList(); + habits.repair(); + System.exit(0); + } + + if (androidExceptionHandler != null) + androidExceptionHandler.uncaughtException(thread, ex); + else System.exit(1); + } + + @Override + protected void onActivityResult(int request, int result, Intent data) + { + if (screen == null) super.onActivityResult(request, result, data); + else screen.onResult(request, result, data); + } + + @Override + protected void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + androidExceptionHandler = Thread.getDefaultUncaughtExceptionHandler(); + Thread.setDefaultUncaughtExceptionHandler(this); + + HabitsApplication app = (HabitsApplication) getApplicationContext(); + + component = DaggerActivityComponent + .builder() + .activityModule(new ActivityModule(this)) + .appComponent(app.getComponent()) + .build(); + + component.getThemeSwitcher().apply(); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/BaseMenu.java b/app/src/main/java/org/isoron/uhabits/activities/BaseMenu.java new file mode 100644 index 000000000..f9d7bf077 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/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.activities; + +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/activities/BaseRootView.java b/app/src/main/java/org/isoron/uhabits/activities/BaseRootView.java new file mode 100644 index 000000000..af3867ead --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/BaseRootView.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.activities; + +import android.content.*; +import android.support.annotation.*; +import android.support.v4.content.res.*; +import android.support.v7.widget.Toolbar; +import android.view.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.utils.*; + +import static android.os.Build.VERSION.*; +import static android.os.Build.VERSION_CODES.*; + +/** + * Base class for all root views in the application. + *

+ * A root view is an Android view that is directly attached to an activity. This + * view usually includes a toolbar and a progress bar. This abstract class hides + * some of the complexity of setting these things up, for every version of + * Android. + */ +public abstract class BaseRootView extends FrameLayout +{ + private final Context context; + + private final BaseActivity activity; + + private final ThemeSwitcher themeSwitcher; + + public BaseRootView(Context context) + { + super(context); + this.context = context; + activity = (BaseActivity) context; + themeSwitcher = activity.getComponent().getThemeSwitcher(); + } + + public boolean getDisplayHomeAsUp() + { + return false; + } + + @NonNull + public abstract Toolbar getToolbar(); + + public int getToolbarColor() + { + if (SDK_INT < LOLLIPOP && !themeSwitcher.isNightMode()) + { + return ResourcesCompat.getColor(context.getResources(), + R.color.grey_900, context.getTheme()); + } + + StyledResources res = new StyledResources(context); + return res.getColor(R.attr.colorPrimary); + } + + protected void initToolbar() + { + if (SDK_INT >= LOLLIPOP) + { + getToolbar().setElevation(InterfaceUtils.dpToPixels(context, 2)); + + View view = findViewById(R.id.toolbarShadow); + if (view != null) view.setVisibility(GONE); + + view = findViewById(R.id.headerShadow); + if(view != null) view.setVisibility(GONE); + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/BaseScreen.java b/app/src/main/java/org/isoron/uhabits/activities/BaseScreen.java new file mode 100644 index 000000000..1954c7c42 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/BaseScreen.java @@ -0,0 +1,301 @@ +/* + * 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.activities; + +import android.content.*; +import android.graphics.*; +import android.graphics.drawable.*; +import android.net.*; +import android.os.*; +import android.support.annotation.*; +import android.support.design.widget.*; +import android.support.v4.content.res.*; +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.utils.*; + +import java.io.*; + +import static android.os.Build.VERSION.*; +import static android.os.Build.VERSION_CODES.*; + +/** + * 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 class BaseScreen +{ + protected BaseActivity activity; + + @Nullable + private BaseRootView rootView; + + @Nullable + private BaseSelectionMenu selectionMenu; + + private Snackbar snackbar; + + 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 (SDK_INT >= 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); + } + } + + @Deprecated + public static int getDefaultActionBarColor(Context context) + { + if (SDK_INT < LOLLIPOP) + { + return ResourcesCompat.getColor(context.getResources(), + R.color.grey_900, context.getTheme()); + } + else + { + StyledResources res = new StyledResources(context); + return res.getColor(R.attr.colorPrimary); + } + } + + /** + * Notifies the screen that its contents should be updated. + */ + public void invalidate() + { + if (rootView == null) return; + rootView.invalidate(); + } + + 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); + }); + } + + /** + * 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. + */ + public 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(@StringRes Integer stringId) + { + if (stringId == null || rootView == null) return; + if (snackbar == null) + { + snackbar = Snackbar.make(rootView, stringId, Snackbar.LENGTH_SHORT); + int tvId = android.support.design.R.id.snackbar_text; + TextView tv = (TextView) snackbar.getView().findViewById(tvId); + tv.setTextColor(Color.WHITE); + } + else snackbar.setText(stringId); + snackbar.show(); + } + + public void showSendEmailScreen(@StringRes int toId, + @StringRes int subjectId, + String content) + { + String to = activity.getString(toId); + String subject = activity.getString(subjectId); + + 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()); + } + + private void setActionBarColor(@NonNull ActionBar actionBar, int color) + { + ColorDrawable drawable = new ColorDrawable(color); + actionBar.setBackgroundDrawable(drawable); + } + + private void setStatusBarColor(int baseColor) + { + if (SDK_INT < LOLLIPOP) return; + + int darkerColor = ColorUtils.mixColors(baseColor, Color.BLACK, 0.75f); + activity.getWindow().setStatusBarColor(darkerColor); + } + + 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/activities/BaseSelectionMenu.java b/app/src/main/java/org/isoron/uhabits/activities/BaseSelectionMenu.java new file mode 100644 index 000000000..a5c7f5cca --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/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.activities; + +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/activities/BaseSystem.java b/app/src/main/java/org/isoron/uhabits/activities/BaseSystem.java new file mode 100644 index 000000000..044fa9600 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/BaseSystem.java @@ -0,0 +1,160 @@ +/* + * 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.activities; + +import android.content.*; +import android.os.*; +import android.support.annotation.*; +import android.view.*; + +import org.isoron.uhabits.*; +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. + */ +@ActivityScope +public class BaseSystem +{ + private Context context; + + @Inject + public BaseSystem(@ActivityContext Context context) + { + this.context = context; + } + + /** + * 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 = + DateFormats.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(); + + String log = "---------- BUG REPORT BEGINS ----------\n"; + log += deviceInfo + "\n" + logcat; + log += "---------- BUG REPORT ENDS ------------\n"; + + return log; + } + + 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(); + } + + 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()); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/ThemeSwitcher.java b/app/src/main/java/org/isoron/uhabits/activities/ThemeSwitcher.java new file mode 100644 index 000000000..d560800ec --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/ThemeSwitcher.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.activities; + +import android.support.annotation.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.preferences.*; + +import javax.inject.*; + +@ActivityScope +public class ThemeSwitcher +{ + public static final int THEME_DARK = 1; + + public static final int THEME_LIGHT = 0; + + @NonNull + private final BaseActivity activity; + + private Preferences preferences; + + @Inject + public ThemeSwitcher(@NonNull BaseActivity activity, + @NonNull Preferences preferences) + { + this.activity = activity; + this.preferences = preferences; + } + + public void apply() + { + switch (getTheme()) + { + case THEME_DARK: + applyDarkTheme(); + break; + + case THEME_LIGHT: + default: + applyLightTheme(); + break; + } + } + + public boolean isNightMode() + { + return getTheme() == THEME_DARK; + } + + public void refreshTheme() + { + + } + + public void toggleNightMode() + { + if (isNightMode()) setTheme(THEME_LIGHT); + else setTheme(THEME_DARK); + } + + private void applyDarkTheme() + { + if (preferences.isPureBlackEnabled()) + activity.setTheme(R.style.AppBaseThemeDark_PureBlack); + else activity.setTheme(R.style.AppBaseThemeDark); + } + + private void applyLightTheme() + { + activity.setTheme(R.style.AppBaseTheme); + } + + private int getTheme() + { + return preferences.getTheme(); + } + + public void setTheme(int theme) + { + preferences.setTheme(theme); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/about/AboutActivity.java b/app/src/main/java/org/isoron/uhabits/activities/about/AboutActivity.java new file mode 100644 index 000000000..b887766a5 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/about/AboutActivity.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.activities.about; + +import android.os.*; + +import org.isoron.uhabits.activities.*; +import org.isoron.uhabits.intents.*; + +/** + * 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 +{ + @Override + protected void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + AboutRootView rootView = new AboutRootView(this, new IntentFactory()); + BaseScreen screen = new BaseScreen(this); + screen.setRootView(rootView); + setScreen(screen); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/about/AboutRootView.java b/app/src/main/java/org/isoron/uhabits/activities/about/AboutRootView.java new file mode 100644 index 000000000..a8ff3abe7 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/about/AboutRootView.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.activities.about; + +import android.content.*; +import android.support.annotation.*; +import android.support.v7.widget.Toolbar; +import android.widget.*; + +import org.isoron.uhabits.BuildConfig; +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.*; +import org.isoron.uhabits.intents.*; +import org.isoron.uhabits.utils.*; + +import butterknife.*; + +public class AboutRootView extends BaseRootView +{ + @BindView(R.id.tvVersion) + TextView tvVersion; + + @BindView(R.id.tvRate) + TextView tvRate; + + @BindView(R.id.tvFeedback) + TextView tvFeedback; + + @BindView(R.id.tvSource) + TextView tvSource; + + @BindView(R.id.toolbar) + Toolbar toolbar; + + private final IntentFactory intents; + + public AboutRootView(Context context, IntentFactory intents) + { + super(context); + this.intents = intents; + + addView(inflate(getContext(), R.layout.about, null)); + ButterKnife.bind(this); + + tvVersion.setText( + String.format(getResources().getString(R.string.version_n), + BuildConfig.VERSION_NAME)); + } + + @Override + public boolean getDisplayHomeAsUp() + { + return true; + } + + @NonNull + @Override + public Toolbar getToolbar() + { + return toolbar; + } + + @Override + public int getToolbarColor() + { + StyledResources res = new StyledResources(getContext()); + if (!res.getBoolean(R.attr.useHabitColorAsPrimary)) + return super.getToolbarColor(); + + return res.getColor(R.attr.aboutScreenColor); + } + + @OnClick(R.id.tvFeedback) + public void onClickFeedback() + { + Intent intent = intents.sendFeedback(getContext()); + getContext().startActivity(intent); + } + + @OnClick(R.id.tvRate) + public void onClickRate() + { + Intent intent = intents.rateApp(getContext()); + getContext().startActivity(intent); + } + + @OnClick(R.id.tvSource) + public void onClickSource() + { + Intent intent = intents.viewSourceCode(getContext()); + getContext().startActivity(intent); + } + + @Override + protected void initToolbar() + { + super.initToolbar(); + toolbar.setTitle(getResources().getString(R.string.about)); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/about/package-info.java b/app/src/main/java/org/isoron/uhabits/activities/about/package-info.java new file mode 100644 index 000000000..e519806ab --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/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.activities.about; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/ColorPickerDialog.java b/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/ColorPickerDialog.java new file mode 100644 index 000000000..c4ab3022c --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/ColorPickerDialog.java @@ -0,0 +1,41 @@ +/* + * 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.activities.common.dialogs; + +import org.isoron.uhabits.utils.*; + +/** + * Dialog that allows the user to choose a color. + */ +public class ColorPickerDialog extends com.android.colorpicker.ColorPickerDialog +{ + public void setListener(OnColorSelectedListener listener) + { + super.setOnColorSelectedListener(c -> { + c = ColorUtils.colorToPaletteIndex(getContext(), c); + listener.onColorSelected(c); + }); + } + + public interface OnColorSelectedListener + { + void onColorSelected(int color); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/ColorPickerDialogFactory.java b/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/ColorPickerDialogFactory.java new file mode 100644 index 000000000..e13368916 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/ColorPickerDialogFactory.java @@ -0,0 +1,52 @@ +/* + * 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.activities.common.dialogs; + +import android.content.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.*; +import org.isoron.uhabits.utils.*; + +import javax.inject.*; + +@ActivityScope +public class ColorPickerDialogFactory +{ + private final Context context; + + @Inject + public ColorPickerDialogFactory(@ActivityContext Context context) + { + this.context = context; + } + + public ColorPickerDialog create(int paletteColor) + { + ColorPickerDialog dialog = new ColorPickerDialog(); + StyledResources res = new StyledResources(context); + int color = ColorUtils.getColor(context, paletteColor); + + dialog.initialize(R.string.color_picker_default_title, res.getPalette(), + color, 4, com.android.colorpicker.ColorPickerDialog.SIZE_SMALL); + + return dialog; + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/ConfirmDeleteDialog.java b/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/ConfirmDeleteDialog.java new file mode 100644 index 000000000..b86ed2f25 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/ConfirmDeleteDialog.java @@ -0,0 +1,63 @@ +/* + * 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.activities.common.dialogs; + +import android.content.*; +import android.support.v7.app.*; + +import com.google.auto.factory.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.*; + +import butterknife.*; + +/** + * Dialog that asks the user confirmation before executing a delete operation. + */ +@AutoFactory(allowSubclasses = true) +public class ConfirmDeleteDialog extends AlertDialog +{ + @BindString(R.string.delete_habits_message) + protected String question; + + @BindString(android.R.string.yes) + protected String yes; + + @BindString(android.R.string.no) + protected String no; + + protected ConfirmDeleteDialog(@Provided @ActivityContext Context context, + Callback callback) + { + super(context); + ButterKnife.bind(this); + + setTitle(R.string.delete_habits); + setMessage(question); + setButton(BUTTON_POSITIVE, yes, (dialog, which) -> callback.run()); + setButton(BUTTON_NEGATIVE, no, (dialog, which) -> {}); + } + + public interface Callback + { + void run(); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/dialogs/FilePickerDialog.java b/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/FilePickerDialog.java similarity index 72% rename from app/src/main/java/org/isoron/uhabits/dialogs/FilePickerDialog.java rename to app/src/main/java/org/isoron/uhabits/activities/common/dialogs/FilePickerDialog.java index 94c574fbd..a2cca7bbc 100644 --- a/app/src/main/java/org/isoron/uhabits/dialogs/FilePickerDialog.java +++ b/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/FilePickerDialog.java @@ -17,63 +17,73 @@ * with this program. If not, see . */ -package org.isoron.uhabits.dialogs; - -import android.app.Activity; -import android.app.Dialog; -import android.support.annotation.NonNull; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager.LayoutParams; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.ListView; -import android.widget.TextView; - -import java.io.File; -import java.io.FileFilter; -import java.util.Arrays; +package org.isoron.uhabits.activities.common.dialogs; +import android.content.*; +import android.support.annotation.*; +import android.support.v7.app.*; +import android.view.*; +import android.view.WindowManager.*; +import android.widget.*; + +import com.google.auto.factory.*; + +import org.isoron.uhabits.activities.*; + +import java.io.*; +import java.util.*; + +/** + * Dialog that allows the user to pick a file. + */ +@AutoFactory(allowSubclasses = true) public class FilePickerDialog implements AdapterView.OnItemClickListener { private static final String PARENT_DIR = ".."; - private final Activity activity; + private final Context context; + private ListView list; - private Dialog dialog; - private File currentPath; - public interface OnFileSelectedListener - { - void onFileSelected(File file); - } + private AppCompatDialog dialog; + + private File currentPath; private OnFileSelectedListener listener; - public FilePickerDialog(Activity activity, File initialDirectory) + public FilePickerDialog(@Provided @ActivityContext Context context, + File initialDirectory) { - this.activity = activity; + this.context = context; - list = new ListView(activity); + list = new ListView(context); list.setOnItemClickListener(this); - dialog = new Dialog(activity); + dialog = new AppCompatDialog(context); dialog.setContentView(list); - dialog.getWindow().setLayout(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + dialog + .getWindow() + .setLayout(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); navigateTo(initialDirectory); } + public AppCompatDialog getDialog() + { + return dialog; + } + @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()) { @@ -86,27 +96,14 @@ public class FilePickerDialog implements AdapterView.OnItemClickListener } } - public void show() - { - dialog.show(); - } - public void setListener(OnFileSelectedListener listener) { this.listener = listener; } - private void navigateTo(File path) + public void show() { - if (!path.exists()) return; - - File[] dirs = path.listFiles(new ReadableDirFilter()); - File[] files = path.listFiles(new RegularReadableFileFilter()); - if(dirs == null || files == null) return; - - this.currentPath = path; - dialog.setTitle(currentPath.getPath()); - list.setAdapter(new FilePickerAdapter(getFileList(path, dirs, files))); + dialog.show(); } @NonNull @@ -138,11 +135,39 @@ public class FilePickerDialog implements AdapterView.OnItemClickListener return fileList; } + private void navigateTo(File path) + { + if (!path.exists()) return; + + File[] dirs = path.listFiles(new ReadableDirFilter()); + File[] files = path.listFiles(new RegularReadableFileFilter()); + if (dirs == null || files == null) return; + + this.currentPath = path; + dialog.setTitle(currentPath.getPath()); + list.setAdapter(new FilePickerAdapter(getFileList(path, dirs, files))); + } + + public interface OnFileSelectedListener + { + void onFileSelected(File file); + } + + private static class ReadableDirFilter implements FileFilter + { + @Override + public boolean accept(File file) + { + return (file.isDirectory() && file.canRead()); + } + } + private class FilePickerAdapter extends ArrayAdapter { public FilePickerAdapter(@NonNull String[] fileList) { - super(FilePickerDialog.this.activity, android.R.layout.simple_list_item_1, fileList); + super(FilePickerDialog.this.context, + android.R.layout.simple_list_item_1, fileList); } @Override @@ -155,15 +180,6 @@ public class FilePickerDialog implements AdapterView.OnItemClickListener } } - private static class ReadableDirFilter implements FileFilter - { - @Override - public boolean accept(File file) - { - return (file.isDirectory() && file.canRead()); - } - } - private class RegularReadableFileFilter implements FileFilter { @Override diff --git a/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/HistoryEditorDialog.java b/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/HistoryEditorDialog.java new file mode 100644 index 000000000..7aac7e87a --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/HistoryEditorDialog.java @@ -0,0 +1,174 @@ +/* + * 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.activities.common.dialogs; + +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.activities.common.views.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.tasks.*; +import org.isoron.uhabits.utils.*; + +public class HistoryEditorDialog extends AppCompatDialogFragment + implements DialogInterface.OnClickListener, ModelObservable.Listener +{ + @Nullable + private Habit habit; + + @Nullable + HistoryChart historyChart; + + @NonNull + private Controller controller; + + private HabitList habitList; + + private TaskRunner taskRunner; + + 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 app = + (HabitsApplication) getActivity().getApplicationContext(); + habitList = app.getComponent().getHabitList(); + taskRunner = app.getComponent().getTaskRunner(); + + 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 onPause() + { + habit.getCheckmarks().observable.removeListener(this); + super.onPause(); + } + + @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 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; + taskRunner.execute(new RefreshTask()); + } + + public interface Controller extends HistoryChart.Controller {} + + private class RefreshTask implements Task + { + public int[] checkmarks; + + @Override + public void doInBackground() + { + checkmarks = habit.getCheckmarks().getAllValues(); + } + + @Override + public void onPostExecute() + { + if (getContext() == null || habit == null || historyChart == null) + return; + + int color = ColorUtils.getColor(getContext(), habit.getColor()); + historyChart.setColor(color); + historyChart.setCheckmarks(checkmarks); + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/dialogs/WeekdayPickerDialog.java b/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/WeekdayPickerDialog.java similarity index 64% rename from app/src/main/java/org/isoron/uhabits/dialogs/WeekdayPickerDialog.java rename to app/src/main/java/org/isoron/uhabits/activities/common/dialogs/WeekdayPickerDialog.java index 5297b80eb..792e403f7 100644 --- a/app/src/main/java/org/isoron/uhabits/dialogs/WeekdayPickerDialog.java +++ b/app/src/main/java/org/isoron/uhabits/activities/common/dialogs/WeekdayPickerDialog.java @@ -17,67 +17,69 @@ * with this program. If not, see . */ -package org.isoron.uhabits.dialogs; +package org.isoron.uhabits.activities.common.dialogs; -import android.app.Dialog; -import android.content.DialogInterface; -import android.os.Bundle; +import android.app.*; +import android.content.*; +import android.os.*; import android.support.v7.app.AlertDialog; -import android.support.v7.app.AppCompatDialogFragment; +import android.support.v7.app.*; -import org.isoron.uhabits.R; -import org.isoron.uhabits.helpers.DateHelper; +import org.isoron.uhabits.*; +import org.isoron.uhabits.utils.*; -public class WeekdayPickerDialog extends AppCompatDialogFragment - implements DialogInterface.OnMultiChoiceClickListener, DialogInterface.OnClickListener +/** + * Dialog that allows the user to pick one or more days of the week. + */ +public class WeekdayPickerDialog extends AppCompatDialogFragment implements + DialogInterface.OnMultiChoiceClickListener, + DialogInterface.OnClickListener { - public interface OnWeekdaysPickedListener - { - void onWeekdaysPicked(boolean[] selectedDays); - } - private boolean[] selectedDays; + private OnWeekdaysPickedListener listener; - public void setListener(OnWeekdaysPickedListener listener) + @Override + public void onClick(DialogInterface dialog, int which, boolean isChecked) { - this.listener = listener; + selectedDays[which] = isChecked; } - public void setSelectedDays(boolean[] selectedDays) + @Override + public void onClick(DialogInterface dialog, int which) { - this.selectedDays = selectedDays; + if (listener != null) listener.onWeekdaysPicked(selectedDays); } @Override 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() - { - @Override - public void onClick(DialogInterface dialog, int which) - { - dismiss(); - } - }); + builder + .setTitle(R.string.select_weekdays) + .setMultiChoiceItems(DateUtils.getLongDayNames(), selectedDays, + this) + .setPositiveButton(android.R.string.yes, this) + .setNegativeButton(android.R.string.cancel, (dialog, which) -> { + dismiss(); + }); return builder.create(); } - @Override - public void onClick(DialogInterface dialog, int which, boolean isChecked) + public void setListener(OnWeekdaysPickedListener listener) { - selectedDays[which] = isChecked; + this.listener = listener; } - @Override - public void onClick(DialogInterface dialog, int which) + public void setSelectedDays(boolean[] selectedDays) { - if(listener != null) listener.onWeekdaysPicked(selectedDays); + this.selectedDays = selectedDays; + } + + public interface OnWeekdaysPickedListener + { + void onWeekdaysPicked(boolean[] selectedDays); } } diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitFrequencyView.java b/app/src/main/java/org/isoron/uhabits/activities/common/views/FrequencyChart.java similarity index 63% rename from app/src/main/java/org/isoron/uhabits/views/HabitFrequencyView.java rename to app/src/main/java/org/isoron/uhabits/activities/common/views/FrequencyChart.java index 7890757dd..fa1d38214 100644 --- a/app/src/main/java/org/isoron/uhabits/views/HabitFrequencyView.java +++ b/app/src/main/java/org/isoron/uhabits/activities/common/views/FrequencyChart.java @@ -17,103 +17,104 @@ * 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.activities.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; + private int maxFreq; - 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; + maxFreq = getMaxFreq(frequency); + postInvalidate(); } - private void createColors() + private int getMaxFreq(HashMap frequency) { - if(habit != null) + int maxValue = 1; + for (Integer[] values : frequency.values()) { - this.primaryColor = ColorHelper.getColor(getContext(), habit.color); + for (Integer value : values) + { + maxValue = Math.max(value, maxValue); + } } + return maxValue; + } - 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); + public void setIsBackgroundTransparent(boolean isBackgroundTransparent) + { + this.isBackgroundTransparent = isBackgroundTransparent; + initColors(); } - protected void createPaints() + protected void initPaints() { pText = new Paint(); pText.setAntiAlias(true); @@ -126,79 +127,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 +142,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 +157,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 +215,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); - - 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); + canvas.drawText(dfMonth.format(time), rect.centerX(), + rect.centerY() - 0.1f * em, pText); - 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 +232,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 +247,83 @@ 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; + // maximal allowed mark radius + float maxRadius = (rect.height() - 2 * padding) / 2.0f; + // the real mark radius is scaled down by a factor depending on the maximal frequency + float scale = 1.0f/maxFreq * value; + float radius = maxRadius * scale; + + int colorIndex = Math.round((colors.length-1) * scale); + pGraph.setColor(colors[colorIndex]); + 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() + { + StyledResources res = new StyledResources(getContext()); + textColor = res.getColor(R.attr.mediumContrastTextColor); + gridColor = res.getColor(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 = DateFormats.fromSkeleton("MMM"); + dfYear = DateFormats.fromSkeleton("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/activities/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/activities/common/views/HabitChart.java index b1e239d5e..cd74cf15c 100644 --- a/app/src/main/java/org/isoron/uhabits/views/HabitDataView.java +++ b/app/src/main/java/org/isoron/uhabits/activities/common/views/HabitChart.java @@ -17,15 +17,13 @@ * with this program. If not, see . */ -package org.isoron.uhabits.views; +package org.isoron.uhabits.activities.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/activities/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/activities/common/views/HistoryChart.java index 96899585e..620a2d59f 100644 --- a/app/src/main/java/org/isoron/uhabits/views/HabitHistoryView.java +++ b/app/src/main/java/org/isoron/uhabits/activities/common/views/HistoryChart.java @@ -17,185 +17,169 @@ * 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.activities.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.*; + +import static org.isoron.uhabits.models.Checkmark.*; + +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(); - - isEditable = false; - checkmarks = new int[0]; - primaryColor = ColorHelper.getColor(getContext(), 7); - dfMonth = DateHelper.getDateFormat("MMM"); - dfYear = DateHelper.getDateFormat("yyyy"); + if (!isEditable) return false; - baseLocation = new RectF(); - } + performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP); - private void updateDate() - { - baseDate = DateHelper.getStartOfTodayCalendar(); - baseDate.add(Calendar.DAY_OF_YEAR, -(getDataOffset() - 1) * 7); + int pointerId = e.getPointerId(0); + float x = e.getX(pointerId); + float y = e.getY(pointerId); - nDays = (nColumns - 1) * 7; - int realWeekday = DateHelper.getStartOfTodayCalendar().get(Calendar.DAY_OF_WEEK); - todayPositionInColumn = (7 + realWeekday - baseDate.getFirstDayOfWeek()) % 7; + final Long timestamp = positionToTimestamp(x, y); + if (timestamp == null) return false; - baseDate.add(Calendar.DAY_OF_YEAR, -nDays); - baseDate.add(Calendar.DAY_OF_YEAR, -todayPositionInColumn); - } + int offset = timestampToOffset(timestamp); + if (offset < checkmarks.length) + { + boolean isChecked = checkmarks[offset] == CHECKED_EXPLICITLY; + checkmarks[offset] = (isChecked ? UNCHECKED : CHECKED_EXPLICITLY); + } - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) - { - int width = MeasureSpec.getSize(widthMeasureSpec); - int height = MeasureSpec.getSize(heightMeasureSpec); - setMeasuredDimension(width, height); + controller.onToggleCheckmark(timestamp); + postInvalidate(); + return true; } - @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 +192,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 +212,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() + { + StyledResources res = new StyledResources(getContext()); - final Long timestamp = positionToTimestamp(x, y); - if(timestamp == null) return false; + if (isBackgroundTransparent) + primaryColor = ColorUtils.setMinValue(primaryColor, 0.75f); - ToggleRepetitionTask task = new ToggleRepetitionTask(habit, timestamp); - task.setListener(this); - task.execute(); + int red = Color.red(primaryColor); + int green = Color.green(primaryColor); + int blue = Color.blue(primaryColor); - return true; + 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] = res.getColor(R.attr.lowContrastTextColor); + colors[1] = Color.argb(127, red, green, blue); + colors[2] = primaryColor; + textColor = res.getColor(R.attr.mediumContrastTextColor); + reverseTextColor = + res.getColor(R.attr.highContrastReverseTextColor); + } + } + + private void initDateFormats() + { + dfMonth = DateFormats.fromSkeleton("MMM"); + dfYear = DateFormats.fromSkeleton("yyyy"); + } + + private void initRects() + { + baseLocation = new RectF(); } private Long positionToTimestamp(float x, float y) @@ -372,41 +402,44 @@ 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 int timestampToOffset(Long timestamp) { - this.isEditable = isEditable; + Long day = DateUtils.millisecondsInOneDay; + Long today = DateUtils.getStartOfToday(); + + return (int) ((today - timestamp) / day); } - @Override - public void onToggleRepetitionFinished() + private void updateDate() { - new BaseTask() - { - @Override - protected void doInBackground() - { - refreshData(); - } + baseDate = DateUtils.getStartOfTodayCalendar(); + baseDate.add(Calendar.DAY_OF_YEAR, -(getDataOffset() - 1) * 7); - @Override - protected void onPostExecute(Void aVoid) - { - invalidate(); - super.onPostExecute(null); - } - }.execute(); + 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); + } + + public interface Controller + { + 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/activities/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/activities/common/views/RingView.java index cac641a22..e093a9f2b 100644 --- a/app/src/main/java/org/isoron/uhabits/views/RingView.java +++ b/app/src/main/java/org/isoron/uhabits/activities/common/views/RingView.java @@ -17,49 +17,55 @@ * 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.activities.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.AttributeSetUtils.*; +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 +76,56 @@ 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 = AttributeSetUtils.getAttribute(ctx, attrs, "text", ""); - enableFontAwesome = UIHelper.getBooleanAttribute(context, attrs, "enableFontAwesome", false); + enableFontAwesome = AttributeSetUtils.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); + + StyledResources res = new StyledResources(getContext()); + + if (backgroundColor == null) + backgroundColor = res.getColor(R.attr.cardBackgroundColor); + + if (inactiveColor == null) + inactiveColor = res.getColor(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/activities/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/activities/common/views/ScoreChart.java index 6258e7b13..902959684 100644 --- a/app/src/main/java/org/isoron/uhabits/views/HabitScoreView.java +++ b/app/src/main/java/org/isoron/uhabits/activities/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.activities.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() + { + StyledResources res = new StyledResources(getContext()); + + primaryColor = Color.BLACK; + textColor = res.getColor(R.attr.mediumContrastTextColor); + gridColor = res.getColor(R.attr.lowContrastTextColor); + backgroundColor = res.getColor(R.attr.cardBackgroundColor); + } + + private void initDateFormats() + { + dfYear = DateFormats.fromSkeleton("yyyy"); + dfMonth = DateFormats.fromSkeleton("MMM"); + dfDay = DateFormats.fromSkeleton("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/activities/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/activities/common/views/ScrollableChart.java index fbae274b7..a762ecf86 100644 --- a/app/src/main/java/org/isoron/uhabits/views/ScrollableDataView.java +++ b/app/src/main/java/org/isoron/uhabits/activities/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.activities.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/activities/common/views/StreakChart.java similarity index 52% rename from app/src/main/java/org/isoron/uhabits/views/HabitStreakView.java rename to app/src/main/java/org/isoron/uhabits/activities/common/views/StreakChart.java index 74fc6c518..155557ea2 100644 --- a/app/src/main/java/org/isoron/uhabits/views/HabitStreakView.java +++ b/app/src/main/java/org/isoron/uhabits/activities/common/views/StreakChart.java @@ -17,192 +17,183 @@ * 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.activities.common.views; + +import android.content.*; +import android.graphics.*; +import android.util.*; +import android.view.*; +import android.view.ViewGroup.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; + +import java.text.*; +import java.util.*; + +import static android.view.View.MeasureSpec.*; + +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() + /** + * Returns the maximum number of streaks this view is able to show, given + * its current size. + * + * @return max number of visible streaks + */ + public int getMaxStreakCount() { - 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); + return (int) Math.floor(getMeasuredHeight() / baseSize); } - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) + public void populateWithRandomData() { - int width = MeasureSpec.getSize(widthMeasureSpec); - int height = MeasureSpec.getSize(heightMeasureSpec); - setMeasuredDimension(width, height); - } + long day = DateUtils.millisecondsInOneDay; + long start = DateUtils.getStartOfToday(); + LinkedList streaks = new LinkedList<>(); - @Override - protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) - { - 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; + 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; + } - updateMaxMin(); + setStreaks(streaks); } - private void createColors() + public void setColor(int color) { - 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); - - 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); + this.primaryColor = color; + postInvalidate(); } - protected void createPaints() + public void setIsBackgroundTransparent(boolean isBackgroundTransparent) { - paint = new Paint(); - paint.setTextAlign(Paint.Align.CENTER); - paint.setAntiAlias(true); + this.isBackgroundTransparent = isBackgroundTransparent; + initColors(); } - public void refreshData() + public void setStreaks(List streaks) { - if(habit == null) return; - streaks = habit.streaks.getAll(maxStreakCount); - updateMaxMin(); - postInvalidate(); + this.streaks = streaks; + initColors(); + updateMaxMinLengths(); + requestLayout(); } @Override 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; + LayoutParams params = getLayoutParams(); - for (Streak s : streaks) + if (params != null && params.height == LayoutParams.WRAP_CONTENT) { - maxLength = Math.max(maxLength, s.length); - minLength = Math.min(minLength, s.length); + int width = getSize(widthSpec); + int height = streaks.size() * baseSize; - 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)); + heightSpec = makeMeasureSpec(height, EXACTLY); + widthSpec = makeMeasureSpec(width, EXACTLY); } - if(width - 2 * maxLabelWidth < width * 0.25f) - { - maxLabelWidth = 0; - shouldShowLabels = false; - } + setMeasuredDimension(widthSpec, heightSpec); + } + + @Override + protected void onSizeChanged(int width, + int height, + int oldWidth, + int oldHeight) + { + 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; + + 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 +201,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 +225,73 @@ 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); + + StyledResources res = new StyledResources(getContext()); + + 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] = res.getColor(R.attr.lowContrastTextColor); + textColor = res.getColor(R.attr.mediumContrastTextColor); + reverseTextColor = res.getColor(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/activities/common/views/package-info.java b/app/src/main/java/org/isoron/uhabits/activities/common/views/package-info.java new file mode 100644 index 000000000..1d50f26f0 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/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.activities.common.views; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/edit/BaseDialog.java b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/BaseDialog.java new file mode 100644 index 000000000..70c0e445f --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/BaseDialog.java @@ -0,0 +1,262 @@ +/* + * 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.activities.habits.edit; + +import android.os.*; +import android.support.annotation.*; +import android.support.v7.app.*; +import android.text.format.*; +import android.view.*; + +import com.android.datetimepicker.time.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.*; +import org.isoron.uhabits.activities.common.dialogs.*; +import org.isoron.uhabits.commands.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.preferences.*; + +import java.util.*; + +import butterknife.*; + +public abstract class BaseDialog extends AppCompatDialogFragment +{ + @Nullable + protected Habit originalHabit; + + @Nullable + protected Habit modifiedHabit; + + @Nullable + protected BaseDialogHelper helper; + + protected Preferences prefs; + + protected CommandRunner commandRunner; + + protected HabitList habitList; + + protected AppComponent appComponent; + + protected ModelFactory modelFactory; + + private ColorPickerDialogFactory colorPickerDialogFactory; + + @Override + public void onActivityCreated(Bundle savedInstanceState) + { + super.onActivityCreated(savedInstanceState); + + BaseActivity activity = (BaseActivity) getActivity(); + colorPickerDialogFactory = + activity.getComponent().getColorPickerDialogFactory(); + } + + @Override + public View onCreateView(LayoutInflater inflater, + ViewGroup container, + Bundle savedInstanceState) + { + View view = inflater.inflate(R.layout.edit_habit, container, false); + + HabitsApplication app = + (HabitsApplication) getContext().getApplicationContext(); + + appComponent = app.getComponent(); + prefs = appComponent.getPreferences(); + habitList = appComponent.getHabitList(); + commandRunner = appComponent.getCommandRunner(); + modelFactory = appComponent.getModelFactory(); + + 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().toInteger()); + } + } + + 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, new WeekdayList(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(reminder.getDays().toArray()); + dialog.show(getFragmentManager(), "weekdayPicker"); + } + + @OnClick(R.id.buttonPickColor) + void showColorPicker() + { + int color = modifiedHabit.getColor(); + ColorPickerDialog picker = colorPickerDialogFactory.create(color); + + picker.setListener(c -> { + prefs.setDefaultHabitColor(c); + modifiedHabit.setColor(c); + helper.populateColor(c); + }); + + 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 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, WeekdayList.EVERY_DAY); + 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(), + new WeekdayList(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/activities/habits/edit/BaseDialogHelper.java b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/BaseDialogHelper.java new file mode 100644 index 000000000..78e69c6d4 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/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.activities.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[] = reminder.getDays().toArray(); + 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/activities/habits/edit/CreateHabitDialog.java b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/CreateHabitDialog.java new file mode 100644 index 000000000..5f34e2e40 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/CreateHabitDialog.java @@ -0,0 +1,54 @@ +/* + * 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.activities.habits.edit; + +import com.google.auto.factory.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.commands.*; +import org.isoron.uhabits.models.*; + +@AutoFactory(allowSubclasses = true) +public class CreateHabitDialog extends BaseDialog +{ + @Override + protected int getTitle() + { + return R.string.create_habit; + } + + @Override + protected void initializeHabits() + { + modifiedHabit = modelFactory.buildHabit(); + modifiedHabit.setFrequency(Frequency.DAILY); + modifiedHabit.setColor( + prefs.getDefaultHabitColor(modifiedHabit.getColor())); + } + + @Override + protected void saveHabit() + { + Command command = appComponent + .getCreateHabitCommandFactory() + .create(habitList, modifiedHabit); + commandRunner.execute(command, null); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitDialog.java b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitDialog.java new file mode 100644 index 000000000..e9c1aca78 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitDialog.java @@ -0,0 +1,52 @@ +/* + * 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.activities.habits.edit; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.commands.*; + +public class EditHabitDialog extends BaseDialog +{ + @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 = modelFactory.buildHabit(); + modifiedHabit.copyFrom(originalHabit); + } + + @Override + protected void saveHabit() + { + Command command = appComponent.getEditHabitCommandFactory(). + create(habitList, originalHabit, modifiedHabit); + commandRunner.execute(command, originalHabit.getId()); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitDialogFactory.java b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitDialogFactory.java new file mode 100644 index 000000000..481658ebf --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitDialogFactory.java @@ -0,0 +1,47 @@ +/* + * 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.activities.habits.edit; + +import android.os.*; +import android.support.annotation.*; + +import org.isoron.uhabits.models.*; + +import javax.inject.*; + +public class EditHabitDialogFactory +{ + @Inject + public EditHabitDialogFactory() + { + } + + public EditHabitDialog create(@NonNull Habit habit) + { + if (habit.getId() == null) + throw new IllegalArgumentException("habit not saved"); + + EditHabitDialog dialog = new EditHabitDialog(); + Bundle args = new Bundle(); + args.putLong("habitId", habit.getId()); + dialog.setArguments(args); + return dialog; + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/edit/package-info.java b/app/src/main/java/org/isoron/uhabits/activities/habits/edit/package-info.java new file mode 100644 index 000000000..3d6e4e626 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/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.activities.habits.edit; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsActivity.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsActivity.java new file mode 100644 index 000000000..d5529c2a1 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsActivity.java @@ -0,0 +1,107 @@ +/* + * 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.activities.habits.list; + +import android.os.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.*; +import org.isoron.uhabits.activities.habits.list.model.*; +import org.isoron.uhabits.preferences.*; + +/** + * Activity that allows the user to see and modify the list of habits. + */ +public class ListHabitsActivity extends BaseActivity +{ + private HabitCardListAdapter adapter; + + private ListHabitsRootView rootView; + + private ListHabitsScreen screen; + + private ListHabitsComponent component; + + private boolean pureBlack; + + private Preferences prefs; + + public ListHabitsComponent getListHabitsComponent() + { + return component; + } + + @Override + protected void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + HabitsApplication app = (HabitsApplication) getApplicationContext(); + + component = DaggerListHabitsComponent + .builder() + .appComponent(app.getComponent()) + .activityModule(new ActivityModule(this)) + .build(); + + ListHabitsMenu menu = component.getMenu(); + ListHabitsSelectionMenu selectionMenu = component.getSelectionMenu(); + ListHabitsController controller = component.getController(); + + adapter = component.getAdapter(); + rootView = component.getRootView(); + screen = component.getScreen(); + + prefs = app.getComponent().getPreferences(); + pureBlack = prefs.isPureBlackEnabled(); + + screen.setMenu(menu); + screen.setController(controller); + screen.setSelectionMenu(selectionMenu); + rootView.setController(controller, selectionMenu); + + setScreen(screen); + controller.onStartup(); + } + + @Override + protected void onPause() + { + screen.onDettached(); + adapter.cancelRefresh(); + super.onPause(); + } + + @Override + protected void onResume() + { + adapter.refresh(); + screen.onAttached(); + rootView.postInvalidate(); + + if (prefs.getTheme() == ThemeSwitcher.THEME_DARK && + prefs.isPureBlackEnabled() != pureBlack) + { + restartWithFade(); + } + + super.onResume(); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsComponent.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsComponent.java new file mode 100644 index 000000000..658d6c573 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsComponent.java @@ -0,0 +1,47 @@ +/* + * 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.activities.habits.list; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.*; +import org.isoron.uhabits.activities.habits.list.controllers.*; +import org.isoron.uhabits.activities.habits.list.model.*; + +import dagger.*; + +@ActivityScope +@Component(modules = { ActivityModule.class }, + dependencies = { AppComponent.class }) +public interface ListHabitsComponent extends ActivityComponent +{ + CheckmarkButtonControllerFactory getCheckmarkButtonControllerFactory(); + + HabitCardListAdapter getAdapter(); + + ListHabitsController getController(); + + ListHabitsMenu getMenu(); + + ListHabitsRootView getRootView(); + + ListHabitsScreen getScreen(); + + ListHabitsSelectionMenu getSelectionMenu(); +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsController.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsController.java new file mode 100644 index 000000000..af89c14ff --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsController.java @@ -0,0 +1,211 @@ +/* + * 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.activities.habits.list; + +import android.support.annotation.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.*; +import org.isoron.uhabits.activities.habits.list.controllers.*; +import org.isoron.uhabits.activities.habits.list.model.*; +import org.isoron.uhabits.commands.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.preferences.*; +import org.isoron.uhabits.tasks.*; +import org.isoron.uhabits.utils.*; +import org.isoron.uhabits.widgets.*; + +import java.io.*; +import java.util.*; + +import javax.inject.*; + +@ActivityScope +public class ListHabitsController + implements HabitCardListController.HabitListener +{ + @NonNull + private final ListHabitsScreen screen; + + @NonNull + private final BaseSystem system; + + @NonNull + private final HabitList habitList; + + @NonNull + private final HabitCardListAdapter adapter; + + @NonNull + private final Preferences prefs; + + @NonNull + private final CommandRunner commandRunner; + + @NonNull + private final TaskRunner taskRunner; + + private ReminderScheduler reminderScheduler; + + private WidgetUpdater widgetUpdater; + + private ImportDataTaskFactory importTaskFactory; + + private ExportCSVTaskFactory exportCSVFactory; + + @Inject + public ListHabitsController(@NonNull BaseSystem system, + @NonNull CommandRunner commandRunner, + @NonNull HabitList habitList, + @NonNull HabitCardListAdapter adapter, + @NonNull ListHabitsScreen screen, + @NonNull Preferences prefs, + @NonNull ReminderScheduler reminderScheduler, + @NonNull TaskRunner taskRunner, + @NonNull WidgetUpdater widgetUpdater, + @NonNull + ImportDataTaskFactory importTaskFactory, + @NonNull ExportCSVTaskFactory exportCSVFactory) + { + this.adapter = adapter; + this.commandRunner = commandRunner; + this.habitList = habitList; + this.prefs = prefs; + this.screen = screen; + this.system = system; + this.taskRunner = taskRunner; + this.reminderScheduler = reminderScheduler; + this.widgetUpdater = widgetUpdater; + this.importTaskFactory = importTaskFactory; + this.exportCSVFactory = exportCSVFactory; + } + + public void onExportCSV() + { + List selected = new LinkedList<>(); + for (Habit h : habitList) selected.add(h); + + taskRunner.execute(exportCSVFactory.create(selected, filename -> { + if (filename != null) screen.showSendFileScreen(filename); + else screen.showMessage(R.string.could_not_export); + })); + } + + public void onExportDB() + { + taskRunner.execute(new ExportDBTask(filename -> { + if (filename != null) screen.showSendFileScreen(filename); + else screen.showMessage(R.string.could_not_export); + })); + } + + @Override + public void onHabitClick(@NonNull Habit h) + { + screen.showHabitScreen(h); + } + + @Override + public void onHabitReorder(@NonNull Habit from, @NonNull Habit to) + { + taskRunner.execute(() -> habitList.reorder(from, to)); + } + + public void onImportData(@NonNull File file) + { + taskRunner.execute(importTaskFactory.create(file, result -> { + switch (result) + { + case ImportDataTask.SUCCESS: + adapter.refresh(); + screen.showMessage(R.string.habits_imported); + break; + + case ImportDataTask.NOT_RECOGNIZED: + screen.showMessage(R.string.file_not_recognized); + break; + + default: + screen.showMessage(R.string.could_not_import); + break; + } + })); + } + + + @Override + public void onInvalidToggle() + { + screen.showMessage(R.string.long_press_to_toggle); + } + + public void onRepairDB() + { + taskRunner.execute(() -> { + habitList.repair(); + screen.showMessage(R.string.database_repaired); + }); + } + + public void onSendBugReport() + { + try + { + system.dumpBugReportToFile(); + } + catch (IOException e) + { + // ignored + } + + try + { + String log = system.getBugReport(); + int to = R.string.bugReportTo; + int subject = R.string.bugReportSubject; + screen.showSendEmailScreen(to, subject, log); + } + catch (IOException e) + { + e.printStackTrace(); + screen.showMessage(R.string.bug_report_failed); + } + } + + public void onStartup() + { + prefs.incrementLaunchCount(); + if (prefs.isFirstRun()) onFirstRun(); + } + + @Override + public void onToggle(@NonNull Habit habit, long timestamp) + { + commandRunner.execute(new ToggleRepetitionCommand(habit, timestamp), + habit.getId()); + } + + private void onFirstRun() + { + prefs.setFirstRun(false); + prefs.updateLastHint(-1, DateUtils.getStartOfToday()); + screen.showIntroScreen(); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsMenu.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsMenu.java new file mode 100644 index 000000000..dc8fe057d --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsMenu.java @@ -0,0 +1,148 @@ +/* + * 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.activities.habits.list; + +import android.support.annotation.*; +import android.view.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.activities.*; +import org.isoron.uhabits.activities.habits.list.model.*; +import org.isoron.uhabits.preferences.*; + +import javax.inject.*; + +@ActivityScope +public class ListHabitsMenu extends BaseMenu +{ + @NonNull + private final ListHabitsScreen screen; + + private final HabitCardListAdapter adapter; + + private boolean showArchived; + + private boolean showCompleted; + + private final Preferences preferences; + + private ThemeSwitcher themeSwitcher; + + @Inject + public ListHabitsMenu(@NonNull BaseActivity activity, + @NonNull ListHabitsScreen screen, + @NonNull HabitCardListAdapter adapter, + @NonNull Preferences preferences, + @NonNull ThemeSwitcher themeSwitcher) + { + super(activity); + this.screen = screen; + this.adapter = adapter; + this.preferences = preferences; + this.themeSwitcher = themeSwitcher; + + showCompleted = preferences.getShowCompleted(); + showArchived = preferences.getShowArchived(); + updateAdapterFilter(); + } + + @Override + public void onCreate(@NonNull Menu menu) + { + MenuItem nightModeItem = menu.findItem(R.id.actionToggleNightMode); + nightModeItem.setChecked(themeSwitcher.isNightMode()); + + MenuItem hideArchivedItem = menu.findItem(R.id.actionHideArchived); + hideArchivedItem.setChecked(!showArchived); + + MenuItem hideCompletedItem = menu.findItem(R.id.actionHideCompleted); + hideCompletedItem.setChecked(!showCompleted); + } + + @Override + public boolean onItemSelected(@NonNull MenuItem item) + { + switch (item.getItemId()) + { + case R.id.actionToggleNightMode: + screen.toggleNightMode(); + return true; + + case R.id.actionAdd: + screen.showCreateHabitScreen(); + return true; + + case R.id.actionFAQ: + screen.showFAQScreen(); + return true; + + case R.id.actionAbout: + screen.showAboutScreen(); + return true; + + case R.id.actionSettings: + screen.showSettingsScreen(); + return true; + + case R.id.actionHideArchived: + toggleShowArchived(); + invalidate(); + return true; + + case R.id.actionHideCompleted: + toggleShowCompleted(); + invalidate(); + return true; + + default: + return false; + } + } + + @Override + protected int getMenuResourceId() + { + return R.menu.list_habits; + } + + private void toggleShowArchived() + { + showArchived = !showArchived; + preferences.setShowArchived(showArchived); + updateAdapterFilter(); + } + + private void toggleShowCompleted() + { + showCompleted = !showCompleted; + preferences.setShowCompleted(showCompleted); + updateAdapterFilter(); + } + + private void updateAdapterFilter() + { + adapter.setFilter(new HabitMatcherBuilder() + .setArchivedAllowed(showArchived) + .setCompletedAllowed(showCompleted) + .build()); + adapter.refresh(); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsRootView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsRootView.java new file mode 100644 index 000000000..7d1bb3568 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsRootView.java @@ -0,0 +1,187 @@ +/* + * 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.activities.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.activities.*; +import org.isoron.uhabits.activities.habits.list.controllers.*; +import org.isoron.uhabits.activities.habits.list.model.*; +import org.isoron.uhabits.activities.habits.list.views.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.tasks.*; +import org.isoron.uhabits.utils.*; + +import javax.inject.*; + +import butterknife.*; + +@ActivityScope +public class ListHabitsRootView extends BaseRootView + implements ModelObservable.Listener, TaskRunner.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; + + @BindView(R.id.header) + HeaderView header; + + @NonNull + private final HabitCardListAdapter listAdapter; + + private final TaskRunner runner; + + @Inject + public ListHabitsRootView(@ActivityContext Context context, + @NonNull HintListFactory hintListFactory, + @NonNull HabitCardListAdapter listAdapter, + @NonNull TaskRunner runner) + { + super(context); + addView(inflate(getContext(), R.layout.list_habits, null)); + ButterKnife.bind(this); + + this.listAdapter = listAdapter; + listView.setAdapter(listAdapter); + listAdapter.setListView(listView); + + this.runner = runner; + progressBar.setIndeterminate(true); + tvStarEmpty.setTypeface(InterfaceUtils.getFontAwesome(getContext())); + + String hints[] = + getContext().getResources().getStringArray(R.array.hints); + HintList hintList = hintListFactory.create(hints); + hintView.setHints(hintList); + + initToolbar(); + } + + @NonNull + @Override + public Toolbar getToolbar() + { + return toolbar; + } + + @Override + public void onModelChange() + { + updateEmptyView(); + } + + @Override + public void onTaskFinished(Task task) + { + updateProgressBar(); + } + + @Override + public void onTaskStarted(Task task) + { + updateProgressBar(); + } + + public void setController(@NonNull ListHabitsController controller, + @NonNull ListHabitsSelectionMenu menu) + { + HabitCardListController listController = + new HabitCardListController(listAdapter); + + listController.setHabitListener(controller); + listController.setSelectionListener(menu); + listView.setController(listController); + menu.setListController(listController); + } + + @Override + protected void onAttachedToWindow() + { + super.onAttachedToWindow(); + runner.addListener(this); + updateProgressBar(); + listAdapter.getObservable().addListener(this); + } + + @Override + protected void onDetachedFromWindow() + { + listAdapter.getObservable().removeListener(this); + runner.removeListener(this); + super.onDetachedFromWindow(); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) + { + int count = getCheckmarkCount(); + header.setButtonCount(count); + listView.setCheckmarkCount(count); + super.onSizeChanged(w, h, oldw, oldh); + } + + private int getCheckmarkCount() + { + Resources res = getResources(); + float labelWidth = Math.max(getMeasuredWidth() / 3, res.getDimension(R.dimen.habitNameWidth)); + float buttonWidth = res.getDimension(R.dimen.checkmarkWidth); + return Math.min(MAX_CHECKMARK_COUNT, Math.max(0, + (int) ((getMeasuredWidth() - labelWidth) / buttonWidth))); + } + + private void updateEmptyView() + { + llEmpty.setVisibility( + listAdapter.getItemCount() > 0 ? View.GONE : View.VISIBLE); + } + + private void updateProgressBar() + { + postDelayed(() -> { + int activeTaskCount = runner.getActiveTaskCount(); + int newVisibility = activeTaskCount > 0 ? VISIBLE : GONE; + if (progressBar.getVisibility() != newVisibility) + progressBar.setVisibility(newVisibility); + }, 500); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.java new file mode 100644 index 000000000..3fb92e817 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsScreen.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.activities.habits.list; + +import android.content.*; +import android.support.annotation.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.*; +import org.isoron.uhabits.activities.common.dialogs.*; +import org.isoron.uhabits.activities.common.dialogs.ColorPickerDialog.*; +import org.isoron.uhabits.activities.habits.edit.*; +import org.isoron.uhabits.commands.*; +import org.isoron.uhabits.intents.*; +import org.isoron.uhabits.io.*; +import org.isoron.uhabits.models.*; + +import java.io.*; + +import javax.inject.*; + +@ActivityScope +public class ListHabitsScreen extends BaseScreen + implements CommandRunner.Listener +{ + 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_REPAIR_DB = 5; + + public static final int RESULT_IMPORT_DATA = 1; + + @Nullable + private ListHabitsController controller; + + @NonNull + private final IntentFactory intentFactory; + + @NonNull + private final DirFinder dirFinder; + + @NonNull + private final CommandRunner commandRunner; + + @NonNull + private final ConfirmDeleteDialogFactory confirmDeleteDialogFactory; + + @NonNull + private final CreateHabitDialogFactory createHabitDialogFactory; + + @NonNull + private final FilePickerDialogFactory filePickerDialogFactory; + + @NonNull + private final ColorPickerDialogFactory colorPickerFactory; + + @NonNull + private final EditHabitDialogFactory editHabitDialogFactory; + + @NonNull + private final ThemeSwitcher themeSwitcher; + + @Inject + public ListHabitsScreen(@NonNull BaseActivity activity, + @NonNull CommandRunner commandRunner, + @NonNull DirFinder dirFinder, + @NonNull ListHabitsRootView rootView, + @NonNull IntentFactory intentFactory, + @NonNull ThemeSwitcher themeSwitcher, + @NonNull ConfirmDeleteDialogFactory confirmDeleteDialogFactory, + @NonNull CreateHabitDialogFactory createHabitDialogFactory, + @NonNull FilePickerDialogFactory filePickerDialogFactory, + @NonNull ColorPickerDialogFactory colorPickerFactory, + @NonNull EditHabitDialogFactory editHabitDialogFactory) + { + super(activity); + setRootView(rootView); + this.editHabitDialogFactory = editHabitDialogFactory; + this.colorPickerFactory = colorPickerFactory; + this.commandRunner = commandRunner; + this.confirmDeleteDialogFactory = confirmDeleteDialogFactory; + this.createHabitDialogFactory = createHabitDialogFactory; + this.dirFinder = dirFinder; + this.filePickerDialogFactory = filePickerDialogFactory; + this.intentFactory = intentFactory; + this.themeSwitcher = themeSwitcher; + } + + public void onAttached() + { + commandRunner.addListener(this); + } + + @Override + public void onCommandExecuted(@NonNull Command command, + @Nullable Long refreshKey) + { + showMessage(command.getExecuteStringId()); + } + + public void onDettached() + { + commandRunner.removeListener(this); + } + + @Override + public void onResult(int requestCode, int resultCode, Intent data) + { + if (controller == null) return; + + switch (resultCode) + { + case RESULT_IMPORT_DATA: + showImportScreen(); + break; + + case RESULT_EXPORT_CSV: + controller.onExportCSV(); + break; + + case RESULT_EXPORT_DB: + controller.onExportDB(); + break; + + case RESULT_BUG_REPORT: + controller.onSendBugReport(); + break; + + case RESULT_REPAIR_DB: + controller.onRepairDB(); + break; + } + } + + public void setController(@Nullable ListHabitsController controller) + { + this.controller = controller; + } + + public void showAboutScreen() + { + Intent intent = intentFactory.startAboutActivity(activity); + activity.startActivity(intent); + } + + /** + * Displays a {@link ColorPickerDialog} to the user. + *

+ * The selected color on the dialog is the color of the given habit. + * + * @param habit the habit + * @param callback + */ + public void showColorPicker(@NonNull Habit habit, + @NonNull OnColorSelectedListener callback) + { + ColorPickerDialog picker = colorPickerFactory.create(habit.getColor()); + picker.setListener(callback); + activity.showDialog(picker, "picker"); + } + + public void showCreateHabitScreen() + { + activity.showDialog(createHabitDialogFactory.create(), "editHabit"); + } + + public void showDeleteConfirmationScreen(ConfirmDeleteDialog.Callback callback) + { + activity.showDialog(confirmDeleteDialogFactory.create(callback)); + } + + public void showEditHabitScreen(Habit habit) + { + EditHabitDialog dialog = editHabitDialogFactory.create(habit); + activity.showDialog(dialog, "editHabit"); + } + + public void showFAQScreen() + { + Intent intent = intentFactory.viewFAQ(activity); + activity.startActivity(intent); + } + + public void showHabitScreen(@NonNull Habit habit) + { + Intent intent = intentFactory.startShowHabitActivity(activity, habit); + activity.startActivity(intent); + } + + public void showImportScreen() + { + File dir = dirFinder.findStorageDir(null); + + if (dir == null) + { + showMessage(R.string.could_not_import); + return; + } + + FilePickerDialog picker = filePickerDialogFactory.create(dir); + + if (controller != null) + picker.setListener(file -> controller.onImportData(file)); + activity.showDialog(picker.getDialog()); + } + + public void showIntroScreen() + { + Intent intent = intentFactory.startIntroActivity(activity); + activity.startActivity(intent); + } + + public void showSettingsScreen() + { + Intent intent = intentFactory.startSettingsActivity(activity); + activity.startActivityForResult(intent, 0); + } + + public void toggleNightMode() + { + themeSwitcher.toggleNightMode(); + activity.restartWithFade(); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsSelectionMenu.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsSelectionMenu.java new file mode 100644 index 000000000..9543fe352 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/ListHabitsSelectionMenu.java @@ -0,0 +1,206 @@ +/* + * 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.activities.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.activities.*; +import org.isoron.uhabits.activities.habits.list.controllers.*; +import org.isoron.uhabits.activities.habits.list.model.*; + +import java.util.*; + +import javax.inject.*; + +@ActivityScope +public class ListHabitsSelectionMenu extends BaseSelectionMenu + implements HabitCardListController.SelectionListener +{ + @NonNull + private final ListHabitsScreen screen; + + @NonNull + CommandRunner commandRunner; + + @NonNull + private final HabitCardListAdapter listAdapter; + + @Nullable + private HabitCardListController listController; + + @NonNull + private final HabitList habitList; + + @Inject + public ListHabitsSelectionMenu(@NonNull HabitList habitList, + @NonNull ListHabitsScreen screen, + @NonNull HabitCardListAdapter listAdapter, + @NonNull CommandRunner commandRunner) + { + this.habitList = habitList; + this.screen = screen; + this.listAdapter = listAdapter; + this.commandRunner = commandRunner; + } + + @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: + showEditScreen(firstHabit); + finish(); + return true; + + case R.id.action_archive_habit: + performArchive(selected); + finish(); + return true; + + case R.id.action_unarchive_habit: + performUnarchive(selected); + finish(); + return true; + + case R.id.action_delete: + performDelete(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 performArchive(@NonNull List selected) + { + commandRunner.execute(new ArchiveHabitsCommand(habitList, selected), + null); + } + + private void performDelete(@NonNull List selected) + { + screen.showDeleteConfirmationScreen(() -> { + listAdapter.performRemove(selected); + commandRunner.execute(new DeleteHabitsCommand(habitList, selected), + null); + finish(); + }); + } + + private void performUnarchive(@NonNull List selected) + { + commandRunner.execute(new UnarchiveHabitsCommand(habitList, selected), + null); + } + + private void showColorPicker(@NonNull List selected, + @NonNull Habit firstHabit) + { + screen.showColorPicker(firstHabit, color -> { + commandRunner.execute( + new ChangeHabitColorCommand(habitList, selected, color), null); + finish(); + }); + } + + private void showEditScreen(@NonNull Habit firstHabit) + { + screen.showEditHabitScreen(firstHabit); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/CheckmarkButtonController.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/CheckmarkButtonController.java new file mode 100644 index 000000000..382e12cda --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/CheckmarkButtonController.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.activities.habits.list.controllers; + +import android.support.annotation.*; + +import com.google.auto.factory.*; + +import org.isoron.uhabits.activities.habits.list.views.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.preferences.*; + +@AutoFactory +public class CheckmarkButtonController +{ + @Nullable + private CheckmarkButtonView view; + + @Nullable + private Listener listener; + + @NonNull + private final Preferences prefs; + + @NonNull + private Habit habit; + + private long timestamp; + + public CheckmarkButtonController(@Provided @NonNull Preferences prefs, + @NonNull Habit habit, + long timestamp) + { + this.habit = habit; + this.timestamp = timestamp; + this.prefs = prefs; + } + + 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/tasks/ToggleRepetitionTask.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/HabitCardController.java similarity index 54% rename from app/src/main/java/org/isoron/uhabits/tasks/ToggleRepetitionTask.java rename to app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/HabitCardController.java index 3b46ec95c..01e2b4643 100644 --- a/app/src/main/java/org/isoron/uhabits/tasks/ToggleRepetitionTask.java +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/HabitCardController.java @@ -17,41 +17,45 @@ * with this program. If not, see . */ -package org.isoron.uhabits.tasks; +package org.isoron.uhabits.activities.habits.list.controllers; + +import android.support.annotation.*; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.activities.habits.list.views.HabitCardView; -public class ToggleRepetitionTask extends BaseTask -{; - public interface Listener { - void onToggleRepetitionFinished(); - } +public class HabitCardController implements HabitCardView.Controller +{ + @Nullable + private HabitCardView view; + @Nullable private Listener listener; - private final Habit habit; - private final Long timestamp; - public ToggleRepetitionTask(Habit habit, Long timestamp) + @Override + public void onInvalidToggle() { - this.timestamp = timestamp; - this.habit = habit; + if (listener != null) listener.onInvalidToggle(); } @Override - protected void doInBackground() + public void onToggle(@NonNull Habit habit, long timestamp) { - habit.repetitions.toggle(timestamp); + if (view != null) view.triggerRipple(timestamp); + if (listener != null) listener.onToggle(habit, timestamp); } - @Override - protected void onPostExecute(Void aVoid) + public void setListener(@Nullable Listener listener) { - if(listener != null) listener.onToggleRepetitionFinished(); - super.onPostExecute(null); + this.listener = listener; } - public void setListener(Listener listener) + public void setView(@Nullable HabitCardView view) + { + this.view = view; + } + + public interface Listener extends CheckmarkButtonController.Listener { - this.listener = listener; } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/HabitCardListController.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/HabitCardListController.java new file mode 100644 index 000000000..d710c3572 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/HabitCardListController.java @@ -0,0 +1,300 @@ +/* + * 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.activities.habits.list.controllers; + +import android.support.annotation.*; + +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.activities.habits.list.model.*; +import org.isoron.uhabits.activities.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 HabitCardListView.Controller +{ + private final Mode NORMAL_MODE = new NormalMode(); + + private final Mode SELECTION_MODE = new SelectionMode(); + + @NonNull + private final HabitCardListAdapter adapter; + + @Nullable + private HabitListener habitListener; + + @Nullable + private SelectionListener selectionListener; + + @NonNull + private Mode activeMode; + + public HabitCardListController(@NonNull HabitCardListAdapter adapter) + { + this.adapter = adapter; + this.activeMode = new NormalMode(); + } + + /** + * Called when the user drags a habit and drops it somewhere. Note that the + * dragging operation is already complete. + * + * @param from the original position of the habit + * @param to the position where the habit was released + */ + @Override + public void drop(int from, int to) + { + if (from == to) return; + cancelSelection(); + + Habit habitFrom = adapter.getItem(from); + Habit habitTo = adapter.getItem(to); + adapter.performReorder(from, to); + + if (habitListener != null) + habitListener.onHabitReorder(habitFrom, habitTo); + } + + /** + * 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(); + 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/activities/habits/list/controllers/package-info.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/controllers/package-info.java new file mode 100644 index 000000000..c5b148812 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/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.activities.habits.list.ListHabitsActivity}. + */ +package org.isoron.uhabits.activities.habits.list.controllers; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HabitCardListAdapter.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HabitCardListAdapter.java new file mode 100644 index 000000000..71480dee6 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HabitCardListAdapter.java @@ -0,0 +1,276 @@ +/* + * 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.activities.habits.list.model; + +import android.support.annotation.*; +import android.support.v7.widget.*; +import android.view.*; + +import org.isoron.uhabits.activities.*; +import org.isoron.uhabits.activities.habits.list.*; +import org.isoron.uhabits.activities.habits.list.views.*; +import org.isoron.uhabits.models.*; + +import java.util.*; + +import javax.inject.*; + +/** + * 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. + */ +@ActivityScope +public class HabitCardListAdapter + extends RecyclerView.Adapter + implements HabitCardListCache.Listener +{ + @NonNull + private ModelObservable observable; + + @Nullable + private HabitCardListView listView; + + @NonNull + private final LinkedList selected; + + @NonNull + private final HabitCardListCache cache; + + @Inject + public HabitCardListAdapter(@NonNull HabitCardListCache cache) + { + this.selected = new LinkedList<>(); + this.observable = new ModelObservable(); + this.cache = cache; + + cache.setListener(this); + cache.setCheckmarkCount(ListHabitsRootView.MAX_CHECKMARK_COUNT); + + setHasStableIds(true); + } + + public void cancelRefresh() + { + cache.cancelTasks(); + } + + /** + * Sets all items as not selected. + */ + public void clearSelection() + { + selected.clear(); + notifyDataSetChanged(); + } + + /** + * 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 + */ + @Deprecated + @NonNull + public Habit getItem(int position) + { + return cache.getHabitByPosition(position); + } + + @Override + public int getItemCount() + { + return cache.getHabitCount(); + } + + @Override + public long getItemId(int position) + { + return getItem(position).getId(); + } + + @NonNull + public ModelObservable getObservable() + { + return observable; + } + + @NonNull + public List getSelected() + { + return new LinkedList<>(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 onBindViewHolder(@Nullable HabitCardViewHolder holder, + int position) + { + if (holder == null) return; + if (listView == null) return; + + Habit habit = cache.getHabitByPosition(position); + int score = cache.getScore(habit.getId()); + int checkmarks[] = cache.getCheckmarks(habit.getId()); + boolean selected = this.selected.contains(habit); + + listView.bindCardView(holder, habit, score, checkmarks, selected); + } + + @Override + public HabitCardViewHolder onCreateViewHolder(ViewGroup parent, + int viewType) + { + if (listView == null) return null; + View view = listView.createCardView(); + return new HabitCardViewHolder(view); + } + + /** + * Notify the adapter that it has been detached from a ListView. + */ + public void onDetached() + { + cache.onDetached(); + } + + @Override + public void onItemChanged(int position) + { + notifyItemChanged(position); + observable.notifyListeners(); + } + + @Override + public void onItemInserted(int position) + { + notifyItemInserted(position); + observable.notifyListeners(); + } + + @Override + public void onItemMoved(int fromPosition, int toPosition) + { + notifyItemMoved(fromPosition, toPosition); + observable.notifyListeners(); + } + + @Override + public void onItemRemoved(int position) + { + notifyItemRemoved(position); + observable.notifyListeners(); + } + + @Override + public void onRefreshFinished() + { + observable.notifyListeners(); + } + + /** + * Removes a list of habits from 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 habits list of habits to be removed + */ + public void performRemove(List habits) + { + for (Habit h : habits) + cache.remove(h.getId()); + } + + /** + * 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 performReorder(int from, int to) + { + cache.reorder(from, to); + } + + public void refresh() + { + cache.refreshAllHabits(); + } + + public void setFilter(HabitMatcher matcher) + { + cache.setFilter(matcher); + } + + /** + * 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; + } + + /** + * 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/activities/habits/list/model/HabitCardListCache.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HabitCardListCache.java new file mode 100644 index 000000000..56356ec3f --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HabitCardListCache.java @@ -0,0 +1,396 @@ +/* + * 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.activities.habits.list.model; + +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.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. + *

+ * Note that this class is singleton-scoped, therefore it is shared among all + * activities. + */ +@AppScope +public class HabitCardListCache implements CommandRunner.Listener +{ + private int checkmarkCount; + + private Task currentFetchTask; + + @NonNull + private Listener listener; + + @NonNull + private CacheData data; + + @NonNull + private HabitList allHabits; + + @NonNull + private HabitList filteredHabits; + + private final TaskRunner taskRunner; + + private final CommandRunner commandRunner; + + @Inject + public HabitCardListCache(@NonNull HabitList allHabits, + @NonNull CommandRunner commandRunner, + @NonNull TaskRunner taskRunner) + { + this.allHabits = allHabits; + this.commandRunner = commandRunner; + this.filteredHabits = allHabits; + this.taskRunner = taskRunner; + + this.listener = new Listener() {}; + data = new CacheData(); + } + + public void cancelTasks() + { + if (currentFetchTask != null) currentFetchTask.cancel(); + } + + 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.habits.get(position); + } + + public int getHabitCount() + { + return data.habits.size(); + } + + public int getScore(long habitId) + { + return data.scores.get(habitId); + } + + public void onAttached() + { + refreshAllHabits(); + commandRunner.addListener(this); + } + + @Override + public void onCommandExecuted(@NonNull Command command, + @Nullable Long refreshKey) + { + if (refreshKey == null) refreshAllHabits(); + else refreshHabit(refreshKey); + } + + public void onDetached() + { + commandRunner.removeListener(this); + } + + public void refreshAllHabits() + { + if (currentFetchTask != null) currentFetchTask.cancel(); + currentFetchTask = new RefreshTask(); + taskRunner.execute(currentFetchTask); + } + + public void refreshHabit(long id) + { + taskRunner.execute(new RefreshTask(id)); + } + + public void remove(@NonNull Long id) + { + Habit h = data.id_to_habit.get(id); + if (h == null) return; + + int position = data.habits.indexOf(h); + data.habits.remove(position); + data.id_to_habit.remove(id); + data.checkmarks.remove(id); + data.scores.remove(id); + + listener.onItemRemoved(position); + } + + public void reorder(int from, int to) + { + Habit fromHabit = data.habits.get(from); + data.habits.remove(from); + data.habits.add(to, fromHabit); + listener.onItemMoved(from, to); + } + + public void setCheckmarkCount(int checkmarkCount) + { + this.checkmarkCount = checkmarkCount; + } + + public void setFilter(HabitMatcher matcher) + { + filteredHabits = allHabits.getFiltered(matcher); + } + + public void setListener(@NonNull 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 + { + default void onItemChanged(int position) {} + + default void onItemInserted(int position) {} + + default void onItemMoved(int oldPosition, int newPosition) {} + + default void onItemRemoved(int position) {} + + default void onRefreshFinished() {} + } + + private class CacheData + { + @NonNull + public HashMap id_to_habit; + + @NonNull + public List habits; + + @NonNull + public HashMap checkmarks; + + @NonNull + public HashMap scores; + + /** + * Creates a new CacheData without any content. + */ + public CacheData() + { + id_to_habit = new HashMap<>(); + habits = new LinkedList<>(); + checkmarks = new HashMap<>(); + scores = new HashMap<>(); + } + + public void copyCheckmarksFrom(@NonNull CacheData oldData) + { + int[] empty = new int[checkmarkCount]; + + for (Long id : id_to_habit.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 : id_to_habit.keySet()) + { + if (oldData.scores.containsKey(id)) + scores.put(id, oldData.scores.get(id)); + else scores.put(id, 0); + } + } + + public void fetchHabits() + { + for (Habit h : filteredHabits) + { + habits.add(h); + id_to_habit.put(h.getId(), h); + } + } + } + + private class RefreshTask implements Task + { + @NonNull + private CacheData newData; + + @Nullable + private Long targetId; + + private boolean isCancelled; + + private TaskRunner runner; + + public RefreshTask() + { + newData = new CacheData(); + targetId = null; + isCancelled = false; + } + + public RefreshTask(long targetId) + { + newData = new CacheData(); + this.targetId = targetId; + } + + @Override + public void cancel() + { + isCancelled = true; + } + + @Override + public void doInBackground() + { + newData.fetchHabits(); + newData.copyScoresFrom(data); + newData.copyCheckmarksFrom(data); + + long day = DateUtils.millisecondsInOneDay; + long dateTo = DateUtils.getStartOfDay(DateUtils.getLocalTime()); + long dateFrom = dateTo - (checkmarkCount - 1) * day; + + runner.publishProgress(this, -1); + + for (int position = 0; position < newData.habits.size(); position++) + { + if (isCancelled) return; + + Habit habit = newData.habits.get(position); + Long id = habit.getId(); + if (targetId != null && !targetId.equals(id)) continue; + + newData.scores.put(id, habit.getScores().getTodayValue()); + newData.checkmarks.put(id, + habit.getCheckmarks().getValues(dateFrom, dateTo)); + + runner.publishProgress(this, position); + } + } + + @Override + public void onAttached(@NonNull TaskRunner runner) + { + this.runner = runner; + } + + @Override + public void onPostExecute() + { + currentFetchTask = null; + listener.onRefreshFinished(); + } + + @Override + public void onProgressUpdate(int currentPosition) + { + if (currentPosition < 0) processRemovedHabits(); + else processPosition(currentPosition); + } + + private void performInsert(Habit habit, int position) + { + Long id = habit.getId(); + data.habits.add(position, habit); + data.id_to_habit.put(id, habit); + data.scores.put(id, newData.scores.get(id)); + data.checkmarks.put(id, newData.checkmarks.get(id)); + listener.onItemInserted(position); + } + + private void performMove(Habit habit, int fromPosition, int toPosition) + { + data.habits.remove(fromPosition); + data.habits.add(toPosition, habit); + listener.onItemMoved(fromPosition, toPosition); + } + + private void performUpdate(Long id, int position) + { + Integer oldScore = data.scores.get(id); + int[] oldCheckmarks = data.checkmarks.get(id); + + Integer newScore = newData.scores.get(id); + int[] newCheckmarks = newData.checkmarks.get(id); + + boolean unchanged = true; + if (!oldScore.equals(newScore)) unchanged = false; + if (!Arrays.equals(oldCheckmarks, newCheckmarks)) unchanged = false; + if (unchanged) return; + + data.scores.put(id, newScore); + data.checkmarks.put(id, newCheckmarks); + listener.onItemChanged(position); + } + + private void processPosition(int currentPosition) + { + Habit habit = newData.habits.get(currentPosition); + Long id = habit.getId(); + + int prevPosition = data.habits.indexOf(habit); + + if (prevPosition < 0) performInsert(habit, currentPosition); + else if (prevPosition == currentPosition) + performUpdate(id, currentPosition); + else performMove(habit, prevPosition, currentPosition); + } + + private void processRemovedHabits() + { + Set before = data.id_to_habit.keySet(); + Set after = newData.id_to_habit.keySet(); + + Set removed = new TreeSet<>(before); + removed.removeAll(after); + + for (Long id : removed) remove(id); + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HabitCardViewHolder.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HabitCardViewHolder.java new file mode 100644 index 000000000..494801e48 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HabitCardViewHolder.java @@ -0,0 +1,31 @@ +/* + * 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.activities.habits.list.model; + +import android.support.v7.widget.*; +import android.view.*; + +public class HabitCardViewHolder extends RecyclerView.ViewHolder +{ + public HabitCardViewHolder(View itemView) + { + super(itemView); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HintList.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/HintList.java new file mode 100644 index 000000000..8fa0d6274 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/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.activities.habits.list.model; + +import android.support.annotation.*; + +import com.google.auto.factory.*; + +import org.isoron.uhabits.preferences.*; +import org.isoron.uhabits.utils.*; + +/** + * Provides a list of hints to be shown at the application startup, and takes + * care of deciding when a new hint should be shown. + */ +@AutoFactory +public class HintList +{ + private final Preferences prefs; + + @NonNull + private final String[] hints; + + /** + * Constructs a new list containing the provided hints. + * + * @param hints initial list of hints + */ + public HintList(@Provided @NonNull Preferences prefs, + @NonNull String hints[]) + { + this.prefs = prefs; + this.hints = hints; + } + + /** + * 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/activities/habits/list/model/package-info.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/model/package-info.java new file mode 100644 index 000000000..755ffcaa1 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/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.activities.habits.list.ListHabitsActivity}. + */ +package org.isoron.uhabits.activities.habits.list.model; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/package-info.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/package-info.java new file mode 100644 index 000000000..1a39e29de --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/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.activities.habits.list; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonView.java new file mode 100644 index 000000000..1e1f98a38 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkButtonView.java @@ -0,0 +1,111 @@ +/* + * 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.activities.habits.list.views; + +import android.content.*; +import android.view.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.habits.list.controllers.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; + +public class CheckmarkButtonView extends TextView +{ + private int color; + + private int value; + + private StyledResources res; + + 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; + updateText(); + } + + public void toggle() + { + value = (value == Checkmark.CHECKED_EXPLICITLY ? Checkmark.UNCHECKED : + Checkmark.CHECKED_EXPLICITLY); + + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + updateText(); + } + + private void init() + { + res = new StyledResources(getContext()); + + setWillNotDraw(false); + setHapticFeedbackEnabled(false); + + setMinHeight( + getResources().getDimensionPixelSize(R.dimen.checkmarkHeight)); + setMinWidth( + getResources().getDimensionPixelSize(R.dimen.checkmarkWidth)); + + setFocusable(false); + setGravity(Gravity.CENTER); + setTypeface(InterfaceUtils.getFontAwesome(getContext())); + } + + private void updateText() + { + int lowContrastColor = res.getColor(R.attr.lowContrastTextColor); + + if (value == Checkmark.CHECKED_EXPLICITLY) + { + setText(R.string.fa_check); + setTextColor(color); + } + + if (value == Checkmark.CHECKED_IMPLICITLY) + { + setText(R.string.fa_check); + setTextColor(lowContrastColor); + } + + if (value == Checkmark.UNCHECKED) + { + setText(R.string.fa_times); + setTextColor(lowContrastColor); + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelView.java new file mode 100644 index 000000000..e5b47800c --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/CheckmarkPanelView.java @@ -0,0 +1,208 @@ +/* + * 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.activities.habits.list.views; + +import android.content.*; +import android.support.annotation.*; +import android.util.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.habits.list.*; +import org.isoron.uhabits.activities.habits.list.controllers.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.preferences.*; +import org.isoron.uhabits.utils.*; + +import static android.view.View.MeasureSpec.*; + +public class CheckmarkPanelView extends LinearLayout implements Preferences.Listener +{ + private static final int CHECKMARK_LEFT_TO_RIGHT = 0; + + private static final int CHECKMARK_RIGHT_TO_LEFT = 1; + + @Nullable + private 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 CheckmarkButtonView indexToButton(int i) + { + int position = i; + + if (getCheckmarkOrder() == CHECKMARK_RIGHT_TO_LEFT) + position = nButtons - i - 1; + + 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; + setupCheckmarkButtons(); + } + + public void setHabit(@NonNull Habit habit) + { + this.habit = habit; + setupCheckmarkButtons(); + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) + { + float buttonWidth = getResources().getDimension(R.dimen.checkmarkWidth); + float buttonHeight = + getResources().getDimension(R.dimen.checkmarkHeight); + + float width = buttonWidth * nButtons; + + widthSpec = makeMeasureSpec((int) width, EXACTLY); + heightSpec = makeMeasureSpec((int) buttonHeight, EXACTLY); + + super.onMeasure(widthSpec, heightSpec); + } + + private void addCheckmarkButtons() + { + removeAllViews(); + + for (int i = 0; i < nButtons; i++) + addView(new CheckmarkButtonView(getContext())); + } + + private int getCheckmarkOrder() + { + if (prefs == null) return CHECKMARK_LEFT_TO_RIGHT; + return prefs.shouldReverseCheckmarks() ? CHECKMARK_RIGHT_TO_LEFT : + CHECKMARK_LEFT_TO_RIGHT; + } + + private void init() + { + Context appContext = getContext().getApplicationContext(); + if(appContext instanceof HabitsApplication) + { + HabitsApplication app = (HabitsApplication) appContext; + prefs = app.getComponent().getPreferences(); + } + + setWillNotDraw(false); + } + + private void setupButtonControllers(long timestamp, + CheckmarkButtonView buttonView) + { + if (controller == null) return; + if (!(getContext() instanceof ListHabitsActivity)) return; + + ListHabitsActivity activity = (ListHabitsActivity) getContext(); + CheckmarkButtonControllerFactory buttonControllerFactory = activity + .getListHabitsComponent() + .getCheckmarkButtonControllerFactory(); + + CheckmarkButtonController buttonController = + buttonControllerFactory.create(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; + } + } + + @Override + protected void onAttachedToWindow() + { + super.onAttachedToWindow(); + if(prefs != null) prefs.addListener(this); + } + + @Override + protected void onDetachedFromWindow() + { + if(prefs != null) prefs.removeListener(this); + super.onDetachedFromWindow(); + } + + @Override + public void onCheckmarkOrderChanged() + { + setupCheckmarkButtons(); + } + + public interface Controller extends CheckmarkButtonController.Listener + { + + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.java new file mode 100644 index 000000000..b193da2c9 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListView.java @@ -0,0 +1,222 @@ +/* + * 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.activities.habits.list.views; + +import android.content.*; +import android.support.annotation.*; +import android.support.v7.widget.*; +import android.support.v7.widget.helper.*; +import android.util.*; +import android.view.*; + +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.activities.habits.list.controllers.*; +import org.isoron.uhabits.activities.habits.list.model.*; + +import java.util.*; + +public class HabitCardListView extends RecyclerView +{ + @Nullable + private HabitCardListAdapter adapter; + + @Nullable + private Controller controller; + + private final ItemTouchHelper touchHelper; + + private int checkmarkCount; + + public HabitCardListView(Context context, AttributeSet attrs) + { + super(context, attrs); + setLongClickable(true); + setHasFixedSize(true); + setLayoutManager(new LinearLayoutManager(getContext())); + + TouchHelperCallback callback = new TouchHelperCallback(); + touchHelper = new ItemTouchHelper(callback); + touchHelper.attachToRecyclerView(this); + } + + /** + * Builds a new HabitCardView to be eventually added to this list, + * containing the given data. + * + * @param holder the ViewHolder containing the HabitCardView that should + * be built + * @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 bindCardView(@NonNull HabitCardViewHolder holder, + @NonNull Habit habit, + int score, + int[] checkmarks, + boolean selected) + { + int visibleCheckmarks[] = + Arrays.copyOfRange(checkmarks, 0, checkmarkCount); + + HabitCardView cardView = (HabitCardView) holder.itemView; + cardView.setHabit(habit); + cardView.setSelected(selected); + cardView.setCheckmarkValues(visibleCheckmarks); + cardView.setScore(score); + if (controller != null) setupCardViewController(holder); + return cardView; + } + + public View createCardView() + { + return new HabitCardView(getContext()); + } + + @Override + public void setAdapter(RecyclerView.Adapter adapter) + { + this.adapter = (HabitCardListAdapter) adapter; + super.setAdapter(adapter); + } + + public void setCheckmarkCount(int checkmarkCount) + { + this.checkmarkCount = checkmarkCount; + } + + public void setController(@Nullable Controller controller) + { + this.controller = controller; + } + + @Override + protected void onAttachedToWindow() + { + super.onAttachedToWindow(); + if (adapter != null) adapter.onAttached(); + } + + @Override + protected void onDetachedFromWindow() + { + if (adapter != null) adapter.onDetached(); + super.onDetachedFromWindow(); + } + + protected void setupCardViewController(@NonNull HabitCardViewHolder holder) + { + HabitCardView cardView = (HabitCardView) holder.itemView; + HabitCardController cardController = new HabitCardController(); + cardController.setListener(controller); + cardView.setController(cardController); + cardController.setView(cardView); + + GestureDetector detector = new GestureDetector(getContext(), + new CardViewGestureDetector(holder)); + + cardView.setOnTouchListener((v, ev) -> { + detector.onTouchEvent(ev); + return true; + }); + } + + public interface Controller + extends CheckmarkButtonController.Listener, HabitCardController.Listener + { + void drop(int from, int to); + + void onItemClick(int pos); + + void onItemLongClick(int pos); + + void startDrag(int position); + } + + private class CardViewGestureDetector + extends GestureDetector.SimpleOnGestureListener + { + @NonNull + private final HabitCardViewHolder holder; + + public CardViewGestureDetector(@NonNull HabitCardViewHolder holder) + { + this.holder = holder; + } + + @Override + public void onLongPress(MotionEvent e) + { + int position = holder.getAdapterPosition(); + if (controller != null) controller.onItemLongClick(position); + touchHelper.startDrag(holder); + } + + @Override + public boolean onSingleTapUp(MotionEvent e) + { + int position = holder.getAdapterPosition(); + if (controller != null) controller.onItemClick(position); + return true; + } + } + + class TouchHelperCallback extends ItemTouchHelper.Callback + { + @Override + public int getMovementFlags(RecyclerView recyclerView, + ViewHolder viewHolder) + { + int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN; + int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END; + return makeMovementFlags(dragFlags, swipeFlags); + } + + @Override + public boolean isItemViewSwipeEnabled() + { + return false; + } + + @Override + public boolean isLongPressDragEnabled() + { + return false; + } + + @Override + public boolean onMove(RecyclerView recyclerView, + ViewHolder from, + ViewHolder to) + { + if (controller == null) return false; + controller.drop(from.getAdapterPosition(), to.getAdapterPosition()); + return true; + } + + @Override + public void onSwiped(ViewHolder viewHolder, int direction) + { + // NOP + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.java new file mode 100644 index 000000000..abcebb79f --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardView.java @@ -0,0 +1,247 @@ +/* + * 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.activities.habits.list.views; + +import android.annotation.*; +import android.content.*; +import android.graphics.drawable.*; +import android.os.*; +import android.support.annotation.*; +import android.util.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.common.views.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; + +import java.util.*; + +import butterknife.*; + +import static android.os.Build.VERSION.*; +import static android.os.Build.VERSION_CODES.*; + +public class HabitCardView extends FrameLayout + implements ModelObservable.Listener +{ + + private static final String EDIT_MODE_HABITS[] = { + "Wake up early", + "Wash dishes", + "Exercise", + "Meditate", + "Play guitar", + "Wash clothes", + "Get a haircut" + }; + + @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(); + + private StyledResources res; + + @Nullable + private Habit habit; + + public HabitCardView(Context context) + { + super(context); + init(); + } + + public HabitCardView(Context context, AttributeSet attrs) + { + super(context, attrs); + init(); + } + + @Override + public void onModelChange() + { + new Handler(Looper.getMainLooper()).post(() -> refresh()); + } + + public void setCheckmarkValues(int checkmarks[]) + { + checkmarkPanel.setCheckmarkValues(checkmarks); + postInvalidate(); + } + + public void setController(Controller controller) + { + checkmarkPanel.setController(null); + if (controller == null) return; + checkmarkPanel.setController(controller); + } + + public void setHabit(@NonNull Habit habit) + { + if (this.habit != null) detachFromHabit(); + + this.habit = habit; + checkmarkPanel.setHabit(habit); + refresh(); + + attachToHabit(); + 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.indexToButton(offset); + + float y = button.getHeight() / 2.0f; + float x = checkmarkPanel.getX() + button.getX() + button.getWidth() / 2; + triggerRipple(x, y); + } + + @Override + protected void onDetachedFromWindow() + { + if (habit != null) detachFromHabit(); + super.onDetachedFromWindow(); + } + + private void attachToHabit() + { + if (habit != null) habit.getObservable().addListener(this); + } + + private void detachFromHabit() + { + if (habit != null) habit.getObservable().removeListener(this); + } + + private int getActiveColor(Habit habit) + { + int mediumContrastColor = res.getColor(R.attr.mediumContrastTextColor); + int activeColor = ColorUtils.getColor(context, habit.getColor()); + if (habit.isArchived()) activeColor = mediumContrastColor; + + return activeColor; + } + + private void init() + { + setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, + LayoutParams.WRAP_CONTENT)); + + res = new StyledResources(getContext()); + + inflate(context, R.layout.list_habits_card, this); + ButterKnife.bind(this); + + innerFrame.setOnTouchListener((v, event) -> { + if (SDK_INT >= LOLLIPOP) + v.getBackground().setHotspot(event.getX(), event.getY()); + return false; + }); + + if (isInEditMode()) initEditMode(); + } + + @SuppressLint("SetTextI18n") + private void initEditMode() + { + Random rand = new Random(); + int color = ColorUtils.getAndroidTestColor(rand.nextInt(10)); + int[] values = new int[5]; + for (int i = 0; i < 5; i++) values[i] = rand.nextInt(3); + + label.setText(EDIT_MODE_HABITS[rand.nextInt(EDIT_MODE_HABITS.length)]); + label.setTextColor(color); + scoreRing.setColor(color); + scoreRing.setPercentage(rand.nextFloat()); + checkmarkPanel.setColor(color); + checkmarkPanel.setCheckmarkValues(values); + } + + private void refresh() + { + int color = getActiveColor(habit); + label.setText(habit.getName()); + label.setTextColor(color); + scoreRing.setColor(color); + checkmarkPanel.setColor(color); + postInvalidate(); + } + + private void triggerRipple(final float x, final float y) + { + final Drawable background = innerFrame.getBackground(); + if (SDK_INT >= LOLLIPOP) 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 (SDK_INT >= LOLLIPOP) + { + if (isSelected) + innerFrame.setBackgroundResource(R.drawable.selected_box); + else innerFrame.setBackgroundResource(R.drawable.ripple); + } + else + { + Drawable background; + + if (isSelected) + background = res.getDrawable(R.attr.selectedBackground); + else background = res.getDrawable(R.attr.cardBackground); + + innerFrame.setBackgroundDrawable(background); + } + } + + public interface Controller extends CheckmarkPanelView.Controller {} +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HeaderView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HeaderView.java new file mode 100644 index 000000000..70ed6ec91 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HeaderView.java @@ -0,0 +1,113 @@ +/* + * 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.activities.habits.list.views; + +import android.content.*; +import android.support.annotation.*; +import android.util.*; +import android.view.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.preferences.*; +import org.isoron.uhabits.utils.*; + +import java.util.*; + +public class HeaderView extends LinearLayout implements Preferences.Listener +{ + private final Context context; + + private int buttonCount; + + @Nullable + private Preferences prefs; + + public HeaderView(Context context, AttributeSet attrs) + { + super(context, attrs); + this.context = context; + + if (isInEditMode()) + { + setButtonCount(5); + } + + Context appContext = context.getApplicationContext(); + if (appContext instanceof HabitsApplication) + { + HabitsApplication app = (HabitsApplication) appContext; + prefs = app.getComponent().getPreferences(); + } + } + + @Override + public void onCheckmarkOrderChanged() + { + createButtons(); + } + + public void setButtonCount(int buttonCount) + { + this.buttonCount = buttonCount; + createButtons(); + } + + @Override + protected void onAttachedToWindow() + { + super.onAttachedToWindow(); + if (prefs != null) prefs.addListener(this); + } + + @Override + protected void onDetachedFromWindow() + { + if (prefs != null) prefs.removeListener(this); + super.onDetachedFromWindow(); + } + + private void createButtons() + { + removeAllViews(); + GregorianCalendar day = DateUtils.getStartOfTodayCalendar(); + + for (int i = 0; i < buttonCount; i++) + addView( + inflate(context, R.layout.list_habits_header_checkmark, null)); + + for (int i = 0; i < getChildCount(); i++) + { + int position = i; + if (shouldReverseCheckmarks()) position = getChildCount() - i - 1; + + View button = getChildAt(position); + TextView label = (TextView) button.findViewById(R.id.tvCheck); + label.setText(DateUtils.formatHeaderDate(day)); + day.add(GregorianCalendar.DAY_OF_MONTH, -1); + } + } + + private boolean shouldReverseCheckmarks() + { + if (prefs == null) return false; + return prefs.shouldReverseCheckmarks(); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HintView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/list/views/HintView.java new file mode 100644 index 000000000..f083b6fd2 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/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.activities.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.activities.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/activities/habits/show/ShowHabitActivity.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitActivity.java new file mode 100644 index 000000000..7d9a90eef --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitActivity.java @@ -0,0 +1,74 @@ +/* + * 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.activities.habits.show; + +import android.content.*; +import android.net.*; +import android.os.*; +import android.support.annotation.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.*; +import org.isoron.uhabits.models.*; + +/** + * 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 +{ + private HabitList habits; + + @Override + protected void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + HabitsApplication app = (HabitsApplication) getApplicationContext(); + habits = app.getComponent().getHabitList(); + Habit habit = getHabitFromIntent(); + + ShowHabitComponent component = DaggerShowHabitComponent + .builder() + .appComponent(app.getComponent()) + .showHabitModule(new ShowHabitModule(this, habit)) + .build(); + + ShowHabitRootView rootView = component.getRootView(); + ShowHabitScreen screen = component.getScreen(); + + setScreen(screen); + screen.setMenu(component.getMenu()); + screen.setController(component.getController()); + rootView.setController(component.getController()); + + screen.reattachDialogs(); + } + + @NonNull + private Habit getHabitFromIntent() + { + Uri data = getIntent().getData(); + Habit habit = habits.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/activities/habits/show/ShowHabitComponent.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitComponent.java new file mode 100644 index 000000000..e1974c23e --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitComponent.java @@ -0,0 +1,39 @@ +/* + * 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.activities.habits.show; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.*; + +import dagger.*; + +@ActivityScope +@Component(modules = { ShowHabitModule.class }, + dependencies = { AppComponent.class }) +public interface ShowHabitComponent +{ + ShowHabitController getController(); + + ShowHabitsMenu getMenu(); + + ShowHabitRootView getRootView(); + + ShowHabitScreen getScreen(); +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitController.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitController.java new file mode 100644 index 000000000..c4a463199 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitController.java @@ -0,0 +1,72 @@ +/* + * 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.activities.habits.show; + +import android.support.annotation.*; + +import org.isoron.uhabits.activities.*; +import org.isoron.uhabits.activities.common.dialogs.*; +import org.isoron.uhabits.commands.*; +import org.isoron.uhabits.models.*; + +import javax.inject.*; + +@ActivityScope +public class ShowHabitController + implements ShowHabitRootView.Controller, HistoryEditorDialog.Controller +{ + @NonNull + private final ShowHabitScreen screen; + + @NonNull + private final Habit habit; + + @NonNull + private final CommandRunner commandRunner; + + @Inject + public ShowHabitController(@NonNull ShowHabitScreen screen, + @NonNull CommandRunner commandRunner, + @NonNull Habit habit) + { + this.screen = screen; + this.habit = habit; + this.commandRunner = commandRunner; + } + + @Override + public void onEditHistoryButtonClick() + { + screen.showEditHistoryDialog(); + } + + @Override + public void onToggleCheckmark(long timestamp) + { + commandRunner.execute(new ToggleRepetitionCommand(habit, timestamp), + null); + } + + @Override + public void onToolbarChanged() + { + screen.invalidateToolbar(); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitModule.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitModule.java new file mode 100644 index 000000000..bf4cbee25 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitModule.java @@ -0,0 +1,43 @@ +/* + * 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.activities.habits.show; + +import org.isoron.uhabits.activities.*; +import org.isoron.uhabits.models.*; + +import dagger.*; + +@Module +public class ShowHabitModule extends ActivityModule +{ + private final Habit habit; + + public ShowHabitModule(BaseActivity activity, Habit habit) + { + super(activity); + this.habit = habit; + } + + @Provides + public Habit getHabit() + { + return habit; + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitRootView.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitRootView.java new file mode 100644 index 000000000..ffeb3ee42 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitRootView.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.activities.habits.show; + +import android.content.*; +import android.os.*; +import android.support.annotation.*; +import android.support.v7.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.*; +import org.isoron.uhabits.activities.habits.show.views.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; + +import javax.inject.*; + +import butterknife.*; + +@ActivityScope +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.scoreCard) + ScoreCard scoreCard; + + @BindView(R.id.historyCard) + HistoryCard historyCard; + + @BindView(R.id.toolbar) + Toolbar toolbar; + + @NonNull + private Controller controller; + + @Inject + public ShowHabitRootView(@NonNull @ActivityContext 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; + } + + @Override + public int getToolbarColor() + { + StyledResources res = new StyledResources(getContext()); + if (!res.getBoolean(R.attr.useHabitColorAsPrimary)) + return super.getToolbarColor(); + + return ColorUtils.getColor(getContext(), habit.getColor()); + } + + @Override + public void onModelChange() + { + new Handler(Looper.getMainLooper()).post(() -> { + toolbar.setTitle(habit.getName()); + }); + + controller.onToolbarChanged(); + } + + public void setController(@NonNull Controller controller) + { + this.controller = controller; + historyCard.setController(controller); + } + + @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/activities/habits/show/ShowHabitScreen.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitScreen.java new file mode 100644 index 000000000..1c238c02e --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitScreen.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.activities.habits.show; + +import android.support.annotation.*; + +import org.isoron.uhabits.activities.*; +import org.isoron.uhabits.activities.common.dialogs.*; +import org.isoron.uhabits.activities.habits.edit.*; +import org.isoron.uhabits.models.*; + +import javax.inject.*; + +@ActivityScope +public class ShowHabitScreen extends BaseScreen +{ + @NonNull + private final Habit habit; + + @Nullable + private ShowHabitController controller; + + @NonNull + private final EditHabitDialogFactory editHabitDialogFactory; + + @Inject + public ShowHabitScreen(@NonNull BaseActivity activity, + @NonNull Habit habit, + @NonNull ShowHabitRootView view, + @NonNull EditHabitDialogFactory editHabitDialogFactory) + { + super(activity); + setRootView(view); + this.editHabitDialogFactory = editHabitDialogFactory; + this.habit = habit; + } + + public void setController(@NonNull ShowHabitController controller) + { + this.controller = controller; + } + + public void reattachDialogs() + { + if(controller == null) throw new IllegalStateException(); + + HistoryEditorDialog historyEditor = (HistoryEditorDialog) activity + .getSupportFragmentManager() + .findFragmentByTag("historyEditor"); + + if (historyEditor != null) + historyEditor.setController(controller); + } + + public void showEditHabitDialog() + { + EditHabitDialog dialog = editHabitDialogFactory.create(habit); + activity.showDialog(dialog, "editHabit"); + } + + public void showEditHistoryDialog() + { + if(controller == null) throw new IllegalStateException(); + + 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/activities/habits/show/ShowHabitsMenu.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitsMenu.java new file mode 100644 index 000000000..92e5af582 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/ShowHabitsMenu.java @@ -0,0 +1,63 @@ +/* + * 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.activities.habits.show; + +import android.support.annotation.*; +import android.view.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.*; + +import javax.inject.*; + +@ActivityScope +public class ShowHabitsMenu extends BaseMenu +{ + @NonNull + private final ShowHabitScreen screen; + + @Inject + 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/activities/habits/show/package-info.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/package-info.java new file mode 100644 index 000000000..ca132a6c7 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/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.activities.habits.show; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/FrequencyCard.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/FrequencyCard.java new file mode 100644 index 000000000..f9438efe3 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/FrequencyCard.java @@ -0,0 +1,109 @@ +/* + * 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.activities.habits.show.views; + +import android.content.*; +import android.support.annotation.*; +import android.util.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.common.views.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.tasks.*; +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; + + @Nullable + private TaskRunner taskRunner; + + public FrequencyCard(Context context) + { + super(context); + init(); + } + + public FrequencyCard(Context context, AttributeSet attrs) + { + super(context, attrs); + init(); + } + + @Override + protected void refreshData() + { + if(taskRunner == null) return; + taskRunner.execute(new RefreshTask()); + } + + private void init() + { + inflate(getContext(), R.layout.show_habit_frequency, this); + ButterKnife.bind(this); + + Context appContext = getContext().getApplicationContext(); + if(appContext instanceof HabitsApplication) + { + HabitsApplication app = (HabitsApplication) appContext; + taskRunner = app.getComponent().getTaskRunner(); + } + + if (isInEditMode()) initEditMode(); + } + + private void initEditMode() + { + int color = ColorUtils.getAndroidTestColor(1); + title.setTextColor(color); + chart.setColor(color); + chart.populateWithRandomData(); + } + + private class RefreshTask implements Task + { + @Override + public void doInBackground() + { + RepetitionList reps = getHabit().getRepetitions(); + HashMap frequency = reps.getWeekdayFrequency(); + chart.setFrequency(frequency); + } + + @Override + public void onPreExecute() + { + int paletteColor = getHabit().getColor(); + int color = ColorUtils.getColor(getContext(), paletteColor); + title.setTextColor(color); + chart.setColor(color); + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/HabitCard.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/HabitCard.java new file mode 100644 index 000000000..0c31112d6 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/HabitCard.java @@ -0,0 +1,103 @@ +/* + * 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.activities.habits.show.views; + +import android.content.*; +import android.support.annotation.*; +import android.util.*; +import android.widget.*; + +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.models.memory.*; + +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 MemoryModelFactory().buildHabit(); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/HistoryCard.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/HistoryCard.java new file mode 100644 index 000000000..4e060b990 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/HistoryCard.java @@ -0,0 +1,130 @@ +/* + * 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.activities.habits.show.views; + +import android.content.*; +import android.support.annotation.*; +import android.util.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.common.views.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.tasks.*; +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; + + @Nullable + private TaskRunner taskRunner; + + 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() + { + if(taskRunner == null) return; + taskRunner.execute(new RefreshTask(getHabit())); + } + + private void init() + { + inflate(getContext(), R.layout.show_habit_history, this); + ButterKnife.bind(this); + + Context appContext = getContext().getApplicationContext(); + if (appContext instanceof HabitsApplication) + { + HabitsApplication app = (HabitsApplication) appContext; + taskRunner = app.getComponent().getTaskRunner(); + } + + 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() {} + } + + private class RefreshTask implements Task + { + private final Habit habit; + + public RefreshTask(Habit habit) {this.habit = habit;} + + @Override + public void doInBackground() + { + int checkmarks[] = habit.getCheckmarks().getAllValues(); + chart.setCheckmarks(checkmarks); + } + + @Override + public void onPreExecute() + { + int color = ColorUtils.getColor(getContext(), habit.getColor()); + title.setTextColor(color); + chart.setColor(color); + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/OverviewCard.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/OverviewCard.java new file mode 100644 index 000000000..24eebec29 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/OverviewCard.java @@ -0,0 +1,189 @@ +/* + * 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.activities.habits.show.views; + +import android.content.*; +import android.support.annotation.*; +import android.util.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.common.views.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.tasks.*; +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.totalCountLabel) + TextView totalCountLabel; + + @BindView(R.id.title) + TextView title; + + private int color; + + @Nullable + private TaskRunner taskRunner; + + public OverviewCard(Context context) + { + super(context); + init(); + } + + public OverviewCard(Context context, AttributeSet attrs) + { + super(context, attrs); + init(); + } + + @Override + protected void refreshData() + { + if(taskRunner == null) return; + taskRunner.execute(new RefreshTask()); + } + + private String formatPercentageDiff(float percentageDiff) + { + return String.format("%s%.0f%%", (percentageDiff >= 0 ? "+" : "\u2212"), + Math.abs(percentageDiff) * 100); + } + + private void init() + { + Context appContext = getContext().getApplicationContext(); + if (appContext instanceof HabitsApplication) + { + HabitsApplication app = (HabitsApplication) appContext; + taskRunner = app.getComponent().getTaskRunner(); + } + + 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)); + totalCountLabel.setText(String.valueOf(cache.totalCount)); + + StyledResources res = new StyledResources(getContext()); + int inactiveColor = res.getColor(R.attr.mediumContrastTextColor); + + monthDiffLabel.setTextColor(monthDiff >= 0 ? color : inactiveColor); + yearDiffLabel.setTextColor(yearDiff >= 0 ? color : inactiveColor); + totalCountLabel.setTextColor(yearDiff >= 0 ? color : inactiveColor); + + postInvalidate(); + } + + private class Cache + { + public float todayScore; + + public float lastMonthScore; + + public float lastYearScore; + + public long totalCount; + } + + private class RefreshTask implements Task + { + @Override + public void doInBackground() + { + Habit habit = getHabit(); + + ScoreList scores = habit.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); + cache.totalCount = habit.getRepetitions().getTotalCount(); + } + + @Override + public void onPostExecute() + { + refreshScore(); + } + + @Override + public void onPreExecute() + { + color = ColorUtils.getColor(getContext(), getHabit().getColor()); + refreshColors(); + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/ScoreCard.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/ScoreCard.java new file mode 100644 index 000000000..f228c00dd --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/ScoreCard.java @@ -0,0 +1,165 @@ +/* + * 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.activities.habits.show.views; + +import android.content.*; +import android.support.annotation.*; +import android.util.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.common.views.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.preferences.*; +import org.isoron.uhabits.tasks.*; +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; + + @Nullable + private TaskRunner taskRunner; + + @Nullable + private Preferences prefs; + + 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 app = + (HabitsApplication) getContext().getApplicationContext(); + app.getComponent().getWidgetUpdater().updateWidgets(); + refreshData(); + } + + @Override + protected void refreshData() + { + if(taskRunner == null) return; + taskRunner.execute(new RefreshTask()); + } + + private int getDefaultSpinnerPosition() + { + if(prefs == null) return 0; + return prefs.getDefaultScoreSpinnerPosition(); + } + + private void init() + { + Context appContext = getContext().getApplicationContext(); + if (appContext instanceof HabitsApplication) + { + HabitsApplication app = (HabitsApplication) appContext; + taskRunner = app.getComponent().getTaskRunner(); + prefs = app.getComponent().getPreferences(); + } + + 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(prefs == null) return; + prefs.setDefaultScoreSpinnerPosition(position); + bucketSize = BUCKET_SIZES[position]; + } + + private class RefreshTask implements Task + { + @Override + public void doInBackground() + { + List scores; + ScoreList scoreList = getHabit().getScores(); + + if (bucketSize == 1) scores = scoreList.toList(); + else scores = scoreList.groupBy(getTruncateField(bucketSize)); + + chart.setScores(scores); + chart.setBucketSize(bucketSize); + } + + @Override + public void onPreExecute() + { + int color = + ColorUtils.getColor(getContext(), getHabit().getColor()); + title.setTextColor(color); + chart.setColor(color); + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/StreakCard.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/StreakCard.java new file mode 100644 index 000000000..3389217fb --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/StreakCard.java @@ -0,0 +1,118 @@ +/* + * 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.activities.habits.show.views; + +import android.content.*; +import android.support.annotation.*; +import android.util.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.common.views.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.tasks.*; +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; + + @Nullable + private TaskRunner taskRunner; + + public StreakCard(Context context) + { + super(context); + init(); + } + + public StreakCard(Context context, AttributeSet attrs) + { + super(context, attrs); + init(); + } + + @Override + protected void refreshData() + { + if(taskRunner == null) return; + taskRunner.execute(new RefreshTask()); + } + + private void init() + { + Context appContext = getContext().getApplicationContext(); + if (appContext instanceof HabitsApplication) + { + HabitsApplication app = (HabitsApplication) appContext; + taskRunner = app.getComponent().getTaskRunner(); + } + + 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 implements Task + { + public List bestStreaks; + + @Override + public void doInBackground() + { + StreakList streaks = getHabit().getStreaks(); + bestStreaks = streaks.getBest(NUM_STREAKS); + } + + @Override + public void onPostExecute() + { + streakChart.setStreaks(bestStreaks); + } + + @Override + public void onPreExecute() + { + int color = + ColorUtils.getColor(getContext(), getHabit().getColor()); + title.setTextColor(color); + streakChart.setColor(color); + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/SubtitleCard.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/SubtitleCard.java new file mode 100644 index 000000000..4d033473e --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/SubtitleCard.java @@ -0,0 +1,114 @@ +/* + * 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.activities.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, 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() + { + inflate(getContext(), 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/activities/habits/show/views/package-info.java b/app/src/main/java/org/isoron/uhabits/activities/habits/show/views/package-info.java new file mode 100644 index 000000000..f9fc65e4b --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/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.activities.habits.show.ShowHabitActivity}. + */ +package org.isoron.uhabits.activities.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/activities/intro/IntroActivity.java similarity index 68% rename from app/src/main/java/org/isoron/uhabits/IntroActivity.java rename to app/src/main/java/org/isoron/uhabits/activities/intro/IntroActivity.java index e298d1c63..4fe80138c 100644 --- a/app/src/main/java/org/isoron/uhabits/IntroActivity.java +++ b/app/src/main/java/org/isoron/uhabits/activities/intro/IntroActivity.java @@ -17,14 +17,19 @@ * with this program. If not, see . */ -package org.isoron.uhabits; +package org.isoron.uhabits.activities.intro; -import android.graphics.Color; -import android.os.Bundle; +import android.graphics.*; +import android.os.*; -import com.github.paolorotolo.appintro.AppIntro2; -import com.github.paolorotolo.appintro.AppIntroFragment; +import com.github.paolorotolo.appintro.*; +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 +38,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/activities/intro/package-info.java b/app/src/main/java/org/isoron/uhabits/activities/intro/package-info.java new file mode 100644 index 000000000..4023d1b94 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/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.activities.intro; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/activities/package-info.java b/app/src/main/java/org/isoron/uhabits/activities/package-info.java new file mode 100644 index 000000000..abc95467c --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/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 activites. + */ +package org.isoron.uhabits.activities; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/ShowHabitActivity.java b/app/src/main/java/org/isoron/uhabits/activities/settings/SettingsActivity.java similarity index 51% rename from app/src/main/java/org/isoron/uhabits/ShowHabitActivity.java rename to app/src/main/java/org/isoron/uhabits/activities/settings/SettingsActivity.java index 8be54b586..914e7e714 100644 --- a/app/src/main/java/org/isoron/uhabits/ShowHabitActivity.java +++ b/app/src/main/java/org/isoron/uhabits/activities/settings/SettingsActivity.java @@ -17,48 +17,35 @@ * with this program. If not, see . */ -package org.isoron.uhabits; +package org.isoron.uhabits.activities.settings; -import android.content.ContentUris; -import android.net.Uri; -import android.os.Bundle; -import android.support.v7.app.ActionBar; +import android.os.*; -import org.isoron.uhabits.helpers.ColorHelper; -import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.*; +import org.isoron.uhabits.utils.*; -public class ShowHabitActivity extends BaseActivity +/** + * Activity that allows the user to view and modify the app settings. + */ +public class SettingsActivity 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(); + setContentView(R.layout.settings_activity); + setupActionBarColor(); } - private void setupHabitActionBar() + private void setupActionBarColor() { - if(habit == null) return; + StyledResources res = new StyledResources(this); + int color = BaseScreen.getDefaultActionBarColor(this); - ActionBar actionBar = getSupportActionBar(); - if(actionBar == null) return; + if (res.getBoolean(R.attr.useHabitColorAsPrimary)) + color = res.getColor(R.attr.aboutScreenColor); - actionBar.setTitle(habit.name); - - setupActionBarColor(ColorHelper.getColor(this, habit.color)); - } - - public Habit getHabit() - { - return habit; + 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/activities/settings/SettingsFragment.java similarity index 55% rename from app/src/main/java/org/isoron/uhabits/fragments/SettingsFragment.java rename to app/src/main/java/org/isoron/uhabits/activities/settings/SettingsFragment.java index 29ab76a4c..9b71bc153 100644 --- a/app/src/main/java/org/isoron/uhabits/fragments/SettingsFragment.java +++ b/app/src/main/java/org/isoron/uhabits/activities/settings/SettingsFragment.java @@ -17,124 +17,121 @@ * with this program. If not, see . */ -package org.isoron.uhabits.fragments; +package org.isoron.uhabits.activities.settings; -import android.app.backup.BackupManager; -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.support.v7.preference.Preference; -import android.support.v7.preference.PreferenceCategory; -import android.support.v7.preference.PreferenceFragmentCompat; +import android.app.backup.*; +import android.content.*; +import android.os.*; +import android.support.v7.preference.*; -import org.isoron.uhabits.MainActivity; import org.isoron.uhabits.R; -import org.isoron.uhabits.helpers.ReminderHelper; -import org.isoron.uhabits.helpers.UIHelper; +import org.isoron.uhabits.activities.habits.list.*; +import org.isoron.uhabits.utils.*; public class SettingsFragment extends PreferenceFragmentCompat - implements SharedPreferences.OnSharedPreferenceChangeListener + implements SharedPreferences.OnSharedPreferenceChangeListener { private static int RINGTONE_REQUEST_CODE = 1; + private SharedPreferences prefs; + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) + { + if (requestCode == RINGTONE_REQUEST_CODE) + { + RingtoneUtils.parseRingtoneData(getContext(), data); + updateRingtoneDescription(); + return; + } + + super.onActivityResult(requestCode, resultCode, data); + } + @Override public void onCreate(Bundle savedInstanceState) { 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", ListHabitsScreen.RESULT_IMPORT_DATA); + setResultOnPreferenceClick("exportCSV", ListHabitsScreen.RESULT_EXPORT_CSV); + setResultOnPreferenceClick("exportDB", ListHabitsScreen.RESULT_EXPORT_DB); + setResultOnPreferenceClick("repairDB", ListHabitsScreen.RESULT_REPAIR_DB); + setResultOnPreferenceClick("bugReport", ListHabitsScreen.RESULT_BUG_REPORT); updateRingtoneDescription(); - if(UIHelper.isLocaleFullyTranslated()) + if (InterfaceUtils.isLocaleFullyTranslated()) removePreference("translate", "linksCategory"); } @Override public void onCreatePreferences(Bundle bundle, String s) { - + // NOP } - private void removePreference(String preferenceKey, String categoryKey) + @Override + public void onPause() { - PreferenceCategory cat = (PreferenceCategory) findPreference(categoryKey); - Preference pref = findPreference(preferenceKey); - cat.removePreference(pref); + prefs.unregisterOnSharedPreferenceChangeListener(this); + super.onPause(); } - private void setResultOnPreferenceClick(String key, final int result) + @Override + public boolean onPreferenceTreeClick(Preference preference) { - Preference pref = findPreference(key); - pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() + String key = preference.getKey(); + if (key == null) return false; + + if (key.equals("reminderSound")) { - @Override - public boolean onPreferenceClick(Preference preference) - { - getActivity().setResult(result); - getActivity().finish(); - return true; - } - }); + RingtoneUtils.startRingtonePickerActivity(this, + RINGTONE_REQUEST_CODE); + return true; + } + + return super.onPreferenceTreeClick(preference); } @Override public void onResume() { super.onResume(); - getPreferenceManager().getSharedPreferences(). - registerOnSharedPreferenceChangeListener(this); + prefs = getPreferenceManager().getSharedPreferences(); + prefs.registerOnSharedPreferenceChangeListener(this); } @Override - public void onPause() - { - getPreferenceManager().getSharedPreferences(). - unregisterOnSharedPreferenceChangeListener(this); - super.onPause(); - } - - @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, + String key) { BackupManager.dataChanged("org.isoron.uhabits"); } - @Override - public boolean onPreferenceTreeClick(Preference preference) + private void removePreference(String preferenceKey, String categoryKey) { - if(preference.getKey() == null) return false; - - if (preference.getKey().equals("reminderSound")) - { - ReminderHelper.startRingtonePickerActivity(this, RINGTONE_REQUEST_CODE); - return true; - } - - return super.onPreferenceTreeClick(preference); + PreferenceCategory cat = + (PreferenceCategory) findPreference(categoryKey); + Preference pref = findPreference(preferenceKey); + cat.removePreference(pref); } - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) + private void setResultOnPreferenceClick(String key, final int result) { - if(requestCode == RINGTONE_REQUEST_CODE) - { - ReminderHelper.parseRingtoneData(getContext(), data); - updateRingtoneDescription(); - return; - } - - super.onActivityResult(requestCode, resultCode, data); + Preference pref = findPreference(key); + pref.setOnPreferenceClickListener(preference -> { + getActivity().setResult(result); + getActivity().finish(); + return true; + }); } private void updateRingtoneDescription() { - String ringtoneName = ReminderHelper.getRingtoneName(getContext()); - if(ringtoneName == null) return; + String ringtoneName = RingtoneUtils.getRingtoneName(getContext()); + if (ringtoneName == null) return; Preference ringtonePreference = findPreference("reminderSound"); ringtonePreference.setSummary(ringtoneName); } diff --git a/app/src/main/java/org/isoron/uhabits/activities/settings/package-info.java b/app/src/main/java/org/isoron/uhabits/activities/settings/package-info.java new file mode 100644 index 000000000..73ed9c7a1 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/activities/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.activities.settings; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/automation/EditSettingActivity.java b/app/src/main/java/org/isoron/uhabits/automation/EditSettingActivity.java new file mode 100644 index 000000000..d92e73480 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/automation/EditSettingActivity.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.automation; + +import android.os.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.*; +import org.isoron.uhabits.models.*; + +public class EditSettingActivity extends BaseActivity +{ + @Override + protected void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + HabitsApplication app = (HabitsApplication) getApplicationContext(); + + HabitList habits = app.getComponent().getHabitList(); + habits = habits.getFiltered(new HabitMatcherBuilder() + .setArchivedAllowed(false) + .setCompletedAllowed(true) + .build()); + + EditSettingController controller = new EditSettingController(this); + EditSettingRootView rootView = + new EditSettingRootView(this, habits, controller); + + BaseScreen screen = new BaseScreen(this); + screen.setRootView(rootView); + setScreen(screen); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/automation/EditSettingController.java b/app/src/main/java/org/isoron/uhabits/automation/EditSettingController.java new file mode 100644 index 000000000..478170496 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/automation/EditSettingController.java @@ -0,0 +1,78 @@ +/* + * 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.automation; + +import android.app.*; +import android.content.*; +import android.os.*; +import android.support.annotation.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; + +import static org.isoron.uhabits.automation.FireSettingReceiver.*; + +public class EditSettingController +{ + @NonNull + private final Activity activity; + + public EditSettingController(@NonNull Activity activity) + { + this.activity = activity; + } + + public void onSave(Habit habit, int action) + { + if (habit.getId() == null) return; + + String actionName = getActionName(action); + String blurb = String.format("%s: %s", actionName, habit.getName()); + + Bundle bundle = new Bundle(); + bundle.putInt("action", action); + bundle.putLong("habit", habit.getId()); + + Intent intent = new Intent(); + intent.putExtra(EXTRA_STRING_BLURB, blurb); + intent.putExtra(EXTRA_BUNDLE, bundle); + + activity.setResult(Activity.RESULT_OK, intent); + activity.finish(); + } + + private String getActionName(int action) + { + switch (action) + { + case ACTION_CHECK: + return activity.getString(R.string.check); + + case ACTION_UNCHECK: + return activity.getString(R.string.uncheck); + + case ACTION_TOGGLE: + return activity.getString(R.string.toggle); + + default: + return "???"; + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/automation/EditSettingRootView.java b/app/src/main/java/org/isoron/uhabits/automation/EditSettingRootView.java new file mode 100644 index 000000000..264ea8a22 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/automation/EditSettingRootView.java @@ -0,0 +1,105 @@ +/* + * 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.automation; + +import android.content.*; +import android.support.annotation.*; +import android.support.v7.widget.*; +import android.support.v7.widget.Toolbar; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; + +import java.util.*; + +import butterknife.*; + +import static android.R.layout.*; + +public class EditSettingRootView extends BaseRootView +{ + @BindView(R.id.toolbar) + Toolbar toolbar; + + @BindView(R.id.habitSpinner) + AppCompatSpinner habitSpinner; + + @BindView(R.id.actionSpinner) + AppCompatSpinner actionSpinner; + + @NonNull + private final HabitList habitList; + + @NonNull + private final EditSettingController controller; + + public EditSettingRootView(@NonNull Context context, + @NonNull HabitList habitList, + @NonNull EditSettingController controller) + { + super(context); + this.habitList = habitList; + this.controller = controller; + + addView(inflate(getContext(), R.layout.automation, null)); + ButterKnife.bind(this); + populateHabitSpinner(); + } + + @NonNull + @Override + public Toolbar getToolbar() + { + return toolbar; + } + + @Override + public int getToolbarColor() + { + StyledResources res = new StyledResources(getContext()); + if (!res.getBoolean(R.attr.useHabitColorAsPrimary)) + return super.getToolbarColor(); + + return res.getColor(R.attr.aboutScreenColor); + } + + @OnClick(R.id.buttonSave) + public void onClickSave() + { + int action = actionSpinner.getSelectedItemPosition(); + int habitPosition = habitSpinner.getSelectedItemPosition(); + Habit habit = habitList.getByPosition(habitPosition); + controller.onSave(habit, action); + } + + private void populateHabitSpinner() + { + List names = new LinkedList<>(); + for (Habit h : habitList) names.add(h.getName()); + + ArrayAdapter adapter = + new ArrayAdapter<>(getContext(), simple_spinner_item, names); + adapter.setDropDownViewResource(simple_spinner_dropdown_item); + habitSpinner.setAdapter(adapter); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/automation/FireSettingReceiver.java b/app/src/main/java/org/isoron/uhabits/automation/FireSettingReceiver.java new file mode 100644 index 000000000..7ff6af588 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/automation/FireSettingReceiver.java @@ -0,0 +1,114 @@ +/* + * 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.automation; + +import android.content.*; +import android.os.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.receivers.*; +import org.isoron.uhabits.utils.*; + +import dagger.*; + +public class FireSettingReceiver extends BroadcastReceiver +{ + public static final int ACTION_CHECK = 0; + + public static final int ACTION_UNCHECK = 1; + + public static final int ACTION_TOGGLE = 2; + + public static final String EXTRA_BUNDLE = + "com.twofortyfouram.locale.intent.extra.BUNDLE"; + + public static final String EXTRA_STRING_BLURB = + "com.twofortyfouram.locale.intent.extra.BLURB"; + + private HabitList allHabits; + + @Override + public void onReceive(Context context, Intent intent) + { + HabitsApplication app = + (HabitsApplication) context.getApplicationContext(); + + ReceiverComponent component = + DaggerFireSettingReceiver_ReceiverComponent + .builder() + .appComponent(app.getComponent()) + .build(); + + allHabits = app.getComponent().getHabitList(); + + Arguments args = parseIntent(intent); + if (args == null) return; + + long timestamp = DateUtils.getStartOfToday(); + WidgetController controller = component.getWidgetController(); + + switch (args.action) + { + case ACTION_CHECK: + controller.onAddRepetition(args.habit, timestamp); + break; + + case ACTION_UNCHECK: + controller.onRemoveRepetition(args.habit, timestamp); + break; + + case ACTION_TOGGLE: + controller.onToggleRepetition(args.habit, timestamp); + break; + } + } + + private Arguments parseIntent(Intent intent) + { + Arguments args = new Arguments(); + + Bundle bundle = intent.getBundleExtra(EXTRA_BUNDLE); + if (bundle == null) return null; + + args.action = bundle.getInt("action"); + if (args.action < 0 || args.action > 2) return null; + + Habit habit = allHabits.getById(bundle.getLong("habit")); + if (habit == null) return null; + args.habit = habit; + + return args; + } + + @ReceiverScope + @Component(dependencies = AppComponent.class) + interface ReceiverComponent + { + WidgetController getWidgetController(); + } + + private class Arguments + { + int action; + + Habit 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..51993e7c7 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java @@ -19,40 +19,49 @@ 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 java.util.List; +import java.util.*; +/** + * Command to archive a list of habits. + */ public class ArchiveHabitsCommand extends Command { + private List selectedHabits; - private List habits; + private final HabitList habitList; - public ArchiveHabitsCommand(List habits) + public ArchiveHabitsCommand(HabitList habitList, List selectedHabits) { - this.habits = habits; + this.habitList = habitList; + this.selectedHabits = selectedHabits; } @Override public void execute() { - Habit.archive(habits); + for (Habit h : selectedHabits) h.setArchived(true); + habitList.update(selectedHabits); } @Override - public void undo() - { - Habit.unarchive(habits); - } - public Integer getExecuteStringId() { return R.string.toast_habit_archived; } + @Override public Integer getUndoStringId() { return R.string.toast_habit_unarchived; } + + @Override + public void undo() + { + for (Habit h : selectedHabits) h.setArchived(false); + habitList.update(selectedHabits); + } } \ No newline at end of file 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..503acc40f 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,60 @@ package org.isoron.uhabits.commands; -import com.activeandroid.ActiveAndroid; +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; -import org.isoron.uhabits.R; -import org.isoron.uhabits.helpers.DatabaseHelper; -import org.isoron.uhabits.models.Habit; - -import java.util.ArrayList; -import java.util.List; +import java.util.*; +/** + * Command to change the color of a list of habits. + */ public class ChangeHabitColorCommand extends Command { - List habits; + HabitList habitList; + + List selected; + List originalColors; + Integer newColor; - public ChangeHabitColorCommand(List habits, Integer newColor) + public ChangeHabitColorCommand(HabitList habitList, + List selected, + Integer newColor) { - this.habits = habits; + this.habitList = habitList; + this.selected = selected; this.newColor = newColor; - this.originalColors = new ArrayList<>(habits.size()); + this.originalColors = new ArrayList<>(selected.size()); - for(Habit h : habits) - originalColors.add(h.color); + for (Habit h : selected) originalColors.add(h.getColor()); } @Override public void execute() { - Habit.setColor(habits, newColor); + for (Habit h : selected) h.setColor(newColor); + habitList.update(selected); } @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 : selected) h.setColor(originalColors.get(k++)); + habitList.update(selected); + } } 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..8ef46f6ec --- /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.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.tasks.*; + +import java.util.*; + +import javax.inject.*; + +/** + * A CommandRunner executes and undoes commands. + *

+ * CommandRunners also allows objects to subscribe to it, and receive events + * whenever a command is performed. + */ +@AppScope +public class CommandRunner +{ + private TaskRunner taskRunner; + + private LinkedList listeners; + + @Inject + public CommandRunner(@NonNull TaskRunner taskRunner) + { + this.taskRunner = taskRunner; + listeners = new LinkedList<>(); + } + + public void addListener(Listener l) + { + listeners.add(l); + } + + public void execute(final Command command, final Long refreshKey) + { + taskRunner.execute(new Task() + { + @Override + public void doInBackground() + { + command.execute(); + } + + @Override + public void onPostExecute() + { + for (Listener l : listeners) + l.onCommandExecuted(command, refreshKey); + } + }); + } + + 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..51f08c482 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,45 @@ package org.isoron.uhabits.commands; -import org.isoron.uhabits.R; -import org.isoron.uhabits.models.Habit; +import android.support.annotation.*; +import com.google.auto.factory.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; + +/** + * Command to create a habit. + */ +@AutoFactory public class CreateHabitCommand extends Command { + private ModelFactory modelFactory; + + HabitList habitList; + private Habit model; + private Long savedId; - public CreateHabitCommand(Habit model) + public CreateHabitCommand(@Provided @NonNull ModelFactory modelFactory, + @NonNull HabitList habitList, + @NonNull Habit model) { + this.modelFactory = modelFactory; + this.habitList = habitList; this.model = model; } @Override public void execute() { - Habit savedHabit = new Habit(model); - if (savedId == null) - { - savedHabit.save(); - savedId = savedHabit.getId(); - } - else - { - savedHabit.save(savedId); - } - } + Habit savedHabit = modelFactory.buildHabit(); + savedHabit.copyFrom(model); + savedHabit.setId(savedId); - @Override - public void undo() - { - Habit habit = Habit.get(savedId); - if(habit == null) throw new RuntimeException("Habit not found"); - - habit.cascadeDelete(); + habitList.add(savedHabit); + savedId = savedHabit.getId(); } @Override @@ -68,4 +72,13 @@ public class CreateHabitCommand extends Command return R.string.toast_habit_deleted; } + @Override + public void undo() + { + Habit habit = habitList.getById(savedId); + if (habit == null) throw new RuntimeException("Habit not found"); + + habitList.remove(habit); + } + } \ No newline at end of file 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..ca184d8bf 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/DeleteHabitsCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/DeleteHabitsCommand.java @@ -19,42 +19,53 @@ 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 java.util.List; +import java.util.*; +/** + * Command to delete a list of habits. + */ public class DeleteHabitsCommand extends Command { + HabitList habitList; + private List habits; - public DeleteHabitsCommand(List habits) + public DeleteHabitsCommand(HabitList habitList, List habits) { this.habits = habits; + this.habitList = habitList; } @Override public void execute() { - for(Habit h : habits) - h.cascadeDelete(); - - Habit.rebuildOrder(); + for (Habit h : habits) + habitList.remove(h); } @Override - public void undo() + public Integer getExecuteStringId() { - throw new UnsupportedOperationException(); + return R.string.toast_habit_deleted; } - public Integer getExecuteStringId() + public List getHabits() { - return R.string.toast_habit_deleted; + return new LinkedList<>(habits); } + @Override public Integer getUndoStringId() { return R.string.toast_habit_restored; } + + @Override + public void undo() + { + throw new UnsupportedOperationException(); + } } 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..8d9605dbb 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,45 @@ package org.isoron.uhabits.commands; -import org.isoron.uhabits.R; -import org.isoron.uhabits.models.Habit; +import android.support.annotation.*; +import com.google.auto.factory.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; + +/** + * Command to modify a habit. + */ +@AutoFactory public class EditHabitCommand extends Command { + HabitList habitList; + private Habit original; + private Habit modified; + private long savedId; - private boolean hasIntervalChanged; - public EditHabitCommand(Habit original, Habit modified) + private boolean hasFrequencyChanged; + + public EditHabitCommand(@Provided @NonNull ModelFactory modelFactory, + @NonNull HabitList habitList, + @NonNull Habit original, + @NonNull Habit modified) { + this.habitList = habitList; this.savedId = original.getId(); - this.modified = new Habit(modified); - this.original = new Habit(original); + this.modified = modelFactory.buildHabit(); + this.original = modelFactory.buildHabit(); + + 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 +66,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 +86,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..5cc2fa8ba 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java @@ -19,8 +19,11 @@ package org.isoron.uhabits.commands; -import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.*; +/** + * 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 @@ -43,4 +46,9 @@ public class ToggleRepetitionCommand extends Command { execute(); } + + public Habit getHabit() + { + return habit; + } } \ No newline at end of file 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..6e45cda7b 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,47 @@ 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 java.util.List; +import java.util.*; +/** + * Command to unarchive a list of habits. + */ public class UnarchiveHabitsCommand extends Command { + HabitList habitList; private List habits; - public UnarchiveHabitsCommand(List habits) + public UnarchiveHabitsCommand(HabitList habitList, List selected) { - this.habits = habits; + this.habits = selected; + this.habitList = habitList; } @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/DatabaseHelper.java b/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java deleted file mode 100644 index d3c3d21e5..000000000 --- a/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.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.helpers; - -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; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.text.SimpleDateFormat; - -public class DatabaseHelper -{ - public static void copy(File src, File dst) throws IOException - { - FileInputStream inStream = new FileInputStream(src); - FileOutputStream outStream = new FileOutputStream(dst); - copy(inStream, outStream); - } - - public static void copy(InputStream inStream, File dst) throws IOException - { - FileOutputStream outStream = new FileOutputStream(dst); - copy(inStream, outStream); - } - - public static void copy(InputStream in, OutputStream out) throws IOException - { - int numBytes; - byte[] buffer = new byte[1024]; - - while ((numBytes = in.read(buffer)) != -1) - 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) - { - File parents[] = new File[]{ Environment.getExternalStorageDirectory() }; - return getDir(parents, relativePath); - } - - @Nullable - public static File getFilesDir(@Nullable String relativePath) - { - Context context = HabitsApplication.getContext(); - if(context == null) - { - Log.e("DatabaseHelper", "getFilesDir: no application context available"); - return null; - } - - File externalFilesDirs[] = ContextCompat.getExternalFilesDirs(context, null); - - if(externalFilesDirs == null) - { - Log.e("DatabaseHelper", "getFilesDir: getExternalFilesDirs returned null"); - return null; - } - - return getDir(externalFilesDirs, relativePath); - } - - @Nullable - private static File getDir(@NonNull File potentialParentDirs[], @Nullable String relativePath) - { - if(relativePath == null) relativePath = ""; - - File chosenDir = null; - for(File dir : potentialParentDirs) - { - if (dir == null || !dir.canWrite()) continue; - chosenDir = dir; - break; - } - - if(chosenDir == null) - { - Log.e("DatabaseHelper", "getDir: all potential parents are null or non-writable"); - return null; - } - - File dir = new File(String.format("%s/%s/", chosenDir.getAbsolutePath(), relativePath)); - if (!dir.exists() && !dir.mkdirs()) - { - Log.e("DatabaseHelper", "getDir: chosen dir does not exist and cannot be created"); - return null; - } - - 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/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/helpers/ReminderHelper.java b/app/src/main/java/org/isoron/uhabits/helpers/ReminderHelper.java deleted file mode 100644 index 2956815a4..000000000 --- a/app/src/main/java/org/isoron/uhabits/helpers/ReminderHelper.java +++ /dev/null @@ -1,173 +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.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; -import android.support.v4.app.Fragment; -import android.util.Log; - -import org.isoron.uhabits.HabitBroadcastReceiver; -import org.isoron.uhabits.R; -import org.isoron.uhabits.models.Habit; - -import java.text.DateFormat; -import java.util.Calendar; -import java.util.Date; - -public class ReminderHelper -{ - public static void createReminderAlarms(Context context) - { - 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 (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.SECOND, 0); - - reminderTime = calendar.getTimeInMillis(); - - if (System.currentTimeMillis() > reminderTime) - reminderTime += AlarmManager.INTERVAL_DAY; - } - - long timestamp = DateHelper.getStartOfDay(DateHelper.toLocalTime(reminderTime)); - - Uri uri = habit.getUri(); - - Intent alarmIntent = new Intent(context, HabitBroadcastReceiver.class); - alarmIntent.setAction(HabitBroadcastReceiver.ACTION_SHOW_REMINDER); - alarmIntent.setData(uri); - 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); - - AlarmManager manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - - if (Build.VERSION.SDK_INT >= 23) - 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); - - String name = habit.name.substring(0, Math.min(3, habit.name.length())); - Log.d("ReminderHelper", String.format("Setting alarm (%s): %s", - DateFormat.getDateTimeInstance().format(new Date(reminderTime)), name)); - } - - @Nullable - public static Uri getRingtoneUri(Context context) - { - 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); - - return ringtoneUri; - } - - public static void parseRingtoneData(Context context, @Nullable Intent data) - { - if(data == null) return; - - 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(); - } - else - { - String off = context.getResources().getString(R.string.none); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - prefs.edit().putString("pref_ringtone_uri", "").apply(); - } - } - - public static void startRingtonePickerActivity(Fragment fragment, int requestCode) - { - Uri existingRingtoneUri = ReminderHelper.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_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); - 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/helpers/UIHelper.java b/app/src/main/java/org/isoron/uhabits/helpers/UIHelper.java deleted file mode 100644 index 9848ff016..000000000 --- a/app/src/main/java/org/isoron/uhabits/helpers/UIHelper.java +++ /dev/null @@ -1,321 +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.app.Activity; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.Typeface; -import android.graphics.drawable.Drawable; -import android.os.Build; -import android.os.Debug; -import android.os.Looper; -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 static final String ISORON_NAMESPACE = "http://isoron.org/android"; - - public static final int THEME_LIGHT = 0; - public static final int THEME_DARK = 1; - - private static Typeface fontAwesome; - private static Integer fixedTheme; - - public static void setFixedTheme(Integer fixedTheme) - { - UIHelper.fixedTheme = fixedTheme; - } - - public interface OnSavedListener - { - void onSaved(Command command, Object savedObject); - } - - public static Typeface getFontAwesome(Context context) - { - if(fontAwesome == null) - fontAwesome = Typeface.createFromAsset(context.getAssets(), "fontawesome-webfont.ttf"); - - 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) - { - int resId = attrs.getAttributeResourceValue(ISORON_NAMESPACE, name, 0); - if (resId != 0) return context.getResources().getString(resId); - - String value = attrs.getAttributeValue(ISORON_NAMESPACE, name); - if(value != null) return value; - else return defaultValue; - } - - public static Integer getColorAttribute(Context context, AttributeSet attrs, String name, - Integer defaultValue) - { - int resId = attrs.getAttributeResourceValue(ISORON_NAMESPACE, name, 0); - if (resId != 0) return context.getResources().getColor(resId); - else return defaultValue; - } - - public static int getIntAttribute(Context context, AttributeSet attrs, String name, - int defaultValue) - { - String number = getAttribute(context, attrs, name, null); - if(number != null) return Integer.parseInt(number); - else return defaultValue; - } - - public static boolean getBooleanAttribute(Context context, AttributeSet attrs, String name, - boolean defaultValue) - { - String boolText = getAttribute(context, attrs, name, null); - if(boolText != null) return Boolean.parseBoolean(boolText); - else return defaultValue; - } - - public static float getFloatAttribute(Context context, AttributeSet attrs, String name, - float defaultValue) - { - String number = getAttribute(context, attrs, name, null); - if(number != null) return Float.parseFloat(number); - else return defaultValue; - } - - public static float dpToPixels(Context context, float dp) - { - Resources resources = context.getResources(); - DisplayMetrics metrics = resources.getDisplayMetrics(); - return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, metrics); - } - - public static float spToPixels(Context context, float sp) - { - Resources resources = context.getResources(); - DisplayMetrics metrics = resources.getDisplayMetrics(); - return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, metrics); - } - - /** - * Throws a runtime exception if called from the main thread. Useful to make sure that - * slow methods never accidentally slow the application down. - * - * @throws RuntimeException when run from main thread - */ - public static void throwIfMainThread() throws RuntimeException - { - Looper looper = Looper.myLooper(); - if(looper == null) return; - - if(looper == Looper.getMainLooper()) - throw new RuntimeException("This method should never be called from the main thread"); - } - - public static void startTracing() - { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) - { - throw new UnsupportedOperationException(); - } - else - { - Debug.startMethodTracingSampling("Android/data/org.isoron.uhabits/perf", - 32 * 1024 * 1024, 100); - } - } - - public static void stopTracing() - { - Debug.stopMethodTracing(); - } - - public static boolean isLocaleFullyTranslated() - { - // TODO: Move this to another place, or detect automatically - String fullyTranslatedLanguages[] = { "ca", "zh", "en", "de", "in", "it", "ko", "pl", "pt", - "es", "tk", "uk", "ja", "fr", "hr", "sl"}; - - final String currentLanguage = Locale.getDefault().getLanguage(); - - for(String lang : fullyTranslatedLanguages) - if(currentLanguage.equals(lang)) return true; - - return false; - } - - public static float getScreenWidth(Context context) - { - return context.getResources().getDisplayMetrics().widthPixels; - } - - public static int getStyledColor(Context context, int attrId) - { - TypedArray ta = getTypedArray(context, attrId); - int color = ta.getColor(0, 0); - ta.recycle(); - - return color; - } - - private static TypedArray getTypedArray(Context context, int attrId) - { - int[] attrs = new int[]{ attrId }; - if(fixedTheme != null) - return context.getTheme().obtainStyledAttributes(fixedTheme, attrs); - else - return context.obtainStyledAttributes(attrs); - } - - public static Drawable getStyledDrawable(Context context, int attrId) - { - TypedArray ta = getTypedArray(context, attrId); - Drawable drawable = ta.getDrawable(0); - ta.recycle(); - - return drawable; - } - - public static boolean getStyledBoolean(Context context, int attrId) - { - TypedArray ta = getTypedArray(context, attrId); - boolean bool = ta.getBoolean(0, false); - ta.recycle(); - - return bool; - } - - public static float getStyledFloat(Context context, int attrId) - { - TypedArray ta = getTypedArray(context, attrId); - float f = ta.getFloat(0, 0); - ta.recycle(); - - return f; - } - - static int getStyleResource(Context context, int attrId) - { - TypedArray ta = getTypedArray(context, attrId); - int resourceId = ta.getResourceId(0, -1); - ta.recycle(); - - return resourceId; - } - - public static void applyCurrentTheme(Activity activity) - { - switch(getCurrentTheme()) - { - case THEME_DARK: - { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity); - boolean pureBlackEnabled = prefs.getBoolean("pref_pure_black", false); - - if(pureBlackEnabled) - activity.setTheme(R.style.AppBaseThemeDark_PureBlack); - else - activity.setTheme(R.style.AppBaseThemeDark); - - break; - } - - case THEME_LIGHT: - default: - activity.setTheme(R.style.AppBaseTheme); - break; - } - } - - private static int getCurrentTheme() - { - Context appContext = HabitsApplication.getContext(); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(appContext); - return prefs.getInt("pref_theme", THEME_LIGHT); - } - - public static void setCurrentTheme(int theme) - { - Context appContext = HabitsApplication.getContext(); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(appContext); - prefs.edit().putInt("pref_theme", theme).apply(); - } - - public static boolean isNightMode() - { - return getCurrentTheme() == THEME_DARK; - } - - - public static void setDefaultScoreInterval(Context context, int position) - { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - prefs.edit().putInt("pref_score_view_interval", position).apply(); - } - - public static int getDefaultScoreInterval(Context context) - { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - int defaultScoreInterval = prefs.getInt("pref_score_view_interval", 1); - if(defaultScoreInterval > 5 || defaultScoreInterval < 0) defaultScoreInterval = 1; - - return defaultScoreInterval; - } -} diff --git a/app/src/main/java/org/isoron/uhabits/intents/IntentFactory.java b/app/src/main/java/org/isoron/uhabits/intents/IntentFactory.java new file mode 100644 index 000000000..8ace599a5 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/intents/IntentFactory.java @@ -0,0 +1,105 @@ +/* + * 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.intents; + +import android.content.*; +import android.net.*; +import android.support.annotation.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.about.*; +import org.isoron.uhabits.activities.habits.show.*; +import org.isoron.uhabits.activities.intro.*; +import org.isoron.uhabits.activities.settings.*; +import org.isoron.uhabits.models.*; + +import javax.inject.*; + +public class IntentFactory +{ + @Inject + public IntentFactory() + { + } + + public Intent rateApp(Context context) + { + String url = context.getString(R.string.playStoreURL); + return buildViewIntent(url); + } + + public Intent sendFeedback(Context context) + { + String url = context.getString(R.string.feedbackURL); + return buildSendToIntent(url); + } + + public Intent startAboutActivity(Context context) + { + return new Intent(context, AboutActivity.class); + } + + public Intent startIntroActivity(Context context) + { + return new Intent(context, IntroActivity.class); + } + + public Intent startSettingsActivity(Context context) + { + return new Intent(context, SettingsActivity.class); + } + + public Intent startShowHabitActivity(Context context, Habit habit) + { + Intent intent = new Intent(context, ShowHabitActivity.class); + intent.setData(habit.getUri()); + return intent; + } + + public Intent viewFAQ(Context context) + { + String url = context.getString(R.string.helpURL); + return buildViewIntent(url); + } + + public Intent viewSourceCode(Context context) + { + String url = context.getString(R.string.sourceCodeURL); + return buildViewIntent(url); + } + + @NonNull + private Intent buildSendToIntent(String url) + { + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_SENDTO); + intent.setData(Uri.parse(url)); + return intent; + } + + @NonNull + private Intent buildViewIntent(String url) + { + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_VIEW); + intent.setData(Uri.parse(url)); + return intent; + } +} diff --git a/app/src/main/java/org/isoron/uhabits/intents/IntentParser.java b/app/src/main/java/org/isoron/uhabits/intents/IntentParser.java new file mode 100644 index 000000000..a6dc23e3f --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/intents/IntentParser.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.intents; + +import android.content.*; +import android.net.*; +import android.support.annotation.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; + +import javax.inject.*; + +import static android.content.ContentUris.*; + +@AppScope +public class IntentParser +{ + private HabitList habits; + + @Inject + public IntentParser(@NonNull HabitList habits) + { + this.habits = habits; + } + + public CheckmarkIntentData parseCheckmarkIntent(@NonNull Intent intent) + { + Uri uri = intent.getData(); + if (uri == null) throw new IllegalArgumentException("uri is null"); + + CheckmarkIntentData data = new CheckmarkIntentData(); + data.habit = parseHabit(uri); + data.timestamp = parseTimestamp(intent); + return data; + } + + @NonNull + protected Habit parseHabit(@NonNull Uri uri) + { + Habit habit = habits.getById(parseId(uri)); + if (habit == null) + throw new IllegalArgumentException("habit not found"); + return habit; + } + + @NonNull + protected Long parseTimestamp(@NonNull Intent intent) + { + long today = DateUtils.getStartOfToday(); + Long timestamp = intent.getLongExtra("timestamp", today); + timestamp = DateUtils.getStartOfDay(timestamp); + + if (timestamp < 0 || timestamp > today) + throw new IllegalArgumentException("timestamp is not valid"); + + return timestamp; + } + + public class CheckmarkIntentData + { + public Habit habit; + + public Long timestamp; + } +} diff --git a/app/src/main/java/org/isoron/uhabits/intents/IntentScheduler.java b/app/src/main/java/org/isoron/uhabits/intents/IntentScheduler.java new file mode 100644 index 000000000..47dd4eebf --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/intents/IntentScheduler.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.intents; + +import android.app.*; +import android.content.*; +import android.os.*; +import android.support.annotation.*; + +import org.isoron.uhabits.*; + +import javax.inject.*; + +import static android.app.AlarmManager.*; +import static android.content.Context.*; + +@AppScope +public class IntentScheduler +{ + private final AlarmManager manager; + + @Inject + public IntentScheduler(@AppContext Context context) + { + manager = (AlarmManager) context.getSystemService(ALARM_SERVICE); + } + + public void schedule(@NonNull Long timestamp, PendingIntent intent) + { + if (Build.VERSION.SDK_INT >= 23) + { + manager.setExactAndAllowWhileIdle(RTC_WAKEUP, timestamp, intent); + return; + } + + if (Build.VERSION.SDK_INT >= 19) + { + manager.setExact(RTC_WAKEUP, timestamp, intent); + return; + } + + manager.set(RTC_WAKEUP, timestamp, intent); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/intents/PendingIntentFactory.java b/app/src/main/java/org/isoron/uhabits/intents/PendingIntentFactory.java new file mode 100644 index 000000000..415f9237a --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/intents/PendingIntentFactory.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.intents; + +import android.app.*; +import android.content.*; +import android.net.*; +import android.support.annotation.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.receivers.*; + +import javax.inject.*; + +import static android.app.PendingIntent.*; + +@AppScope +public class PendingIntentFactory +{ + private final Context context; + + private IntentFactory intentFactory; + + @Inject + public PendingIntentFactory(@AppContext Context context, + @NonNull IntentFactory intentFactory) + { + this.context = context; + this.intentFactory = intentFactory; + } + + public PendingIntent addCheckmark(@NonNull Habit habit, + @Nullable Long timestamp) + { + Intent checkIntent = new Intent(context, WidgetReceiver.class); + checkIntent.setData(habit.getUri()); + checkIntent.setAction(WidgetReceiver.ACTION_ADD_REPETITION); + if (timestamp != null) checkIntent.putExtra("timestamp", timestamp); + return PendingIntent.getBroadcast(context, 1, checkIntent, + FLAG_UPDATE_CURRENT); + } + + public PendingIntent dismissNotification(@NonNull Habit habit) + { + Intent deleteIntent = new Intent(context, ReminderReceiver.class); + deleteIntent.setAction(WidgetReceiver.ACTION_DISMISS_REMINDER); + deleteIntent.setData(habit.getUri()); + return PendingIntent.getBroadcast(context, 0, deleteIntent, + FLAG_UPDATE_CURRENT); + } + + public PendingIntent showHabit(Habit habit) + { + Intent intent = intentFactory.startShowHabitActivity(context, habit); + + return android.support.v4.app.TaskStackBuilder + .create(context) + .addNextIntentWithParentStack(intent) + .getPendingIntent(0, FLAG_UPDATE_CURRENT); + } + + public PendingIntent showReminder(@NonNull Habit habit, + @Nullable Long reminderTime, + long timestamp) + { + Uri uri = habit.getUri(); + + Intent intent = new Intent(context, ReminderReceiver.class); + intent.setAction(ReminderReceiver.ACTION_SHOW_REMINDER); + intent.setData(uri); + intent.putExtra("timestamp", timestamp); + intent.putExtra("reminderTime", reminderTime); + int reqCode = ((int) (habit.getId() % Integer.MAX_VALUE)) + 1; + return PendingIntent.getBroadcast(context, reqCode, intent, + FLAG_UPDATE_CURRENT); + } + + public PendingIntent snoozeNotification(@NonNull Habit habit) + { + Uri data = habit.getUri(); + Intent snoozeIntent = new Intent(context, ReminderReceiver.class); + snoozeIntent.setData(data); + snoozeIntent.setAction(ReminderReceiver.ACTION_SNOOZE_REMINDER); + return PendingIntent.getBroadcast(context, 0, snoozeIntent, + FLAG_UPDATE_CURRENT); + } + + public PendingIntent toggleCheckmark(@NonNull Habit habit, + @Nullable Long timestamp) + { + Uri data = habit.getUri(); + Intent checkIntent = new Intent(context, WidgetReceiver.class); + checkIntent.setData(data); + checkIntent.setAction(WidgetReceiver.ACTION_TOGGLE_REPETITION); + if (timestamp != null) checkIntent.putExtra("timestamp", timestamp); + return PendingIntent.getBroadcast(context, 2, checkIntent, + FLAG_UPDATE_CURRENT); + } +} 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..d20979e78 100644 --- a/app/src/main/java/org/isoron/uhabits/io/AbstractImporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/AbstractImporter.java @@ -19,15 +19,26 @@ package org.isoron.uhabits.io; -import android.support.annotation.NonNull; +import android.support.annotation.*; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.util.Arrays; +import org.isoron.uhabits.models.*; +import java.io.*; +import java.util.*; + +/** + * AbstractImporter is the base class for all classes that import data from + * files into the app. + */ public abstract class AbstractImporter { + protected final HabitList habits; + + public AbstractImporter(HabitList habits) + { + this.habits = habits; + } + 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/DirFinder.java b/app/src/main/java/org/isoron/uhabits/io/DirFinder.java new file mode 100644 index 000000000..392f7716c --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/io/DirFinder.java @@ -0,0 +1,106 @@ +/* + * 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.io; + +import android.content.*; +import android.os.*; +import android.support.annotation.*; +import android.util.*; + +import org.isoron.uhabits.*; + +import java.io.*; + +import javax.inject.*; + +import static android.support.v4.content.ContextCompat.*; + +/** + * A DirFinder locates suitable directories for storing user files. + */ +public class DirFinder +{ + private static final String TAG = "DirFinder"; + + private final Context context; + + @Inject + public DirFinder(@AppContext Context context) + { + this.context = context; + } + + @Nullable + public File findSDCardDir(@Nullable String subpath) + { + File parents[] = new File[]{ + Environment.getExternalStorageDirectory() + }; + + return findDir(parents, subpath); + } + + @Nullable + public File findStorageDir(@Nullable String relativePath) + { + File potentialParents[] = getExternalFilesDirs(context, null); + + if (potentialParents == null) + { + Log.e(TAG, "getFilesDir: getExternalFilesDirs returned null"); + return null; + } + + return findDir(potentialParents, relativePath); + } + + @Nullable + private File findDir(@NonNull File potentialParents[], + @Nullable String relativePath) + { + if (relativePath == null) relativePath = ""; + + File chosenDir = null; + for (File dir : potentialParents) + { + if (dir == null || !dir.canWrite()) continue; + chosenDir = dir; + break; + } + + if (chosenDir == null) + { + Log.e(TAG, + "getDir: all potential parents are null or non-writable"); + return null; + } + + File dir = new File( + String.format("%s/%s/", chosenDir.getAbsolutePath(), relativePath)); + if (!dir.exists() && !dir.mkdirs()) + { + Log.e(TAG, + "getDir: chosen dir does not exist and cannot be created"); + return null; + } + + return dir; + } +} 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..dcdf9e354 100644 --- a/app/src/main/java/org/isoron/uhabits/io/GenericImporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/GenericImporter.java @@ -19,31 +19,43 @@ package org.isoron.uhabits.io; -import android.support.annotation.NonNull; +import android.support.annotation.*; -import java.io.File; -import java.io.IOException; -import java.util.LinkedList; -import java.util.List; +import org.isoron.uhabits.models.*; +import java.io.*; +import java.util.*; + +import javax.inject.*; + +/** + * 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; - public GenericImporter() + @Inject + public GenericImporter(@NonNull HabitList habits, + @NonNull LoopDBImporter loopDBImporter, + @NonNull RewireDBImporter rewireDBImporter, + @NonNull TickmateDBImporter tickmateDBImporter, + @NonNull HabitBullCSVImporter habitBullCSVImporter) { + super(habits); importers = new LinkedList<>(); - importers.add(new LoopDBImporter()); - importers.add(new RewireDBImporter()); - importers.add(new TickmateDBImporter()); - importers.add(new HabitBullCSVImporter()); + importers.add(loopDBImporter); + importers.add(rewireDBImporter); + importers.add(tickmateDBImporter); + importers.add(habitBullCSVImporter); } @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 +63,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..42fb9b09b 100644 --- a/app/src/main/java/org/isoron/uhabits/io/HabitBullCSVImporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/HabitBullCSVImporter.java @@ -19,23 +19,34 @@ package org.isoron.uhabits.io; -import android.support.annotation.NonNull; +import android.support.annotation.*; -import com.activeandroid.ActiveAndroid; -import com.opencsv.CSVReader; +import com.activeandroid.*; +import com.opencsv.*; -import org.isoron.uhabits.helpers.DateHelper; -import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.util.Calendar; -import java.util.HashMap; +import java.io.*; +import java.util.*; +import javax.inject.*; + +/** + * Class that imports data from HabitBull CSV files. + */ public class HabitBullCSVImporter extends AbstractImporter { + private ModelFactory modelFactory; + + @Inject + public HabitBullCSVImporter(@NonNull HabitList habits, + @NonNull ModelFactory modelFactory) + { + super(habits); + this.modelFactory = modelFactory; + } + @Override public boolean canHandle(@NonNull File file) throws IOException { @@ -63,7 +74,7 @@ public class HabitBullCSVImporter extends AbstractImporter private void parseFile(@NonNull File file) throws IOException { CSVReader reader = new CSVReader(new FileReader(file)); - HashMap habits = new HashMap<>(); + HashMap map = new HashMap<>(); for(String line[] : reader) { @@ -76,7 +87,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(); @@ -84,21 +95,20 @@ public class HabitBullCSVImporter extends AbstractImporter int value = Integer.parseInt(line[4]); if(value != 1) continue; - Habit h = habits.get(name); + Habit h = map.get(name); if(h == null) { - h = new Habit(); - h.name = name; - h.description = description; - h.freqNum = h.freqDen = 1; - h.save(); - - habits.put(name, h); + h = modelFactory.buildHabit(); + h.setName(name); + h.setDescription(description); + h.setFrequency(Frequency.DAILY); + habits.add(h); + map.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 67ddd3a15..fc0a29c92 100644 --- a/app/src/main/java/org/isoron/uhabits/io/HabitsCSVExporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/HabitsCSVExporter.java @@ -19,73 +19,121 @@ package org.isoron.uhabits.io; -import android.support.annotation.NonNull; - -import org.isoron.uhabits.helpers.DateHelper; -import org.isoron.uhabits.models.CheckmarkList; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.models.ScoreList; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.FileWriter; -import java.io.IOException; -import java.text.SimpleDateFormat; -import java.util.LinkedList; -import java.util.List; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; +import android.support.annotation.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; + +import java.io.*; +import java.text.*; +import java.util.*; +import java.util.zip.*; + +/** + * Class that exports the application data to CSV files. + */ public class HabitsCSVExporter { - private List habits; + private List selectedHabits; private List generateDirs; + private List generateFilenames; private String exportDirName; + /** + * Delimiter used in a CSV file. + */ + private final String DELIMITER = ","; + + @NonNull + private final HabitList allHabits; - public HabitsCSVExporter(List habits, File dir) + public HabitsCSVExporter(@NonNull HabitList allHabits, + @NonNull List selectedHabits, + @NonNull File dir) { - this.habits = habits; + this.allHabits = allHabits; + this.selectedHabits = selectedHabits; this.exportDirName = dir.getAbsolutePath() + "/"; generateDirs = new LinkedList<>(); generateFilenames = new LinkedList<>(); } + public String writeArchive() throws IOException + { + String zipFilename; + + writeHabits(); + zipFilename = writeZipFile(); + cleanup(); + + return zipFilename; + } + + private void addFileToZip(ZipOutputStream zos, String filename) + throws IOException + { + FileInputStream fis = + new FileInputStream(new File(exportDirName + filename)); + ZipEntry ze = new ZipEntry(filename); + zos.putNextEntry(ze); + + int length; + byte bytes[] = new byte[1024]; + while ((length = fis.read(bytes)) >= 0) zos.write(bytes, 0, length); + + zos.closeEntry(); + fis.close(); + } + + private void cleanup() + { + for (String filename : generateFilenames) + new File(exportDirName + filename).delete(); + + for (String filename : generateDirs) + new File(exportDirName + filename).delete(); + + new File(exportDirName).delete(); + } + + @NonNull + private String sanitizeFilename(String name) + { + String s = name.replaceAll("[^ a-zA-Z0-9\\._-]+", ""); + return s.substring(0, Math.min(s.length(), 100)); + } + private void writeHabits() throws IOException { String filename = "Habits.csv"; new File(exportDirName).mkdirs(); FileWriter out = new FileWriter(exportDirName + filename); generateFilenames.add(filename); - Habit.writeCSV(habits, out); + allHabits.writeCSV(out); out.close(); - for(Habit h : habits) + for (Habit h : selectedHabits) { - 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", allHabits.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()); } - } - @NonNull - private String sanitizeFilename(String name) - { - String s = name.replaceAll("[^a-zA-Z0-9\\._-]+", ""); - return s.substring(0, Math.min(s.length(), 100)); + writeMultipleHabits(); } - private void writeScores(String habitDirName, ScoreList scores) throws IOException + private void writeScores(String habitDirName, ScoreList scores) + throws IOException { String path = habitDirName + "Scores.csv"; FileWriter out = new FileWriter(exportDirName + path); @@ -94,7 +142,8 @@ public class HabitsCSVExporter out.close(); } - private void writeCheckmarks(String habitDirName, CheckmarkList checkmarks) throws IOException + private void writeCheckmarks(String habitDirName, CheckmarkList checkmarks) + throws IOException { String filename = habitDirName + "Checkmarks.csv"; FileWriter out = new FileWriter(exportDirName + filename); @@ -103,58 +152,124 @@ public class HabitsCSVExporter out.close(); } - private String writeZipFile() throws IOException + /** + * Writes a scores file and a checkmarks file containing scores and checkmarks of every habit. + * The first column corresponds to the date. Subsequent columns correspond to a habit. + * Habits are taken from the list of selected habits. + * Dates are determined from the oldest repetition date to the newest repetition date found in + * the list of habits. + * + * @throws IOException if there was problem writing the files + */ + private void writeMultipleHabits() throws IOException { - SimpleDateFormat dateFormat = DateHelper.getCSVDateFormat(); - String date = dateFormat.format(DateHelper.getStartOfToday()); - String zipFilename = String.format("%s/Loop Habits CSV %s.zip", exportDirName, date); - - FileOutputStream fos = new FileOutputStream(zipFilename); - ZipOutputStream zos = new ZipOutputStream(fos); + String scoresFileName = "Scores.csv"; + String checksFileName = "Checkmarks.csv"; + generateFilenames.add(scoresFileName); + generateFilenames.add(checksFileName); + FileWriter scoresWriter = new FileWriter(exportDirName + scoresFileName); + FileWriter checksWriter = new FileWriter(exportDirName + checksFileName); - for(String filename : generateFilenames) - addFileToZip(zos, filename); + writeMultipleHabitsHeader(scoresWriter); + writeMultipleHabitsHeader(checksWriter); - zos.close(); - fos.close(); + long[] timeframe = getTimeframe(); + long oldest = timeframe[0]; + long newest = DateUtils.getStartOfToday(); - return zipFilename; - } + List checkmarks = new ArrayList<>(); + List scores = new ArrayList<>(); + for (Habit h : selectedHabits) + { + checkmarks.add(h.getCheckmarks().getValues(oldest, newest)); + scores.add(h.getScores().getValues(oldest, newest)); + } - private void addFileToZip(ZipOutputStream zos, String filename) throws IOException - { - FileInputStream fis = new FileInputStream(new File(exportDirName + filename)); - ZipEntry ze = new ZipEntry(filename); - zos.putNextEntry(ze); + int days = DateUtils.getDaysBetween(oldest, newest); + SimpleDateFormat dateFormat = DateFormats.getCSVDateFormat(); + for (int i = 0; i <= days; i++) + { + Date day = new Date(newest - i * DateUtils.millisecondsInOneDay); - int length; - byte bytes[] = new byte[1024]; - while((length = fis.read(bytes)) >= 0) - zos.write(bytes, 0, length); + String date = dateFormat.format(day); + StringBuilder sb = new StringBuilder(); + sb.append(date).append(DELIMITER); + checksWriter.write(sb.toString()); + scoresWriter.write(sb.toString()); - zos.closeEntry(); - fis.close(); + for(int j = 0; j < selectedHabits.size(); j++) + { + checksWriter.write(String.valueOf(checkmarks.get(j)[i])); + checksWriter.write(DELIMITER); + String score = + String.format("%.4f", ((float) scores.get(j)[i]) / Score.MAX_VALUE); + scoresWriter.write(score); + scoresWriter.write(DELIMITER); + } + checksWriter.write("\n"); + scoresWriter.write("\n"); + } + scoresWriter.close(); + checksWriter.close(); } - public String writeArchive() throws IOException + /** + * Writes the first row, containing header information, using the given writer. + * This consists of the date title and the names of the selected habits. + * + * @param out the writer to use + * @throws IOException if there was a problem writing + */ + private void writeMultipleHabitsHeader(Writer out) throws IOException { - String zipFilename; - - writeHabits(); - zipFilename = writeZipFile(); - cleanup(); + out.write("Date" + DELIMITER); + for (Habit h : selectedHabits) { + out.write(h.getName()); + out.write(DELIMITER); + } + out.write("\n"); + } - return zipFilename; + /** + * Gets the overall timeframe of the selected habits. + * The timeframe is an array containing the oldest timestamp among the habits and the + * newest timestamp among the habits. + * Both timestamps are in milliseconds. + * + * @return the timeframe containing the oldest timestamp and the newest timestamp + */ + private long[] getTimeframe() + { + long oldest = Long.MAX_VALUE; + long newest = -1; + for (Habit h : selectedHabits) + { + if(h.getRepetitions().getOldest() == null || h.getRepetitions().getNewest() == null) + continue; + long currOld = h.getRepetitions().getOldest().getTimestamp(); + long currNew = h.getRepetitions().getNewest().getTimestamp(); + oldest = currOld > oldest ? oldest : currOld; + newest = currNew < newest ? newest : currNew; + } + return new long[]{oldest, newest}; } - private void cleanup() + private String writeZipFile() throws IOException { - for(String filename : generateFilenames) - new File(exportDirName + filename).delete(); + SimpleDateFormat dateFormat = DateFormats.getCSVDateFormat(); + String date = dateFormat.format(DateUtils.getStartOfToday()); + String zipFilename = + String.format("%s/Loop Habits CSV %s.zip", exportDirName, date); - for(String filename : generateDirs) - new File(exportDirName + filename).delete(); + FileOutputStream fos = new FileOutputStream(zipFilename); + ZipOutputStream zos = new ZipOutputStream(fos); - new File(exportDirName).delete(); + for (String filename : generateFilenames) + addFileToZip(zos, filename); + + zos.close(); + fos.close(); + + return 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..69fd1d4f0 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,37 @@ import android.support.annotation.NonNull; import com.activeandroid.ActiveAndroid; -import org.isoron.uhabits.helpers.DatabaseHelper; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.DatabaseUtils; +import org.isoron.uhabits.utils.FileUtils; import java.io.File; import java.io.IOException; +import javax.inject.*; + +/** + * Class that imports data from database files exported by Loop Habit Tracker. + */ public class LoopDBImporter extends AbstractImporter { + @Inject + public LoopDBImporter(@NonNull HabitList habits) + { + super(habits); + } + @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 +68,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..0865671b7 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,46 @@ 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.*; +import javax.inject.*; + +import static android.database.sqlite.SQLiteDatabase.*; + +/** + * Class that imports database files exported by Rewire. + */ public class RewireDBImporter extends AbstractImporter { + private ModelFactory modelFactory; + + @Inject + public RewireDBImporter(@NonNull HabitList habits, + @NonNull ModelFactory modelFactory) + { + super(habits); + this.modelFactory = modelFactory; + } + @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 db = openDatabase(file.getPath(), null, 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,19 +70,44 @@ public class RewireDBImporter extends AbstractImporter @Override public void importHabitsFromFile(@NonNull File file) throws IOException { - final SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null, - SQLiteDatabase.OPEN_READONLY); + String path = file.getPath(); + final SQLiteDatabase db = openDatabase(path, null, OPEN_READONLY); - DatabaseHelper.executeAsTransaction(new DatabaseHelper.Command() + DatabaseUtils.executeAsTransaction(() -> createHabits(db)); + db.close(); + } + + private void createCheckmarks(@NonNull SQLiteDatabase db, + @NonNull Habit habit, + int rewireHabitId) + { + Cursor c = null; + + try { - @Override - public void execute() + 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 { - createHabits(db); - } - }); + 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)); - db.close(); + 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) @@ -75,8 +116,9 @@ public class RewireDBImporter extends AbstractImporter 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 @@ -90,37 +132,41 @@ public class RewireDBImporter extends AbstractImporter int days = c.getInt(6); int periodIndex = c.getInt(7); - Habit habit = new Habit(); - habit.name = name; - habit.description = description; + Habit habit = modelFactory.buildHabit(); + 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)); + habits.add(habit); createReminder(db, habit, id); createCheckmarks(db, habit, id); - } - while (c.moveToNext()); + } while (c.moveToNext()); } finally { @@ -128,14 +174,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 +194,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(); - } - } - - 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)); + int hour = rewireReminder / 60; + int minute = rewireReminder % 60; + WeekdayList days = new WeekdayList(reminderDays); - 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); + habits.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..2282f7293 100644 --- a/app/src/main/java/org/isoron/uhabits/io/TickmateDBImporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/TickmateDBImporter.java @@ -19,30 +19,45 @@ 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.*; +import javax.inject.*; + +/** + * Class that imports data from database files exported by Tickmate. + */ public class TickmateDBImporter extends AbstractImporter { + private ModelFactory modelFactory; + + @Inject + public TickmateDBImporter(@NonNull HabitList habits, + @NonNull ModelFactory modelFactory) + { + super(habits); + this.modelFactory = modelFactory; + } + @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 +69,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 +109,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 = modelFactory.buildHabit(); + habit.setName(name); + habit.setDescription(description); + habit.setFrequency(Frequency.DAILY); + habits.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..86488a8d1 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,195 @@ 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. + * 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. * - * 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. - * - * @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); + Repetition oldestRep = habit.getRepetitions().getOldest(); + if (oldestRep == null) return new int[0]; - if(fromTimestamp > toTimestamp) return new int[0]; + Long fromTimestamp = oldestRep.getTimestamp(); + Long toTimestamp = DateUtils.getStartOfToday(); - String query = "select value, timestamp from Checkmarks where " + - "habit = ? and timestamp >= ? and timestamp <= ?"; + return getValues(fromTimestamp, toTimestamp); + } - SQLiteDatabase db = Cache.openDatabase(); - String args[] = { habit.getId().toString(), Long.toString(fromTimestamp), - Long.toString(toTimestamp) }; - Cursor cursor = db.rawQuery(query, args); + /** + * 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); - long day = DateHelper.millisecondsInOneDay; - int nDays = (int) ((toTimestamp - fromTimestamp) / day) + 1; - int[] checks = new int[nDays]; + /** + * Returns the checkmark for today. + * + * @return checkmark for today + */ + @Nullable + public final Checkmark getToday() + { + computeAll(); + return getNewestComputed(); + } - if (cursor.moveToFirst()) - { - do - { - long timestamp = cursor.getLong(1); - int offset = (int) ((timestamp - fromTimestamp) / day); - checks[nDays - offset - 1] = cursor.getInt(0); + /** + * Returns the value of today's checkmark. + * + * @return value of today's checkmark + */ + public final int getTodayValue() + { + Checkmark today = getToday(); + if (today != null) return today.getValue(); + else return Checkmark.UNCHECKED; + } - } while (cursor.moveToNext()); - } + /** + * 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. + * + * @param from timestamp for the oldest checkmark + * @param to timestamp for the newest checkmark + * @return values for the checkmarks inside the given interval + */ + public final int[] getValues(long from, long to) + { + if(from > to) return new int[0]; + + List checkmarks = getByInterval(from, to); + int values[] = new int[checkmarks.size()]; + + int i = 0; + for (Checkmark c : checkmarks) + values[i++] = c.getValue(); - cursor.close(); - return checks; + return values; } /** - * 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. + * 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. * - * 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 timestamp the timestamp + */ + public abstract void invalidateNewerThan(long timestamp); + + /** + * Writes the entire list of checkmarks to the given writer, in CSV format. * - * @return values for the checkmarks in the interval + * @param out the writer where the CSV will be output + * @throws IOException in case write operations fail */ - @NonNull - public int[] getAllValues() + public final void writeCSV(Writer out) throws IOException { - Repetition oldestRep = habit.repetitions.getOldest(); - if(oldestRep == null) return new int[0]; + computeAll(); - Long fromTimestamp = oldestRep.timestamp; - Long toTimestamp = DateHelper.getStartOfToday(); + int values[] = getAllValues(); + long timestamp = DateUtils.getStartOfToday(); + SimpleDateFormat dateFormat = DateFormats.getCSVDateFormat(); - return getValues(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, since the first repetition until today. - * 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. + * + * This method assumes the list of computed checkmarks has no holes. That + * is, if there is a checkmark computed at time t1 and another at time t2, + * then every checkmark between t1 and t2 is also computed. + * + * @param from timestamp for the beginning of the interval + * @param to timestamp for the end of the interval */ - protected void computeAll() + protected final synchronized void compute(long from, long to) { - long fromTimestamp = habit.repetitions.getOldestTimestamp(); - if(fromTimestamp == 0) return; + final long day = DateUtils.millisecondsInOneDay; - Long toTimestamp = DateHelper.getStartOfToday(); + Checkmark newest = getNewestComputed(); + Checkmark oldest = getOldestComputed(); - compute(fromTimestamp, toTimestamp); + if (newest == null) + { + forceRecompute(from, to); + } + else + { + forceRecompute(from, oldest.getTimestamp() - day); + forceRecompute(newest.getTimestamp() + day, to); + } } /** - * 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. + * Returns oldest checkmark that has already been computed. + * + * @return oldest checkmark already computed + */ + protected abstract Checkmark getOldestComputed(); + + /** + * Computes and stores one checkmark for each day that falls inside the + * specified interval of time. + * + * This method does not check if the checkmarks have already been + * computed or not. If they have, then duplicate checkmarks will + * be stored, which is a bad thing. * * @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) + private synchronized void forceRecompute(long from, long to) { - UIHelper.throwIfMainThread(); - - final long day = DateHelper.millisecondsInOneDay; - - Checkmark newestCheckmark = findNewest(); - if(newestCheckmark != null) - from = Math.max(from, newestCheckmark.timestamp + day); + if (from > to) return; - if(from > to) return; + final long day = DateUtils.millisecondsInOneDay; + 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 +238,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 +246,44 @@ 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(); - } - finally { - db.endTransaction(); + int value = checks[i]; + long timestamp = to - i * day; + checkmarks.add(new Checkmark(timestamp, value)); } - } - /** - * 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 of the habit 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. * - * @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..5ba711407 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Habit.java +++ b/app/src/main/java/org/isoron/uhabits/models/Habit.java @@ -19,496 +19,259 @@ 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.*; - /** - * 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; - /** - * List of checkmarks belonging to this habit. - */ @NonNull - public CheckmarkList checkmarks; + private RepetitionList repetitions; - /** - * Constructs a habit with the same attributes as the specified habit. - * - * @param model the model whose attributes should be copied from - */ - public Habit(Habit model) - { - reminderDays = DateHelper.ALL_WEEK_DAYS; + @NonNull + private CheckmarkList checkmarks; - copyAttributes(model); + @Nullable + private Reminder reminder; - checkmarks = new CheckmarkList(this); - streaks = new StreakList(this); - scores = new ScoreList(this); - repetitions = new RepetitionList(this); - } + private ModelObservable observable = new ModelObservable(); /** - * 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() + @Inject + Habit(@NonNull ModelFactory factory) { 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); + + checkmarks = factory.buildCheckmarkList(this); + streaks = factory.buildStreakList(this); + scores = factory.buildScoreList(this); + repetitions = factory.buildRepetitionList(this); } /** - * Returns the habit with specified id. - * - * @param id the id of the habit - * @return the habit, or null if none exist + * Clears the reminder for a habit. */ - @Nullable - public static Habit get(long id) + public void clearReminder() { - return Habit.load(Habit.class, id); + reminder = null; + observable.notifyListeners(); } /** - * Returns a list of all habits, optionally including archived habits. + * Copies all the attributes of the specified habit into this habit * - * @param includeArchive whether archived habits should be included the list - * @return list of all habits + * @param model the model whose attributes should be copied from */ - @NonNull - public static List getAll(boolean includeArchive) + public void copyFrom(@NonNull Habit model) { - if(includeArchive) return selectWithArchived().execute(); - else return select().execute(); + 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(); } /** - * 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 + * List of checkmarks belonging to this habit. */ - @Nullable - public static Habit getByPosition(int position) + @NonNull + public CheckmarkList getCheckmarks() { - return selectWithArchived().where("position = ?", position).executeSingle(); + return checkmarks; } /** - * Changes the id of a habit on the database. - * - * @param oldId the original id - * @param newId the new id + * 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). */ - @SuppressLint("DefaultLocale") - public static void updateId(long oldId, long newId) + @NonNull + public Integer getColor() { - SQLiteUtils.execSql(String.format("update Habits set Id = %d where Id = %d", newId, oldId)); + return color; } - @NonNull - protected static From select() + public void setColor(@NonNull Integer color) { - return new Select().from(Habit.class).where("archived = 0").orderBy("position"); + this.color = color; } @NonNull - protected static From selectWithArchived() + public String getDescription() { - return new Select().from(Habit.class).orderBy("position"); + return description; } - /** - * Returns the total number of unarchived habits. - * - * @return number of unarchived habits - */ - public static int count() + public void setDescription(@NonNull String description) { - return select().count(); + this.description = description; } - /** - * Returns the total number of habits, including archived habits. - * - * @return number of habits, including archived - */ - public static int countWithArchived() + @NonNull + public Frequency getFrequency() { - return selectWithArchived().count(); + return frequency; } - /** - * 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 void setFrequency(@NonNull Frequency frequency) { - return select().where("reminder_hour is not null").execute(); + this.frequency = 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) + @Nullable + public Long getId() { - 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(); + return id; } - /** - * Recomputes the position for every habit in the database. It should never be necessary - * to call this method. - */ - public static void rebuildOrder() + public void setId(@Nullable Long id) { - List habits = selectWithArchived().execute(); - - ActiveAndroid.beginTransaction(); - try - { - int i = 0; - for (Habit h : habits) - { - h.position = i++; - h.save(); - } - - ActiveAndroid.setTransactionSuccessful(); - } - finally - { - ActiveAndroid.endTransaction(); - } - + 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..dd2b81f23 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/HabitList.java @@ -0,0 +1,231 @@ +/* + * 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 implements Iterable +{ + private ModelObservable observable; + + @NonNull + protected final HabitMatcher filter; + + /** + * 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(); + filter = new HabitMatcherBuilder() + .setArchivedAllowed(true) + .build(); + } + + protected HabitList(@NonNull HabitMatcher filter) + { + observable = new ModelObservable(); + this.filter = filter; + } + + /** + * 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 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 + * @throws IndexOutOfBoundsException when the position is invalid + */ + @NonNull + 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 abstract HabitList getFiltered(HabitMatcher matcher); + + public ModelObservable getObservable() + { + return observable; + } + + /** + * 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); + + public boolean isEmpty() + { + return size() == 0; + } + + /** + * 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); + + /** + * Removes all the habits from the list. + */ + public void removeAll() + { + List copy = new LinkedList<>(); + for (Habit h : this) copy.add(h); + for (Habit h : copy) remove(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); + + public void repair() + { + for(Habit h : this) + { + h.getCheckmarks().invalidateNewerThan(0); + h.getStreaks().invalidateNewerThan(0); + h.getScores().invalidateNewerThan(0); + } + } + + /** + * Returns the number of habits in this list. + * + * @return number of habits + */ + public abstract int size(); + + /** + * 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 : this) + { + 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(); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/HabitMatcher.java b/app/src/main/java/org/isoron/uhabits/models/HabitMatcher.java new file mode 100644 index 000000000..afc8d6ff7 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/HabitMatcher.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.models; + +import android.support.annotation.*; + +import java.util.*; + +import static org.isoron.uhabits.models.Checkmark.*; + +public class HabitMatcher +{ + public static final HabitMatcher WITH_ALARM = new HabitMatcherBuilder() + .setArchivedAllowed(true) + .setReminderRequired(true) + .build(); + + private final boolean archivedAllowed; + + private final boolean reminderRequired; + + private final boolean completedAllowed; + + private final List allowedColors; + + public HabitMatcher(boolean allowArchived, + boolean reminderRequired, + boolean completedAllowed, + @NonNull List allowedColors) + { + this.archivedAllowed = allowArchived; + this.reminderRequired = reminderRequired; + this.completedAllowed = completedAllowed; + this.allowedColors = allowedColors; + } + + public List getAllowedColors() + { + return allowedColors; + } + + public boolean isArchivedAllowed() + { + return archivedAllowed; + } + + public boolean isCompletedAllowed() + { + return completedAllowed; + } + + public boolean isReminderRequired() + { + return reminderRequired; + } + + public boolean matches(Habit habit) + { + if (!isArchivedAllowed() && habit.isArchived()) return false; + if (isReminderRequired() && !habit.hasReminder()) return false; + + if(!isCompletedAllowed()) + { + int todayCheckmark = habit.getCheckmarks().getTodayValue(); + if (todayCheckmark != UNCHECKED) return false; + } + + if(!allowedColors.contains(habit.getColor())) return false; + return true; + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/HabitMatcherBuilder.java b/app/src/main/java/org/isoron/uhabits/models/HabitMatcherBuilder.java new file mode 100644 index 000000000..facc8cc4b --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/HabitMatcherBuilder.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.models; + +import org.isoron.uhabits.utils.*; + +import java.util.*; + +public class HabitMatcherBuilder +{ + private boolean archivedAllowed = false; + + private boolean reminderRequired = false; + + private boolean completedAllowed = true; + + private List allowedColors = allColors(); + + private static List allColors() + { + List colors = new ArrayList<>(); + for(int i = 0; i < ColorUtils.CSV_PALETTE.length; i++) + colors.add(i); + return colors; + } + + public HabitMatcher build() + { + return new HabitMatcher(archivedAllowed, reminderRequired, + completedAllowed, allowedColors); + } + + public HabitMatcherBuilder setArchivedAllowed(boolean archivedAllowed) + { + this.archivedAllowed = archivedAllowed; + return this; + } + + public HabitMatcherBuilder setAllowedColors(List allowedColors) + { + this.allowedColors = allowedColors; + return this; + } + + public HabitMatcherBuilder setCompletedAllowed(boolean completedAllowed) + { + this.completedAllowed = completedAllowed; + return this; + } + + public HabitMatcherBuilder setReminderRequired(boolean reminderRequired) + { + this.reminderRequired = reminderRequired; + return this; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/models/HabitNotFoundException.java b/app/src/main/java/org/isoron/uhabits/models/HabitNotFoundException.java new file mode 100644 index 000000000..74302e2ad --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/HabitNotFoundException.java @@ -0,0 +1,38 @@ +/* + * 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 class HabitNotFoundException extends RuntimeException { + public HabitNotFoundException() { + super(); + } + + public HabitNotFoundException(String message) { + super(message); + } + + public HabitNotFoundException(Throwable cause) { + super(cause); + } + + public HabitNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} 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..50c352411 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/ModelFactory.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.models; + +/** + * Interface implemented by factories that provide concrete implementations of + * the core model classes. + */ +public interface ModelFactory +{ + CheckmarkList buildCheckmarkList(Habit habit); + + default Habit buildHabit() + { + return new Habit(this); + } + + HabitList buildHabitList(); + + RepetitionList buildRepetitionList(Habit habit); + + 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..88d54e293 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/Reminder.java @@ -0,0 +1,54 @@ +/* + * 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.*; + +public final class Reminder +{ + private final int hour; + + private final int minute; + + private final WeekdayList days; + + public Reminder(int hour, int minute, @NonNull WeekdayList days) + { + this.hour = hour; + this.minute = minute; + this.days = days; + } + + @NonNull + public WeekdayList 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..07fa7b681 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,194 @@ 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); + // TODO: Change order timestamp desc + public abstract List getByInterval(long fromTimestamp, + long toTimestamp); - habit.scores.invalidateNewerThan(timestamp); - habit.checkmarks.deleteNewerThan(timestamp); - habit.streaks.deleteNewerThan(timestamp); - } + /** + * Returns the repetition that has the given timestamp, or null if none + * exists. + * + * @param timestamp the repetition timestamp. + * @return the repetition that has the given timestamp. + */ + @Nullable + public abstract Repetition getByTimestamp(long timestamp); - private void insert(long timestamp) + @NonNull + public ModelObservable getObservable() { - String[] args = { habit.getId().toString(), Long.toString(timestamp) }; - SQLiteUtils.execSql("insert into Repetitions(habit, timestamp) values (?,?)", args); + return observable; } /** - * Returns the oldest repetition for the habit. If there is no repetition, returns null. - * 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 oldest repetition for the habit + * @return oldest repetition in the list, or null if list is empty. */ @Nullable - public Repetition getOldest() - { - return (Repetition) select().limit(1).executeSingle(); - } - + public abstract Repetition getOldest(); + @Nullable /** - * Returns the timestamp of the oldest repetition. If there are no repetitions, returns zero. - * Repetitions in the future are discarded. + * Returns the newest repetition in the list. + *

+ * If the list is empty, returns null. Repetitions in the past are + * discarded. * - * @return timestamp of the oldest repetition + * @return newest 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); - } + public abstract Repetition getNewest(); /** - * 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); + List reps = getByInterval(0, DateUtils.getStartOfToday()); + HashMap map = new HashMap<>(); - if(!cursor.moveToFirst()) return new HashMap<>(); - - HashMap map = new HashMap<>(); - GregorianCalendar date = DateHelper.getStartOfTodayCalendar(); - - 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 = DateUtils.getWeekday(r.getTimestamp()); + 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 from beginning of the interval - * @param to end of the interval - * @return number of repetition in the given interval + * @param repetition the repetition to be removed */ - public int count(long from, long to) + 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 timestamp the timestamp for the timestamp that should be added or + * removed. + * @return the repetition that has been added or removed. + */ + @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; } + + /** + * Returns the number of all repetitions + * + * @return number of all repetitions + */ + @NonNull + public abstract long getTotalCount(); } 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..0891cc01d 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,305 @@ 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; + observable = new ModelObservable(); } - protected From select() + /** + * 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 scores the scores to add. + */ + public abstract void add(List scores); + + public ModelObservable getObservable() { - return new Select() - .from(Score.class) - .where("habit = ?", habit.getId()) - .orderBy("timestamp desc"); + return observable; } /** - * 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. + * Returns the value of the score for today. * - * @param timestamp the oldest timestamp that should be invalidated + * @return value of today's score */ - public void invalidateNewerThan(long timestamp) + public int getTodayValue() { - new Delete().from(Score.class) - .where("habit = ?", habit.getId()) - .and("timestamp >= ?", timestamp) - .execute(); + return getValue(DateUtils.getStartOfToday()); } /** - * Computes and saves the scores that are missing since the first repetition of the habit. + * Returns the value of the score for a given day. + *

+ * If the timestamp given happens before the first repetition of the habit + * then returns zero. + * + * @param timestamp the timestamp of a day + * @return score value for that day */ - private void computeAll() + public final int getValue(long timestamp) { - long fromTimestamp = habit.repetitions.getOldestTimestamp(); - if(fromTimestamp == 0) return; - - long toTimestamp = DateHelper.getStartOfToday(); - compute(fromTimestamp, toTimestamp); + compute(timestamp, timestamp); + Score s = getComputedByTimestamp(timestamp); + if(s == null) throw new IllegalStateException(); + return s.getValue(); } /** - * 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. + * Returns the list of scores that fall within the given interval. + *

+ * There is exactly one score per day in the interval. The endpoints of + * the interval are included. The list is ordered by timestamp (decreasing). + * That is, the first score corresponds to the newest timestamp, and the + * last score corresponds to the oldest timestamp. * - * 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 fromTimestamp timestamp of the beginning of the interval. + * @param toTimestamp timestamp of the end of the interval. + * @return the list of scores within the interval. + */ + @NonNull + public abstract List getByInterval(long fromTimestamp, + long toTimestamp); + + /** + * Returns the values of the scores 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. * - * @param from timestamp of the beginning of the interval - * @param to timestamp of the end of the time interval + * @param from timestamp for the oldest score + * @param to timestamp for the newest score + * @return values for the scores inside the given interval */ - protected void compute(long from, long to) + public final int[] getValues(long from, long to) { - 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; + List scores = getByInterval(from, to); + int[] values = new int[scores.size()]; - final int checkmarkValues[] = habit.checkmarks.getValues(from, to); - final long beginning = from; + for(int i = 0; i < values.length; i++) + values[i] = scores.get(i).getValue(); - 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; - } + return values; + } - insert(timestamps, values); + public List groupBy(DateUtils.TruncateField field) + { + computeAll(); + HashMap> groups = getGroupedValues(field); + List scores = groupsToAvgScores(groups); + Collections.sort(scores, (s1, s2) -> s2.compareNewer(s1)); + return scores; } /** - * Returns the value of the most recent score that was already computed. If no score has been - * computed yet, returns zero. + * 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. * - * @return value of newest score, or zero if none exist + * @param timestamp the oldest timestamp that should be invalidated */ - protected int findNewestValue() + public abstract void invalidateNewerThan(long timestamp); + + @Override + public Iterator iterator() { - String args[] = { habit.getId().toString() }; - String query = "select score from Score where habit = ? order by timestamp desc limit 1"; - return SQLiteUtils.intQuery(query, args); + return toList().iterator(); } - private long findNewestTimestamp() + /** + * Returns a Java list of scores, containing one score for each day, from + * the first repetition of the habit until today. + *

+ * The scores are sorted by decreasing timestamp. The first score + * corresponds to today. + * + * @return list of scores + */ + public abstract List toList(); + + public void writeCSV(Writer out) throws IOException { - String args[] = { habit.getId().toString() }; - String query = "select timestamp from Score where habit = ? order by timestamp desc limit 1"; - return DatabaseHelper.longQuery(query, args); + computeAll(); + SimpleDateFormat dateFormat = DateFormats.getCSVDateFormat(); + + 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)); + } } - private void insert(long timestamps[], long values[]) + /** + * Computes and stores one score for each day inside the given 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 method assumes the list of computed scores has no holes. That is, if + * there is a score computed at time t1 and another at time t2, then every + * score between t1 and t2 is also computed. + * + * @param from timestamp of the beginning of the interval + * @param to timestamp of the end of the time interval + */ + protected synchronized void compute(long from, long to) { - String query = "insert into Score(habit, timestamp, score) values (?,?,?)"; + final long day = DateUtils.millisecondsInOneDay; - SQLiteDatabase db = Cache.openDatabase(); - db.beginTransaction(); + Score newest = getNewestComputed(); + Score oldest = getOldestComputed(); - try + if (newest == null) { - 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(); + Repetition oldestRep = habit.getRepetitions().getOldest(); + if (oldestRep != null) + from = Math.min(from, oldestRep.getTimestamp()); + forceRecompute(from, to, 0); } - finally + else { - db.endTransaction(); + if (oldest == null) throw new IllegalStateException(); + forceRecompute(from, oldest.getTimestamp() - day, 0); + forceRecompute(newest.getTimestamp() + day, to, + newest.getValue()); } } /** - * Returns the score for a certain day. - * - * @param timestamp the timestamp for the day - * @return the score for the day + * Computes and saves the scores that are missing since the first repetition + * of the habit. */ - @Nullable - protected Score get(long timestamp) + protected void computeAll() { - Repetition oldestRep = habit.repetitions.getOldest(); - if(oldestRep == null) return null; - - compute(oldestRep.timestamp, timestamp); + Repetition oldestRep = habit.getRepetitions().getOldest(); + if (oldestRep == null) return; - return select().where("timestamp = ?", timestamp).executeSingle(); + long today = DateUtils.getStartOfToday(); + compute(oldestRep.getTimestamp(), today); } /** - * Returns the value of the score for a given day. + * Returns the score that has the given timestamp, if it has already been + * computed. If that score has not been computed yet, returns null. * - * @param timestamp the timestamp of a day - * @return score for that day + * @param timestamp the timestamp of the score + * @return the score with given timestamp, or null not yet computed. */ - public int getValue(long timestamp) - { - computeAll(); - String[] args = { habit.getId().toString(), Long.toString(timestamp) }; - return SQLiteUtils.intQuery("select score from Score where habit = ? and timestamp = ?", args); - } + @Nullable + protected abstract Score getComputedByTimestamp(long timestamp); /** - * 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 + * Returns the most recent score that has already been computed. If no score + * has been computed yet, returns null. */ - @NonNull - public int[] getAllValues(long divisor) - { - Repetition oldestRep = habit.repetitions.getOldest(); - if(oldestRep == null) return new int[0]; + @Nullable + protected abstract Score getNewestComputed(); - long fromTimestamp = oldestRep.timestamp; - long toTimestamp = DateHelper.getStartOfToday(); - return getValues(fromTimestamp, toTimestamp, divisor); - } + /** + * Returns oldest score already computed. If no score has been computed yet, + * returns null. + */ + @Nullable + protected abstract Score getOldestComputed(); /** - * Same as getAllValues(long), but using a specified interval. + * Computes and stores one score for each day inside the given interval. + *

+ * This function does not check if the scores have already been computed. If + * they have, then it stores duplicate scores, which is a bad thing. * - * @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 interval + * @param previousValue value of the score on the day immediately before the + * interval begins */ - @NonNull - protected int[] getValues(long from, long to, long divisor) + private void forceRecompute(long from, long to, int previousValue) { - compute(from, to); - - divisor *= DateHelper.millisecondsInOneDay; - Long offset = to + divisor; + if(from > to) return; - String query = "select ((timestamp - ?) / ?) as time, avg(score) from Score " + - "where habit = ? and timestamp >= ? and timestamp <= ? " + - "group by time order by time desc"; + final long day = DateUtils.millisecondsInOneDay; - String params[] = { offset.toString(), Long.toString(divisor), habit.getId().toString(), - Long.toString(from), Long.toString(to) }; + final double freq = habit.getFrequency().toDouble(); + final int checkmarkValues[] = habit.getCheckmarks().getValues(from, to); - SQLiteDatabase db = Cache.openDatabase(); - Cursor cursor = db.rawQuery(query, params); + List scores = new LinkedList<>(); - if(!cursor.moveToFirst()) return new int[0]; - - int k = 0; - int[] scores = new int[cursor.getCount()]; - - do + for (int i = 0; i < checkmarkValues.length; i++) { - scores[k++] = (int) cursor.getFloat(1); + int value = checkmarkValues[checkmarkValues.length - i - 1]; + previousValue = Score.compute(freq, previousValue, value); + scores.add(new Score(from + day * i, previousValue)); } - while (cursor.moveToNext()); - cursor.close(); - return scores; + add(scores); } - /** - * Returns the score for today. - * - * @return score for today - */ - @Nullable - protected Score getToday() - { - return get(DateHelper.getStartOfToday()); - } - - /** - * Returns the value of the score for today. - * - * @return value of today's score - */ - public int getTodayValue() + @NonNull + private HashMap> getGroupedValues(DateUtils.TruncateField field) { - return getValue(DateHelper.getStartOfToday()); - } + HashMap> groups = new HashMap<>(); - /** - * Returns the star status for today. The returned value is either Score.EMPTY_STAR, - * Score.HALF_STAR or Score.FULL_STAR. - * - * @return star status for today - */ - public int getTodayStarStatus() - { - Score score = getToday(); - if(score != null) return score.getStarStatus(); - else return Score.EMPTY_STAR; - } - - public void writeCSV(Writer out) throws IOException - { - computeAll(); + for (Score s : this) + { + long groupTimestamp = DateUtils.truncate(field, s.getTimestamp()); - SimpleDateFormat dateFormat = DateHelper.getCSVDateFormat(); + if (!groups.containsKey(groupTimestamp)) + groups.put(groupTimestamp, new ArrayList<>()); - String query = "select timestamp, score from score where habit = ? order by timestamp"; - String params[] = { habit.getId().toString() }; + groups.get(groupTimestamp).add((long) s.getValue()); + } - SQLiteDatabase db = Cache.openDatabase(); - Cursor cursor = db.rawQuery(query, params); + return groups; + } - if(!cursor.moveToFirst()) return; + @NonNull + private List groupsToAvgScores(HashMap> groups) + { + List scores = new LinkedList<>(); - do + 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..1176aa71c 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 synchronized 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(); - - long beginning; - long today = DateHelper.getStartOfToday(); - long day = DateHelper.millisecondsInOneDay; + ArrayList transitions = getTransitions(beginning, checks); - 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/WeekdayList.java b/app/src/main/java/org/isoron/uhabits/models/WeekdayList.java new file mode 100644 index 000000000..21a35b97a --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/WeekdayList.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; + +import java.util.*; + +public class WeekdayList +{ + public static WeekdayList EVERY_DAY = new WeekdayList(127); + + private final boolean[] weekdays; + + public WeekdayList(int packedList) + { + if(packedList == 0) packedList = 127; + weekdays = new boolean[7]; + + int current = 1; + for (int i = 0; i < 7; i++) + { + if ((packedList & current) != 0) weekdays[i] = true; + current = current << 1; + } + } + + public WeekdayList(boolean weekdays[]) + { + boolean isEmpty = true; + for(boolean b : weekdays) if(b) isEmpty = false; + if(isEmpty) throw new IllegalArgumentException("empty list"); + + this.weekdays = Arrays.copyOf(weekdays, 7); + } + + public boolean[] toArray() + { + return weekdays; + } + + public int toInteger() + { + int packedList = 0; + int current = 1; + + for (int i = 0; i < 7; i++) + { + if (weekdays[i]) packedList |= current; + current = current << 1; + } + + return packedList; + } +} 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..5b02be26a --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryCheckmarkList.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.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 getOldestComputed() + { + if(list.isEmpty()) return null; + return list.getLast(); + } + + @Override + protected Checkmark getNewestComputed() + { + if(list.isEmpty()) return null; + return list.getFirst(); + } + +} 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..e014107ce --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryHabitList.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.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() + { + super(); + list = new LinkedList<>(); + } + + protected MemoryHabitList(@NonNull HabitMatcher matcher) + { + super(matcher); + 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 Habit getById(long id) + { + for (Habit h : list) + { + if (h.getId() == null) continue; + if (h.getId() == id) return h; + } + return null; + } + + @NonNull + @Override + public Habit getByPosition(int position) + { + return list.get(position); + } + + @NonNull + @Override + public HabitList getFiltered(HabitMatcher matcher) + { + MemoryHabitList habits = new MemoryHabitList(matcher); + for(Habit h : this) if (matcher.matches(h)) habits.add(h); + return habits; + } + + @Override + public int indexOf(@NonNull Habit h) + { + return list.indexOf(h); + } + + @Override + public Iterator iterator() + { + return Collections.unmodifiableCollection(list).iterator(); + } + + @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 int size() + { + return list.size(); + } + + @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..0a934d97a --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryModelFactory.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.models.memory; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; + +import dagger.*; + +@Module +public class MemoryModelFactory implements ModelFactory +{ + @Provides + @AppScope + public static HabitList provideHabitList() + { + return new MemoryHabitList(); + } + + @Provides + @AppScope + public static ModelFactory provideModelFactory() + { + return new MemoryModelFactory(); + } + + @Override + public CheckmarkList buildCheckmarkList(Habit habit) + { + return new MemoryCheckmarkList(habit); + } + + @Override + public HabitList buildHabitList() + { + return new MemoryHabitList(); + } + + @Override + public RepetitionList buildRepetitionList(Habit habit) + { + return new MemoryRepetitionList(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..dbac82b40 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryRepetitionList.java @@ -0,0 +1,128 @@ +/* + * 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; + } + + @Nullable + @Override + public Repetition getNewest() + { + long newestTime = -1; + Repetition newestRep = null; + + for (Repetition rep : list) + { + if (rep.getTimestamp() > newestTime) + { + newestRep = rep; + newestTime = rep.getTimestamp(); + } + + } + + return newestRep; + } + + @Override + public void remove(@NonNull Repetition repetition) + { + list.remove(repetition); + observable.notifyListeners(); + } + + @NonNull + @Override + public long getTotalCount() + { + return list.size(); + } +} 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..5dafb166d --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryScoreList.java @@ -0,0 +1,106 @@ +/* + * 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 +{ + LinkedList list; + + public MemoryScoreList(Habit habit) + { + super(habit); + list = new LinkedList<>(); + } + + @Override + public void add(List scores) + { + list.addAll(scores); + Collections.sort(list, + (s1, s2) -> Long.signum(s2.getTimestamp() - s1.getTimestamp())); + } + + @NonNull + @Override + public List getByInterval(long fromTimestamp, long toTimestamp) + { + compute(fromTimestamp, toTimestamp); + + List filtered = new LinkedList<>(); + + for (Score s : list) + if (s.getTimestamp() >= fromTimestamp && + s.getTimestamp() <= toTimestamp) filtered.add(s); + + return filtered; + } + + @Nullable + @Override + public Score getComputedByTimestamp(long timestamp) + { + for (Score s : list) + if (s.getTimestamp() == timestamp) return s; + + return null; + } + + @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 toList() + { + computeAll(); + return new LinkedList<>(list); + } + + @Nullable + @Override + protected Score getNewestComputed() + { + if (list.isEmpty()) return null; + return list.getFirst(); + } + + @Nullable + @Override + protected Score getOldestComputed() + { + if (list.isEmpty()) return null; + return list.getLast(); + } +} 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/InconsistentDatabaseException.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/InconsistentDatabaseException.java new file mode 100644 index 000000000..2a3d862ca --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/InconsistentDatabaseException.java @@ -0,0 +1,28 @@ +/* + * 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; + +public class InconsistentDatabaseException extends RuntimeException +{ + public InconsistentDatabaseException(String message) + { + super(message); + } +} 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..7cdbcbc4f --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLModelFactory.java @@ -0,0 +1,75 @@ +/* + * 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.*; +import org.isoron.uhabits.models.*; + +import dagger.*; + +/** + * Factory that provides models backed by an SQLite database. + */ +@Module +public class SQLModelFactory implements ModelFactory +{ + @Provides + public static ModelFactory provideModelFactory() + { + return new SQLModelFactory(); + } + + @Provides + @AppScope + public static HabitList provideHabitList() + { + return SQLiteHabitList.getInstance(provideModelFactory()); + } + + @Override + public CheckmarkList buildCheckmarkList(Habit habit) + { + return new SQLiteCheckmarkList(habit); + } + + @Override + public HabitList buildHabitList() + { + return SQLiteHabitList.getInstance(provideModelFactory()); + } + + @Override + public RepetitionList buildRepetitionList(Habit habit) + { + return new SQLiteRepetitionList(habit); + } + + @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..7c9b552e0 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteCheckmarkList.java @@ -0,0 +1,182 @@ +/* + * 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.isoron.uhabits.utils.*; +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()); + compute(fromTimestamp, toTimestamp); + + 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; + + int nDays = DateUtils.getDaysBetween(fromTimestamp, toTimestamp) + 1; + if (records.size() != nDays) + { + throw new InconsistentDatabaseException( + String.format("habit=%s, %d expected, %d found", + habit.getName(), nDays, records.size())); + } + + 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()) }; + return getSingleCheckmarkFromQuery(query, params); + } + + @Override + protected Checkmark getOldestComputed() + { + check(habit.getId()); + String query = "select habit, timestamp, value " + + "from checkmarks " + + "where habit = ? " + + "order by timestamp asc " + + "limit 1"; + + String params[] = { Long.toString(habit.getId()) }; + return getSingleCheckmarkFromQuery(query, params); + } + + @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"); + } + + @Nullable + private Checkmark getSingleCheckmarkFromQuery(String query, String params[]) + { + CheckmarkRecord record = sqlite.querySingle(query, params); + if (record == null) return null; + record.habit = habitRecord; + return record.toCheckmark(); + } + + @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..6541a81d3 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteHabitList.java @@ -0,0 +1,292 @@ +/* + * 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 com.activeandroid.query.*; +import com.activeandroid.util.*; + +import org.apache.commons.lang3.*; +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 HashMap cache; + + private static SQLiteHabitList instance; + + private final SQLiteUtils sqlite; + + private final ModelFactory modelFactory; + + public SQLiteHabitList(@NonNull ModelFactory modelFactory) + { + super(); + this.modelFactory = modelFactory; + + if (cache == null) cache = new HashMap<>(); + sqlite = new SQLiteUtils<>(HabitRecord.class); + } + + protected SQLiteHabitList(@NonNull ModelFactory modelFactory, + @NonNull HabitMatcher filter) + { + super(filter); + this.modelFactory = modelFactory; + + if (cache == null) cache = new HashMap<>(); + sqlite = new SQLiteUtils<>(HabitRecord.class); + } + + public static SQLiteHabitList getInstance( + @NonNull ModelFactory modelFactory) + { + if (instance == null) instance = new SQLiteHabitList(modelFactory); + 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 = size(); + + Long id = habit.getId(); + if (id == null) id = record.save(); + else record.save(id); + + if (id < 0) + throw new IllegalArgumentException("habit could not be saved"); + + habit.setId(id); + cache.put(id, habit); + } + + @Override + @Nullable + public Habit getById(long id) + { + if (!cache.containsKey(id)) + { + HabitRecord record = HabitRecord.get(id); + if (record == null) return null; + + Habit habit = modelFactory.buildHabit(); + record.copyTo(habit); + cache.put(id, habit); + } + + return cache.get(id); + } + + @Override + @NonNull + public Habit getByPosition(int position) + { + return toList().get(position); + } + + @NonNull + @Override + public HabitList getFiltered(HabitMatcher filter) + { + return new SQLiteHabitList(modelFactory, filter); + } + + @Override + public int indexOf(@NonNull Habit h) + { + return toList().indexOf(h); + } + + @Override + public Iterator iterator() + { + return Collections.unmodifiableCollection(toList()).iterator(); + } + + public void rebuildOrder() + { + List habits = toList(); + + 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 removeAll() + { + sqlite.query("delete from checkmarks", null); + sqlite.query("delete from score", null); + sqlite.query("delete from streak", null); + sqlite.query("delete from repetitions", null); + sqlite.query("delete from habits", null); + } + + @Override + public synchronized void reorder(Habit from, Habit to) + { + if (from == to) return; + + HabitRecord fromRecord = HabitRecord.get(from.getId()); + HabitRecord toRecord = HabitRecord.get(to.getId()); + + if (fromRecord == null) + throw new RuntimeException("habit not in database"); + if (toRecord == null) + throw new RuntimeException("habit not in database"); + + Integer fromPos = fromRecord.position; + Integer toPos = toRecord.position; + + Log.d("SQLiteHabitList", + String.format("reorder: %d %d", fromPos, toPos)); + + 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(); + } + + fromRecord.position = toPos; + fromRecord.save(); + update(from); + getObservable().notifyListeners(); + } + + @Override + public int size() + { + return toList().size(); + } + + @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(); + } + } + + protected List toList() + { + String query = buildSelectQuery(); + List 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"); + + if (!filter.matches(habit)) continue; + habits.add(habit); + } + + return habits; + } + + private void appendOrderBy(StringBuilder query) + { + query.append("order by position "); + } + + private void appendSelect(StringBuilder query) + { + query.append(HabitRecord.SELECT); + } + + private void appendWhere(StringBuilder query) + { + ArrayList where = new ArrayList<>(); + if (filter.isReminderRequired()) where.add("reminder_hour is not null"); + if (!filter.isArchivedAllowed()) where.add("archived = 0"); + + if (where.isEmpty()) return; + query.append("where "); + query.append(StringUtils.join(where, " and ")); + query.append(" "); + } + + private String buildSelectQuery() + { + StringBuilder query = new StringBuilder(); + appendSelect(query); + appendWhere(query); + appendOrderBy(query); + return query.toString(); + } + + @Override + public void repair() + { + super.repair(); + rebuildOrder(); + } +} 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..6278863e9 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteRepetitionList.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.models.sqlite; + +import android.database.DatabaseUtils; +import android.database.sqlite.SQLiteDatabase; +import android.support.annotation.*; +import android.support.annotation.Nullable; + +import com.activeandroid.Cache; +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 Repetition getNewest() + { + check(habit.getId()); + String query = "select habit, timestamp " + + "from Repetitions " + + "where habit = ? " + + "order by timestamp desc " + + "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; + } + + @NonNull + @Override + public long getTotalCount() + { + SQLiteDatabase db = Cache.openDatabase(); + + return DatabaseUtils.queryNumEntries(db, "Repetitions", + "habit=?", new String[] { Long.toString(habit.getId()) }); + } +} 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..e44e7e6ed --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteScoreList.java @@ -0,0 +1,207 @@ +/* + * 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(); + } + } + + @NonNull + @Override + public List getByInterval(long fromTimestamp, long toTimestamp) + { + check(habit.getId()); + compute(fromTimestamp, toTimestamp); + + String query = "select habit, timestamp, score " + + "from Score " + + "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 (ScoreRecord record : records) record.habit = habitRecord; + return toScores(records); + } + + @Override + @Nullable + public Score getComputedByTimestamp(long timestamp) + { + check(habit.getId()); + + 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) }; + + return getScoreFromQuery(query, params); + } + + @Override + public void invalidateNewerThan(long timestamp) + { + new Delete() + .from(ScoreRecord.class) + .where("habit = ?", habit.getId()) + .and("timestamp >= ?", timestamp) + .execute(); + + getObservable().notifyListeners(); + } + + @Override + @NonNull + public List toList() + { + 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; + + return toScores(records); + } + + @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()) }; + return getScoreFromQuery(query, params); + } + + @Nullable + @Override + protected Score getOldestComputed() + { + check(habit.getId()); + String query = "select habit, timestamp, score from Score " + + "where habit = ? order by timestamp asc " + + "limit 1"; + + String params[] = { Long.toString(habit.getId()) }; + return getScoreFromQuery(query, params); + } + + @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"); + } + + @Nullable + private Score getScoreFromQuery(String query, String[] params) + { + ScoreRecord record = sqlite.querySingle(query, params); + if (record == null) return null; + record.habit = habitRecord; + return record.toScore(); + } + + @NonNull + private List toScores(@NonNull List records) + { + List scores = new LinkedList<>(); + for (ScoreRecord r : records) scores.add(r.toScore()); + return scores; + } +} 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..6938b505d --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/records/CheckmarkRecord.java @@ -0,0 +1,67 @@ +/* + * 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.*; + +/** + * 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() + { + 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..71f4cbdd3 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/records/HabitRecord.java @@ -0,0 +1,220 @@ +/* + * 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(); + this.reminderDays = 0; + this.reminderMin = null; + this.reminderHour = null; + + if (model.hasReminder()) + { + Reminder reminder = model.getReminder(); + this.reminderHour = reminder.getHour(); + this.reminderMin = reminder.getMinute(); + this.reminderDays = reminder.getDays().toInteger(); + } + } + + @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, + new WeekdayList(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..5f831495f --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/records/RepetitionRecord.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.models.sqlite.records; + +import android.database.*; + +import com.activeandroid.*; +import com.activeandroid.annotation.*; + +import org.isoron.uhabits.models.*; + +/** + * 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() + { + 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..daac00d3f --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/records/ScoreRecord.java @@ -0,0 +1,67 @@ +/* + * 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.*; + +/** + * 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() + { + 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..1638089db --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/records/StreakRecord.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.models.sqlite.records; + +import android.database.*; + +import com.activeandroid.*; +import com.activeandroid.annotation.*; + +import org.isoron.uhabits.models.*; + +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() + { + 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/notifications/NotificationTray.java b/app/src/main/java/org/isoron/uhabits/notifications/NotificationTray.java new file mode 100644 index 000000000..d6a0253a2 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/notifications/NotificationTray.java @@ -0,0 +1,248 @@ +/* + * 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.notifications; + +import android.app.*; +import android.content.*; +import android.graphics.*; +import android.support.annotation.*; +import android.support.v4.app.*; +import android.support.v4.app.NotificationCompat.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.commands.*; +import org.isoron.uhabits.intents.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.preferences.*; +import org.isoron.uhabits.tasks.*; +import org.isoron.uhabits.utils.*; + +import java.util.*; + +import javax.inject.*; + +import static android.graphics.BitmapFactory.*; +import static org.isoron.uhabits.utils.RingtoneUtils.*; + +@AppScope +public class NotificationTray + implements CommandRunner.Listener, Preferences.Listener +{ + @NonNull + private final Context context; + + @NonNull + private final TaskRunner taskRunner; + + @NonNull + private final PendingIntentFactory pendingIntents; + + @NonNull + private final CommandRunner commandRunner; + + @NonNull + private final Preferences preferences; + + @NonNull + private final HashMap active; + + @Inject + public NotificationTray(@AppContext @NonNull Context context, + @NonNull TaskRunner taskRunner, + @NonNull PendingIntentFactory pendingIntents, + @NonNull CommandRunner commandRunner, + @NonNull Preferences preferences) + { + this.context = context; + this.taskRunner = taskRunner; + this.pendingIntents = pendingIntents; + this.commandRunner = commandRunner; + this.preferences = preferences; + this.active = new HashMap<>(); + } + + public void cancel(@NonNull Habit habit) + { + int notificationId = getNotificationId(habit); + NotificationManagerCompat.from(context).cancel(notificationId); + active.remove(habit); + } + + @Override + public void onCommandExecuted(@NonNull Command command, + @Nullable Long refreshKey) + { + if (command instanceof ToggleRepetitionCommand) + { + ToggleRepetitionCommand toggleCmd = + (ToggleRepetitionCommand) command; + + Habit habit = toggleCmd.getHabit(); + if (habit.getCheckmarks().getTodayValue() != Checkmark.UNCHECKED) + cancel(habit); + } + + if (command instanceof DeleteHabitsCommand) + { + DeleteHabitsCommand deleteCommand = (DeleteHabitsCommand) command; + List deleted = deleteCommand.getHabits(); + for (Habit habit : deleted) + cancel(habit); + } + } + + @Override + public void onNotificationsChanged() + { + reshowAll(); + } + + public void show(@NonNull Habit habit, long timestamp, long reminderTime) + { + NotificationData data = new NotificationData(timestamp, reminderTime); + active.put(habit, data); + taskRunner.execute(new ShowNotificationTask(habit, data)); + } + + public void startListening() + { + commandRunner.addListener(this); + preferences.addListener(this); + } + + public void stopListening() + { + commandRunner.removeListener(this); + preferences.removeListener(this); + } + + private int getNotificationId(Habit habit) + { + Long id = habit.getId(); + if (id == null) return 0; + return (int) (id % Integer.MAX_VALUE); + } + + private void reshowAll() + { + for (Habit habit : active.keySet()) + { + NotificationData data = active.get(habit); + taskRunner.execute(new ShowNotificationTask(habit, data)); + } + } + + class NotificationData + { + public final long timestamp; + + public final long reminderTime; + + public NotificationData(long timestamp, long reminderTime) + { + this.timestamp = timestamp; + this.reminderTime = reminderTime; + } + } + + private class ShowNotificationTask implements Task + { + int todayValue; + + private final Habit habit; + + private final long timestamp; + + private final long reminderTime; + + public ShowNotificationTask(Habit habit, NotificationData data) + { + this.habit = habit; + this.timestamp = data.timestamp; + this.reminderTime = data.reminderTime; + } + + @Override + public void doInBackground() + { + todayValue = habit.getCheckmarks().getTodayValue(); + } + + @Override + public void onPostExecute() + { + if (todayValue != Checkmark.UNCHECKED) return; + if (!shouldShowReminderToday()) return; + if (!habit.hasReminder()) return; + + Action checkAction = new Action(R.drawable.ic_action_check, + context.getString(R.string.check), + pendingIntents.addCheckmark(habit, timestamp)); + + Action snoozeAction = new Action(R.drawable.ic_action_snooze, + context.getString(R.string.snooze), + pendingIntents.snoozeNotification(habit)); + + Bitmap wearableBg = + decodeResource(context.getResources(), R.drawable.stripe); + + // Even though the set of actions is the same on the phone and + // on the watch, Pebble requires us to add them to the + // WearableExtender. + WearableExtender wearableExtender = new WearableExtender() + .setBackground(wearableBg) + .addAction(checkAction) + .addAction(snoozeAction); + + Notification notification = new NotificationCompat.Builder(context) + .setSmallIcon(R.drawable.ic_notification) + .setContentTitle(habit.getName()) + .setContentText(habit.getDescription()) + .setContentIntent(pendingIntents.showHabit(habit)) + .setDeleteIntent(pendingIntents.dismissNotification(habit)) + .addAction(checkAction) + .addAction(snoozeAction) + .setSound(getRingtoneUri(context)) + .extend(wearableExtender) + .setWhen(reminderTime) + .setShowWhen(true) + .setOngoing(preferences.shouldMakeNotificationsSticky()) + .build(); + + NotificationManager notificationManager = + (NotificationManager) context.getSystemService( + Activity.NOTIFICATION_SERVICE); + + int notificationId = getNotificationId(habit); + notificationManager.notify(notificationId, notification); + } + + private boolean shouldShowReminderToday() + { + if (!habit.hasReminder()) return false; + Reminder reminder = habit.getReminder(); + + boolean reminderDays[] = reminder.getDays().toArray(); + int weekday = DateUtils.getWeekday(timestamp); + + return reminderDays[weekday]; + } + } +} 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/preferences/Preferences.java b/app/src/main/java/org/isoron/uhabits/preferences/Preferences.java new file mode 100644 index 000000000..86e5e5278 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/preferences/Preferences.java @@ -0,0 +1,239 @@ +/* + * 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.preferences; + +import android.content.*; +import android.preference.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.*; + +import java.util.*; + +import javax.inject.*; + +@AppScope +public class Preferences + implements SharedPreferences.OnSharedPreferenceChangeListener +{ + private final Context context; + + private SharedPreferences prefs; + + private Boolean shouldReverseCheckmarks = null; + + private LinkedList listeners; + + @Inject + public Preferences(@AppContext Context context) + { + this.context = context; + listeners = new LinkedList<>(); + + prefs = PreferenceManager.getDefaultSharedPreferences(context); + prefs.registerOnSharedPreferenceChangeListener(this); + } + + public void addListener(Listener listener) + { + listeners.add(listener); + } + + public Integer getDefaultHabitColor(int fallbackColor) + { + return prefs.getInt("pref_default_habit_palette_color", fallbackColor); + } + + public int getDefaultScoreSpinnerPosition() + { + int defaultScoreInterval = prefs.getInt("pref_score_view_interval", 1); + if (defaultScoreInterval > 5 || defaultScoreInterval < 0) + defaultScoreInterval = 1; + return defaultScoreInterval; + } + + public void setDefaultScoreSpinnerPosition(int position) + { + prefs.edit().putInt("pref_score_view_interval", position).apply(); + } + + /** + * 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 boolean getShowArchived() + { + return prefs.getBoolean("pref_show_archived", false); + } + + public void setShowArchived(boolean showArchived) + { + prefs.edit().putBoolean("pref_show_archived", showArchived).apply(); + } + + public boolean getShowCompleted() + { + return prefs.getBoolean("pref_show_completed", true); + } + + public void setShowCompleted(boolean showCompleted) + { + prefs.edit().putBoolean("pref_show_completed", showCompleted).apply(); + } + + public long getSnoozeInterval() + { + return Long.parseLong(prefs.getString("pref_snooze_interval", "15")); + } + + public int getTheme() + { + return prefs.getInt("pref_theme", ThemeSwitcher.THEME_LIGHT); + } + + public void setTheme(int theme) + { + prefs.edit().putInt("pref_theme", theme).apply(); + } + + 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 isPureBlackEnabled() + { + return prefs.getBoolean("pref_pure_black", false); + } + + public boolean isShortToggleEnabled() + { + return prefs.getBoolean("pref_short_toggle", false); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, + String key) + { + if (key.equals("pref_checkmark_reverse_order")) + { + shouldReverseCheckmarks = null; + for(Listener l : listeners) l.onCheckmarkOrderChanged(); + } + + if(key.equals("pref_sticky_notifications")) + { + for(Listener l : listeners) l.onNotificationsChanged(); + } + } + + public void removeListener(Listener listener) + { + listeners.remove(listener); + } + + public void setDefaultHabitColor(int color) + { + prefs.edit().putInt("pref_default_habit_palette_color", color).apply(); + } + + public void setShouldReverseCheckmarks(boolean reverse) + { + shouldReverseCheckmarks = null; + prefs + .edit() + .putBoolean("pref_checkmark_reverse_order", reverse) + .apply(); + + for(Listener l : listeners) l.onCheckmarkOrderChanged(); + } + + public boolean shouldReverseCheckmarks() + { + if (shouldReverseCheckmarks == null) shouldReverseCheckmarks = + prefs.getBoolean("pref_checkmark_reverse_order", false); + + return shouldReverseCheckmarks; + } + + public boolean shouldMakeNotificationsSticky() + { + return prefs.getBoolean("pref_sticky_notifications", 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(); + } + + public interface Listener + { + default void onCheckmarkOrderChanged() {} + + default void onNotificationsChanged() {} + } +} diff --git a/app/src/main/java/org/isoron/uhabits/preferences/WidgetPreferences.java b/app/src/main/java/org/isoron/uhabits/preferences/WidgetPreferences.java new file mode 100644 index 000000000..f7830e2ba --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/preferences/WidgetPreferences.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.preferences; + +import android.content.*; +import android.preference.*; + +import org.isoron.uhabits.*; + +import javax.inject.*; + +@AppScope +public class WidgetPreferences +{ + private final SharedPreferences prefs; + + @Inject + public WidgetPreferences(@AppContext Context context) + { + 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/receivers/PebbleReceiver.java b/app/src/main/java/org/isoron/uhabits/receivers/PebbleReceiver.java new file mode 100644 index 000000000..ce0ab7eed --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/receivers/PebbleReceiver.java @@ -0,0 +1,161 @@ +/* + * 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.receivers; + +import android.content.*; +import android.support.annotation.*; +import android.util.*; + +import com.getpebble.android.kit.*; +import com.getpebble.android.kit.PebbleKit.*; +import com.getpebble.android.kit.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.*; + +public class PebbleReceiver extends PebbleDataReceiver +{ + public static final UUID WATCHAPP_UUID = + UUID.fromString("82629d99-8ea6-4631-a022-9ca77a12a058"); + + private HabitList allHabits; + + private CommandRunner commandRunner; + + private TaskRunner taskRunner; + + private HabitList filteredHabits; + + public PebbleReceiver() + { + super(WATCHAPP_UUID); + } + + @Override + public void receiveData(@Nullable Context context, + int transactionId, + @Nullable PebbleDictionary data) + { + if (context == null) throw new RuntimeException("context is null"); + if (data == null) throw new RuntimeException("data is null"); + + HabitsApplication app = + (HabitsApplication) context.getApplicationContext(); + + commandRunner = app.getComponent().getCommandRunner(); + taskRunner = app.getComponent().getTaskRunner(); + allHabits = app.getComponent().getHabitList(); + + HabitMatcher build = new HabitMatcherBuilder() + .setArchivedAllowed(false) + .setCompletedAllowed(false) + .build(); + + filteredHabits = allHabits.getFiltered(build); + + PebbleKit.sendAckToPebble(context, transactionId); + Log.d("PebbleReceiver", "<-- " + data.getString(0)); + + taskRunner.execute(() -> { + switch (data.getString(0)) + { + case "COUNT": + sendCount(); + break; + + case "FETCH": + processFetch(data); + break; + + case "TOGGLE": + processToggle(data); + break; + } + }); + } + + private void processFetch(@NonNull PebbleDictionary dict) + { + Long position = dict.getInteger(1); + if (position == null) return; + if (position < 0 || position >= filteredHabits.size()) return; + + Habit habit = filteredHabits.getByPosition(position.intValue()); + if (habit == null) return; + + sendHabit(habit); + } + + private void processToggle(@NonNull PebbleDictionary dict) + { + Long habitId = dict.getInteger(1); + if (habitId == null) return; + + Habit habit = allHabits.getById(habitId); + if (habit == null) return; + + long today = DateUtils.getStartOfToday(); + commandRunner.execute(new ToggleRepetitionCommand(habit, today), + habitId); + + sendOK(); + } + + private void sendCount() + { + PebbleDictionary dict = new PebbleDictionary(); + dict.addString(0, "COUNT"); + dict.addInt32(1, filteredHabits.size()); + sendDict(dict); + + Log.d("PebbleReceiver", + String.format("--> COUNT %d", filteredHabits.size())); + } + + private void sendDict(@NonNull PebbleDictionary dict) + { + PebbleKit.sendDataToPebble(HabitsApplication.getContext(), + PebbleReceiver.WATCHAPP_UUID, dict); + } + + private void sendHabit(@NonNull Habit habit) + { + if (habit.getId() == null) return; + + PebbleDictionary response = new PebbleDictionary(); + response.addString(0, "HABIT"); + response.addInt32(1, habit.getId().intValue()); + response.addString(2, habit.getName()); + response.addInt32(3, habit.getCheckmarks().getTodayValue()); + sendDict(response); + } + + private void sendOK() + { + PebbleDictionary dict = new PebbleDictionary(); + dict.addString(0, "OK"); + sendDict(dict); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/receivers/ReceiverScope.java b/app/src/main/java/org/isoron/uhabits/receivers/ReceiverScope.java new file mode 100644 index 000000000..76fec08c2 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/receivers/ReceiverScope.java @@ -0,0 +1,25 @@ +/* + * 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.receivers; + +import javax.inject.*; + +@Scope +public @interface ReceiverScope { } diff --git a/app/src/main/java/org/isoron/uhabits/receivers/ReminderController.java b/app/src/main/java/org/isoron/uhabits/receivers/ReminderController.java new file mode 100644 index 000000000..22fd4bcd0 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/receivers/ReminderController.java @@ -0,0 +1,80 @@ +/* + * 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.receivers; + +import android.support.annotation.*; + +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.notifications.*; +import org.isoron.uhabits.preferences.*; +import org.isoron.uhabits.utils.*; + +import javax.inject.*; + +@ReceiverScope +public class ReminderController +{ + @NonNull + private final ReminderScheduler reminderScheduler; + + @NonNull + private final NotificationTray notificationTray; + + private Preferences preferences; + + @Inject + public ReminderController(@NonNull ReminderScheduler reminderScheduler, + @NonNull NotificationTray notificationTray, + @NonNull Preferences preferences) + { + this.reminderScheduler = reminderScheduler; + this.notificationTray = notificationTray; + this.preferences = preferences; + } + + public void onBootCompleted() + { + reminderScheduler.scheduleAll(); + } + + public void onShowReminder(@NonNull Habit habit, + long timestamp, + long reminderTime) + { + notificationTray.show(habit, timestamp, reminderTime); + reminderScheduler.scheduleAll(); + } + + public void onSnooze(@NonNull Habit habit) + { + long snoozeInterval = preferences.getSnoozeInterval(); + + long now = DateUtils.getLocalTime(); + long reminderTime = now + snoozeInterval * 60 * 1000; + + reminderScheduler.schedule(habit, reminderTime); + notificationTray.cancel(habit); + } + + public void onDismiss(@NonNull Habit habit) + { + notificationTray.cancel(habit); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/receivers/ReminderReceiver.java b/app/src/main/java/org/isoron/uhabits/receivers/ReminderReceiver.java new file mode 100644 index 000000000..c8d022f61 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/receivers/ReminderReceiver.java @@ -0,0 +1,113 @@ +/* + * 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.receivers; + +import android.content.*; +import android.util.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; + +import dagger.*; + +import static android.content.ContentUris.*; + +/** + * The Android BroadcastReceiver for Loop Habit Tracker. + *

+ * All broadcast messages are received and processed by this class. + */ +public class ReminderReceiver extends BroadcastReceiver +{ + public static final String ACTION_DISMISS_REMINDER = + "org.isoron.uhabits.ACTION_DISMISS_REMINDER"; + + public static final String ACTION_SHOW_REMINDER = + "org.isoron.uhabits.ACTION_SHOW_REMINDER"; + + public static final String ACTION_SNOOZE_REMINDER = + "org.isoron.uhabits.ACTION_SNOOZE_REMINDER"; + + private static final String TAG = "ReminderReceiver"; + + @Override + public void onReceive(final Context context, Intent intent) + { + HabitsApplication app = + (HabitsApplication) context.getApplicationContext(); + + ReminderComponent component = DaggerReminderReceiver_ReminderComponent + .builder() + .appComponent(app.getComponent()) + .build(); + + HabitList habits = app.getComponent().getHabitList(); + ReminderController reminderController = + component.getReminderController(); + + Log.i(TAG, String.format("Received intent: %s", intent.toString())); + + Habit habit = null; + long today = DateUtils.getStartOfToday(); + + if (intent.getData() != null) + habit = habits.getById(parseId(intent.getData())); + final Long timestamp = intent.getLongExtra("timestamp", today); + final Long reminderTime = intent.getLongExtra("reminderTime", today); + + try + { + switch (intent.getAction()) + { + case ACTION_SHOW_REMINDER: + if (habit == null) return; + reminderController.onShowReminder(habit, timestamp, + reminderTime); + break; + + case ACTION_DISMISS_REMINDER: + if (habit == null) return; + reminderController.onDismiss(habit); + break; + + case ACTION_SNOOZE_REMINDER: + if (habit == null) return; + reminderController.onSnooze(habit); + break; + + case Intent.ACTION_BOOT_COMPLETED: + reminderController.onBootCompleted(); + break; + } + } + catch (RuntimeException e) + { + Log.e(TAG, "could not process intent", e); + } + } + + @ReceiverScope + @Component(dependencies = AppComponent.class) + interface ReminderComponent + { + ReminderController getReminderController(); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/receivers/WidgetController.java b/app/src/main/java/org/isoron/uhabits/receivers/WidgetController.java new file mode 100644 index 000000000..5aecbb970 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/receivers/WidgetController.java @@ -0,0 +1,71 @@ +/* + * 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.receivers; + +import android.support.annotation.*; + +import org.isoron.uhabits.commands.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.notifications.*; + +import javax.inject.*; + +@ReceiverScope +public class WidgetController +{ + @NonNull + private final CommandRunner commandRunner; + + private NotificationTray notificationTray; + + @Inject + public WidgetController(@NonNull CommandRunner commandRunner, + @NonNull NotificationTray notificationTray) + { + this.commandRunner = commandRunner; + this.notificationTray = notificationTray; + } + + public void onAddRepetition(@NonNull Habit habit, long timestamp) + { + Repetition rep = habit.getRepetitions().getByTimestamp(timestamp); + if (rep != null) return; + performToggle(habit, timestamp); + notificationTray.cancel(habit); + } + + public void onRemoveRepetition(@NonNull Habit habit, long timestamp) + { + Repetition rep = habit.getRepetitions().getByTimestamp(timestamp); + if (rep == null) return; + performToggle(habit, timestamp); + } + + public void onToggleRepetition(@NonNull Habit habit, long timestamp) + { + performToggle(habit, timestamp); + } + + private void performToggle(@NonNull Habit habit, long timestamp) + { + commandRunner.execute(new ToggleRepetitionCommand(habit, timestamp), + habit.getId()); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/receivers/WidgetReceiver.java b/app/src/main/java/org/isoron/uhabits/receivers/WidgetReceiver.java new file mode 100644 index 000000000..763c41871 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/receivers/WidgetReceiver.java @@ -0,0 +1,95 @@ +/* + * 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.receivers; + +import android.content.*; +import android.util.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.intents.*; + +import dagger.*; + +/** + * The Android BroadcastReceiver for Loop Habit Tracker. + *

+ * All broadcast messages are received and processed by this class. + */ +public class WidgetReceiver extends BroadcastReceiver +{ + public static final String ACTION_ADD_REPETITION = + "org.isoron.uhabits.ACTION_ADD_REPETITION"; + + public static final String ACTION_DISMISS_REMINDER = + "org.isoron.uhabits.ACTION_DISMISS_REMINDER"; + + public static final String ACTION_REMOVE_REPETITION = + "org.isoron.uhabits.ACTION_REMOVE_REPETITION"; + + public static final String ACTION_TOGGLE_REPETITION = + "org.isoron.uhabits.ACTION_TOGGLE_REPETITION"; + + @Override + public void onReceive(final Context context, Intent intent) + { + HabitsApplication app = + (HabitsApplication) context.getApplicationContext(); + + WidgetComponent component = DaggerWidgetReceiver_WidgetComponent + .builder() + .appComponent(app.getComponent()) + .build(); + + IntentParser parser = app.getComponent().getIntentParser(); + WidgetController controller = component.getWidgetController(); + + try + { + IntentParser.CheckmarkIntentData data; + data = parser.parseCheckmarkIntent(intent); + + switch (intent.getAction()) + { + case ACTION_ADD_REPETITION: + controller.onAddRepetition(data.habit, data.timestamp); + break; + + case ACTION_TOGGLE_REPETITION: + controller.onToggleRepetition(data.habit, data.timestamp); + break; + + case ACTION_REMOVE_REPETITION: + controller.onRemoveRepetition(data.habit, data.timestamp); + break; + } + } + catch (RuntimeException e) + { + Log.e("WidgetReceiver", "could not process intent", e); + } + } + + @ReceiverScope + @Component(dependencies = AppComponent.class) + interface WidgetComponent + { + WidgetController getWidgetController(); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/tasks/AndroidTaskRunner.java b/app/src/main/java/org/isoron/uhabits/tasks/AndroidTaskRunner.java new file mode 100644 index 000000000..e1626fe34 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/tasks/AndroidTaskRunner.java @@ -0,0 +1,136 @@ +/* + * 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.os.*; + +import org.isoron.uhabits.*; + +import java.util.*; + +import dagger.*; + +@Module +public class AndroidTaskRunner implements TaskRunner +{ + private final LinkedList activeTasks; + + private final HashMap taskToAsyncTask; + + private LinkedList listeners; + + public AndroidTaskRunner() + { + activeTasks = new LinkedList<>(); + taskToAsyncTask = new HashMap<>(); + listeners = new LinkedList<>(); + } + + @Provides + @AppScope + public static TaskRunner provideTaskRunner() + { + return new AndroidTaskRunner(); + } + + @Override + public void addListener(Listener listener) + { + listeners.add(listener); + } + + @Override + public void execute(Task task) + { + task.onAttached(this); + new CustomAsyncTask(task).execute(); + } + + @Override + public int getActiveTaskCount() + { + return activeTasks.size(); + } + + @Override + public void publishProgress(Task task, int progress) + { + CustomAsyncTask asyncTask = taskToAsyncTask.get(task); + if (asyncTask == null) return; + asyncTask.publish(progress); + } + + @Override + public void removeListener(Listener listener) + { + listeners.remove(listener); + } + + private class CustomAsyncTask extends AsyncTask + { + private final Task task; + + public CustomAsyncTask(Task task) + { + this.task = task; + } + + public Task getTask() + { + return task; + } + + public void publish(int progress) + { + publishProgress(progress); + } + + @Override + protected Void doInBackground(Void... params) + { + task.doInBackground(); + return null; + } + + @Override + protected void onPostExecute(Void aVoid) + { + task.onPostExecute(); + activeTasks.remove(this); + taskToAsyncTask.remove(task); + for (Listener l : listeners) l.onTaskFinished(task); + } + + @Override + protected void onPreExecute() + { + for (Listener l : listeners) l.onTaskStarted(task); + activeTasks.add(this); + taskToAsyncTask.put(task, this); + task.onPreExecute(); + } + + @Override + protected void onProgressUpdate(Integer... values) + { + task.onProgressUpdate(values[0]); + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/tasks/BaseTask.java b/app/src/main/java/org/isoron/uhabits/tasks/BaseTask.java deleted file mode 100644 index d9542c84b..000000000 --- a/app/src/main/java/org/isoron/uhabits/tasks/BaseTask.java +++ /dev/null @@ -1,72 +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.tasks; - -import android.os.AsyncTask; -import android.os.Build; - -import java.util.concurrent.TimeoutException; - -public abstract class BaseTask extends AsyncTask -{ - private static int activeTaskCount; - - @Override - protected void onPreExecute() - { - super.onPreExecute(); - activeTaskCount++; - } - - @Override - protected void onPostExecute(Void aVoid) - { - activeTaskCount--; - super.onPostExecute(null); - } - - @Override - protected final Void doInBackground(Void... params) - { - doInBackground(); - return null; - } - - protected abstract void doInBackground(); - - public static void waitForTasks(long timeout) - throws TimeoutException, InterruptedException - { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) - throw new UnsupportedOperationException("waitForTasks requires API 16+"); - - int poolInterval = 100; - - while(timeout > 0) - { - if(activeTaskCount == 0) return; - - timeout -= poolInterval; - Thread.sleep(poolInterval); - } - - throw new TimeoutException(); - } -} 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..27f81f0b5 100644 --- a/app/src/main/java/org/isoron/uhabits/tasks/ExportCSVTask.java +++ b/app/src/main/java/org/isoron/uhabits/tasks/ExportCSVTask.java @@ -19,79 +19,66 @@ package org.isoron.uhabits.tasks; -import android.support.annotation.Nullable; -import android.view.View; -import android.widget.ProgressBar; +import android.support.annotation.*; -import org.isoron.uhabits.helpers.DatabaseHelper; -import org.isoron.uhabits.io.HabitsCSVExporter; -import org.isoron.uhabits.models.Habit; +import com.google.auto.factory.*; -import java.io.File; -import java.io.IOException; -import java.util.List; +import org.isoron.uhabits.io.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; -public class ExportCSVTask extends BaseTask +import java.io.*; +import java.util.*; + +@AutoFactory(allowSubclasses = true) +public class ExportCSVTask implements Task { - public interface Listener - { - void onExportCSVFinished(@Nullable String archiveFilename); - } + private String archiveFilename; - private ProgressBar progressBar; + @NonNull private final List selectedHabits; - private String archiveFilename; - private ExportCSVTask.Listener listener; - public ExportCSVTask(List selectedHabits, ProgressBar progressBar) - { - this.selectedHabits = selectedHabits; - this.progressBar = progressBar; - } + @NonNull + private final ExportCSVTask.Listener listener; - public void setListener(Listener listener) + @NonNull + private final HabitList habitList; + + public ExportCSVTask(@Provided @NonNull HabitList habitList, + @NonNull List selectedHabits, + @NonNull Listener listener) { this.listener = listener; + this.habitList = habitList; + this.selectedHabits = selectedHabits; } @Override - protected void onPreExecute() + public void doInBackground() { - super.onPreExecute(); + try + { + File dir = FileUtils.getFilesDir("CSV"); + if (dir == null) return; - if(progressBar != null) + HabitsCSVExporter exporter; + exporter = new HabitsCSVExporter(habitList, selectedHabits, dir); + archiveFilename = exporter.writeArchive(); + } + catch (IOException e) { - progressBar.setIndeterminate(true); - progressBar.setVisibility(View.VISIBLE); + e.printStackTrace(); } } @Override - protected void onPostExecute(Void aVoid) + public void onPostExecute() { - if(listener != null) - listener.onExportCSVFinished(archiveFilename); - - if(progressBar != null) - progressBar.setVisibility(View.GONE); - - super.onPostExecute(null); + listener.onExportCSVFinished(archiveFilename); } - @Override - protected void doInBackground() + public interface Listener { - try - { - File dir = DatabaseHelper.getFilesDir("CSV"); - if(dir == null) return; - - HabitsCSVExporter exporter = new HabitsCSVExporter(selectedHabits, dir); - archiveFilename = exporter.writeArchive(); - } - catch (IOException e) - { - e.printStackTrace(); - } + void onExportCSVFinished(@Nullable String archiveFilename); } } 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..b70ee65d4 100644 --- a/app/src/main/java/org/isoron/uhabits/tasks/ExportDBTask.java +++ b/app/src/main/java/org/isoron/uhabits/tasks/ExportDBTask.java @@ -19,75 +19,50 @@ package org.isoron.uhabits.tasks; -import android.support.annotation.Nullable; -import android.view.View; -import android.widget.ProgressBar; +import android.support.annotation.*; -import org.isoron.uhabits.helpers.DatabaseHelper; +import org.isoron.uhabits.utils.*; -import java.io.File; -import java.io.IOException; +import java.io.*; -public class ExportDBTask extends BaseTask +public class ExportDBTask implements Task { - public interface Listener - { - void onExportDBFinished(@Nullable String filename); - } - - private ProgressBar progressBar; private String filename; - private Listener listener; - public ExportDBTask(ProgressBar progressBar) - { - this.progressBar = progressBar; - } + @NonNull + private final Listener listener; - public void setListener(Listener listener) + public ExportDBTask(@NonNull Listener listener) { this.listener = listener; } @Override - protected void onPreExecute() + public void doInBackground() { - super.onPreExecute(); + filename = null; + + try + { + File dir = FileUtils.getFilesDir("Backups"); + if (dir == null) return; - if(progressBar != null) + filename = DatabaseUtils.saveDatabaseCopy(dir); + } + catch (IOException e) { - progressBar.setIndeterminate(true); - progressBar.setVisibility(View.VISIBLE); + e.printStackTrace(); } } @Override - protected void onPostExecute(Void aVoid) + public void onPostExecute() { - if(listener != null) - listener.onExportDBFinished(filename); - - if(progressBar != null) - progressBar.setVisibility(View.GONE); - - super.onPostExecute(null); + listener.onExportDBFinished(filename); } - @Override - protected void doInBackground() + public interface Listener { - filename = null; - - try - { - File dir = DatabaseHelper.getFilesDir("Backups"); - if(dir == null) return; - - filename = DatabaseHelper.saveDatabaseCopy(dir); - } - catch(IOException e) - { - e.printStackTrace(); - } + void onExportDBFinished(@Nullable String filename); } } 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..e19e7b8fc 100644 --- a/app/src/main/java/org/isoron/uhabits/tasks/ImportDataTask.java +++ b/app/src/main/java/org/isoron/uhabits/tasks/ImportDataTask.java @@ -19,78 +19,48 @@ package org.isoron.uhabits.tasks; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.view.View; -import android.widget.ProgressBar; +import android.support.annotation.*; -import org.isoron.uhabits.io.GenericImporter; +import com.google.auto.factory.*; -import java.io.File; +import org.isoron.uhabits.io.*; -public class ImportDataTask extends BaseTask +import java.io.*; + +@AutoFactory(allowSubclasses = true) +public class ImportDataTask implements Task { - public static final int SUCCESS = 1; - public static final int NOT_RECOGNIZED = 2; public static final int FAILED = 3; - public interface Listener - { - void onImportFinished(int result); - } + public static final int NOT_RECOGNIZED = 2; - @Nullable - private final ProgressBar progressBar; + public static final int SUCCESS = 1; + + int result; @NonNull private final File file; - @Nullable - private Listener listener; + private GenericImporter importer; - int result; - - public ImportDataTask(@NonNull File file, @Nullable ProgressBar progressBar) - { - this.file = file; - this.progressBar = progressBar; - } + @NonNull + private final Listener listener; - public void setListener(@Nullable Listener listener) + public ImportDataTask(@Provided @NonNull GenericImporter importer, + @NonNull File file, + @NonNull Listener listener) { + this.importer = importer; this.listener = listener; + this.file = file; } @Override - protected void onPreExecute() - { - super.onPreExecute(); - - if(progressBar != null) - { - progressBar.setIndeterminate(true); - progressBar.setVisibility(View.VISIBLE); - } - } - - @Override - protected void onPostExecute(Void aVoid) - { - if(progressBar != null) - progressBar.setVisibility(View.GONE); - - if(listener != null) listener.onImportFinished(result); - - super.onPostExecute(null); - } - - @Override - protected void doInBackground() + public void doInBackground() { try { - GenericImporter importer = new GenericImporter(); - if(importer.canHandle(file)) + if (importer.canHandle(file)) { importer.importHabitsFromFile(file); result = SUCCESS; @@ -106,4 +76,15 @@ public class ImportDataTask extends BaseTask e.printStackTrace(); } } + + @Override + public void onPostExecute() + { + listener.onImportDataFinished(result); + } + + public interface Listener + { + void onImportDataFinished(int result); + } } \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/tasks/SingleThreadTaskRunner.java b/app/src/main/java/org/isoron/uhabits/tasks/SingleThreadTaskRunner.java new file mode 100644 index 000000000..db05b0955 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/tasks/SingleThreadTaskRunner.java @@ -0,0 +1,68 @@ +/* + * 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 org.isoron.uhabits.*; + +import dagger.*; + +@Module +public class SingleThreadTaskRunner implements TaskRunner +{ + @Provides + @AppScope + public static TaskRunner provideTaskRunner() + { + return new SingleThreadTaskRunner(); + } + + @Override + public void addListener(Listener listener) + { + throw new UnsupportedOperationException(); + } + + @Override + public void execute(Task task) + { + task.onAttached(this); + task.onPreExecute(); + task.doInBackground(); + task.onPostExecute(); + } + + @Override + public int getActiveTaskCount() + { + return 0; + } + + @Override + public void publishProgress(Task task, int progress) + { + task.onProgressUpdate(progress); + } + + @Override + public void removeListener(Listener listener) + { + throw new UnsupportedOperationException(); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/tasks/Task.java b/app/src/main/java/org/isoron/uhabits/tasks/Task.java new file mode 100644 index 000000000..a1b69098a --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/tasks/Task.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.tasks; + +import android.support.annotation.*; + +public interface Task +{ + default void cancel() {} + + void doInBackground(); + + default void onAttached(@NonNull TaskRunner runner) {} + + default void onPostExecute() {} + + default void onPreExecute() {} + + default void onProgressUpdate(int value) {} +} diff --git a/app/src/main/java/org/isoron/uhabits/tasks/TaskRunner.java b/app/src/main/java/org/isoron/uhabits/tasks/TaskRunner.java new file mode 100644 index 000000000..c5c38d7c0 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/tasks/TaskRunner.java @@ -0,0 +1,40 @@ +/* + * 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 interface TaskRunner +{ + void addListener(Listener listener); + + void removeListener(Listener listener); + + void execute(Task task); + + void publishProgress(Task task, int progress); + + int getActiveTaskCount(); + + interface Listener + { + void onTaskStarted(Task task); + + void onTaskFinished(Task task); + } +} 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/utils/AttributeSetUtils.java b/app/src/main/java/org/isoron/uhabits/utils/AttributeSetUtils.java new file mode 100644 index 000000000..c633a9b72 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/utils/AttributeSetUtils.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.utils; + +import android.content.*; +import android.support.annotation.*; +import android.support.annotation.Nullable; +import android.util.*; + +import org.jetbrains.annotations.*; + +public class AttributeSetUtils +{ + public static final String ISORON_NAMESPACE = "http://isoron.org/android"; + + @Nullable + public static String getAttribute(@NonNull Context context, + @NonNull AttributeSet attrs, + @NonNull String name, + @Nullable String defaultValue) + { + int resId = attrs.getAttributeResourceValue(ISORON_NAMESPACE, name, 0); + if (resId != 0) return context.getResources().getString(resId); + + String value = attrs.getAttributeValue(ISORON_NAMESPACE, name); + if (value != null) return value; + else return defaultValue; + } + + public static boolean getBooleanAttribute(@NonNull Context context, + @NonNull AttributeSet attrs, + @NonNull String name, + boolean defaultValue) + { + String boolText = getAttribute(context, attrs, name, null); + if (boolText != null) return Boolean.parseBoolean(boolText); + else return defaultValue; + } + + @Contract("_,_,_,!null -> !null") + public static Integer getColorAttribute(@NonNull Context context, + @NonNull AttributeSet attrs, + @NonNull String name, + @Nullable Integer defaultValue) + { + int resId = attrs.getAttributeResourceValue(ISORON_NAMESPACE, name, 0); + if (resId != 0) return context.getResources().getColor(resId); + else return defaultValue; + } + + public static float getFloatAttribute(@NonNull Context context, + @NonNull AttributeSet attrs, + @NonNull String name, + float defaultValue) + { + String number = getAttribute(context, attrs, name, null); + if (number != null) return Float.parseFloat(number); + else return defaultValue; + } +} 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..25434bacc --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/utils/ColorUtils.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.utils; + +import android.content.*; +import android.graphics.*; +import android.util.*; + +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) + { + StyledResources res = new StyledResources(context); + int[] palette = res.getPalette(); + + 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"); + + StyledResources res = new StyledResources(context); + int palette[] = res.getPalette(); + 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 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..0a7c0fbbf --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/utils/DatabaseUtils.java @@ -0,0 +1,103 @@ +/* + * 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.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(); + String databaseFilename = getDatabaseFilename(); + String root = context.getFilesDir().getPath(); + + String format = "%s/../databases/%s"; + String filename = String.format(format, root, databaseFilename); + + return new File(filename); + } + + @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(); + 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); + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + public static String saveDatabaseCopy(File dir) throws IOException + { + SimpleDateFormat dateFormat = DateFormats.getBackupDateFormat(); + String date = dateFormat.format(DateUtils.getLocalTime()); + String format = "%s/Loop Habits Backup %s.db"; + String filename = String.format(format, dir.getAbsolutePath(), date); + + File db = getDatabaseFile(); + File dbCopy = new File(filename); + FileUtils.copy(db, dbCopy); + + return dbCopy.getAbsolutePath(); + } + + public interface Callback + { + void execute(); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/utils/DateFormats.java b/app/src/main/java/org/isoron/uhabits/utils/DateFormats.java new file mode 100644 index 000000000..fac0d63bd --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/utils/DateFormats.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.utils; + +import android.support.annotation.*; + +import java.text.*; +import java.util.*; + +import static android.os.Build.VERSION.*; +import static android.os.Build.VERSION_CODES.*; +import static android.text.format.DateFormat.*; + +public class DateFormats +{ + @NonNull + private static SimpleDateFormat fromSkeleton(@NonNull String skeleton, + @NonNull Locale locale) + { + SimpleDateFormat df = new SimpleDateFormat(skeleton, locale); + df.setTimeZone(TimeZone.getTimeZone("UTC")); + return df; + } + + @NonNull + public static SimpleDateFormat fromSkeleton(@NonNull String skeleton) + { + Locale locale = Locale.getDefault(); + + if (SDK_INT >= JELLY_BEAN) + skeleton = getBestDateTimePattern(locale, skeleton); + + return fromSkeleton(skeleton, locale); + } + + public static SimpleDateFormat getBackupDateFormat() + { + return fromSkeleton("yyyy-MM-dd HHmmss", Locale.US); + } + + public static SimpleDateFormat getCSVDateFormat() + { + return fromSkeleton("yyyy-MM-dd", Locale.US); + } +} 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 51% 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..c85f90df2 100644 --- a/app/src/main/java/org/isoron/uhabits/helpers/DateHelper.java +++ b/app/src/main/java/org/isoron/uhabits/utils/DateUtils.java @@ -17,74 +17,41 @@ * 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; +import android.content.*; +import android.text.format.*; -import org.isoron.uhabits.R; +import org.isoron.uhabits.*; -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.Locale; -import java.util.TimeZone; +import java.util.*; -public class DateHelper -{ - public static long millisecondsInOneDay = 24 * 60 * 60 * 1000; - public static int ALL_WEEK_DAYS = 127; +import static java.util.Calendar.*; +public abstract class DateUtils +{ 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); - } + private static TimeZone fixedTimeZone = null; - 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 long applyTimezone(long localTimestamp) { - GregorianCalendar day = getCalendar(timestamp); - return day.get(GregorianCalendar.DAY_OF_WEEK) % 7; + TimeZone tz = getTimezone(); + long now = new Date(localTimestamp).getTime(); + return now - tz.getOffset(now); } - public static long getStartOfToday() + public static String formatHeaderDate(GregorianCalendar day) { - return getStartOfDay(DateHelper.getLocalTime()); + Locale locale = Locale.getDefault(); + String dayOfMonth = Integer.toString(day.get(DAY_OF_MONTH)); + String dayOfWeek = day.getDisplayName(DAY_OF_WEEK, SHORT, locale); + return dayOfWeek + "\n" + dayOfMonth; } public static String formatTime(Context context, int hours, int minutes) @@ -98,74 +65,43 @@ public class DateHelper return df.format(date); } - public static SimpleDateFormat getDateFormat(String skeleton) - { - String pattern; - Locale locale = Locale.getDefault(); - - 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")); - - return format; - } - - public static SimpleDateFormat getCSVDateFormat() - { - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.US); - dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - - return dateFormat; - } - - public static SimpleDateFormat getBackupDateFormat() - { - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HHmmss", Locale.US); - dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - - return dateFormat; - } - - public static String formatHeaderDate(GregorianCalendar day) + public static String formatWeekdayList(Context context, boolean weekday[]) { - 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; - } + String shortDayNames[] = getShortDayNames(); + String longDayNames[] = getLongDayNames(); + StringBuilder buffer = new StringBuilder(); - public static int differenceInDays(Date from, Date to) - { - long milliseconds = getStartOfDay(to.getTime()) - getStartOfDay(from.getTime()); - return (int) (milliseconds / millisecondsInOneDay); - } + 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 String[] getLongDayNames() - { - return getDayNames(GregorianCalendar.LONG); + 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(); } - - /** - * 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) + public static GregorianCalendar getCalendar(long timestamp) { - return number % 7; + GregorianCalendar day = + new GregorianCalendar(TimeZone.getTimeZone("GMT")); + day.setTimeInMillis(timestamp); + return day; } public static String[] getDayNames(int format) @@ -173,18 +109,27 @@ public class DateHelper String[] wdays = new String[7]; Calendar day = new GregorianCalendar(); - day.set(GregorianCalendar.DAY_OF_WEEK, Calendar.SATURDAY); + day.set(DAY_OF_WEEK, Calendar.SATURDAY); for (int i = 0; i < wdays.length; i++) { - wdays[i] = day.getDisplayName(GregorianCalendar.DAY_OF_WEEK, format, - Locale.getDefault()); - day.add(GregorianCalendar.DAY_OF_MONTH, 1); + wdays[i] = + day.getDisplayName(DAY_OF_WEEK, format, Locale.getDefault()); + day.add(DAY_OF_MONTH, 1); } return wdays; } + public static long getLocalTime() + { + if (fixedLocalTime != null) return fixedLocalTime; + + TimeZone tz = getTimezone(); + 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,88 +139,151 @@ public class DateHelper String[] days = new String[7]; Calendar calendar = new GregorianCalendar(); - calendar.set(GregorianCalendar.DAY_OF_WEEK, calendar.getFirstDayOfWeek()); + calendar.set(DAY_OF_WEEK, calendar.getFirstDayOfWeek()); for (int i = 0; i < days.length; i++) { - days[i] = calendar.getDisplayName(GregorianCalendar.DAY_OF_WEEK, format, - Locale.getDefault()); - calendar.add(GregorianCalendar.DAY_OF_MONTH, 1); + days[i] = calendar.getDisplayName(DAY_OF_WEEK, format, + Locale.getDefault()); + calendar.add(DAY_OF_MONTH, 1); } return days; } /** - * @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(DAY_OF_WEEK, calendar.getFirstDayOfWeek()); for (int i = 0; i < dayNumbers.length; i++) { - dayNumbers[i] = calendar.get(GregorianCalendar.DAY_OF_WEEK); - calendar.add(GregorianCalendar.DAY_OF_MONTH, 1); + dayNumbers[i] = calendar.get(DAY_OF_WEEK); + calendar.add(DAY_OF_MONTH, 1); } 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(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 Integer packWeekdayList(boolean weekday[]) + public static GregorianCalendar getStartOfTodayCalendar() { - int list = 0; - int current = 1; + return getCalendar(getStartOfToday()); + } - for(int i = 0; i < 7; i++) - { - if(weekday[i]) list |= current; - current = current << 1; - } + public static TimeZone getTimezone() + { + if(fixedTimeZone != null) return fixedTimeZone; + return TimeZone.getDefault(); + } + + public static void setFixedTimeZone(TimeZone tz) + { + fixedTimeZone = tz; + } + + public static int getWeekday(long timestamp) + { + GregorianCalendar day = getCalendar(timestamp); + return javaWeekdayToLoopWeekday(day.get(DAY_OF_WEEK)); + } + + /** + * 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 list; + public static long removeTimezone(long timestamp) + { + TimeZone tz = getTimezone(); + long now = new Date(timestamp).getTime(); + return now + tz.getOffset(now); } - public static boolean[] unpackWeekdayList(int list) + public static void setFixedLocalTime(Long timestamp) { - boolean[] weekday = new boolean[7]; - int current = 1; + fixedLocalTime = timestamp; + } - for(int i = 0; i < 7; i++) + public static Long truncate(TruncateField field, long timestamp) + { + GregorianCalendar cal = DateUtils.getCalendar(timestamp); + + switch (field) { - if((list & current) != 0) weekday[i] = true; - current = current << 1; + case MONTH: + cal.set(DAY_OF_MONTH, 1); + return cal.getTimeInMillis(); + + case WEEK_NUMBER: + int firstWeekday = cal.getFirstDayOfWeek(); + int weekday = cal.get(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(DAY_OF_MONTH, 1); + cal.set(Calendar.MONTH, quarter * 3); + return cal.getTimeInMillis(); + + case YEAR: + cal.set(Calendar.MONTH, Calendar.JANUARY); + cal.set(DAY_OF_MONTH, 1); + return cal.getTimeInMillis(); + + default: + throw new IllegalArgumentException(); } + } + + public enum TruncateField + { + MONTH, WEEK_NUMBER, YEAR, QUARTER + } - return weekday; + /** + * Gets the number of days between two timestamps (exclusively). + * + * @param t1 the first timestamp to use in milliseconds + * @param t2 the second timestamp to use in milliseconds + * @return the number of days between the two timestamps + */ + public static int getDaysBetween(long t1, long t2) + { + Date d1 = new Date(t1); + Date d2 = new Date(t2); + return (int) (Math.abs((d2.getTime() - d1.getTime()) / millisecondsInOneDay)); } } diff --git a/app/src/main/java/org/isoron/uhabits/utils/FileUtils.java b/app/src/main/java/org/isoron/uhabits/utils/FileUtils.java new file mode 100644 index 000000000..4bc4a5d76 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/utils/FileUtils.java @@ -0,0 +1,113 @@ +/* + * 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.os.*; +import android.support.annotation.*; +import android.support.v4.content.*; +import android.util.*; + +import org.isoron.uhabits.*; + +import java.io.*; + +public abstract class FileUtils +{ + public static void copy(File src, File dst) throws IOException + { + FileInputStream inStream = new FileInputStream(src); + FileOutputStream outStream = new FileOutputStream(dst); + copy(inStream, outStream); + } + + public static void copy(InputStream inStream, File dst) throws IOException + { + FileOutputStream outStream = new FileOutputStream(dst); + copy(inStream, outStream); + } + + public static void copy(InputStream in, OutputStream out) throws IOException + { + int numBytes; + byte[] buffer = new byte[1024]; + + while ((numBytes = in.read(buffer)) != -1) + out.write(buffer, 0, numBytes); + } + + @Nullable + private static File getDir(@NonNull File potentialParentDirs[], + @Nullable String relativePath) + { + if (relativePath == null) relativePath = ""; + + File chosenDir = null; + for (File dir : potentialParentDirs) + { + if (dir == null || !dir.canWrite()) continue; + chosenDir = dir; + break; + } + + if (chosenDir == null) + { + Log.e("DatabaseHelper", + "getDir: all potential parents are null or non-writable"); + return null; + } + + File dir = new File( + String.format("%s/%s/", chosenDir.getAbsolutePath(), relativePath)); + if (!dir.exists() && !dir.mkdirs()) + { + Log.e("DatabaseHelper", + "getDir: chosen dir does not exist and cannot be created"); + return null; + } + + return dir; + } + + @Nullable + public static File getFilesDir(@Nullable String relativePath) + { + Context context = HabitsApplication.getContext(); + File externalFilesDirs[] = + ContextCompat.getExternalFilesDirs(context, null); + + if (externalFilesDirs == null) + { + Log.e("DatabaseHelper", + "getFilesDir: getExternalFilesDirs returned null"); + return null; + } + + return getDir(externalFilesDirs, relativePath); + } + + @Nullable + public static File getSDCardDir(@Nullable String relativePath) + { + File parents[] = + new File[]{ Environment.getExternalStorageDirectory() }; + return getDir(parents, relativePath); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/utils/InterfaceUtils.java b/app/src/main/java/org/isoron/uhabits/utils/InterfaceUtils.java new file mode 100644 index 000000000..5db2a875a --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/utils/InterfaceUtils.java @@ -0,0 +1,72 @@ +/* + * 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.content.res.*; +import android.graphics.*; +import android.util.*; + +import java.util.*; + +public abstract class InterfaceUtils +{ + + // TODO: Move this to another place, or detect automatically + private static String fullyTranslatedLanguages[] = { + "ca", "zh", "en", "de", "in", "it", "ko", "pl", "pt", "es", "tk", "uk", + "ja", "fr", "hr", "sl" + }; + + private static Typeface fontAwesome; + + public static Typeface getFontAwesome(Context context) + { + if(fontAwesome == null) + fontAwesome = Typeface.createFromAsset(context.getAssets(), "fontawesome-webfont.ttf"); + + return fontAwesome; + } + + public static float dpToPixels(Context context, float dp) + { + Resources resources = context.getResources(); + DisplayMetrics metrics = resources.getDisplayMetrics(); + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, metrics); + } + + public static float spToPixels(Context context, float sp) + { + Resources resources = context.getResources(); + DisplayMetrics metrics = resources.getDisplayMetrics(); + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, metrics); + } + + public static boolean isLocaleFullyTranslated() + { + final String currentLanguage = Locale.getDefault().getLanguage(); + + for(String lang : fullyTranslatedLanguages) + if(currentLanguage.equals(lang)) return true; + + return false; + } + +} diff --git a/app/src/main/java/org/isoron/uhabits/utils/ReminderScheduler.java b/app/src/main/java/org/isoron/uhabits/utils/ReminderScheduler.java new file mode 100644 index 000000000..e0d02e85b --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/utils/ReminderScheduler.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.utils; + +import android.app.*; +import android.support.annotation.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.commands.*; +import org.isoron.uhabits.intents.*; +import org.isoron.uhabits.models.*; + +import java.util.*; + +import javax.inject.*; + +import static org.isoron.uhabits.utils.DateUtils.*; + +@AppScope +public class ReminderScheduler implements CommandRunner.Listener +{ + private final PendingIntentFactory pendingIntentFactory; + + private final IntentScheduler intentScheduler; + + private final HabitLogger logger; + + private CommandRunner commandRunner; + + private HabitList habitList; + + @Inject + public ReminderScheduler(@NonNull PendingIntentFactory pendingIntentFactory, + @NonNull IntentScheduler intentScheduler, + @NonNull HabitLogger logger, + @NonNull CommandRunner commandRunner, + @NonNull HabitList habitList) + { + this.pendingIntentFactory = pendingIntentFactory; + this.intentScheduler = intentScheduler; + this.logger = logger; + this.commandRunner = commandRunner; + this.habitList = habitList; + } + + @Override + public void onCommandExecuted(@NonNull Command command, + @Nullable Long refreshKey) + { + if(command instanceof ToggleRepetitionCommand) return; + if(command instanceof ChangeHabitColorCommand) return; + scheduleAll(); + } + + public void schedule(@NonNull Habit habit, @Nullable Long reminderTime) + { + if (!habit.hasReminder()) return; + if (habit.isArchived()) return; + Reminder reminder = habit.getReminder(); + if (reminderTime == null) reminderTime = getReminderTime(reminder); + long timestamp = getStartOfDay(removeTimezone(reminderTime)); + + PendingIntent intent = + pendingIntentFactory.showReminder(habit, reminderTime, timestamp); + intentScheduler.schedule(reminderTime, intent); + logger.logReminderScheduled(habit, reminderTime); + } + + public void scheduleAll() + { + HabitList reminderHabits = + habitList.getFiltered(HabitMatcher.WITH_ALARM); + for (Habit habit : reminderHabits) + schedule(habit, null); + } + + public void startListening() + { + commandRunner.addListener(this); + } + + public void stopListening() + { + commandRunner.removeListener(this); + } + + @NonNull + private Long getReminderTime(@NonNull Reminder reminder) + { + Calendar calendar = DateUtils.getStartOfTodayCalendar(); + calendar.set(Calendar.HOUR_OF_DAY, reminder.getHour()); + calendar.set(Calendar.MINUTE, reminder.getMinute()); + calendar.set(Calendar.SECOND, 0); + Long time = calendar.getTimeInMillis(); + + if (DateUtils.getLocalTime() > time) time += AlarmManager.INTERVAL_DAY; + + return applyTimezone(time); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/utils/RingtoneUtils.java b/app/src/main/java/org/isoron/uhabits/utils/RingtoneUtils.java new file mode 100644 index 000000000..c6a3b4a30 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/utils/RingtoneUtils.java @@ -0,0 +1,118 @@ +/* + * 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.media.*; +import android.net.*; +import android.preference.*; +import android.provider.*; +import android.support.annotation.*; +import android.support.v4.app.*; + +import org.isoron.uhabits.*; + +import static android.media.RingtoneManager.*; + +public abstract class RingtoneUtils +{ + @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 = getRingtone(context, ringtoneUri); + if (ringtone != null) + { + ringtoneName = ringtone.getTitle(context); + ringtone.stop(); + } + } + + return ringtoneName; + } + catch (RuntimeException e) + { + e.printStackTrace(); + return null; + } + } + + @Nullable + public static Uri getRingtoneUri(Context context) + { + 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); + + return ringtoneUri; + } + + public static void parseRingtoneData(Context context, @Nullable Intent data) + { + if (data == null) return; + + Uri ringtoneUri = data.getParcelableExtra(EXTRA_RINGTONE_PICKED_URI); + + if (ringtoneUri != null) + { + 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); + prefs.edit().putString("pref_ringtone_uri", "").apply(); + } + } + + public static void startRingtonePickerActivity(Fragment fragment, + int requestCode) + { + Uri existingRingtoneUri = getRingtoneUri(fragment.getContext()); + Uri defaultRingtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI; + + Intent intent = new Intent(ACTION_RINGTONE_PICKER); + intent.putExtra(EXTRA_RINGTONE_TYPE, TYPE_NOTIFICATION); + intent.putExtra(EXTRA_RINGTONE_SHOW_DEFAULT, true); + intent.putExtra(EXTRA_RINGTONE_SHOW_SILENT, true); + intent.putExtra(EXTRA_RINGTONE_DEFAULT_URI, defaultRingtoneUri); + intent.putExtra(EXTRA_RINGTONE_EXISTING_URI, existingRingtoneUri); + fragment.startActivityForResult(intent, requestCode); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/utils/StyledResources.java b/app/src/main/java/org/isoron/uhabits/utils/StyledResources.java new file mode 100644 index 000000000..1297f8962 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/utils/StyledResources.java @@ -0,0 +1,107 @@ +/* + * 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.content.res.*; +import android.graphics.drawable.*; +import android.support.annotation.*; + +import org.isoron.uhabits.*; + +public class StyledResources +{ + private static Integer fixedTheme; + + private final Context context; + + public StyledResources(@NonNull Context context) + { + this.context = context; + } + + public static void setFixedTheme(Integer theme) + { + fixedTheme = theme; + } + + public boolean getBoolean(@AttrRes int attrId) + { + TypedArray ta = getTypedArray(attrId); + boolean bool = ta.getBoolean(0, false); + ta.recycle(); + + return bool; + } + + public int getColor(@AttrRes int attrId) + { + TypedArray ta = getTypedArray(attrId); + int color = ta.getColor(0, 0); + ta.recycle(); + + return color; + } + + public Drawable getDrawable(@AttrRes int attrId) + { + TypedArray ta = getTypedArray(attrId); + Drawable drawable = ta.getDrawable(0); + ta.recycle(); + + return drawable; + } + + public float getFloat(@AttrRes int attrId) + { + TypedArray ta = getTypedArray(attrId); + float f = ta.getFloat(0, 0); + ta.recycle(); + + return f; + } + + public int[] getPalette() + { + int resourceId = getStyleResource(R.attr.palette); + if (resourceId < 0) throw new RuntimeException("resource not found"); + + return context.getResources().getIntArray(resourceId); + } + + int getStyleResource(@AttrRes int attrId) + { + TypedArray ta = getTypedArray(attrId); + int resourceId = ta.getResourceId(0, -1); + ta.recycle(); + + return resourceId; + } + + private TypedArray getTypedArray(@AttrRes int attrId) + { + int[] attrs = new int[]{ attrId }; + + if (fixedTheme != null) + return context.getTheme().obtainStyledAttributes(fixedTheme, attrs); + + return context.obtainStyledAttributes(attrs); + } +} 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..45b992205 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/utils/WidgetUtils.java @@ -0,0 +1,72 @@ +/* + * 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.widgets.*; + +import static android.appwidget.AppWidgetManager.*; +import static android.os.Build.VERSION.*; +import static android.os.Build.VERSION_CODES.*; +import static org.isoron.uhabits.utils.InterfaceUtils.*; + +public abstract class WidgetUtils +{ + @NonNull + public static WidgetDimensions getDimensionsFromOptions( + @NonNull Context ctx, @NonNull Bundle options) + { + if (SDK_INT < JELLY_BEAN) + throw new AssertionError("method requires jelly-bean"); + + int maxWidth = + (int) dpToPixels(ctx, options.getInt(OPTION_APPWIDGET_MAX_WIDTH)); + int maxHeight = + (int) dpToPixels(ctx, options.getInt(OPTION_APPWIDGET_MAX_HEIGHT)); + int minWidth = + (int) dpToPixels(ctx, options.getInt(OPTION_APPWIDGET_MIN_WIDTH)); + int minHeight = + (int) dpToPixels(ctx, options.getInt(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/BaseWidget.java b/app/src/main/java/org/isoron/uhabits/widgets/BaseWidget.java new file mode 100644 index 000000000..d90c0f613 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/widgets/BaseWidget.java @@ -0,0 +1,202 @@ +/* + * 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.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.intents.*; +import org.isoron.uhabits.preferences.*; + +import static android.os.Build.VERSION.*; +import static android.os.Build.VERSION_CODES.*; +import static android.view.View.MeasureSpec.*; + +public abstract class BaseWidget +{ + private final WidgetPreferences prefs; + + private final int id; + + @NonNull + private WidgetDimensions dimensions; + + @NonNull + private final Context context; + + protected final PendingIntentFactory pendingIntentFactory; + + public BaseWidget(@NonNull Context context, int id) + { + this.id = id; + this.context = context; + + HabitsApplication app = + (HabitsApplication) context.getApplicationContext(); + + prefs = app.getComponent().getWidgetPreferences(); + pendingIntentFactory = app.getComponent().getPendingIntentFactory(); + dimensions = new WidgetDimensions(getDefaultWidth(), getDefaultHeight(), + getDefaultWidth(), getDefaultHeight()); + } + + public void delete() + { + prefs.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 }; + } + + @NonNull + private Bitmap getBitmapFromView(View view) + { + view.invalidate(); + view.setDrawingCacheEnabled(true); + view.buildDrawingCache(true); + Bitmap drawingCache = view.getDrawingCache(); + + if(drawingCache == null) + throw new IllegalStateException("bitmap is null"); + + return drawingCache; + } + + @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/widgets/BaseWidgetProvider.java b/app/src/main/java/org/isoron/uhabits/widgets/BaseWidgetProvider.java index 9c36fe398..6c6008d36 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,144 @@ 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.preferences.*; -import java.io.FileOutputStream; -import java.io.IOException; +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); - - protected abstract int getLayoutId(); - - protected abstract View buildCustomView(Context context, Habit habit); - - public static String getHabitIdKey(long widgetId) - { - return String.format("widget-%06d-habit", widgetId); - } + private HabitList habits; - @Override - public void onDeleted(Context context, int[] appWidgetIds) - { - 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); - } + private WidgetPreferences widgetPrefs; @Override - public void onUpdate(Context context, AppWidgetManager manager, int[] appWidgetIds) - { - for(int id : appWidgetIds) - { - Bundle options = null; - - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) - options = manager.getAppWidgetOptions(id); - - updateWidget(context, manager, id, options); - } - } - - 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) - { - drawErrorWidget(context, manager, widgetId); - return; - } - - new RenderWidgetTask(widgetId, context, habit, dim, manager).execute(); - } - - private void drawErrorWidget(Context context, AppWidgetManager manager, int widgetId) - { - RemoteViews errorView = new RemoteViews(context.getPackageName(), R.layout.widget_error); - manager.updateAppWidget(widgetId, errorView); - } - - protected abstract void refreshCustomViewData(View widgetView); - - private void savePreview(Context context, int widgetId, Bitmap widgetCache, int width, - int height, String label) + public void onAppWidgetOptionsChanged(@Nullable Context context, + @Nullable AppWidgetManager manager, + int widgetId, + @Nullable Bundle options) { try { - 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 (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); - if(previewCache != null) - previewCache.compress(Bitmap.CompressFormat.PNG, 100, out); + updateDependencies(context); - out.close(); + BaseWidget widget = getWidgetFromId(context, widgetId); + WidgetDimensions dims = getDimensionsFromOptions(context, options); + widget.setDimensions(dims); + updateAppWidget(manager, widget); } - catch (IOException e) + catch (RuntimeException e) { + drawErrorWidget(context, manager, widgetId, e); e.printStackTrace(); } } - private WidgetDimensions getWidgetDimensions(Context context, Bundle options) + @Override + public void onDeleted(@Nullable Context context, @Nullable int[] ids) { - int maxWidth = getDefaultWidth(); - int minWidth = getDefaultWidth(); - int maxHeight = getDefaultHeight(); - int minHeight = getDefaultHeight(); + if (context == null) throw new RuntimeException("context is null"); + if (ids == null) throw new RuntimeException("ids is null"); + + updateDependencies(context); - if (options != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) + for (int id : ids) { - 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)); + BaseWidget widget = getWidgetFromId(context, id); + widget.delete(); } - - WidgetDimensions ws = new WidgetDimensions(); - ws.portraitWidth = minWidth; - ws.portraitHeight = maxHeight; - ws.landscapeWidth = maxWidth; - ws.landscapeHeight = minHeight; - return ws; } - private void measureCustomView(Context context, int w, int h, View customView) + @Override + public void onUpdate(@Nullable Context context, + @Nullable AppWidgetManager manager, + @Nullable int[] widgetIds) { - 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()); - - View imageView = entireView.findViewById(R.id.imageView); - w = imageView.getMeasuredWidth(); - h = imageView.getMeasuredHeight(); - - 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()); + 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); + + updateDependencies(context); + + new Thread(() -> { + Looper.prepare(); + for (int id : widgetIds) + update(context, manager, id); + }).start(); } - private class RenderWidgetTask extends BaseTask + @NonNull + protected Habit getHabitFromWidgetId(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; - } + long habitId = widgetPrefs.getHabitIdFromWidgetId(widgetId); + Habit habit = habits.getById(habitId); + if (habit == null) throw new HabitNotFoundException(); + return habit; + } - @Override - protected void onPreExecute() - { - super.onPreExecute(); - context.setTheme(R.style.TransparentWidgetTheme); + @NonNull + protected abstract BaseWidget getWidgetFromId(@NonNull Context context, + int id); - portraitRemoteViews = new RemoteViews(context.getPackageName(), getLayoutId()); - portraitWidgetView = buildCustomView(context, habit); - measureCustomView(context, dim.portraitWidth, dim.portraitHeight, portraitWidgetView); + private void drawErrorWidget(Context context, + AppWidgetManager manager, + int widgetId, + RuntimeException e) + { + RemoteViews errorView = + new RemoteViews(context.getPackageName(), R.layout.widget_error); - landscapeRemoteViews = new RemoteViews(context.getPackageName(), getLayoutId()); - landscapeWidgetView = buildCustomView(context, habit); - measureCustomView(context, dim.landscapeWidth, dim.landscapeHeight, - landscapeWidgetView); + if(e instanceof HabitNotFoundException) { + errorView.setCharSequence(R.id.label, "setText", context.getString(R.string.habit_not_found)); } - 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); - } + manager.updateAppWidget(widgetId, errorView); + } - @Override - protected void doInBackground() + private void update(@NonNull Context context, + @NonNull AppWidgetManager manager, + int widgetId) + { + try { - refreshCustomViewData(portraitWidgetView); - refreshCustomViewData(landscapeWidgetView); - } + BaseWidget widget = getWidgetFromId(context, widgetId); - @Override - protected void onPostExecute(Void aVoid) - { - try + if (SDK_INT > JELLY_BEAN) { - buildRemoteViews(portraitWidgetView, portraitRemoteViews, - dim.portraitWidth, dim.portraitHeight); - buildRemoteViews(landscapeWidgetView, landscapeRemoteViews, - dim.landscapeWidth, dim.landscapeHeight); - updateAppWidget(); - } - catch (Exception e) - { - 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); + e.printStackTrace(); } } - private int[] getPadding(int entireWidth, int entireHeight, int imageWidth, - int imageHeight) + private void updateDependencies(Context context) { - int w = (int) (((float) entireWidth - imageWidth) / 2); - int h = (int) (((float) entireHeight - imageHeight) / 2); - - return new int[]{ w, h, w, h }; + HabitsApplication app = + (HabitsApplication) context.getApplicationContext(); + habits = app.getComponent().getHabitList(); + widgetPrefs = app.getComponent().getWidgetPreferences(); } } diff --git a/app/src/main/java/org/isoron/uhabits/widgets/CheckmarkWidget.java b/app/src/main/java/org/isoron/uhabits/widgets/CheckmarkWidget.java new file mode 100644 index 000000000..3266d16a2 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/widgets/CheckmarkWidget.java @@ -0,0 +1,83 @@ +/* + * 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.widgets; + +import android.app.*; +import android.content.*; +import android.support.annotation.*; +import android.view.*; + +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; +import org.isoron.uhabits.widgets.views.*; + +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 pendingIntentFactory.toggleCheckmark(habit, null); + } + + @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/widgets/CheckmarkWidgetProvider.java b/app/src/main/java/org/isoron/uhabits/widgets/CheckmarkWidgetProvider.java index 6f6e12dbc..cd3fe8e62 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,18 @@ */ 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.*; 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/FrequencyWidget.java b/app/src/main/java/org/isoron/uhabits/widgets/FrequencyWidget.java new file mode 100644 index 000000000..ff76fdac4 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/widgets/FrequencyWidget.java @@ -0,0 +1,83 @@ +/* + * 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.widgets; + +import android.app.*; +import android.content.*; +import android.support.annotation.*; +import android.view.*; + +import org.isoron.uhabits.activities.common.views.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; +import org.isoron.uhabits.widgets.views.*; + +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 pendingIntentFactory.showHabit(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/FrequencyWidgetProvider.java b/app/src/main/java/org/isoron/uhabits/widgets/FrequencyWidgetProvider.java index 2fdbedb71..2d3e5fce9 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,18 @@ 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.*; 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/HabitPickerDialog.java b/app/src/main/java/org/isoron/uhabits/widgets/HabitPickerDialog.java index d09881acf..e648db7e6 100644 --- a/app/src/main/java/org/isoron/uhabits/widgets/HabitPickerDialog.java +++ b/app/src/main/java/org/isoron/uhabits/widgets/HabitPickerDialog.java @@ -19,75 +19,82 @@ 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 +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.preferences.*; + +import java.util.*; + +import static android.appwidget.AppWidgetManager.*; + +public class HabitPickerDialog extends Activity + implements AdapterView.OnItemClickListener { + private HabitList habitList; + + private 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 app = (HabitsApplication) getApplicationContext(); + app.getComponent().getWidgetUpdater().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 app = (HabitsApplication) getApplicationContext(); + AppComponent component = app.getComponent(); + habitList = component.getHabitList(); + preferences = component.getWidgetPreferences(); + 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) + for (Habit h : habitList) { 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/widgets/HistoryWidget.java b/app/src/main/java/org/isoron/uhabits/widgets/HistoryWidget.java new file mode 100644 index 000000000..0553e35ff --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/widgets/HistoryWidget.java @@ -0,0 +1,83 @@ +/* + * 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.widgets; + +import android.app.*; +import android.content.*; +import android.support.annotation.*; +import android.view.*; + +import org.isoron.uhabits.activities.common.views.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; +import org.isoron.uhabits.widgets.views.*; + +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 pendingIntentFactory.showHabit(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/widgets/HistoryWidgetProvider.java b/app/src/main/java/org/isoron/uhabits/widgets/HistoryWidgetProvider.java index bb8be7e25..6f377ca3a 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,18 @@ */ 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.*; -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/ScoreWidget.java b/app/src/main/java/org/isoron/uhabits/widgets/ScoreWidget.java new file mode 100644 index 000000000..7ff8cf1fe --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/widgets/ScoreWidget.java @@ -0,0 +1,103 @@ +/* + * 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.widgets; + +import android.app.*; +import android.content.*; +import android.support.annotation.*; +import android.view.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.activities.common.views.*; +import org.isoron.uhabits.activities.habits.show.views.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.preferences.*; +import org.isoron.uhabits.utils.*; +import org.isoron.uhabits.widgets.views.*; + +import java.util.*; + +public class ScoreWidget extends BaseWidget +{ + @NonNull + private Habit habit; + + private final Preferences prefs; + + public ScoreWidget(@NonNull Context context, int id, @NonNull Habit habit) + { + super(context, id); + this.habit = habit; + + HabitsApplication app = + (HabitsApplication) context.getApplicationContext(); + prefs = app.getComponent().getPreferences(); + } + + @Override + public PendingIntent getOnClickPendingIntent(Context context) + { + return pendingIntentFactory.showHabit(habit); + } + + @Override + public void refreshData(View view) + { + int defaultScoreInterval = prefs.getDefaultScoreSpinnerPosition(); + 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.toList(); + 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/widgets/ScoreWidgetProvider.java b/app/src/main/java/org/isoron/uhabits/widgets/ScoreWidgetProvider.java index 2608887b4..002be7f17 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,18 @@ */ 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.*; 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/StreakWidget.java b/app/src/main/java/org/isoron/uhabits/widgets/StreakWidget.java new file mode 100644 index 000000000..ecf3fe957 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/widgets/StreakWidget.java @@ -0,0 +1,91 @@ +/* + * 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.widgets; + +import android.app.*; +import android.content.*; +import android.support.annotation.*; +import android.view.*; +import android.view.ViewGroup.*; + +import org.isoron.uhabits.activities.common.views.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; +import org.isoron.uhabits.widgets.views.*; + +import java.util.*; + +import static android.view.ViewGroup.LayoutParams.*; + +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 pendingIntentFactory.showHabit(habit); + } + + @Override + public void refreshData(View view) + { + GraphWidgetView widgetView = (GraphWidgetView) view; + StreakChart chart = (StreakChart) widgetView.getDataView(); + + int color = ColorUtils.getColor(getContext(), habit.getColor()); + + int count = chart.getMaxStreakCount(); + List streaks = habit.getStreaks().getBest(count); + + chart.setColor(color); + chart.setStreaks(streaks); + } + + @Override + protected View buildView() + { + StreakChart dataView = new StreakChart(getContext()); + GraphWidgetView view = new GraphWidgetView(getContext(), dataView); + LayoutParams params = new LayoutParams(MATCH_PARENT, MATCH_PARENT); + view.setTitle(habit.getName()); + view.setLayoutParams(params); + return view; + } + + @Override + protected int getDefaultHeight() + { + return 200; + } + + @Override + protected int getDefaultWidth() + { + return 200; + } +} 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..211246eb3 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,18 @@ */ 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.*; -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; - } - - @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 StreakWidget(context, id, habit); } } diff --git a/app/src/main/java/org/isoron/uhabits/widgets/WidgetDimensions.java b/app/src/main/java/org/isoron/uhabits/widgets/WidgetDimensions.java new file mode 100644 index 000000000..5c88a2114 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/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.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/widgets/WidgetUpdater.java b/app/src/main/java/org/isoron/uhabits/widgets/WidgetUpdater.java new file mode 100644 index 000000000..81928f917 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/widgets/WidgetUpdater.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.widgets; + +import android.appwidget.*; +import android.content.*; +import android.support.annotation.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.commands.*; + +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 +{ + @NonNull + private final CommandRunner commandRunner; + + @NonNull + private final Context context; + + @Inject + public WidgetUpdater(@NonNull @AppContext Context context, + @NonNull CommandRunner commandRunner) + { + this.context = context; + this.commandRunner = commandRunner; + } + + @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/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/java/org/isoron/uhabits/views/CheckmarkWidgetView.java b/app/src/main/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetView.java similarity index 56% rename from app/src/main/java/org/isoron/uhabits/views/CheckmarkWidgetView.java rename to app/src/main/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetView.java index 869c91afe..b27d4f246 100644 --- a/app/src/main/java/org/isoron/uhabits/views/CheckmarkWidgetView.java +++ b/app/src/main/java/org/isoron/uhabits/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.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.activities.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,83 +56,91 @@ 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; - Context context = getContext(); + StyledResources res = new StyledResources(getContext()); String text; - int backgroundColor; - int foregroundColor; + int bgColor; + int fgColor; switch (checkmarkValue) { case Checkmark.CHECKED_EXPLICITLY: text = getResources().getString(R.string.fa_check); - backgroundColor = activeColor; - foregroundColor = - UIHelper.getStyledColor(context, R.attr.highContrastReverseTextColor); + bgColor = activeColor; + fgColor = res.getColor(R.attr.highContrastReverseTextColor); setShadowAlpha(0x4f); rebuildBackground(); - backgroundPaint.setColor(backgroundColor); + backgroundPaint.setColor(bgColor); frame.setBackgroundDrawable(background); break; 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); + bgColor = res.getColor(R.attr.cardBackgroundColor); + fgColor = res.getColor(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); + bgColor = res.getColor(R.attr.cardBackgroundColor); + fgColor = res.getColor(R.attr.mediumContrastTextColor); + + setShadowAlpha(0x00); + rebuildBackground(); + break; } ring.setPercentage(percentage); - ring.setColor(foregroundColor); - ring.setBackgroundColor(backgroundColor); + ring.setColor(fgColor); + ring.setBackgroundColor(bgColor); ring.setText(text); label.setText(name); - label.setTextColor(foregroundColor); + label.setTextColor(fgColor); requestLayout(); postInvalidate(); } + public void setActiveColor(int activeColor) + { + this.activeColor = activeColor; + } + + 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; + } + + @Override + @NonNull + protected Integer getInnerLayoutId() + { + return R.layout.widget_checkmark; + } + @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { @@ -147,16 +154,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 +175,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/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/widgets/views/GraphWidgetView.java index bb142ca7d..e11fda10f 100644 --- a/app/src/main/java/org/isoron/uhabits/views/GraphWidgetView.java +++ b/app/src/main/java/org/isoron/uhabits/widgets/views/GraphWidgetView.java @@ -17,62 +17,57 @@ * with this program. If not, see . */ -package org.isoron.uhabits.views; +package org.isoron.uhabits.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/widgets/views/HabitWidgetView.java similarity index 57% rename from app/src/main/java/org/isoron/uhabits/views/HabitWidgetView.java rename to app/src/main/java/org/isoron/uhabits/widgets/views/HabitWidgetView.java index bab859aaa..02f7cf671 100644 --- a/app/src/main/java/org/isoron/uhabits/views/HabitWidgetView.java +++ b/app/src/main/java/org/isoron/uhabits/widgets/views/HabitWidgetView.java @@ -17,27 +17,25 @@ * 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.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.*; + +import static org.isoron.uhabits.utils.InterfaceUtils.*; + +public abstract class HabitWidgetView extends FrameLayout { @Nullable protected InsetDrawable background; @@ -45,17 +43,12 @@ 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; + private StyledResources res; + public HabitWidgetView(Context context) { super(context); @@ -68,27 +61,27 @@ 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) (255 * res.getFloat(R.attr.widgetBackgroundAlpha)); - int shadowRadius = (int) UIHelper.dpToPixels(context, 2); - int shadowOffset = (int) UIHelper.dpToPixels(context, 1); + int shadowRadius = (int) dpToPixels(context, 2); + int shadowOffset = (int) dpToPixels(context, 1); int shadowColor = Color.argb(shadowAlpha, 0, 0, 0); - float cornerRadius = UIHelper.dpToPixels(context, 5); + float cornerRadius = dpToPixels(context, 5); float[] radii = new float[8]; Arrays.fill(radii, cornerRadius); @@ -98,20 +91,24 @@ 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(res.getColor(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); + res = new StyledResources(getContext()); + shadowAlpha = (int) (255 * res.getFloat(R.attr.widgetShadowAlpha)); + rebuildBackground(); } } diff --git a/app/src/main/java/org/isoron/uhabits/widgets/views/package-info.java b/app/src/main/java/org/isoron/uhabits/widgets/views/package-info.java new file mode 100644 index 000000000..2fbf2799c --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/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.widgets.views; \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/ic_action_filter_dark.png b/app/src/main/res/drawable-hdpi/ic_action_filter_dark.png new file mode 100644 index 000000000..7e8a6b536 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_filter_dark.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_filter_light.png b/app/src/main/res/drawable-hdpi/ic_action_filter_light.png new file mode 100644 index 000000000..a966cb9bd Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_filter_light.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_filter_dark.png b/app/src/main/res/drawable-mdpi/ic_action_filter_dark.png new file mode 100644 index 000000000..59a2ec755 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_filter_dark.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_filter_light.png b/app/src/main/res/drawable-mdpi/ic_action_filter_light.png new file mode 100644 index 000000000..d86492b42 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_filter_light.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_filter_dark.png b/app/src/main/res/drawable-xhdpi/ic_action_filter_dark.png new file mode 100644 index 000000000..9416c70ec Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_filter_dark.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_filter_light.png b/app/src/main/res/drawable-xhdpi/ic_action_filter_light.png new file mode 100644 index 000000000..b64df3612 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_filter_light.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_filter_dark.png b/app/src/main/res/drawable-xxhdpi/ic_action_filter_dark.png new file mode 100644 index 000000000..1263ae82e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_filter_dark.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_filter_light.png b/app/src/main/res/drawable-xxhdpi/ic_action_filter_light.png new file mode 100644 index 000000000..2314642f9 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_filter_light.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_filter_dark.png b/app/src/main/res/drawable-xxxhdpi/ic_action_filter_dark.png new file mode 100644 index 000000000..cb2207f11 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_filter_dark.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_filter_light.png b/app/src/main/res/drawable-xxxhdpi/ic_action_filter_light.png new file mode 100644 index 000000000..9319c4bb4 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_action_filter_light.png differ diff --git a/app/src/main/res/drawable/card_light_background.xml b/app/src/main/res/drawable/card_light_background.xml index 60d7d0fa4..ce34e3ca9 100644 --- a/app/src/main/res/drawable/card_light_background.xml +++ b/app/src/main/res/drawable/card_light_background.xml @@ -52,7 +52,7 @@ android:left="0.5dp" android:right="0.5dp"> - + \ No newline at end of file diff --git a/app/src/main/res/drawable/habits_item_check_normal.xml b/app/src/main/res/drawable/habits_item_check_normal.xml deleted file mode 100644 index f0c7ae544..000000000 --- a/app/src/main/res/drawable/habits_item_check_normal.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/habits_item_check_pressed.xml b/app/src/main/res/drawable/habits_item_check_pressed.xml deleted file mode 100644 index 1760e40d2..000000000 --- a/app/src/main/res/drawable/habits_item_check_pressed.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/habits_list_header_amoled_background.xml b/app/src/main/res/drawable/habits_list_header_amoled_background.xml deleted file mode 100644 index b65fb29c3..000000000 --- a/app/src/main/res/drawable/habits_list_header_amoled_background.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/habits_list_header_dark_background.xml b/app/src/main/res/drawable/habits_list_header_dark_background.xml deleted file mode 100644 index 88989c254..000000000 --- a/app/src/main/res/drawable/habits_list_header_dark_background.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/habits_list_header_light_background.xml b/app/src/main/res/drawable/habits_list_header_light_background.xml deleted file mode 100644 index 66413f277..000000000 --- a/app/src/main/res/drawable/habits_list_header_light_background.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/about.xml b/app/src/main/res/layout/about.xml index c1da159e0..0d391a70c 100644 --- a/app/src/main/res/layout/about.xml +++ b/app/src/main/res/layout/about.xml @@ -105,6 +105,19 @@ + + + + + + + + + + + @@ -176,6 +197,10 @@ style="@style/About.Item" android:text="Álinson Xavier (Português)"/> + + @@ -202,7 +227,11 @@ + android:text="Yurii Stavytskyi (Українська)"/> + + + + @@ -235,12 +268,21 @@ + + + + + style="@style/ToolbarShadow" + android:layout_below="@id/toolbar"/> diff --git a/app/src/main/res/layout/automation.xml b/app/src/main/res/layout/automation.xml new file mode 100644 index 000000000..f127a9f2d --- /dev/null +++ b/app/src/main/res/layout/automation.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +