Compare commits

...

322 Commits

Author SHA1 Message Date
bd6cba8303 Merge branch 'hotfix/1.5.2' 2016-05-19 12:01:29 -04:00
befc3a0ad8 Update changelog 2016-05-19 12:01:24 -04:00
f4c963e2c1 Update Japanese translation 2016-05-19 11:58:20 -04:00
e9a4a047c1 Trim habit names on log files 2016-05-19 11:50:38 -04:00
7ce1988d2e Fix NullPointerException on RingView 2016-05-19 11:46:02 -04:00
53911fa410 Send bug report on the body of email, instead of attachment 2016-05-19 11:39:17 -04:00
f5f43d9a16 Bump version to 1.5.2 2016-05-19 10:40:45 -04:00
d0e475ad78 Merge branch 'hotfix/1.5.1' 2016-05-17 09:35:51 -04:00
b3199cf092 Update version and changelog 2016-05-17 09:35:10 -04:00
73e6f2a2d4 Remove ActiveAndroid.jar 2016-05-17 09:29:53 -04:00
097a5be864 Merge branch 'release/1.5.0' 2016-05-15 08:43:26 -04:00
0287473e61 Update changelog 2016-05-15 08:40:58 -04:00
f403dc6f77 Update view tests 2016-05-15 08:40:27 -04:00
23466523df Remove minTextSize
This was causing problems, since some launchers completely
ignore minimum widget sizes. When widgets were too small,
the text started to overflow.
2016-05-15 08:21:46 -04:00
83ca136346 Update changelog 2016-05-13 07:09:09 -04:00
7830b599db Bump version to 1.5.0 2016-05-13 06:52:32 -04:00
49376d4f41 Minor changes to widget colors 2016-05-13 06:50:35 -04:00
8ee9c9c0b1 Update screenshots 2016-05-13 06:50:22 -04:00
ee9da29422 Update introduction 2016-05-13 06:42:22 -04:00
ae7b0408b2 Update list of complete translations 2016-05-13 06:29:19 -04:00
d926d9dbd6 Keep name of the app consistent across translations 2016-05-13 06:14:40 -04:00
5694da2413 Add new translations and update list of contributors 2016-05-13 06:08:09 -04:00
7e0ae144b8 Update translations 2016-05-13 04:56:02 -04:00
1426d887e7 CircleCI: Ignore some branches 2016-05-13 04:41:15 -04:00
acb8e820fd Use AlertDialog from Support Library 2016-05-11 07:38:43 -04:00
eae0d66f51 Dismiss notification automatically when user adds a checkmark 2016-05-11 07:30:45 -04:00
e933cbbc43 Fix up navigation when opening the app from widget
Fixes #94
2016-05-11 06:44:11 -04:00
0b95b6a78c Install build-tools before compiling 2016-05-08 08:51:08 -04:00
5e873a3659 Make frequency text more natural 2016-05-08 08:46:45 -04:00
d292ecd988 Increase default amount of memory for gradle 2016-05-08 08:24:14 -04:00
19b484c368 Fix crash on settings screen 2016-05-08 08:23:50 -04:00
834ae92d87 Reschedule alarms on boot
Fixes #93
2016-05-08 08:04:24 -04:00
26ce92d381 Update versions 2016-05-08 07:38:57 -04:00
0eb12812d4 Allow user to reverse the order of days 2016-05-04 06:30:06 -04:00
a1a636c718 Make ScoreView use custom interval also on widgets 2016-05-01 10:06:02 -04:00
b2811b9797 Allow user to select reminder sound
Closes #63
2016-05-01 09:43:28 -04:00
4db7a6e89c Show larger ripple when toggling check marks
Closes #78
2016-05-01 08:21:36 -04:00
677a643e5b Allow theme to be fixed during tests 2016-04-30 19:13:36 -04:00
dbca3238f6 Small changes to ScoreView 2016-04-30 17:14:44 -04:00
ae5dd700f3 Fix widget colors and size 2016-04-30 16:42:27 -04:00
7e4c508d7d Merge branch 'feature/ring-view' into dev 2016-04-30 12:44:25 -04:00
d0c056eeaa Update screenshots 2016-04-30 12:43:18 -04:00
f172c69eed Update tests 2016-04-30 12:30:20 -04:00
0cd4a41438 Bump gradle version 2016-04-29 11:48:46 -04:00
d77c78249c Tweak layout to work with older phones with smaller screen 2016-04-29 11:48:06 -04:00
d9d48ff984 Minor UI changes 2016-04-29 07:55:45 -04:00
7be71ba4f8 Fix icon colors and remove some warnings 2016-04-29 07:44:06 -04:00
f0534fefbc Show more information on score overview 2016-04-29 07:35:38 -04:00
30ef75bb45 Alternative design for header bar 2016-04-29 06:40:02 -04:00
44ed74a693 Correct colors 2016-04-29 06:38:19 -04:00
4aea527e07 Fix some crashes on widgets 2016-04-27 06:01:10 -04:00
638539259a Bump gradle version 2016-04-26 20:09:06 -04:00
75dec88411 Update widget previews 2016-04-26 20:08:59 -04:00
2dc4fcbc46 Fix transparency on widgets 2016-04-25 22:22:37 -04:00
7977d5247c Tweak transparency and colors 2016-04-25 21:03:23 -04:00
8b18e32a16 Create color palette for widgets 2016-04-24 06:42:14 -04:00
dbcad9a5f4 Use dark theme for widget colors 2016-04-24 06:42:14 -04:00
3bba75ff50 Add ripple effect to check mark widget 2016-04-24 06:42:14 -04:00
9bbeee66e2 Impose max number of lines on habit list 2016-04-24 06:42:14 -04:00
c6e76f4cfd Show multiple lines on check mark widget 2016-04-24 06:42:14 -04:00
980db1d171 Add ring to check mark widget 2016-04-24 06:42:14 -04:00
aee5d975db Replace star by ring 2016-04-24 06:42:14 -04:00
e2bb4371d3 Fix UI test for settings screen 2016-04-24 06:37:15 -04:00
fcee8552f0 Switch to compat ActionMode; fix tests 2016-04-24 05:55:42 -04:00
a4864e4612 Extract some strings; move definitions to correct file
Fixes #81
2016-04-21 06:05:38 -04:00
8ddf4574e3 Fix failed view tests 2016-04-20 09:27:44 -04:00
f1ca8c5449 Merge branch 'feature/material-compat' into dev 2016-04-20 07:52:06 -04:00
81a188f125 Customize action mode colors 2016-04-20 07:50:12 -04:00
44402bf3a4 Fix progress bar position 2016-04-20 07:31:02 -04:00
7f159149ef Minor layout changes on the spinners 2016-04-19 22:50:41 -04:00
0c1e8d5131 Switch to AppCompatDialogFragment and compat Fragment 2016-04-19 22:10:25 -04:00
a4e3f7e037 Fix spinners on EditHabitDialog 2016-04-19 21:53:14 -04:00
b354a0765b Use grey toolbar on pre-Lollipop 2016-04-19 21:02:05 -04:00
bbd959dfda Fix borders on HistoryEditorDialog 2016-04-19 09:01:36 -04:00
d05d404c55 Show toolbar shadow on pre-Lollipop 2016-04-19 08:27:45 -04:00
8938b0c9a6 Improve settings screen 2016-04-19 07:50:05 -04:00
bd6fcd066c Fix dialog borders 2016-04-19 05:36:34 -04:00
6a5f2abb76 Switch to AppCompatActivity 2016-04-18 07:59:41 -04:00
33b5215b00 Use SwitchPreference instead of checkmarks 2016-04-18 06:21:04 -04:00
5d808dd2e8 Update padding on widgets 2016-04-16 09:20:33 -04:00
767ec1b6de Minor layout improvements for HabitScoreView 2016-04-16 09:16:39 -04:00
9e5f3d8f58 Fix blinking when updating widgets 2016-04-16 09:03:45 -04:00
775d028629 Add missing icon 2016-04-16 08:51:56 -04:00
4bfb839370 Draw separate widgets for landscape and portrait modes
Fixes #76
2016-04-16 08:51:20 -04:00
8f64b696d8 Remove debug messages 2016-04-16 08:10:12 -04:00
c4bf31e778 Adjust text size and position on HistoryView and StreakView 2016-04-16 08:06:01 -04:00
c757ce6548 Enforce min text size on HabitScoreView 2016-04-16 07:36:39 -04:00
78994bb3c4 Add screenshots for night mode 2016-04-15 19:12:45 -04:00
2373ba5407 Update screenshots 2016-04-15 19:10:38 -04:00
b601a643dd Merge branch 'feature/dark-theme' into dev 2016-04-15 18:29:39 -04:00
90e513e778 Remove warning when palette cannot be found 2016-04-15 18:26:01 -04:00
728c9557f0 Migrate DB to new color format 2016-04-15 18:25:30 -04:00
5bd0c842f5 Minor typo 2016-04-15 18:10:34 -04:00
97711087f9 Fix invisible text on widgets 2016-04-15 17:49:10 -04:00
2fe2d8834a Fix spinner theme on pre-lollipop 2016-04-15 17:41:16 -04:00
93a893d660 Replace icons with Material icons
Fixes #9
2016-04-15 16:34:22 -04:00
feff9a18e6 Fix action icons 2016-04-15 16:13:52 -04:00
b42565b770 Backport dark theme to pre-lollipop devices 2016-04-13 11:54:24 -04:00
3de702ced2 Fix colors in About screen 2016-04-13 10:44:11 -04:00
42f7f4042d Add option for AMOLED night mode 2016-04-13 10:38:20 -04:00
2115a590f2 Add fade transition when switching themes 2016-04-13 09:53:37 -04:00
f5f3ec9f88 Update tests for custom views 2016-04-12 23:42:02 -04:00
6d5a8f5753 Merge branch 'sciamano-day-of-week' into dev 2016-04-12 23:29:10 -04:00
06fc04092b Code cleanup 2016-04-12 23:21:00 -04:00
Denis
24c02605d1 Select first day of week according to the current locale 2016-04-12 23:11:32 -04:00
11fcb67624 Improve custom views' colors 2016-04-11 18:11:18 -04:00
abc92e390a Update list of translators 2016-04-10 12:59:42 -04:00
52c07660b1 Implement menu item to switch between themes 2016-04-10 10:41:52 -04:00
35f778c376 Do not use habit color as primary color on dark themes 2016-04-10 08:32:01 -04:00
436ae7e574 Update selected_box colors for dark theme 2016-04-10 08:17:36 -04:00
5c683a77c2 Fix ripple on ListHabitsFragment 2016-04-10 08:13:05 -04:00
dd1f6c9efc Change habit.color palette according to current theme 2016-04-09 20:01:35 -04:00
cba089407b Merge tag 'v1.4.1' into dev
Loop 1.4.1
2016-04-09 05:36:12 -04:00
a7b0395a2a Merge branch 'hotfix/1.4.1' 2016-04-09 05:36:05 -04:00
7d2946360f Update changelog 2016-04-09 05:31:07 -04:00
04e8432522 Widgets: show error message instead of crashing 2016-04-09 05:27:39 -04:00
13d34945b4 Merge branch 'poeditor' into hotfix/1.4.1 2016-04-09 05:10:41 -04:00
756578508e Bump version to 1.4.1 2016-04-09 05:10:20 -04:00
38fc7650b9 Update translations 2016-04-09 05:08:02 -04:00
cf06ec0a4b Fix chart colors 2016-04-09 04:58:26 -04:00
f0701f7b35 First version of dark theme 2016-04-08 17:15:14 -04:00
de8018af7d Delete unused resources 2016-04-08 15:53:47 -04:00
ac885e1503 Refactor ListHabitsFragment layouts and styles 2016-04-08 15:50:51 -04:00
f85e09288c Add link to open beta on Google Play 2016-04-07 18:03:46 -04:00
6f11596bb7 Merge tag 'v1.4.0' into dev
Loop 1.4.0
2016-04-07 17:48:50 -04:00
64c4367706 Merge branch 'release/1.4.0' 2016-04-07 17:48:33 -04:00
c08702341a Update Spanish translations 2016-04-07 17:23:01 -04:00
abb8c09bc9 Update widgets after executing command 2016-04-07 17:15:33 -04:00
87255ceb25 RingView: invalidate after updating percentage and color
Fixes #79
2016-04-07 17:02:02 -04:00
aedbfded0f Show POEditor link if language is not fully translated 2016-04-06 06:33:52 -04:00
779c0040bd Add some options back to settings screen 2016-04-05 13:32:46 -04:00
7bba6a887f Increase width of chart labels as needed 2016-04-05 08:11:37 -04:00
52a1d19793 Allow labels to expand 2016-04-05 06:50:42 -04:00
35e346e7ea Remove empty strings from translations 2016-04-05 06:42:32 -04:00
3102158bdb Merge branch 'poeditor' into release/1.4.0 2016-04-05 06:34:00 -04:00
7afa65a53c Add Czech translation 2016-04-05 06:33:48 -04:00
2e5533d480 Update translations (POEditor) 2016-04-05 06:28:35 -04:00
b29c7e695a Save failed views to SD card root if writable 2016-04-05 05:47:09 -04:00
7df745eb82 Update CHANGELOG 2016-04-04 20:49:57 -04:00
c0d518ca4d Bump version to 1.4.0 2016-04-04 19:48:49 -04:00
eeff14a2ca Bump version to 1.4.0 2016-04-04 19:31:35 -04:00
af3b18f518 Fix incorrect date format for some languages 2016-04-04 19:05:24 -04:00
3811f4b339 Rename tests for compatibility with AWS Device Farm 2016-04-04 11:15:17 -04:00
50a8aece6f Move expensive call to background thread 2016-04-04 08:18:59 -04:00
2f66cfc417 Minor string change 2016-04-04 08:07:10 -04:00
132ce36a57 More detailed logs in case getFilesDir fails 2016-04-04 07:58:07 -04:00
147f010d1b Update website URL 2016-04-04 07:57:28 -04:00
afc91ed89a Merge branch 'feature/performance' into dev 2016-04-04 07:04:22 -04:00
684c37d167 Update list of translators 2016-04-04 07:02:41 -04:00
8b296ce2ae Make setup method public 2016-04-04 06:56:02 -04:00
e2e9e2e27a Remove debug code 2016-04-04 06:54:12 -04:00
8b2f31c44a Update widget preview 2016-04-04 06:53:55 -04:00
83db9fe2f2 Hide progress bar on startup 2016-04-04 06:46:16 -04:00
9891139b5a Fix tests on ICS 2016-04-04 06:43:42 -04:00
cbc8cbfea3 Fix scores with null habit 2016-04-04 06:02:56 -04:00
96c46b655d Minor refactoring 2016-04-04 05:43:07 -04:00
0de52d4fa3 BaseTask: move register/unregister to onPre/PostExecute 2016-04-04 05:42:27 -04:00
16d85dc4c7 Update README.md 2016-04-03 12:41:57 -04:00
c51b1fd399 Remove progress bar; switch to BaseTask 2016-04-03 07:51:54 -04:00
eb8dd1d450 Simplify and optimize loading time of ShowHabitFragment 2016-04-03 07:46:08 -04:00
357646152f Use postInvalidate instead of invalidate 2016-04-03 07:27:37 -04:00
ae5be202b2 Add convenience methods for tracing 2016-04-03 07:27:16 -04:00
cc3e2153d0 Allow BaseTask to publish integer progress 2016-04-03 07:26:40 -04:00
7433a2413d Refactor BaseTask interface 2016-04-03 06:45:10 -04:00
1cd8eb6849 NumberView: do not request layout on setNumber 2016-04-03 05:11:45 -04:00
7f009e2365 Create indexes in database 2016-04-03 05:10:13 -04:00
4cec7d7367 Implement methods to generate huge test database 2016-04-03 05:08:57 -04:00
f59c0bd7ff Replace badges 2016-04-02 13:58:38 -04:00
2d2bb8b4ed Render widgets in background 2016-04-02 13:32:04 -04:00
e476096ae1 Fix view tests 2016-04-02 11:47:28 -04:00
e2c814a982 Fix ShowHabitFragment and HistoryEditorDialog 2016-04-02 10:48:33 -04:00
4fcaa68baf Throw exception when slow methods are executed on the main thread 2016-04-02 10:27:45 -04:00
02e45dbe27 Rename DialogHelper to UIHelper 2016-04-02 10:14:21 -04:00
bf6562f854 Update views on background thread 2016-04-01 17:50:14 -04:00
3c927e009a Improve performance of Score computation 2016-04-01 17:49:17 -04:00
a9bcae0f2f Clean up CheckmarkView 2016-04-01 17:24:12 -04:00
bc54f3f916 Improve performance of toggleCheckmark 2016-04-01 16:21:22 -04:00
5763d76167 Make setup method public 2016-04-01 16:08:51 -04:00
86b66b1388 Add tests for DeleteHabitsCommand 2016-03-31 08:02:44 -04:00
5e0422848b Add tests for EditHabitCommand 2016-03-31 07:54:00 -04:00
a67a3297b1 Add tests for ToggleRepetitionCommand 2016-03-31 07:34:20 -04:00
b2f2d4ad28 Add tests for UnarchiveHabitsCommand 2016-03-31 07:24:43 -04:00
4dc8201dcb Add tests for CreateHabitCommand 2016-03-31 07:20:20 -04:00
f24e300b40 Implement tests for Archive and ChangeColor commands 2016-03-31 07:11:24 -04:00
21157eaa19 Increase similarity threshold 2016-03-31 07:09:47 -04:00
0ea2c8e5c8 HistoryView: test tap at invalid locations 2016-03-31 06:31:13 -04:00
826491bc90 Test custom views with transparent background 2016-03-31 06:21:01 -04:00
72f5ca5531 Add tests for NumberView 2016-03-31 06:03:16 -04:00
f9da90a93a Add tests for HabitStreakView 2016-03-30 21:02:05 -04:00
96c6a97ad0 Add tests for HabitScoreView 2016-03-30 19:46:37 -04:00
20469789ea Add tests for HabitFrequencyView 2016-03-30 18:41:00 -04:00
abafffcff1 Increase similarity threshold 2016-03-30 12:25:45 -04:00
d45a4445cc Minor changes to HabitHistoryView rendering; enable haptic feedback 2016-03-30 12:25:24 -04:00
94f56a8869 Make all unit tests inherit from BaseTest 2016-03-30 08:49:42 -04:00
bc331d0a4d Write tests for HabitHistoryView 2016-03-30 08:28:21 -04:00
2ba7246e5f Refactor Tasks for better testability 2016-03-30 08:28:06 -04:00
67b88c5012 Show log when build fails 2016-03-30 08:24:42 -04:00
e0b637e84d Merge branch 'feature/unit-test-views' into dev 2016-03-28 21:14:42 -04:00
e14d73cf32 Add translator 2016-03-28 21:00:07 -04:00
16b9584868 Minor changes to strings.xml 2016-03-28 20:20:18 -04:00
c091116105 Compute padding/margins more precisely on CheckmarkView 2016-03-28 20:06:32 -04:00
37e1a6233c Copy failed generated images to CircleCI artifacts folder 2016-03-27 18:47:26 -04:00
6838a0f2d3 Restore missing asset 2016-03-27 06:40:56 -04:00
53e737c169 Allow API version-specific view tests; use relative image distance 2016-03-27 06:39:40 -04:00
285f79ed95 Add tests for CheckmarkView 2016-03-26 20:11:17 -04:00
4f65c8bef1 Add script to pull failed generated images from device 2016-03-26 19:47:53 -04:00
51b7cfc8fa Remove useless asset 2016-03-26 19:47:04 -04:00
55ed2585ea Additional tests for RingView 2016-03-26 19:46:33 -04:00
2c62e1578a Create base test class for custom views 2016-03-26 19:25:50 -04:00
e3d96c0431 Upload code coverage to codecov.io 2016-03-26 17:03:21 -04:00
b4c2d2237a Draw month name on the following column.
This reverts commit f9377e17. See #55 for reason.
2016-03-26 08:57:18 -04:00
d14fbbd130 Make streak colors consistent 2016-03-26 08:49:02 -04:00
e2c99d745e Improve visualization of streaks
Closes #20
2016-03-26 08:27:23 -04:00
dfe41176bc Reintroduce vibrate permission 2016-03-25 17:01:41 -04:00
1ef4f8d456 Use system haptic feedback instead of custom vibration; enable on short press
Closes #66
2016-03-25 14:39:57 -04:00
6c810ee7a3 Allow user to send bug report from settings screen 2016-03-25 14:39:57 -04:00
5115379fdd Merge branch 'feature/import-data' into dev 2016-03-25 09:21:47 -04:00
d176ea91fb Use custom matcher for settings activity 2016-03-25 09:04:07 -04:00
62df660079 Implement UI tests for import/export and help 2016-03-25 08:10:10 -04:00
790beb185a Do not save backup when importing Loop DB 2016-03-25 08:09:33 -04:00
59c0af17d7 Use application context to initialize ActiveAndroid 2016-03-25 07:09:41 -04:00
28dad560a6 Write tests for CSV exporter 2016-03-25 07:09:06 -04:00
9e410f8395 Refactor and write tests for IO tasks 2016-03-25 06:18:07 -04:00
3656c51e95 Disable pre-dexing 2016-03-24 22:14:54 -04:00
d3ebb4ff25 Create SD card 2016-03-24 21:52:59 -04:00
3c595bc79d Add null check on DatabaseHelper 2016-03-24 21:51:02 -04:00
1120f56dd4 Write tests for CSV export 2016-03-24 21:36:41 -04:00
2dfbcfcdb0 Revert to alpha version of gradle
Build fails otherwise
2016-03-24 20:14:26 -04:00
90a3964f0f Close database 2016-03-24 18:39:49 -04:00
6c05366f0f Remove illegal characters from filename 2016-03-24 18:39:32 -04:00
dcc771abe7 Chose external dir better 2016-03-24 18:39:14 -04:00
743431ef67 Refactor DatabaseHelper; write tests for data import 2016-03-24 06:59:41 -04:00
d5fc1a6886 Allow user to import full database
Closes #67
2016-03-23 19:21:06 -04:00
eeb0b109ae Remove export CSV from context menu 2016-03-23 19:20:12 -04:00
ad391fa791 Remove extra permissions; better organize files dir 2016-03-23 18:50:02 -04:00
e8bbae8ef9 Allow user to export a full copy of the database 2016-03-23 18:01:25 -04:00
c9793df7c7 Reorganize files 2016-03-23 17:20:26 -04:00
3c6114fc87 Update README.md 2016-03-23 14:19:30 -04:00
4ea4201b6a Merge pull request #69 from XuToTo/patch-1
Update strings.xml
2016-03-23 14:10:42 -04:00
XuToTo
30b5b90091 Update strings.xml
Make some zh-translation words more friendly :)
2016-03-24 00:53:37 +08:00
e6b7b8b590 Export all habits as CSV from the settings menu
Closes #28
2016-03-22 22:54:02 -04:00
2d675ed9b0 Use DB transaction to perform import 2016-03-22 22:54:02 -04:00
1db2f69f05 Import data from HabitBull
Closes #44
2016-03-22 22:54:02 -04:00
49a80faca3 Trap all exceptions 2016-03-22 22:54:02 -04:00
aa5df56437 Use stable version of gradle 2016-03-22 22:54:01 -04:00
581197be03 Import data from Tickmate and Rewire
Closes #36, closes #41
2016-03-22 22:53:16 -04:00
dfe5c4954e Refactor CSVExporter 2016-03-21 06:25:48 -04:00
a0582b65d5 Merge tag 'v1.3.3' into dev
Version 1.3.3
2016-03-20 17:55:16 -04:00
9edc3e12c9 Merge branch 'hotfix/1.3.3' 2016-03-20 17:55:07 -04:00
7bb3db213f Update changelog 2016-03-20 17:46:10 -04:00
e05a69b527 Merge branch 'poeditor' into hotfix/1.3.3 2016-03-20 17:31:08 -04:00
d5df6ddcdb Add Spanish and Korean translations
Closes #40
2016-03-20 17:30:55 -04:00
3016263750 Update translations 2016-03-20 17:25:06 -04:00
f9377e1768 Draw month name on the correct column
Fixes #55
2016-03-20 17:10:57 -04:00
ae0dad9120 Use DateHelper instead of instantiating GregorianCalendar directly
Fixes #58
2016-03-20 17:06:22 -04:00
be114bde7f Bump version to 1.3.3 2016-03-20 17:01:58 -04:00
e7148abc2e Open statistics page when user taps on widget
Closes #19
2016-03-20 08:36:16 -04:00
2d87076a48 Change the way score are grouped 2016-03-20 08:19:57 -04:00
7da4ddf91b Allow user to change score view interval
Closes #10
2016-03-20 08:06:12 -04:00
e4e8d77acc Add link to FAQ
Closes #24
2016-03-19 13:30:04 -04:00
7945a5faf9 Set default habit frequency to daily 2016-03-19 11:58:25 -04:00
3f7d25461d Wake up device in Doze mode 2016-03-19 10:19:28 -04:00
519737a1c3 Minor changes to English strings 2016-03-19 10:02:49 -04:00
c3ff1fbe03 Add quick selection for commonly used habit frequencies
Closes #25
2016-03-19 09:46:42 -04:00
d39e1978a2 Add Spanish and Korean translations
Closes #40
2016-03-19 05:03:23 -04:00
536c095f23 Merge tag 'v1.3.2' into dev
Version 1.3.2
2016-03-18 11:36:48 -04:00
e693504183 Update translations 2016-03-18 11:01:25 -04:00
7d9a94ae9e Add line to disable large tests 2016-03-18 10:58:18 -04:00
326cb8f73f Minor changes to javadoc and method visibility 2016-03-18 10:42:47 -04:00
6826bd1ddc Update test 2016-03-18 10:36:56 -04:00
b5bc347624 Use default instead of null for reminderDays 2016-03-18 07:43:11 -04:00
55c058ff42 Minor spelling mistakes 2016-03-18 07:24:26 -04:00
e829bb8109 Enable parallel and daemon (gradle) 2016-03-18 07:18:50 -04:00
0921f9346e Refactor and write docs for Score and ScoreList 2016-03-18 07:16:31 -04:00
eb017bf99b Write missing tests and docs for RepetitionList 2016-03-17 06:49:11 -04:00
79b6ef8200 Improve null check for Checkmark and CheckmarkList 2016-03-17 06:39:29 -04:00
7ba62d6784 Minor formatting 2016-03-17 06:39:12 -04:00
f5e4a88415 Implement missing tests for Habit; remove some dead code 2016-03-17 06:20:52 -04:00
075b7812eb Refactor and write documentation for Habit 2016-03-16 07:49:40 -04:00
3d42505fb9 Merge tag 'v1.3.1' into dev 2016-03-15 20:38:51 -04:00
ffdc923268 Make labels more clear and customizable 2016-03-15 06:10:39 -04:00
9232378d04 Refactor RingView; make text size consistent 2016-03-15 05:30:27 -04:00
b20fd44cbc Scroll to view before clicking 2016-03-14 20:48:39 -04:00
ded8800017 Extract common style 2016-03-14 19:36:05 -04:00
606f66b8d9 Merge branch 'feature/repetition-count' into dev 2016-03-14 19:30:58 -04:00
1bb6cd405b Externalize strings 2016-03-14 19:13:33 -04:00
babf7d64f0 Display repetition count for last week, month, etc
Closes #21
2016-03-14 19:07:32 -04:00
dfbcf78dd7 Update list of translators 2016-03-14 15:46:52 -04:00
e01c668e4d Add Swedish translation 2016-03-14 15:46:40 -04:00
2eef696027 Add Russian translation 2016-03-14 15:46:30 -04:00
ddd10cacd1 Add Polish translation 2016-03-14 15:38:42 -04:00
0cdde4901e Add Italian translation 2016-03-14 15:38:29 -04:00
f13e9b7362 Update German translation 2016-03-14 15:38:12 -04:00
558f72d7c5 Update French translation 2016-03-14 15:33:06 -04:00
6e493f55bc Declare RTL support in the manifest 2016-03-14 15:18:58 -04:00
5186ab840a Add Arabic translation 2016-03-14 15:11:41 -04:00
65fd82d888 Fix indentation 2016-03-14 15:04:15 -04:00
866b62987c Wake up device before running UI tests 2016-03-14 14:55:47 -04:00
18abb2038f Check if habit is null on BaseWidgetProvider 2016-03-14 14:54:43 -04:00
f7f4b5eeb0 Simplify code for drawing header 2016-03-14 14:35:57 -04:00
45a7433773 Use StaticLayout to draw RingView label
Fixes #29
2016-03-14 13:34:21 -04:00
40420c3a77 Save logcat to reports 2016-03-14 08:37:02 -04:00
988b39f2e5 Add missing debug messages to SystemHelper 2016-03-14 08:33:46 -04:00
a0803966f9 Merge branch 'feature/unit-tests' into dev 2016-03-14 08:14:56 -04:00
e4af662836 Merge pull request #33 from vanniktech/patch-1 2016-03-14 08:10:10 -04:00
e95adab422 Include CircleCI badge 2016-03-14 07:39:43 -04:00
012fed01eb Fix CircleCI; remove Travis-CI 2016-03-14 07:39:43 -04:00
4cf2b8072b Unlock screen before running UI tests 2016-03-14 07:39:43 -04:00
196a79a88c Add CircleCI config file 2016-03-14 07:39:43 -04:00
4b7e2e79a7 Update tools versions 2016-03-14 07:39:43 -04:00
9156bba267 Disable animations when testing 2016-03-14 07:39:43 -04:00
1a18bb939d Refactor and write unit tests for RepetitionList 2016-03-14 07:39:43 -04:00
144524e53b Refactor and write tests for checkmarks 2016-03-14 07:39:43 -04:00
3d1c53396c Allow date to be fixed at a certain timestamp 2016-03-14 07:39:43 -04:00
1930db3cd1 Wait after toggling checkmarks 2016-03-14 07:39:43 -04:00
a2c2a5531a Use temporary database for tests 2016-03-14 07:39:43 -04:00
eee2605f74 Add first unit tests for habit 2016-03-14 07:39:43 -04:00
Niklas Baudy
9df0c9ae9e Update EditText of Edit Habit Description to capitalize sentences 2016-03-14 10:19:47 +01:00
e0533505c1 Merge pull request #30 from vanniktech/patch-1
Update EditText of Edit Habit to capitalize sentences
2016-03-14 04:41:09 -04:00
Niklas Baudy
433894336c Update EditText of Edit Habit to capitalize sentences
Basically when entering a word the first letter will be capitalized. Something minor that bugs me always a bit.
2016-03-13 23:46:29 +01:00
9061182301 Update translators' names 2016-03-12 19:39:03 -05:00
8c45f52d56 Merge tag 'v1.3.0' into dev
Release v1.3.0
2016-03-12 06:03:02 -05:00
313 changed files with 15126 additions and 3967 deletions

1
.gitignore vendored
View File

@@ -33,3 +33,4 @@ build/
*.iml
art/
*.actual.png

View File

@@ -1,8 +1,57 @@
# Changelog
### 1.5.2 (May 19, 2016)
* Fix missing attachment on bug reports
* Fix bug that prevents some widgets from rendering
* Complete Japanese translation
### 1.5.1 (May 17, 2016)
* Fix build on F-Droid
### 1.5.0 (May 15, 2016)
* Add night mode, with AMOLED support
* Backport material design to older devices
* Display more information on statistics screen
* Display score on main screen and checkmark widget
* Make widgets react immediately to touch
* Reschedule reminders after reboot
* Pick first day of the week according to country
* Add option to reverse order of days on main screen
* Add option to change notification sounds
* Add Catalan, Indonesian, Turkish, Ukrainian translations
* Switch between Simplified/Traditional Chinese according to country
### 1.4.1 (April 9, 2016)
* Show error message on widgets, instead of crashing
* Complete French translation
* Minor fixes to other translations
### 1.4.0 (April 7, 2016)
* Ability to import data from third-party apps
* Ability to save and restore full database backup
* Show more information on streak chart
* Simplify interface for creating habits
* Add link to Frequently Asked Questions (FAQ)
* Reduce app loading time and lag on widgets
* Generate bug reports on crash and from settings screen
* Disable vibration according to phone settings
* Add Czech translation
* Fix wrong month names for some languages
### 1.3.3 (March 20, 2016)
* Add Spanish and Korean translations
* Make small corrections to other translations
* Fix incorrect date in history calendar
### 1.3.2 (March 18, 2016)
* Arabic, Italian, Polish, Russian and Swedish translations
* Add Arabic, Italian, Polish, Russian and Swedish translations
* Minor fixes to German and French translations
* Minor bug fixes
@@ -16,7 +65,7 @@
* New frequency plot: view total repetitions per day of week
* New history editor: put checkmarks in the past
* German, French and Japanese translations
* Add German, French and Japanese translations
* Add about screen, with credits to all contributors
* Fix small bug that prevented habit from being reordered
* Fix small bug caused by rotating the device

View File

@@ -89,3 +89,22 @@ Material design icons are the official icon set from Google that are designed
under the material design guidelines. Available under the Creative Common
Attribution 4.0 International License (CC-BY 4.0).
### Android Flow Layout
<https://github.com/ApmeM/android-flowlayout>
Extended linear layout that wrap its content when there is no place in the current line.
Copyright 2011, Artem Votincev (apmem.org)
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.

View File

@@ -1,12 +1,20 @@
# Loop Habit Tracker
<a href="https://circleci.com/gh/iSoron/uhabits/tree/dev">
<img src="https://img.shields.io/circleci/project/iSoron/uhabits/dev.svg">
</a>
<a href="https://codecov.io/github/iSoron/uhabits?branch=dev">
<img src="https://img.shields.io/codecov/c/github/iSoron/uhabits.svg" alt="Coverage via Codecov" />
</a>
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
show you how your habits improved over time. It is completely ad-free and open
source.
<p align="center">
<a href="https://play.google.com/store/apps/details?id=org.isoron.uhabits&utm_source=global_co&utm_medium=prtnr&utm_content=Mar2515&utm_campaign=PartBadge&pcampaignid=MKT-AC-global-none-all-co-pr-py-PartBadges-Oct1515-1"><img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/apps/en-play-badge-border.png" height="75px"/></a>
<a href="http://f-droid.org/app/org.isoron.uhabits"><img alt="Git if on F-Droid" src="http://i.imgur.com/baSPE7X.png" height="75px"/></a>
</p>
## Features
@@ -46,6 +54,7 @@ source.
[![Habit strength][screen3th]][screen3]
[![Habit history and streaks][screen4th]][screen4]
[![Widgets][screen5th]][screen5]
[![Night mode][screen6th]][screen6]
## Installing
@@ -63,11 +72,17 @@ contribute, even if you are not a software developer.
* **Report bugs, suggest features.** The easiest way to contribute is to simply
use the app and let us know if you find any problems or have any suggestions
to improve it. You can either use the link inside the app, or open an issue
at GitHub.
at GitHub. If you would like to receive the newest versions of the app
earlier than everyone else, [join our open beta on Google Play][beta].
* **Spread the word.** If you like the app, share it with your family, friends
and colleagues. You can also rate and review the app on Google Play Store, to help
other users find it more easily.
* **Translate the app into your own language.** If you are not a native English
speaker, and would like to see the app translated into your own language,
please join our [open translation project at POEditor][poedit].
please join our [open translation project at POEditor][poedit]. If the translation
is already completed, you are also very welcome to join and proofread it.
* **Write some code.** If you are an Android developer, you are very welcome to
contribute with code. Please, see the [developer guidelines][dev-guide] for more details.
@@ -92,14 +107,17 @@ contribute, even if you are not a software developer.
[screen3]: screenshots/original/uhabits3.png
[screen4]: screenshots/original/uhabits4.png
[screen5]: screenshots/original/uhabits5.png
[screen6]: screenshots/original/uhabits6.png
[screen1th]: screenshots/thumbs/uhabits1.png
[screen2th]: screenshots/thumbs/uhabits2.png
[screen3th]: screenshots/thumbs/uhabits3.png
[screen4th]: screenshots/thumbs/uhabits4.png
[screen5th]: screenshots/thumbs/uhabits5.png
[screen6th]: screenshots/thumbs/uhabits6.png
[poedit]: https://poeditor.com/join/project/8DWX5pfjS0
[playstore]: https://play.google.com/store/apps/details?id=org.isoron.uhabits
[releases]: https://github.com/iSoron/uhabits/releases
[fdroid]: http://f-droid.org/app/org.isoron.uhabits
[dev-guide]: https://github.com/iSoron/uhabits/wiki/Developer-guidelines
[build]: https://github.com/iSoron/uhabits/wiki/Developer-guidelines#building
[beta]: https://play.google.com/apps/testing/org.isoron.uhabits

View File

@@ -2,14 +2,18 @@ apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "21.1.2"
buildToolsVersion "23.0.3"
defaultConfig {
applicationId "org.isoron.uhabits"
minSdkVersion 15
targetSdkVersion 23
buildConfigField "Integer", "databaseVersion", "14"
buildConfigField "String", "databaseFilename", "\"uhabits.db\""
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
//testInstrumentationRunnerArgument "size", "small"
}
buildTypes {
@@ -28,15 +32,41 @@ android {
}
dependencies {
compile 'com.android.support:support-v4:23.1.1'
compile 'com.android.support:support-v4:23.3.0'
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.github.paolorotolo:appintro:3.4.0'
compile project(':libs:drag-sort-listview:library')
compile files('libs/ActiveAndroid.jar')
compile 'org.apmem.tools:layouts:1.10@aar'
compile 'com.opencsv:opencsv:3.7'
compile 'com.michaelpardo:activeandroid:3.1.0-SNAPSHOT'
androidTestCompile 'com.android.support:support-annotations:23.1.1'
androidTestCompile 'com.android.support.test:runner:0.4.1'
androidTestCompile 'com.android.support.test:rules:0.4.1'
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1'
androidTestCompile 'com.android.support.test.espresso:espresso-intents:2.2.1'
compile project(':libs:drag-sort-listview:library')
androidTestCompile 'com.android.support:support-annotations:23.3.0'
androidTestCompile 'com.android.support.test:runner:0.5'
androidTestCompile 'com.android.support.test:rules:0.5'
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.1') {
exclude group: 'com.android.support'
}
androidTestCompile('com.android.support.test.espresso:espresso-intents:2.2.1') {
exclude group: 'com.android.support'
}
androidTestCompile('com.android.support.test.espresso:espresso-contrib:2.2.1') {
exclude group: 'com.android.support'
}
}
task grantAnimationPermission(type: Exec, dependsOn: 'installDebug') {
commandLine "adb shell pm grant org.isoron.uhabits android.permission.SET_ANIMATION_SCALE".split(' ')
}
tasks.whenTaskAdded { task ->
if (task.name.startsWith('connected')) {
task.dependsOn grantAnimationPermission
}
}

Binary file not shown.

View File

@@ -0,0 +1,19 @@
HabitName,HabitDescription,HabitCategory,CalendarDate,Value,CommentText
Breed dragons,with love and fire,Diet & Food,2016-03-18,1,
Breed dragons,with love and fire,Diet & Food,2016-03-19,1,
Breed dragons,with love and fire,Diet & Food,2016-03-21,1,
Reduce sleep,only 2 hours per day,Time Management,2016-03-15,1,
Reduce sleep,only 2 hours per day,Time Management,2016-03-16,1,
Reduce sleep,only 2 hours per day,Time Management,2016-03-17,1,
Reduce sleep,only 2 hours per day,Time Management,2016-03-21,1,
No-arms pushup,Become like water my friend!,Fitness,2016-03-15,1,
No-arms pushup,Become like water my friend!,Fitness,2016-03-16,1,
No-arms pushup,Become like water my friend!,Fitness,2016-03-18,1,
No-arms pushup,Become like water my friend!,Fitness,2016-03-21,1,
No-arms pushup,Become like water my friend!,Fitness,2016-03-15,1,
No-arms pushup,Become like water my friend!,Fitness,2016-03-16,1,
No-arms pushup,Become like water my friend!,Fitness,2016-03-18,1,
No-arms pushup,Become like water my friend!,Fitness,2016-03-21,1,
Grow spiritually,"transcend ego, practice compassion, smile and breath",Meditation,2016-03-15,1,
Grow spiritually,"transcend ego, practice compassion, smile and breath",Meditation,2016-03-17,1,
Grow spiritually,"transcend ego, practice compassion, smile and breath",Meditation,2016-03-21,1,
1 HabitName HabitDescription HabitCategory CalendarDate Value CommentText
2 Breed dragons with love and fire Diet & Food 2016-03-18 1
3 Breed dragons with love and fire Diet & Food 2016-03-19 1
4 Breed dragons with love and fire Diet & Food 2016-03-21 1
5 Reduce sleep only 2 hours per day Time Management 2016-03-15 1
6 Reduce sleep only 2 hours per day Time Management 2016-03-16 1
7 Reduce sleep only 2 hours per day Time Management 2016-03-17 1
8 Reduce sleep only 2 hours per day Time Management 2016-03-21 1
9 No-arms pushup Become like water my friend! Fitness 2016-03-15 1
10 No-arms pushup Become like water my friend! Fitness 2016-03-16 1
11 No-arms pushup Become like water my friend! Fitness 2016-03-18 1
12 No-arms pushup Become like water my friend! Fitness 2016-03-21 1
13 No-arms pushup Become like water my friend! Fitness 2016-03-15 1
14 No-arms pushup Become like water my friend! Fitness 2016-03-16 1
15 No-arms pushup Become like water my friend! Fitness 2016-03-18 1
16 No-arms pushup Become like water my friend! Fitness 2016-03-21 1
17 Grow spiritually transcend ego, practice compassion, smile and breath Meditation 2016-03-15 1
18 Grow spiritually transcend ego, practice compassion, smile and breath Meditation 2016-03-17 1
19 Grow spiritually transcend ego, practice compassion, smile and breath Meditation 2016-03-21 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

View File

@@ -0,0 +1,5 @@
#!/bin/bash
P=/sdcard/Android/data/org.isoron.uhabits/cache/Failed/
adb pull $P Failed/
adb shell rm -r $P

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

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

View File

@@ -1,200 +0,0 @@
package org.isoron.uhabits;
import android.content.Context;
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.models.Habit;
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.openActionBarOverflowOrOptionsMenu;
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.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 android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.isoron.uhabits.HabitMatchers.withName;
import static org.isoron.uhabits.HabitViewActions.clickAtRandomLocations;
import static org.isoron.uhabits.HabitViewActions.toggleAllCheckmarks;
import static org.isoron.uhabits.MainActivityActions.addHabit;
import static org.isoron.uhabits.MainActivityActions.assertHabitExists;
import static org.isoron.uhabits.MainActivityActions.assertHabitsDontExist;
import static org.isoron.uhabits.MainActivityActions.assertHabitsExist;
import static org.isoron.uhabits.MainActivityActions.clickActionModeMenuItem;
import static org.isoron.uhabits.MainActivityActions.deleteHabit;
import static org.isoron.uhabits.MainActivityActions.deleteHabits;
import static org.isoron.uhabits.MainActivityActions.selectHabit;
import static org.isoron.uhabits.MainActivityActions.selectHabits;
import static org.isoron.uhabits.MainActivityActions.typeHabitData;
import static org.isoron.uhabits.ShowHabitActivityActions.openHistoryEditor;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class MainTest
{
@Rule
public IntentsTestRule<MainActivity> activityRule = new IntentsTestRule<>(
MainActivity.class);
@Before
public void skipTutorial()
{
try
{
for (int i = 0; i < 10; i++)
onView(allOf(withClassName(endsWith("AppCompatImageButton")),
isDisplayed())).perform(click());
}
catch (NoMatchingViewException e)
{
// ignored
}
}
@Test
public void testArchiveHabits()
{
List<String> names = new LinkedList<>();
Context context = InstrumentationRegistry.getTargetContext();
for(int i = 0; i < 3; i++)
names.add(addHabit());
selectHabits(names);
clickActionModeMenuItem(R.string.archive);
assertHabitsDontExist(names);
openActionBarOverflowOrOptionsMenu(context);
onView(withText(R.string.show_archived))
.perform(click());
assertHabitsExist(names);
selectHabits(names);
clickActionModeMenuItem(R.string.unarchive);
openActionBarOverflowOrOptionsMenu(context);
onView(withText(R.string.show_archived))
.perform(click());
assertHabitsExist(names);
deleteHabits(names);
}
@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()));
}
@Test
public void testAddHabitAndViewStats()
{
String name = addHabit(true);
onData(allOf(is(instanceOf(Habit.class)), withName(name)))
.onChildView(withId(R.id.llButtons))
.perform(toggleAllCheckmarks());
onData(allOf(is(instanceOf(Habit.class)), withName(name)))
.onChildView(withId(R.id.label))
.perform(click());
onView(withId(R.id.scoreView))
.perform(swipeRight());
onView(withId(R.id.punchcardView))
.perform(scrollTo());
}
@Test
public void testEditHabit()
{
String name = addHabit();
onData(allOf(is(instanceOf(Habit.class)), withName(name)))
.onChildView(withId(R.id.label))
.perform(longClick());
clickActionModeMenuItem(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);
clickActionModeMenuItem(R.string.color_picker_default_title);
pressBack();
deleteHabit(modifiedName);
}
@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());
}
@Test
public void testSettings()
{
Context context = InstrumentationRegistry.getContext();
openActionBarOverflowOrOptionsMenu(context);
onView(withText(R.string.settings)).perform(click());
}
@Test
public void testAbout()
{
Context context = InstrumentationRegistry.getContext();
openActionBarOverflowOrOptionsMenu(context);
onView(withText(R.string.about)).perform(click());
onView(isRoot()).perform(swipeUp());
}
}

View File

@@ -17,12 +17,14 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits;
package org.isoron.uhabits.ui;
import android.preference.Preference;
import android.view.View;
import android.widget.Adapter;
import android.widget.AdapterView;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
@@ -76,4 +78,23 @@ public class HabitMatchers
}
};
}
public static Matcher<?> isPreferenceWithText(final String text)
{
return (Matcher<?>) new BaseMatcher()
{
@Override
public boolean matches(Object o)
{
if(!(o instanceof Preference)) return false;
return o.toString().contains(text);
}
@Override
public void describeTo(Description description)
{
description.appendText(String.format("is preference with text '%s'", text));
}
};
}
}

View File

@@ -17,7 +17,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits;
package org.isoron.uhabits.ui;
import android.support.test.espresso.UiController;
import android.support.test.espresso.ViewAction;
@@ -32,6 +32,7 @@ import android.widget.LinearLayout;
import android.widget.TextView;
import org.hamcrest.Matcher;
import org.isoron.uhabits.R;
import java.security.InvalidParameterException;
import java.util.Random;

View File

@@ -17,11 +17,12 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits;
package org.isoron.uhabits.ui;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
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;
@@ -31,22 +32,29 @@ 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.openContextualActionModeOverflowMenu;
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.isoron.uhabits.HabitMatchers.containsHabit;
import static org.isoron.uhabits.HabitMatchers.withName;
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
{
@@ -97,6 +105,20 @@ public class MainActivityActions
.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))
@@ -150,13 +172,13 @@ public class MainActivityActions
public static void deleteHabits(List<String> names)
{
selectHabits(names);
clickActionModeMenuItem(R.string.delete);
clickMenuItem(R.string.delete);
onView(withText("OK"))
.perform(click());
assertHabitsDontExist(names);
}
public static void clickActionModeMenuItem(int stringId)
public static void clickMenuItem(int stringId)
{
try
{
@@ -170,9 +192,34 @@ public class MainActivityActions
}
catch(Exception e2)
{
openContextualActionModeOverflowMenu();
onView(withText(stringId)).perform(click());
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()));
}
}

View File

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

View File

@@ -17,18 +17,21 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits;
package org.isoron.uhabits.ui;
import android.support.test.espresso.matcher.ViewMatchers;
import org.isoron.uhabits.R;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.action.ViewActions.scrollTo;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
public class ShowHabitActivityActions
{
public static void openHistoryEditor()
{
onView(withId(R.id.btEditHistory))
onView(ViewMatchers.withId(R.id.btEditHistory))
.perform(scrollTo(), click());
}
}

View File

@@ -0,0 +1,105 @@
package org.isoron.uhabits.ui;
import android.app.KeyguardManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.IBinder;
import android.os.PowerManager;
import android.support.test.runner.AndroidJUnitRunner;
import android.util.Log;
import java.lang.reflect.Method;
public final class SystemHelper extends AndroidJUnitRunner
{
private static final String ANIMATION_PERMISSION = "android.permission.SET_ANIMATION_SCALE";
private static final float DISABLED = 0.0f;
private static final float DEFAULT = 1.0f;
private final Context context;
private PowerManager.WakeLock wakeLock;
SystemHelper(Context context)
{
this.context = context;
}
void acquireWakeLock()
{
PowerManager power = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
wakeLock = power.newWakeLock(PowerManager.FULL_WAKE_LOCK |
PowerManager.ACQUIRE_CAUSES_WAKEUP |
PowerManager.ON_AFTER_RELEASE, getClass().getSimpleName());
wakeLock.acquire();
}
void releaseWakeLock()
{
if(wakeLock != null)
wakeLock.release();
}
void unlockScreen()
{
Log.i("SystemHelper", "Trying to unlock screen");
try
{
KeyguardManager mKeyGuardManager =
(KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
KeyguardManager.KeyguardLock mLock = mKeyGuardManager.newKeyguardLock("lock");
mLock.disableKeyguard();
Log.e("SystemHelper", "Successfully unlocked screen");
} catch (Exception e)
{
Log.e("SystemHelper", "Could not unlock screen");
e.printStackTrace();
}
}
void disableAllAnimations()
{
Log.i("SystemHelper", "Trying to disable animations");
int permStatus = context.checkCallingOrSelfPermission(ANIMATION_PERMISSION);
if (permStatus == PackageManager.PERMISSION_GRANTED) setSystemAnimationsScale(DISABLED);
else Log.e("SystemHelper", "Permission denied");
}
void enableAllAnimations()
{
int permStatus = context.checkCallingOrSelfPermission(ANIMATION_PERMISSION);
if (permStatus == PackageManager.PERMISSION_GRANTED)
{
setSystemAnimationsScale(DEFAULT);
}
}
private void setSystemAnimationsScale(float animationScale)
{
try
{
Class<?> windowManagerStubClazz = Class.forName("android.view.IWindowManager$Stub");
Method asInterface =
windowManagerStubClazz.getDeclaredMethod("asInterface", IBinder.class);
Class<?> serviceManagerClazz = Class.forName("android.os.ServiceManager");
Method getService = serviceManagerClazz.getDeclaredMethod("getService", String.class);
Class<?> windowManagerClazz = Class.forName("android.view.IWindowManager");
Method setAnimationScales =
windowManagerClazz.getDeclaredMethod("setAnimationScales", float[].class);
Method getAnimationScales = windowManagerClazz.getDeclaredMethod("getAnimationScales");
IBinder windowManagerBinder = (IBinder) getService.invoke(null, "window");
Object windowManagerObj = asInterface.invoke(null, windowManagerBinder);
float[] currentScales = (float[]) getAnimationScales.invoke(windowManagerObj);
for (int i = 0; i < currentScales.length; i++)
currentScales[i] = animationScale;
setAnimationScales.invoke(windowManagerObj, new Object[]{currentScales});
Log.i("SystemHelper", "All animations successfully disabled");
}
catch (Exception e)
{
Log.e("SystemHelper", "Could not change animation scale to " + animationScale + " :'(");
}
}
}

View File

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

View File

@@ -0,0 +1,51 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit;
import android.os.Build;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.HabitsApplication;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class HabitsApplicationTest
{
@Test
public void test_getLogcat() throws IOException
{
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN)
return;
String msg = "LOGCAT TEST";
new RuntimeException(msg).printStackTrace();
String log = HabitsApplication.getLogcat();
assertThat(log, containsString(msg));
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.commands;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.commands.ArchiveHabitsCommand;
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.Collections;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ArchiveHabitsCommandTest extends BaseTest
{
private ArchiveHabitsCommand command;
private Habit habit;
@Before
public void setup()
{
super.setup();
habit = HabitFixtures.createShortHabit();
command = new ArchiveHabitsCommand(Collections.singletonList(habit));
}
@Test
public void testExecuteUndoRedo()
{
assertFalse(habit.isArchived());
command.execute();
assertTrue(habit.isArchived());
command.undo();
assertFalse(habit.isArchived());
command.execute();
assertTrue(habit.isArchived());
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.commands;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.commands.ChangeHabitColorCommand;
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.LinkedList;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ChangeHabitColorCommandTest extends BaseTest
{
private ChangeHabitColorCommand command;
private LinkedList<Habit> habits;
@Before
public void setup()
{
super.setup();
habits = new LinkedList<>();
for(int i = 0; i < 3; i ++)
{
Habit habit = HabitFixtures.createShortHabit();
habit.color = i+1;
habit.save();
habits.add(habit);
}
command = new ChangeHabitColorCommand(habits, 0);
}
@Test
public void testExecuteUndoRedo()
{
checkOriginalColors();
command.execute();
checkNewColors();
command.undo();
checkOriginalColors();
command.execute();
checkNewColors();
}
private void checkOriginalColors()
{
int k = 0;
for(Habit h : habits)
assertThat(h.color, equalTo(++k));
}
private void checkNewColors()
{
for(Habit h : habits)
assertThat(h.color, equalTo(0));
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,71 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.commands;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.commands.ToggleRepetitionCommand;
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 static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ToggleRepetitionCommandTest extends BaseTest
{
private ToggleRepetitionCommand command;
private Habit habit;
private long today;
@Before
public void setup()
{
super.setup();
habit = HabitFixtures.createShortHabit();
today = DateHelper.getStartOfToday();
command = new ToggleRepetitionCommand(habit, today);
}
@Test
public void testExecuteUndoRedo()
{
assertTrue(habit.repetitions.contains(today));
command.execute();
assertFalse(habit.repetitions.contains(today));
command.undo();
assertTrue(habit.repetitions.contains(today));
command.execute();
assertFalse(habit.repetitions.contains(today));
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.commands;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.commands.UnarchiveHabitsCommand;
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.Collections;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class UnarchiveHabitsCommandTest extends BaseTest
{
private UnarchiveHabitsCommand command;
private Habit habit;
@Before
public void setup()
{
super.setup();
habit = HabitFixtures.createShortHabit();
Habit.archive(Collections.singletonList(habit));
command = new UnarchiveHabitsCommand(Collections.singletonList(habit));
}
@Test
public void testExecuteUndoRedo()
{
assertTrue(habit.isArchived());
command.execute();
assertFalse(habit.isArchived());
command.undo();
assertTrue(habit.isArchived());
command.execute();
assertFalse(habit.isArchived());
}
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.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;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class HabitsCSVExporterTest extends BaseTest
{
private File baseDir;
@Before
public void setup()
{
super.setup();
HabitFixtures.purgeHabits();
HabitFixtures.createShortHabit();
HabitFixtures.createEmptyHabit();
Context targetContext = InstrumentationRegistry.getTargetContext();
baseDir = targetContext.getCacheDir();
}
private void unzip(File file) throws IOException
{
ZipFile zip = new ZipFile(file);
Enumeration<? extends ZipEntry> 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<Habit> habits = Habit.getAll(true);
HabitsCSVExporter exporter = new HabitsCSVExporter(habits, baseDir);
String filename = exporter.writeArchive();
assertAbsolutePathExists(filename);
File archive = new File(filename);
unzip(archive);
assertPathExists("Habits.csv");
assertPathExists("001 Wake up early");
assertPathExists("001 Wake up early/Checkmarks.csv");
assertPathExists("001 Wake up early/Scores.csv");
assertPathExists("002 Meditate/Checkmarks.csv");
assertPathExists("002 Meditate/Scores.csv");
}
private void assertPathExists(String s)
{
assertAbsolutePathExists(String.format("%s/%s", baseDir.getAbsolutePath(), s));
}
private void assertAbsolutePathExists(String s)
{
File file = new File(s);
assertTrue(String.format("File %s should exist", file.getAbsolutePath()), file.exists());
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,108 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.models;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.models.Checkmark;
import org.isoron.uhabits.models.Score;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ScoreTest extends BaseTest
{
@Before
public void setup()
{
super.setup();
}
@Test
public void test_compute_withDailyHabit()
{
int checkmark = Checkmark.UNCHECKED;
assertThat(Score.compute(1, 0, checkmark), equalTo(0));
assertThat(Score.compute(1, 5000000, checkmark), equalTo(4740387));
assertThat(Score.compute(1, 10000000, checkmark), equalTo(9480775));
assertThat(Score.compute(1, Score.MAX_VALUE, checkmark), equalTo(18259478));
checkmark = Checkmark.CHECKED_IMPLICITLY;
assertThat(Score.compute(1, 0, checkmark), equalTo(0));
assertThat(Score.compute(1, 5000000, checkmark), equalTo(4740387));
assertThat(Score.compute(1, 10000000, checkmark), equalTo(9480775));
assertThat(Score.compute(1, Score.MAX_VALUE, checkmark), equalTo(18259478));
checkmark = Checkmark.CHECKED_EXPLICITLY;
assertThat(Score.compute(1, 0, checkmark), equalTo(1000000));
assertThat(Score.compute(1, 5000000, checkmark), equalTo(5740387));
assertThat(Score.compute(1, 10000000, checkmark), equalTo(10480775));
assertThat(Score.compute(1, Score.MAX_VALUE, checkmark), equalTo(Score.MAX_VALUE));
}
@Test
public void test_compute_withNonDailyHabit()
{
int checkmark = Checkmark.CHECKED_EXPLICITLY;
assertThat(Score.compute(1/3.0, 0, checkmark), equalTo(1000000));
assertThat(Score.compute(1/3.0, 5000000, checkmark), equalTo(5916180));
assertThat(Score.compute(1/3.0, 10000000, checkmark), equalTo(10832360));
assertThat(Score.compute(1/3.0, Score.MAX_VALUE, checkmark), equalTo(Score.MAX_VALUE));
assertThat(Score.compute(1/7.0, 0, checkmark), equalTo(1000000));
assertThat(Score.compute(1/7.0, 5000000, checkmark), equalTo(5964398));
assertThat(Score.compute(1/7.0, 10000000, checkmark), equalTo(10928796));
assertThat(Score.compute(1/7.0, Score.MAX_VALUE, checkmark), equalTo(Score.MAX_VALUE));
}
@Test
public void test_getStarStatus()
{
Score s = new Score();
s.score = Score.FULL_STAR_CUTOFF + 1;
assertThat(s.getStarStatus(), equalTo(Score.FULL_STAR));
s.score = Score.FULL_STAR_CUTOFF;
assertThat(s.getStarStatus(), equalTo(Score.FULL_STAR));
s.score = Score.FULL_STAR_CUTOFF - 1;
assertThat(s.getStarStatus(), equalTo(Score.HALF_STAR));
s.score = Score.HALF_STAR_CUTOFF + 1;
assertThat(s.getStarStatus(), equalTo(Score.HALF_STAR));
s.score = Score.HALF_STAR_CUTOFF;
assertThat(s.getStarStatus(), equalTo(Score.HALF_STAR));
s.score = Score.HALF_STAR_CUTOFF - 1;
assertThat(s.getStarStatus(), equalTo(Score.EMPTY_STAR));
s.score = 0;
assertThat(s.getStarStatus(), equalTo(Score.EMPTY_STAR));
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,80 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.views;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
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;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class HabitFrequencyViewTest extends ViewTest
{
private HabitFrequencyView view;
@Before
public void setup()
{
super.setup();
HabitFixtures.purgeHabits();
Habit habit = HabitFixtures.createLongHabit();
view = new HabitFrequencyView(targetContext);
view.setHabit(habit);
refreshData(view);
measureView(dpToPixels(300), dpToPixels(100), view);
}
@Test
public void testRender() throws Throwable
{
assertRenders(view, "HabitFrequencyView/render.png");
}
@Test
public void testRender_withTransparentBackground() throws Throwable
{
view.setIsBackgroundTransparent(true);
assertRenders(view, "HabitFrequencyView/renderTransparent.png");
}
@Test
public void testRender_withDifferentSize() throws Throwable
{
measureView(dpToPixels(200), dpToPixels(200), view);
assertRenders(view, "HabitFrequencyView/renderDifferentSize.png");
}
@Test
public void testRender_withDataOffset() throws Throwable
{
view.onScroll(null, null, -dpToPixels(150), 0);
view.invalidate();
assertRenders(view, "HabitFrequencyView/renderDataOffset.png");
}
}

View File

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

View File

@@ -0,0 +1,104 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.views;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import android.util.Log;
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;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class HabitScoreViewTest extends ViewTest
{
private Habit habit;
private HabitScoreView view;
@Before
public void setup()
{
super.setup();
HabitFixtures.purgeHabits();
habit = HabitFixtures.createLongHabit();
view = new HabitScoreView(targetContext);
view.setHabit(habit);
view.setBucketSize(7);
refreshData(view);
measureView(dpToPixels(300), dpToPixels(200), view);
}
@Test
public void testRender() throws Throwable
{
Log.d("HabitScoreViewTest", String.format("height=%d", dpToPixels(100)));
assertRenders(view, "HabitScoreView/render.png");
}
@Test
public void testRender_withTransparentBackground() throws Throwable
{
view.setIsTransparencyEnabled(true);
assertRenders(view, "HabitScoreView/renderTransparent.png");
}
@Test
public void testRender_withDifferentSize() throws Throwable
{
measureView(dpToPixels(200), dpToPixels(200), view);
assertRenders(view, "HabitScoreView/renderDifferentSize.png");
}
@Test
public void testRender_withDataOffset() throws Throwable
{
view.onScroll(null, null, -dpToPixels(150), 0);
view.invalidate();
assertRenders(view, "HabitScoreView/renderDataOffset.png");
}
@Test
public void testRender_withMonthlyBucket() throws Throwable
{
view.setBucketSize(30);
view.refreshData();
view.invalidate();
assertRenders(view, "HabitScoreView/renderMonthly.png");
}
@Test
public void testRender_withYearlyBucket() throws Throwable
{
view.setBucketSize(365);
view.refreshData();
view.invalidate();
assertRenders(view, "HabitScoreView/renderYearly.png");
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.views;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
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;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class HabitStreakViewTest extends ViewTest
{
private HabitStreakView view;
@Before
public void setup()
{
super.setup();
HabitFixtures.purgeHabits();
Habit habit = HabitFixtures.createLongHabit();
view = new HabitStreakView(targetContext);
measureView(dpToPixels(300), dpToPixels(100), view);
view.setHabit(habit);
refreshData(view);
}
@Test
public void testRender() throws Throwable
{
assertRenders(view, "HabitStreakView/render.png");
}
@Test
public void testRender_withTransparentBackground() throws Throwable
{
view.setIsBackgroundTransparent(true);
assertRenders(view, "HabitStreakView/renderTransparent.png");
}
@Test
public void testRender_withSmallSize() throws Throwable
{
measureView(dpToPixels(100), dpToPixels(100), view);
refreshData(view);
assertRenders(view, "HabitStreakView/renderSmallSize.png");
}
}

View File

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

View File

@@ -0,0 +1,69 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.views;
import android.graphics.Color;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
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 java.io.IOException;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class RingViewTest extends ViewTest
{
private RingView view;
@Before
public void setup()
{
super.setup();
view = new RingView(targetContext);
view.setPercentage(0.6f);
view.setText("60%");
view.setColor(ColorHelper.CSV_PALETTE[0]);
view.setBackgroundColor(Color.WHITE);
view.setThickness(dpToPixels(3));
}
@Test
public void testRender_base() throws IOException
{
measureView(dpToPixels(100), dpToPixels(100), view);
assertRenders(view, "RingView/render.png");
}
@Test
public void testRender_withDifferentParams() throws IOException
{
view.setPercentage(0.25f);
view.setColor(ColorHelper.CSV_PALETTE[5]);
measureView(dpToPixels(200), dpToPixels(200), view);
assertRenders(view, "RingView/renderDifferentParams.png");
}
}

View File

@@ -0,0 +1,226 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.views;
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 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 java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import static junit.framework.Assert.fail;
public class ViewTest extends BaseTest
{
protected static final double 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);
view.measure(specWidth, specHeight);
view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
}
protected void assertRenders(View view, String expectedImagePath) throws IOException
{
StringBuilder errorMessage = new StringBuilder();
expectedImagePath = getVersionedViewAssetPath(expectedImagePath);
view.setDrawingCacheEnabled(true);
view.buildDrawingCache();
Bitmap actual = view.getDrawingCache();
Bitmap expected = getBitmapFromAssets(expectedImagePath);
int width = actual.getWidth();
int height = actual.getHeight();
Bitmap scaledExpected = Bitmap.createScaledBitmap(expected, width, height, true);
double distance;
boolean similarEnough = true;
if ((distance = compareHistograms(getHistogram(actual), getHistogram(scaledExpected))) > SIMILARITY_CUTOFF)
{
similarEnough = false;
errorMessage.append(String.format(
"Rendered image has wrong histogram (distance=%f). ",
distance));
}
if(!similarEnough)
{
saveBitmap(expectedImagePath, ".expected", scaledExpected);
String path = saveBitmap(expectedImagePath, "", actual);
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
{
InputStream stream = testContext.getAssets().open(path);
return BitmapFactory.decodeStream(stream);
}
private String getVersionedViewAssetPath(String path)
{
String result = null;
if (android.os.Build.VERSION.SDK_INT >= 21)
{
try
{
String vpath = "views-v21/" + path;
testContext.getAssets().open(vpath);
result = vpath;
}
catch (IOException e)
{
// ignored
}
}
if(result == null)
result = "views/" + path;
return result;
}
private String saveBitmap(String filename, String suffix, Bitmap bitmap)
throws IOException
{
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");
filename = filename.replaceAll("\\.png$", suffix + ".png");
String absolutePath = String.format("%s/%s", dir.getAbsolutePath(), filename);
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;
}
private int[][] getHistogram(Bitmap bitmap)
{
int histogram[][] = new int[4][256 / HISTOGRAM_BIN_SIZE];
for(int x = 0; x < bitmap.getWidth(); x++)
{
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
};
histogram[0][argb[0] / HISTOGRAM_BIN_SIZE]++;
histogram[1][argb[1] / HISTOGRAM_BIN_SIZE]++;
histogram[2][argb[2] / HISTOGRAM_BIN_SIZE]++;
histogram[3][argb[3] / HISTOGRAM_BIN_SIZE]++;
}
}
return histogram;
}
private double compareHistograms(int[][] actualHistogram, int[][] expectedHistogram)
{
long diff = 0;
long total = 0;
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]);
total += actualHistogram[0][i];
total += actualHistogram[1][i];
total += actualHistogram[2][i];
total += actualHistogram[3][i];
}
return (double) diff / total / 2;
}
protected int dpToPixels(int dp)
{
return (int) UIHelper.dpToPixels(targetContext, dp);
}
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();
}
protected void refreshData(final HabitDataView view)
{
new BaseTask()
{
@Override
protected void doInBackground()
{
view.refreshData();
}
}.execute();
try
{
waitForAsyncTasks();
}
catch (Exception e)
{
throw new RuntimeException("Time out");
}
}
}

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
~
~ This file is part of Loop Habit Tracker.
~
~ Loop Habit Tracker is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by the
~ Free Software Foundation, either version 3 of the License, or (at your
~ option) any later version.
~
~ Loop Habit Tracker is distributed in the hope that it will be useful, but
~ WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
~ or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details.
~
~ You should have received a copy of the GNU General Public License along
~ with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<manifest
package="org.isoron.uhabits"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.SET_ANIMATION_SCALE"/>
<uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
tools:replace="maxSdkVersion"
android:maxSdkVersion="99" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:replace="maxSdkVersion"
android:maxSdkVersion="99" />
</manifest>

View File

@@ -21,21 +21,23 @@
<manifest
package="org.isoron.uhabits"
xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="12"
android:versionName="1.3.2">
android:versionCode="18"
android:versionName="1.5.2">
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="18"/>
android:maxSdkVersion="18" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="18"/>
android:maxSdkVersion="18" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:name="com.activeandroid.app.Application"
android:name="HabitsApplication"
android:allowBackup="true"
android:backupAgent=".HabitsBackupAgent"
android:icon="@mipmap/ic_launcher"
@@ -43,12 +45,6 @@
android:theme="@style/AppBaseTheme"
android:supportsRtl="true">
<meta-data
android:name="AA_DB_NAME"
android:value="uhabits.db"/>
<meta-data
android:name="AA_DB_VERSION"
android:value="12"/>
<meta-data
android:name="com.google.android.backup.api_key"
android:value="AEdPqrEAAAAI6aeWncbnMNo8E5GWeZ44dlc5cQ7tCROwFhOtiw"/>
@@ -65,8 +61,7 @@
<activity
android:name=".ShowHabitActivity"
android:label="@string/title_activity_show_habit"
android:parentActivityName=".MainActivity">
android:label="@string/title_activity_show_habit">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.isoron.uhabits.MainActivity"/>
@@ -74,8 +69,7 @@
<activity
android:name=".SettingsActivity"
android:label="@string/settings"
android:parentActivityName=".MainActivity">
android:label="@string/settings">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.isoron.uhabits.MainActivity"/>
@@ -96,8 +90,10 @@
<activity
android:name=".AboutActivity"
android:label="@string/about"
android:parentActivityName=".MainActivity">
android:label="@string/about">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".MainActivity" />
</activity>
<receiver
@@ -160,7 +156,11 @@
android:resource="@xml/widget_frequency_info"/>
</receiver>
<receiver android:name=".HabitBroadcastReceiver"/>
<receiver android:name=".HabitBroadcastReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>

View File

@@ -0,0 +1,4 @@
create index idx_score_habit_timestamp on score(habit, timestamp);
create index idx_checkmark_habit_timestamp on checkmarks(habit, timestamp);
create index idx_repetitions_habit_timestamp on repetitions(habit, timestamp);
create index idx_streak_habit_end on streak(habit, end);

View File

@@ -0,0 +1,14 @@
update habits set color=0 where color=-2937041;
update habits set color=1 where color=-1684967;
update habits set color=2 where color=-415707;
update habits set color=3 where color=-5262293;
update habits set color=4 where color=-13070788;
update habits set color=5 where color=-16742021;
update habits set color=6 where color=-16732991;
update habits set color=7 where color=-16540699;
update habits set color=8 where color=-10603087;
update habits set color=9 where color=-7461718;
update habits set color=10 where color=-2614432;
update habits set color=11 where color=-13619152;
update habits set color=12 where color=-5592406;
update habits set color=0 where color<0 or color>12;

View File

@@ -16,24 +16,24 @@
package com.android.colorpicker;
import org.isoron.uhabits.R;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.os.Bundle;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatDialogFragment;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ProgressBar;
import com.android.colorpicker.ColorPickerSwatch.OnColorSelectedListener;
import org.isoron.uhabits.R;
/**
* A dialog which takes in as input an array of palette and creates a palette allowing the user to
* select a specific color swatch, which invokes a listener.
*/
public class ColorPickerDialog extends DialogFragment implements OnColorSelectedListener {
public class ColorPickerDialog extends AppCompatDialogFragment implements OnColorSelectedListener {
public static final int SIZE_LARGE = 1;
public static final int SIZE_SMALL = 2;

View File

@@ -30,6 +30,7 @@ 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.util.Log;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
@@ -49,7 +50,7 @@ import com.android.datetimepicker.time.RadialPickerLayout.OnValueSelectedListene
/**
* Dialog to set a time.
*/
public class TimePickerDialog extends DialogFragment implements OnValueSelectedListener{
public class TimePickerDialog extends AppCompatDialogFragment implements OnValueSelectedListener{
private static final String TAG = "TimePickerDialog";
private static final String KEY_HOUR_OF_DAY = "hour_of_day";

View File

@@ -1,95 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.helpers;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Typeface;
import android.os.Vibrator;
import android.preference.PreferenceManager;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import org.isoron.uhabits.BuildConfig;
public abstract class DialogHelper
{
public static final String ISORON_NAMESPACE = "http://isoron.org/android";
private static Typeface fontawesome;
public interface OnSavedListener
{
void onSaved(Command command, Object savedObject);
}
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 vibrate(Context context, int duration)
{
Vibrator vb = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
vb.vibrate(duration);
}
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)
{
int resId = attrs.getAttributeResourceValue(ISORON_NAMESPACE, name, 0);
if(resId != 0)
return context.getResources().getString(resId);
else
return attrs.getAttributeValue(ISORON_NAMESPACE, name);
}
public static float dpToPixels(Context context, float dp)
{
Resources resources = context.getResources();
DisplayMetrics metrics = resources.getDisplayMetrics();
return dp * (metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT);
}
}

View File

@@ -19,33 +19,27 @@
package org.isoron.uhabits;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import org.isoron.helpers.ColorHelper;
import org.isoron.uhabits.helpers.UIHelper;
public class AboutActivity extends Activity implements View.OnClickListener
public class AboutActivity extends BaseActivity implements View.OnClickListener
{
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.about);
if (android.os.Build.VERSION.SDK_INT >= 21)
{
int color = getResources().getColor(R.color.blue_700);
int darkerColor = ColorHelper.mixColors(color, Color.BLACK, 0.75f);
getActionBar().setBackgroundDrawable(new ColorDrawable(color));
getWindow().setStatusBarColor(darkerColor);
}
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);
@@ -68,7 +62,7 @@ public class AboutActivity extends Activity implements View.OnClickListener
{
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setData(Uri.parse("market://details?id=org.isoron.uhabits"));
intent.setData(Uri.parse(getString(R.string.playStoreURL)));
startActivity(intent);
break;
}
@@ -77,8 +71,7 @@ public class AboutActivity extends Activity implements View.OnClickListener
{
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SENDTO);
intent.setData(Uri.parse("mailto:isoron+habits@gmail.com?" +
"subject=Feedback%20about%20Loop%20Habit%20Tracker"));
intent.setData(Uri.parse(getString(R.string.feedbackURL)));
startActivity(intent);
break;
}
@@ -87,7 +80,7 @@ public class AboutActivity extends Activity implements View.OnClickListener
{
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setData(Uri.parse("https://github.com/iSoron/uhabits"));
intent.setData(Uri.parse(getString(R.string.sourceCodeURL)));
startActivity(intent);
break;
}

View File

@@ -17,19 +17,27 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.helpers;
package org.isoron.uhabits;
import android.app.Activity;
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.R;
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 ReplayableActivity extends Activity
abstract public class BaseActivity extends AppCompatActivity implements Thread.UncaughtExceptionHandler
{
private static int MAX_UNDO_LEVEL = 15;
@@ -37,11 +45,18 @@ abstract public class ReplayableActivity extends Activity
private LinkedList<Command> 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<>();
}
@@ -103,7 +118,7 @@ abstract public class ReplayableActivity extends Activity
@Override
protected void onPostExecute(Void aVoid)
{
ReplayableActivity.this.onPostExecuteCommand(refreshKey);
BaseActivity.this.onPostExecuteCommand(refreshKey);
BackupManager.dataChanged("org.isoron.uhabits");
}
}.execute();
@@ -112,7 +127,78 @@ abstract public class ReplayableActivity extends Activity
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);
}
}

View File

@@ -29,16 +29,18 @@ import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.BitmapFactory;
import android.media.RingtoneManager;
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.helpers.DateHelper;
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;
@@ -56,7 +58,7 @@ public class HabitBroadcastReceiver extends BroadcastReceiver
{
case ACTION_SHOW_REMINDER:
createNotification(context, intent);
createReminderAlarms(context);
createReminderAlarmsDelayed(context);
break;
case ACTION_DISMISS:
@@ -70,10 +72,14 @@ public class HabitBroadcastReceiver extends BroadcastReceiver
case ACTION_SNOOZE:
snoozeHabit(context, intent);
break;
case Intent.ACTION_BOOT_COMPLETED:
ReminderHelper.createReminderAlarms(context);
break;
}
}
private void createReminderAlarms(final Context context)
private void createReminderAlarmsDelayed(final Context context)
{
new Handler().postDelayed(new Runnable()
{
@@ -124,11 +130,7 @@ public class HabitBroadcastReceiver extends BroadcastReceiver
private void dismissAllHabits()
{
for (Habit h : Habit.getHighlightedHabits())
{
h.highlight = 0;
h.save();
}
}
private void dismissNotification(Context context, Long habitId)
@@ -141,62 +143,77 @@ public class HabitBroadcastReceiver extends BroadcastReceiver
}
private void createNotification(Context context, Intent intent)
private void createNotification(final Context context, final Intent intent)
{
Uri data = intent.getData();
Habit habit = Habit.get(ContentUris.parseId(data));
Long timestamp = intent.getLongExtra("timestamp", DateHelper.getStartOfToday());
Long reminderTime = intent.getLongExtra("reminderTime", DateHelper.getStartOfToday());
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;
if (habit.repetitions.hasImplicitRepToday()) return;
habit.highlight = 1;
habit.save();
new BaseTask()
{
int todayValue;
if (!checkWeekday(intent, habit)) return;
@Override
protected void doInBackground()
{
todayValue = habit.checkmarks.getTodayValue();
}
// Check if reminder has been turned off after alarm was scheduled
if (habit.reminderHour == null) return;
@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, 0);
Intent contentIntent = new Intent(context, MainActivity.class);
contentIntent.setData(data);
PendingIntent contentPendingIntent =
PendingIntent.getActivity(context, 0, contentIntent, 0);
PendingIntent dismissPendingIntent = buildDismissIntent(context);
PendingIntent checkIntentPending = buildCheckIntent(context, habit, timestamp);
PendingIntent snoozeIntentPending = buildSnoozeIntent(context, habit);
PendingIntent dismissPendingIntent = buildDismissIntent(context);
PendingIntent checkIntentPending = buildCheckIntent(context, habit, timestamp);
PendingIntent snoozeIntentPending = buildSnoozeIntent(context, habit);
Uri soundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
Uri ringtoneUri = ReminderHelper.getRingtoneUri(context);
NotificationCompat.WearableExtender wearableExtender =
new NotificationCompat.WearableExtender().setBackground(
BitmapFactory.decodeResource(context.getResources(), R.drawable.stripe));
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(soundUri)
.extend(wearableExtender)
.setWhen(reminderTime)
.setShowWhen(true)
.build();
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;
notification.flags |= Notification.FLAG_AUTO_CANCEL;
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(Activity.NOTIFICATION_SERVICE);
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(
Activity.NOTIFICATION_SERVICE);
int notificationId = (int) (habit.getId() % Integer.MAX_VALUE);
notificationManager.notify(notificationId, notification);
int notificationId = (int) (habit.getId() % Integer.MAX_VALUE);
notificationManager.notify(notificationId, notification);
super.onPostExecute(aVoid);
}
}.execute();
}
public static PendingIntent buildSnoozeIntent(Context context, Habit habit)
@@ -225,6 +242,16 @@ public class HabitBroadcastReceiver extends BroadcastReceiver
return PendingIntent.getBroadcast(context, 0, deleteIntent, 0);
}
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());
@@ -235,4 +262,13 @@ public class HabitBroadcastReceiver extends BroadcastReceiver
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);
}
}

View File

@@ -0,0 +1,166 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits;
import 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 com.activeandroid.ActiveAndroid;
import org.isoron.uhabits.helpers.DatabaseHelper;
import org.isoron.uhabits.helpers.DateHelper;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.LinkedList;
public class HabitsApplication extends Application
{
@Nullable
private static Context context;
public static boolean isTestMode()
{
try
{
if(context != null)
context.getClassLoader().loadClass("org.isoron.uhabits.unit.models.HabitTest");
return true;
}
catch (final Exception e)
{
return false;
}
}
@Nullable
public static Context getContext()
{
return context;
}
@Override
public void onCreate()
{
super.onCreate();
HabitsApplication.context = this;
if (isTestMode())
{
File db = DatabaseHelper.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);
InputStreamReader in = new InputStreamReader(process.getInputStream());
BufferedReader bufferedReader = new BufferedReader(in);
LinkedList<String> log = new LinkedList<>();
String line;
while ((line = bufferedReader.readLine()) != null)
{
log.addLast(line);
if(log.size() > maxNLines) log.removeFirst();
}
for(String l : log)
{
builder.append(l);
builder.append('\n');
}
return builder.toString();
}
public static String getDeviceInfo()
{
if(context == null) return "";
StringBuilder b = new StringBuilder();
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
b.append(String.format("App Version Name: %s\n", BuildConfig.VERSION_NAME));
b.append(String.format("App Version Code: %s\n", BuildConfig.VERSION_CODE));
b.append(String.format("OS Version: %s (%s)\n", System.getProperty("os.version"),
android.os.Build.VERSION.INCREMENTAL));
b.append(String.format("OS API Level: %s\n", android.os.Build.VERSION.SDK));
b.append(String.format("Device: %s\n", android.os.Build.DEVICE));
b.append(String.format("Model (Product): %s (%s)\n", android.os.Build.MODEL,
android.os.Build.PRODUCT));
b.append(String.format("Manufacturer: %s\n", android.os.Build.MANUFACTURER));
b.append(String.format("Other tags: %s\n", android.os.Build.TAGS));
b.append(String.format("Screen Width: %s\n", wm.getDefaultDisplay().getWidth()));
b.append(String.format("Screen Height: %s\n", wm.getDefaultDisplay().getHeight()));
b.append(String.format("SD Card state: %s\n\n", Environment.getExternalStorageState()));
return b.toString();
}
@NonNull
public static File dumpBugReportToFile() throws IOException
{
String date = DateHelper.getBackupDateFormat().format(DateHelper.getLocalTime());
if(context == null) throw new RuntimeException("application context should not be null");
File dir = DatabaseHelper.getFilesDir("Logs");
if (dir == null) throw new IOException("log dir should not be null");
File logFile = new File(String.format("%s/Log %s.txt", dir.getPath(), date));
FileWriter output = new FileWriter(logFile);
output.write(generateBugReport());
output.close();
return logFile;
}
@NonNull
public static String generateBugReport() throws IOException
{
String logcat = getLogcat();
String deviceInfo = getDeviceInfo();
return deviceInfo + "\n" + logcat;
}
}

View File

@@ -40,10 +40,6 @@ public class IntroActivity extends AppIntro2
getString(R.string.intro_description_2), R.drawable.intro_icon_2,
Color.parseColor("#ffa726")));
addSlide(AppIntroFragment.newInstance(getString(R.string.intro_title_3),
getString(R.string.intro_description_3), R.drawable.intro_icon_3,
Color.parseColor("#7cb342")));
addSlide(AppIntroFragment.newInstance(getString(R.string.intro_title_4),
getString(R.string.intro_description_4), R.drawable.intro_icon_4,
Color.parseColor("#9575cd")));

View File

@@ -26,27 +26,36 @@ 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.helpers.DateHelper;
import org.isoron.helpers.DialogHelper;
import org.isoron.helpers.ReplayableActivity;
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;
public class MainActivity extends ReplayableActivity
import java.io.IOException;
public class MainActivity extends BaseActivity
implements ListHabitsFragment.OnHabitClickListener
{
private ListHabitsFragment listHabitsFragment;
@@ -56,28 +65,49 @@ public class MainActivity extends ReplayableActivity
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) getFragmentManager().findFragmentById(R.id.fragment1);
(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);
DialogHelper.incrementLaunchCount(this);
DialogHelper.updateLastAppVersion(this);
UIHelper.incrementLaunchCount(this);
UIHelper.updateLastAppVersion(this);
showTutorial();
new AsyncTask<Void, Void, Void>() {
@@ -111,7 +141,12 @@ public class MainActivity extends ReplayableActivity
@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;
}
@@ -120,10 +155,21 @@ public class MainActivity extends ReplayableActivity
{
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);
startActivity(intent);
startActivityForResult(intent, 0);
return true;
}
@@ -134,11 +180,92 @@ public class MainActivity extends ReplayableActivity
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)
{
@@ -152,15 +279,24 @@ public class MainActivity extends ReplayableActivity
{
listHabitsFragment.onPostExecuteCommand(refreshKey);
new AsyncTask<Void, Void, Void>()
new BaseTask()
{
@Override
protected Void doInBackground(Void... params)
protected void doInBackground()
{
dismissNotifications(MainActivity.this);
updateWidgets(MainActivity.this);
return null;
}
};
}.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)
@@ -197,4 +333,14 @@ public class MainActivity extends ReplayableActivity
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();
}
}

View File

@@ -19,19 +19,20 @@
package org.isoron.uhabits;
import android.app.Activity;
import android.os.Bundle;
import org.isoron.uhabits.fragments.SettingsFragment;
import org.isoron.uhabits.helpers.UIHelper;
public class SettingsActivity extends Activity
public class SettingsActivity extends BaseActivity
{
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
getFragmentManager().beginTransaction()
.replace(android.R.id.content, new SettingsFragment())
.commit();
setContentView(R.layout.settings_activity);
setupSupportActionBar(true);
int color = UIHelper.getStyledColor(this, R.attr.aboutScreenColor);
setupActionBarColor(color);
}
}

View File

@@ -19,29 +19,17 @@
package org.isoron.uhabits;
import android.app.ActionBar;
import android.content.BroadcastReceiver;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.app.ActionBar;
import org.isoron.helpers.ReplayableActivity;
import org.isoron.uhabits.fragments.ShowHabitFragment;
import org.isoron.uhabits.helpers.ColorHelper;
import org.isoron.uhabits.models.Habit;
public class ShowHabitActivity extends ReplayableActivity
public class ShowHabitActivity extends BaseActivity
{
public Habit habit;
private Receiver receiver;
private LocalBroadcastManager localBroadcastManager;
private ShowHabitFragment fragment;
private Habit habit;
@Override
protected void onCreate(Bundle savedInstanceState)
@@ -50,39 +38,27 @@ public class ShowHabitActivity extends ReplayableActivity
Uri data = getIntent().getData();
habit = Habit.get(ContentUris.parseId(data));
ActionBar actionBar = getActionBar();
if(actionBar != null)
{
actionBar.setTitle(habit.name);
if (android.os.Build.VERSION.SDK_INT >= 21)
actionBar.setBackgroundDrawable(new ColorDrawable(habit.color));
}
setContentView(R.layout.show_habit_activity);
fragment = (ShowHabitFragment) getFragmentManager().findFragmentById(R.id.fragment2);
receiver = new Receiver();
localBroadcastManager = LocalBroadcastManager.getInstance(this);
localBroadcastManager.registerReceiver(receiver,
new IntentFilter(MainActivity.ACTION_REFRESH));
setupSupportActionBar(true);
setupHabitActionBar();
}
class Receiver extends BroadcastReceiver
private void setupHabitActionBar()
{
@Override
public void onReceive(Context context, Intent intent)
{
fragment.refreshData();
}
if(habit == null) return;
ActionBar actionBar = getSupportActionBar();
if(actionBar == null) return;
actionBar.setTitle(habit.name);
setupActionBarColor(ColorHelper.getColor(this, habit.color));
}
@Override
protected void onDestroy()
public Habit getHabit()
{
localBroadcastManager.unregisterReceiver(receiver);
super.onDestroy();
return habit;
}
}

View File

@@ -19,11 +19,9 @@
package org.isoron.uhabits.commands;
import org.isoron.helpers.Command;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit;
import java.util.LinkedList;
import java.util.List;
public class ArchiveHabitsCommand extends Command
@@ -31,12 +29,6 @@ public class ArchiveHabitsCommand extends Command
private List<Habit> habits;
public ArchiveHabitsCommand(Habit habit)
{
habits = new LinkedList<>();
habits.add(habit);
}
public ArchiveHabitsCommand(List<Habit> habits)
{
this.habits = habits;
@@ -45,15 +37,13 @@ public class ArchiveHabitsCommand extends Command
@Override
public void execute()
{
for(Habit h : habits)
h.archive();
Habit.archive(habits);
}
@Override
public void undo()
{
for(Habit h : habits)
h.unarchive();
Habit.unarchive(habits);
}
public Integer getExecuteStringId()

View File

@@ -21,8 +21,8 @@ package org.isoron.uhabits.commands;
import com.activeandroid.ActiveAndroid;
import org.isoron.helpers.Command;
import org.isoron.uhabits.R;
import org.isoron.uhabits.helpers.DatabaseHelper;
import org.isoron.uhabits.models.Habit;
import java.util.ArrayList;
@@ -47,44 +47,25 @@ public class ChangeHabitColorCommand extends Command
@Override
public void execute()
{
ActiveAndroid.beginTransaction();
try
{
for(Habit h : habits)
{
h.color = newColor;
h.save();
}
ActiveAndroid.setTransactionSuccessful();
}
finally
{
ActiveAndroid.endTransaction();
}
Habit.setColor(habits, newColor);
}
@Override
public void undo()
{
ActiveAndroid.beginTransaction();
try
DatabaseHelper.executeAsTransaction(new DatabaseHelper.Command()
{
int k = 0;
for(Habit h : habits)
@Override
public void execute()
{
h.color = originalColors.get(k++);
h.save();
int k = 0;
for(Habit h : habits)
{
h.color = originalColors.get(k++);
h.save();
}
}
ActiveAndroid.setTransactionSuccessful();
}
finally
{
ActiveAndroid.endTransaction();
}
});
}
public Integer getExecuteStringId()

View File

@@ -17,7 +17,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.helpers;
package org.isoron.uhabits.commands;
public abstract class Command
{

View File

@@ -19,7 +19,6 @@
package org.isoron.uhabits.commands;
import org.isoron.helpers.Command;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit;
@@ -51,7 +50,10 @@ public class CreateHabitCommand extends Command
@Override
public void undo()
{
Habit.get(savedId).delete();
Habit habit = Habit.get(savedId);
if(habit == null) throw new RuntimeException("Habit not found");
habit.cascadeDelete();
}
@Override

View File

@@ -19,7 +19,6 @@
package org.isoron.uhabits.commands;
import org.isoron.helpers.Command;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit;

View File

@@ -19,7 +19,6 @@
package org.isoron.uhabits.commands;
import org.isoron.helpers.Command;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit;
@@ -40,29 +39,36 @@ public class EditHabitCommand extends Command
!this.original.freqNum.equals(this.modified.freqNum));
}
@Override
public void execute()
{
Habit habit = Habit.get(savedId);
habit.copyAttributes(modified);
habit.save();
if (hasIntervalChanged)
{
habit.checkmarks.deleteNewerThan(0);
habit.streaks.deleteNewerThan(0);
habit.scores.deleteNewerThan(0);
}
copyAttributes(this.modified);
}
@Override
public void undo()
{
copyAttributes(this.original);
}
private void copyAttributes(Habit model)
{
Habit habit = Habit.get(savedId);
habit.copyAttributes(original);
if(habit == null) throw new RuntimeException("Habit not found");
habit.copyAttributes(model);
habit.save();
invalidateIfNeeded(habit);
}
private void invalidateIfNeeded(Habit habit)
{
if (hasIntervalChanged)
{
habit.checkmarks.deleteNewerThan(0);
habit.streaks.deleteNewerThan(0);
habit.scores.deleteNewerThan(0);
habit.scores.invalidateNewerThan(0);
}
}

View File

@@ -19,7 +19,6 @@
package org.isoron.uhabits.commands;
import org.isoron.helpers.Command;
import org.isoron.uhabits.models.Habit;
public class ToggleRepetitionCommand extends Command

View File

@@ -19,11 +19,9 @@
package org.isoron.uhabits.commands;
import org.isoron.helpers.Command;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit;
import java.util.LinkedList;
import java.util.List;
public class UnarchiveHabitsCommand extends Command
@@ -31,12 +29,6 @@ public class UnarchiveHabitsCommand extends Command
private List<Habit> habits;
public UnarchiveHabitsCommand(Habit habit)
{
habits = new LinkedList<>();
habits.add(habit);
}
public UnarchiveHabitsCommand(List<Habit> habits)
{
this.habits = habits;
@@ -45,15 +37,13 @@ public class UnarchiveHabitsCommand extends Command
@Override
public void execute()
{
for(Habit h : habits)
h.unarchive();
Habit.unarchive(habits);
}
@Override
public void undo()
{
for(Habit h : habits)
h.archive();
Habit.archive(habits);
}
public Integer getExecuteStringId()

View File

@@ -17,20 +17,22 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.fragments;
package org.isoron.uhabits.dialogs;
import android.app.DialogFragment;
import android.annotation.SuppressLint;
import android.content.SharedPreferences;
import android.graphics.Color;
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;
@@ -38,21 +40,20 @@ import com.android.colorpicker.ColorPickerSwatch;
import com.android.datetimepicker.time.RadialPickerLayout;
import com.android.datetimepicker.time.TimePickerDialog;
import org.isoron.helpers.ColorHelper;
import org.isoron.helpers.Command;
import org.isoron.helpers.DateHelper;
import org.isoron.helpers.DialogHelper.OnSavedListener;
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.dialogs.WeekdayPickerDialog;
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 EditHabitFragment extends DialogFragment
public class EditHabitDialogFragment extends AppCompatDialogFragment
implements OnClickListener, WeekdayPickerDialog.OnWeekdaysPickedListener,
TimePickerDialog.OnTimeSetListener
TimePickerDialog.OnTimeSetListener, Spinner.OnItemSelectedListener
{
private Integer mode;
static final int EDIT_MODE = 0;
@@ -70,12 +71,16 @@ public class EditHabitFragment extends DialogFragment
private TextView tvReminderTime;
private TextView tvReminderDays;
private Spinner sFrequency;
private ViewGroup llCustomFrequency;
private ViewGroup llReminderDays;
private SharedPreferences prefs;
private boolean is24HourMode;
public static EditHabitFragment editSingleHabitFragment(long id)
public static EditHabitDialogFragment editSingleHabitFragment(long id)
{
EditHabitFragment frag = new EditHabitFragment();
EditHabitDialogFragment frag = new EditHabitDialogFragment();
Bundle args = new Bundle();
args.putLong("habitId", id);
args.putInt("editMode", EDIT_MODE);
@@ -83,9 +88,9 @@ public class EditHabitFragment extends DialogFragment
return frag;
}
public static EditHabitFragment createHabitFragment()
public static EditHabitDialogFragment createHabitFragment()
{
EditHabitFragment frag = new EditHabitFragment();
EditHabitDialogFragment frag = new EditHabitDialogFragment();
Bundle args = new Bundle();
args.putInt("editMode", CREATE_MODE);
frag.setArguments(args);
@@ -104,6 +109,10 @@ public class EditHabitFragment extends DialogFragment
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);
@@ -113,6 +122,7 @@ public class EditHabitFragment extends DialogFragment
tvReminderTime.setOnClickListener(this);
tvReminderDays.setOnClickListener(this);
buttonPickColor.setOnClickListener(this);
sFrequency.setOnItemSelectedListener(this);
prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
@@ -125,18 +135,16 @@ public class EditHabitFragment extends DialogFragment
{
getDialog().setTitle(R.string.create_habit);
modifiedHabit = new Habit();
int defaultNum = prefs.getInt("pref_default_habit_freq_num", modifiedHabit.freqNum);
int defaultDen = prefs.getInt("pref_default_habit_freq_den", modifiedHabit.freqDen);
int defaultColor = prefs.getInt("pref_default_habit_color", modifiedHabit.color);
modifiedHabit.color = defaultColor;
modifiedHabit.freqNum = defaultNum;
modifiedHabit.freqDen = defaultDen;
modifiedHabit.freqNum = 1;
modifiedHabit.freqDen = 1;
modifiedHabit.color = prefs.getInt("pref_default_habit_palette_color", modifiedHabit.color);
}
else if (mode == EDIT_MODE)
{
originalHabit = Habit.get((Long) args.get("habitId"));
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);
@@ -152,50 +160,46 @@ public class EditHabitFragment extends DialogFragment
modifiedHabit.reminderDays = savedInstanceState.getInt("reminderDays", -1);
if(modifiedHabit.reminderMin < 0)
{
modifiedHabit.reminderMin = null;
modifiedHabit.reminderHour = null;
modifiedHabit.reminderDays = 127;
}
modifiedHabit.clearReminder();
}
tvFreqNum.append(modifiedHabit.freqNum.toString());
tvFreqDen.append(modifiedHabit.freqDen.toString());
changeColor(modifiedHabit.color);
updateFrequency();
updateReminder();
return view;
}
private void changeColor(Integer color)
private void changeColor(int paletteColor)
{
modifiedHabit.color = color;
tvName.setTextColor(color);
modifiedHabit.color = paletteColor;
tvName.setTextColor(ColorHelper.getColor(getActivity(), paletteColor));
SharedPreferences.Editor editor = prefs.edit();
editor.putInt("pref_default_habit_color", color);
editor.putInt("pref_default_habit_palette_color", paletteColor);
editor.apply();
}
@SuppressWarnings("ConstantConditions")
private void updateReminder()
{
if (modifiedHabit.reminderHour != null)
if (modifiedHabit.hasReminder())
{
tvReminderTime.setTextColor(Color.BLACK);
tvReminderTime.setText(DateHelper.formatTime(getActivity(), modifiedHabit.reminderHour,
modifiedHabit.reminderMin));
tvReminderDays.setVisibility(View.VISIBLE);
llReminderDays.setVisibility(View.VISIBLE);
boolean weekdays[] = DateHelper.unpackWeekdayList(modifiedHabit.reminderDays);
tvReminderDays.setText(DateHelper.formatWeekdayList(getActivity(), weekdays));
}
else
{
tvReminderTime.setTextColor(Color.GRAY);
tvReminderTime.setText(R.string.reminder_off);
tvReminderDays.setVisibility(View.GONE);
llReminderDays.setVisibility(View.GONE);
}
boolean weekdays[] = DateHelper.unpackWeekdayList(modifiedHabit.reminderDays);
tvReminderDays.setText(DateHelper.formatWeekdayList(getActivity(), weekdays));
}
public void setOnSavedListener(OnSavedListener onSavedListener)
@@ -232,15 +236,18 @@ public class EditHabitFragment extends DialogFragment
private void onColorButtonClick()
{
int originalAndroidColor = ColorHelper.getColor(getActivity(), modifiedHabit.color);
ColorPickerDialog picker = ColorPickerDialog.newInstance(
R.string.color_picker_default_title, ColorHelper.palette, modifiedHabit.color, 4,
ColorPickerDialog.SIZE_SMALL);
R.string.color_picker_default_title, ColorHelper.getPalette(getActivity()),
originalAndroidColor, 4, ColorPickerDialog.SIZE_SMALL);
picker.setOnColorSelectedListener(new ColorPickerSwatch.OnColorSelectedListener()
{
public void onColorSelected(int color)
public void onColorSelected(int androidColor)
{
changeColor(color);
int paletteColor = ColorHelper.colorToPaletteIndex(getActivity(), androidColor);
changeColor(paletteColor);
}
});
picker.show(getFragmentManager(), "picker");
@@ -257,11 +264,6 @@ public class EditHabitFragment extends DialogFragment
if (!validate()) return;
SharedPreferences.Editor editor = prefs.edit();
editor.putInt("pref_default_habit_freq_num", modifiedHabit.freqNum);
editor.putInt("pref_default_habit_freq_den", modifiedHabit.freqDen);
editor.apply();
Command command = null;
Habit savedHabit = null;
@@ -305,12 +307,13 @@ public class EditHabitFragment extends DialogFragment
return valid;
}
@SuppressWarnings("ConstantConditions")
private void onDateSpinnerClick()
{
int defaultHour = 8;
int defaultMin = 0;
if (modifiedHabit.reminderHour != null)
if (modifiedHabit.hasReminder())
{
defaultHour = modifiedHabit.reminderHour;
defaultMin = modifiedHabit.reminderMin;
@@ -321,8 +324,11 @@ public class EditHabitFragment extends DialogFragment
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));
@@ -334,14 +340,14 @@ public class EditHabitFragment extends DialogFragment
{
modifiedHabit.reminderHour = hour;
modifiedHabit.reminderMin = minute;
modifiedHabit.reminderDays = DateHelper.ALL_WEEK_DAYS;
updateReminder();
}
@Override
public void onTimeCleared(RadialPickerLayout view)
{
modifiedHabit.reminderHour = null;
modifiedHabit.reminderMin = null;
modifiedHabit.clearReminder();
updateReminder();
}
@@ -359,16 +365,93 @@ public class EditHabitFragment extends DialogFragment
}
@Override
@SuppressWarnings("ConstantConditions")
public void onSaveInstanceState(Bundle outState)
{
super.onSaveInstanceState(outState);
outState.putInt("color", modifiedHabit.color);
if(modifiedHabit.reminderHour != null)
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)
{
}
}

View File

@@ -0,0 +1,175 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.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;
public class FilePickerDialog implements AdapterView.OnItemClickListener
{
private static final String PARENT_DIR = "..";
private final Activity activity;
private ListView list;
private Dialog dialog;
private File currentPath;
public interface OnFileSelectedListener
{
void onFileSelected(File file);
}
private OnFileSelectedListener listener;
public FilePickerDialog(Activity activity, File initialDirectory)
{
this.activity = activity;
list = new ListView(activity);
list.setOnItemClickListener(this);
dialog = new Dialog(activity);
dialog.setContentView(list);
dialog.getWindow().setLayout(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
navigateTo(initialDirectory);
}
@Override
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 (file.isDirectory())
{
navigateTo(file);
}
else
{
if (listener != null) listener.onFileSelected(file);
dialog.dismiss();
}
}
public void show()
{
dialog.show();
}
public void setListener(OnFileSelectedListener listener)
{
this.listener = listener;
}
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)));
}
@NonNull
private String[] getFileList(File path, File[] dirs, File[] files)
{
int count = 0;
int length = dirs.length + files.length;
String[] fileList;
if (path.getParentFile() == null || !path.getParentFile().canRead())
{
fileList = new String[length];
}
else
{
fileList = new String[length + 1];
fileList[count++] = PARENT_DIR;
}
Arrays.sort(dirs);
Arrays.sort(files);
for (File dir : dirs)
fileList[count++] = dir.getName();
for (File file : files)
fileList[count++] = file.getName();
return fileList;
}
private class FilePickerAdapter extends ArrayAdapter<String>
{
public FilePickerAdapter(@NonNull String[] fileList)
{
super(FilePickerDialog.this.activity, android.R.layout.simple_list_item_1, fileList);
}
@Override
public View getView(int pos, View view, ViewGroup parent)
{
view = super.getView(pos, view, parent);
TextView tv = (TextView) view;
tv.setSingleLine(true);
return view;
}
}
private static class ReadableDirFilter implements FileFilter
{
@Override
public boolean accept(File file)
{
return (file.isDirectory() && file.canRead());
}
}
private class RegularReadableFileFilter implements FileFilter
{
@Override
public boolean accept(File file)
{
return !file.isDirectory() && file.canRead();
}
}
}

View File

@@ -19,20 +19,20 @@
package org.isoron.uhabits.dialogs;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
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 android.util.Log;
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 DialogFragment
public class HistoryEditorDialog extends AppCompatDialogFragment
implements DialogInterface.OnClickListener
{
private Habit habit;
@@ -44,7 +44,6 @@ public class HistoryEditorDialog extends DialogFragment
{
Context context = getActivity();
historyView = new HabitHistoryView(context, null);
int p = (int) getResources().getDimension(R.dimen.history_editor_padding);
if(savedInstanceState != null)
{
@@ -52,7 +51,8 @@ public class HistoryEditorDialog extends DialogFragment
if(id > 0) this.habit = Habit.get(id);
}
historyView.setPadding(p, 0, p, 0);
int padding = (int) getResources().getDimension(R.dimen.history_editor_padding);
historyView.setPadding(padding, 0, padding, 0);
historyView.setHabit(habit);
historyView.setIsEditable(true);
@@ -61,9 +61,23 @@ public class HistoryEditorDialog extends DialogFragment
.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()
{
@@ -74,8 +88,6 @@ public class HistoryEditorDialog extends DialogFragment
int width = metrics.widthPixels;
int height = Math.min(metrics.heightPixels, maxHeight);
Log.d("HistoryEditorDialog", String.format("h=%d max_h=%d", height, maxHeight));
getDialog().getWindow().setLayout(width, height);
}

View File

@@ -19,16 +19,16 @@
package org.isoron.uhabits.dialogs;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatDialogFragment;
import org.isoron.helpers.DateHelper;
import org.isoron.uhabits.R;
import org.isoron.uhabits.helpers.DateHelper;
public class WeekdayPickerDialog extends DialogFragment
public class WeekdayPickerDialog extends AppCompatDialogFragment
implements DialogInterface.OnMultiChoiceClickListener, DialogInterface.OnClickListener
{

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