Compare commits

...

218 Commits

Author SHA1 Message Date
16b0682229 Ignore all performance tests 2020-11-29 15:05:28 -06:00
a77798f293 PerformanceTest: Increase time limit 2020-11-29 13:38:09 -06:00
08050ff616 WidgetUpdater: Remove rate limit 2020-11-29 13:07:17 -06:00
12b080152b Speed up CreateHabitCommand and CreateRepetitionCommand 2020-11-29 12:46:01 -06:00
df3d660e83 SyncActivity: Remove password field 2020-11-29 07:19:40 -06:00
4908709296 Merge branch 'feature/sync' into dev 2020-11-29 07:12:12 -06:00
5717ae1bf1 RemoteSyncServer: Fix test 2020-11-29 07:11:38 -06:00
872c8d9d81 SyncManager: Small fix in logging 2020-11-29 06:47:18 -06:00
0b6110f0f9 Add alpha to version name 2020-11-28 22:29:24 -06:00
6df5e9ebe9 Monitor network availability; other minor fixes 2020-11-28 22:28:54 -06:00
2b9fd74a1d Close database 2020-11-28 22:28:46 -06:00
4a4b3c6aeb Server: change dir structure 2020-11-28 22:27:55 -06:00
7979f74bea Server: data persistence 2020-11-28 16:23:21 -06:00
b0336fb495 SyncManager: Log sync key 2020-11-28 16:22:33 -06:00
328fcd23f4 SyncManager: Run tasks in the same thread 2020-11-28 11:37:35 -06:00
9c0951ae58 Minor fixes to sync protocol 2020-11-28 11:32:28 -06:00
3f51561271 GitHub Actions: Upload only build outputs 2020-11-28 10:10:01 -06:00
1787c0e74e Upgrade to Android Gradle plugin 4.1.0 2020-11-28 10:06:53 -06:00
49faacda1c Wrap base64 functions; close gzip stream before reading 2020-11-28 09:44:45 -06:00
339eeff1ff Merge branch 'dev' into feature/sync 2020-11-28 08:55:12 -06:00
849212fd2f Run medium tests three times 2020-11-28 08:48:55 -06:00
356b2b06e4 Update README.md 2020-11-27 18:58:02 -06:00
b6eefbdb36 setSystemTime: Try two methods 2020-11-27 18:35:52 -06:00
2a72601153 Increase minSdkVersion to 23 2020-11-27 18:22:44 -06:00
2bf7358207 Fix medium tests for Android APIs 23-25 2020-11-27 18:00:56 -06:00
8c6e2ef461 GitHub Actions: Test multiple Android versions 2020-11-27 17:32:01 -06:00
ce0cbb6ee2 Sync: Improve encryption and preferences API 2020-11-27 10:55:55 -06:00
67ef3bb90c SyncManager: Switch to coroutines 2020-11-27 09:13:17 -06:00
b82af419f8 Merge branch 'dev' into feature/sync 2020-11-26 23:56:53 -06:00
49ff9a7edf Fix failing tests 2020-11-26 23:47:40 -06:00
53ebdf4f14 Skip upload if database has not changed 2020-11-26 23:43:47 -06:00
4762b54701 Merge branch 'dev' into feature/sync 2020-11-26 15:39:44 -06:00
aa288ac406 Make question marks optional
Fixes #96
2020-11-26 15:36:44 -06:00
2228dbf0f4 Create new UNKNOWN checkmark state 2020-11-26 15:08:49 -06:00
9ca1c8e459 Rewrite WidgetBehavior and associated tests 2020-11-26 14:19:15 -06:00
61414d62f4 Remove calls to Repetition.add and Repetition.remove 2020-11-26 14:19:02 -06:00
f97fed3b9b Repetition: Replace toggle by setValue 2020-11-26 13:29:12 -06:00
d45281d137 Merge branch 'master' into dev 2020-11-26 08:59:52 -06:00
1bb6ad41b2 Merge branch 'hotfix/1.8.10' 2020-11-26 08:22:50 -06:00
56c180183e Update release notes 2020-11-26 08:22:37 -06:00
af8d983cca Add dummy settings.gradle file to help F-Droid locate app metadata 2020-11-26 08:20:10 -06:00
0a49232ebd Add dummy settings.gradle file to help F-Droid locate app metadata 2020-11-26 08:11:40 -06:00
cff8e26428 Update translations 2020-11-25 22:33:41 -06:00
e892bccb32 Bump version to 1.8.10 2020-11-25 18:23:27 -06:00
68ccf37fd5 Add UUID to habits 2020-11-24 08:28:16 -06:00
659c528744 SyncManager: First version 2020-11-24 06:55:37 -06:00
b1560dd694 LoopDBImporter: Use commands instead of directly modifying DB 2020-11-24 06:54:28 -06:00
0de86ac66c Update widgets at most once per minute 2020-11-24 06:53:20 -06:00
06e5d517cc Downgrade Ktor 2020-11-24 06:51:34 -06:00
35ca041bc2 EncExt: Trim keys 2020-11-24 06:51:08 -06:00
576a334dc9 Update sync intent-filter 2020-11-24 06:32:21 -06:00
294aee5d12 Implement cryptography extensions 2020-11-22 22:39:22 -06:00
0859cec853 Implement intent filter; hide password for now 2020-11-22 17:00:37 -06:00
23f2978a64 Add sync preferences to settings screen 2020-11-22 15:39:08 -06:00
a2400172e2 Make registration functional 2020-11-22 13:00:59 -06:00
5376f4bff8 Implement SyncActivity (with static data) 2020-11-22 10:07:34 -06:00
0497890cb0 Update docker registry URL 2020-11-22 10:02:53 -06:00
2848c4e77b Server: Implement get version 2020-11-22 09:49:35 -06:00
8fa3ba1b18 Add docker tasks to gradle 2020-11-21 20:49:48 -06:00
b4f36dd258 Initial version of Ktor sync server 2020-11-21 20:12:14 -06:00
008902d3b7 AutoBackup: Use getLocalTime instead of getStartOfToday; improve logging 2020-11-21 10:12:52 -06:00
4764c07f3b Bump version to 2.0.0 2020-11-19 19:29:26 -06:00
8f0cfa8614 Update CHANGELOG.md 2020-11-19 19:27:27 -06:00
865e1969e6 Update CHANGELOG 2020-11-19 19:22:10 -06:00
Quentin Hibon
bfddc42f5e Allow user to sort by status (#660) 2020-11-19 19:05:21 -06:00
dc0b8deccf Export backups daily
Fixes #178
2020-11-19 18:31:28 -06:00
b674d14b49 Opt-in skips: Update tests 2020-11-18 22:26:45 -06:00
d594d3b085 Make skip days an opt-in feature 2020-11-18 22:13:03 -06:00
bef85bf93a CHANGELOG: Minor fixes 2020-11-18 07:16:17 -06:00
76eaefc95b Merge branch 'master' into dev 2020-11-18 07:11:45 -06:00
Sunxy88
2d488a67f2 Use dark theme in settings window. (#655) 2020-11-14 07:57:07 -06:00
Kristian Tashkov
d997b1378d Setting custom start of the day (#621) 2020-09-19 19:23:00 -05:00
720f98f9bd Write tests for IntentScheduler 2020-09-19 19:11:57 -05:00
ddea9e78a9 ScoreList: Fix interaction between SKIP and rolling sum 2020-09-16 07:38:59 -05:00
c429cb41c0 Fix method rename 2020-09-15 22:32:46 -05:00
ae286cec14 Take frequency into account when computing score for numerical habits 2020-09-15 22:30:44 -05:00
31d631b155 Update test screenshots 2020-09-15 22:30:44 -05:00
20142d5f94 Reduce time required to form non-daily habits; smooth out irregular schedules 2020-09-15 22:30:44 -05:00
ef186d55c6 Update test screenshots 2020-09-15 22:30:44 -05:00
8b847ae9fa ScoreList: Use rolling sum method also for boolean habits
See #641
2020-09-15 22:30:44 -05:00
Kristian Tashkov
a4ef657897 Make skips freeze score (#630) 2020-09-13 17:01:15 -05:00
f44556e281 ScoreList: Use rolling sum for non-daily numerical habits 2020-09-12 21:40:59 -05:00
8a895b2d20 Fix colors in BarChart and HistoryChard 2020-09-12 16:09:12 -05:00
61f32449dd Numerical habits: allow Tasker to increment/decrement value 2020-09-12 15:10:46 -05:00
Kristian Tashkov
07f8583c3d Don't show reminders from archived habits (#639) 2020-09-12 13:15:23 -05:00
Kristian Tashkov
69f11c9d4e Fix clearing of reminders (#638) 2020-09-12 12:47:03 -05:00
Nguyen Ly Nam
1ffc079042 Numerical habits: update notifications and detail screen (#627) 2020-09-11 17:32:20 -05:00
5fa3f412c0 Show YES_AUTO as grey checkmark
This reverts a change introduced recently where YES_AUTO (previously CHECKED_IMPLICITLY)
was shown as a grey dash.
2020-09-05 18:46:48 -05:00
b72cad5316 Rename checkmark values to NO, YES_AUTO, YES_MANUAL and SKIP
This makes the source code consistent with the user interface.
2020-09-05 18:04:04 -05:00
Kristian Tashkov
d59ab89426 Update widgets at midnight (#634) 2020-09-05 17:25:46 -05:00
ea019321e6 Revert "NumberPickerFactory: Show and hide keyboard using InputMethodManager"
This reverts commit 6967def950. InputMethodManager method
does not work reliably on widgets. It also cannot reliably hide the keyboard.
2020-09-03 22:22:07 -05:00
6967def950 NumberPickerFactory: Show and hide keyboard using InputMethodManager
In about 1 every 10 attempts, the previous solution randomly failed to show
the keyboard, although the text field was focused. This solution seems more reliable.
2020-09-03 21:55:03 -05:00
6d4cac427f NumberPickerFactory: Automatically show keyboard 2020-09-03 21:06:46 -05:00
152b2d5427 Merge pull request #631 from KristianTashkov/habit_selection_dark_theme
Fix habit selection menu item background color
2020-09-02 08:09:26 -05:00
9d28fbe7b5 ScoreList: Revert recent changes to computation of scores 2020-09-02 07:01:54 -05:00
c846dfc75a Make skips equivalent to implicit checks; make their visual representation consistent 2020-09-01 22:23:42 -05:00
ee7eb4ef51 build.sh: Always use GRADLE variable 2020-09-01 22:22:38 -05:00
KristianTashkov
57bfe3d801 Fix habit selection menu item background color 2020-09-01 15:53:30 +03:00
a5ee96f988 HistoryChart: Make skipped days a bit more clear 2020-08-23 15:09:13 -05:00
7b0eddeac5 Set minimum widget size 2020-08-23 14:48:58 -05:00
e8e52db9b1 HabitPickerDialog: Show "no habits found" message 2020-08-23 14:39:11 -05:00
cddbf558e6 Reactivate widget view tests; update widget previews 2020-08-23 14:09:50 -05:00
84523869e8 Manage exceptions for when activities don't exist to handle intents
Fixes #181
2020-08-23 08:43:22 -05:00
8067fd5313 MemoryHabitList: Inherit parent's order
Fixes #598
2020-08-23 07:41:42 -05:00
Christoph Hennemann
d2dc756a34 Improve readability of history and streak charts
Fixes #432

Co-authored-by: Alinson S. Xavier <git@axavier.org>
2020-08-22 23:37:43 -05:00
2af1dbf3a6 Reactivate HistoryChart tests; fix IndexOutOfBoundsException 2020-08-22 19:42:46 -05:00
ebab6f08ee Remove obsolete regression test 2020-08-22 19:04:29 -05:00
4a2b21855a Update ListHabitsRegressionTest 2020-08-22 19:04:11 -05:00
42d5edec26 WidgetTest: Tap twice to remove checkmark 2020-08-22 18:50:39 -05:00
f368e43158 CreateRepetitionCommand: Run update() after executing 2020-08-22 18:27:12 -05:00
09eb8c9f4d Fix widget tests on API 29+ 2020-08-22 17:52:53 -05:00
209e709163 Make widgets fully opaque by default 2020-08-22 16:26:59 -05:00
d20a2be7e6 Remove obsolete test screenshots 2020-08-22 15:58:26 -05:00
bd68f8fc5a Revert changes to HistoryCard view screenshots 2020-08-22 15:56:24 -05:00
KristianTashkov
1a05f7d85d Allow user to skip days without breaking streak
Co-authored-by: Alinson S. Xavier <git@axavier.org>
2020-08-22 15:35:35 -05:00
TacoTheDank
d9ff429c28 Deprecation fixes
Co-authored-by: Alinson S. Xavier <git@axavier.org>
2020-08-22 12:14:35 -05:00
3554895a5d Widgets: Show shadow if widget is completely opaque 2020-08-18 08:09:12 -05:00
16491c142a Widgets: Fix title size 2020-08-18 08:00:38 -05:00
859fea5ff5 Hide streaks for numerical habits 2020-08-18 07:57:57 -05:00
34c73e89db Merge branch 'hotfix/1.8.9' into dev 2020-08-15 15:18:21 -05:00
48e43869c7 Merge branch 'hotfix/1.8.9' into dev 2020-08-15 14:18:50 -05:00
963fb58309 Revert changes to android-pickers 2020-08-15 13:58:03 -05:00
3ef3be4d16 Tidy up strings.xml 2020-08-15 12:47:32 -05:00
bae0e3bcc1 Remove unused resources 2020-08-15 12:45:34 -05:00
3e99d821a5 Allow user to sort habits in reverse order
Closes #556, closes #497
2020-08-15 12:29:49 -05:00
olegivo
acb5051eec Konvert BaseActivity
Co-authored-by: Alinson S. Xavier <git@axavier.org>
2020-08-15 11:46:13 -05:00
olegivo
b76882dd1d Konvert BaseMenu
Co-authored-by: Alinson S. Xavier <git@axavier.org>
2020-08-15 11:41:19 -05:00
olegivo
978946baab Konvert BaseSelectionMenu
Co-authored-by: Alinson S. Xavier <git@axavier.org>
2020-08-15 11:38:41 -05:00
olegivo
d202f14c14 Konvert BaseScreen
Co-authored-by: Alinson S. Xavier <git@axavier.org>
2020-08-15 11:36:03 -05:00
olegivo
17a85e517a Konvert BaseRootView
Co-authored-by: Alinson S. Xavier <git@axavier.org>
2020-08-13 07:53:37 -05:00
olegivo
c5bc5deff0 Konvert ActivityScope
Co-authored-by: Alinson S. Xavier <git@axavier.org>
2020-08-13 07:44:07 -05:00
olegivo
b7f04957a5 Konvert ActivityContext
Co-authored-by: Alinson S. Xavier <git@axavier.org>
2020-08-13 07:43:19 -05:00
olegivo
b0f5f96eee Konvert StyledResources
Co-authored-by: Alinson S. Xavier <git@axavier.org>
2020-08-13 07:40:49 -05:00
olegivo
fd76a3c6fd Konvert InterfaceUtils
Co-authored-by: Alinson S. Xavier <git@axavier.org>
2020-08-13 07:32:41 -05:00
b31482881b Merge pull request #608 from KristianTashkov/kris/fix_yes_no
Only save numerical habit features if the habit is numerical
2020-08-04 07:29:50 -05:00
KristianTashkov
87231d7fa4 fix saving of non-numerical habits 2020-07-20 13:12:58 +03:00
olegivo
4d18a1335c Convert FileUtils to Kotlin
Co-authored-by: Alinson S. Xavier <git@axavier.org>
2020-07-12 10:22:18 -05:00
olegivo
424a417a13 Convert ColorUtils to Kotlin
Co-authored-by: Alinson S. Xavier <git@axavier.org>
2020-07-12 10:00:23 -05:00
Thomas S
96d23bdf22 Update checkmark widget for numerical support
Co-authored-by: Alinson S. Xavier <git@axavier.org>
2020-07-12 09:25:49 -05:00
a8e77b8df8 Update test screenshots 2020-07-02 08:04:03 -05:00
5413569ce3 Update test screenshots 2020-07-02 07:31:02 -05:00
d80b85ac8c Make bars round 2020-06-25 07:16:47 -05:00
40bc35935f Create target widget 2020-06-25 07:04:43 -05:00
a6060f468d Create new TargetCard and TargetChart 2020-06-25 06:12:26 -05:00
6ec9d51a1e CheckmarkList: Implement getThisIntervalValue 2020-06-24 20:07:56 -05:00
de28a5e74e EditHabitActivity: Do not divide target by freq denominator 2020-06-24 20:07:21 -05:00
3ba503604b Show target in SubtitleCard; replace some bitmap icons by FontAwesome 2020-06-23 06:19:08 -05:00
6d48b53861 Merge branch 'master' into dev 2020-06-21 17:28:41 -05:00
0b7697d172 Make save button functional for numerical habits 2020-06-20 08:39:55 -05:00
b9850fa085 Create form for numerical habits 2020-06-20 07:45:38 -05:00
ecb3978bdd Fix dialog animations 2020-06-19 20:57:35 -05:00
fc57a9db6c Merge branch 'feature/numerical' into dev 2020-06-19 07:50:50 -05:00
6c9c2a6c1a Update tests 2020-06-19 07:48:40 -05:00
3e0529d515 Remove old EditHabitDialog 2020-06-19 06:59:16 -05:00
7d8d89fbbd EditHabitActivity: Adjust inputType 2020-06-19 06:58:36 -05:00
c43f3c2fd7 EditHabitActivity: Dismiss all fragments on device rotation 2020-06-19 06:39:21 -05:00
403ed8b250 EditHabitActivity: Make save button functional 2020-06-19 06:24:51 -05:00
9ccb2b2737 Implement reminder day picker 2020-06-18 22:19:59 -05:00
424a282847 Update gitignore 2020-06-18 07:45:19 -05:00
309b6cbcaf Implement reminder time picker; customize picker color 2020-06-18 07:43:58 -05:00
72ad14119a Create HabitTypeDialog 2020-06-17 07:00:25 -05:00
Alex Johnson
652ed50d09 Reset habit strength graph scroll location when switching scale
- Create ScrollableChart.reset() to set x coordinate of scroller
- Call reset() from doInBackground() in ScoreCard to reset scroll location
2020-06-16 20:49:14 -07:00
6070a7af2e Merge branch 'dev' into edit-redesign 2020-06-16 07:19:57 -05:00
8fd8c2802b Remove AboutBehavior and AboutModule 2020-06-16 06:44:28 -05:00
923b923745 build.sh: Remove .gradle directory when cleaning project 2020-06-16 06:43:38 -05:00
59c8031372 Minor style changes 2020-06-15 08:27:48 -05:00
0058089e7d Remove redundant repositories section 2020-06-15 08:27:31 -05:00
d5a840388c Merge branch 'dev' into 2kotlin-androidbase 2020-06-15 08:00:34 -05:00
4b07d7d5b1 Fix build script; remove some obsolete tests 2020-06-15 07:59:35 -05:00
2fffc25128 Merge pull request #585 from olegivo/update-agp 2020-06-15 06:19:27 -05:00
4a4501276c Merge pull request #590 from Gelma/typos
Fix typos
2020-06-15 06:16:54 -05:00
Andrea Gelmini
c8e3735dd6 Fix typos 2020-06-13 18:25:05 +02:00
olegivo
61267e40e7 konvert SSLContextProvider 2020-06-04 11:56:03 +03:00
olegivo
c0b664e1e4 konvert BaseExceptionHandler 2020-06-04 11:56:03 +03:00
olegivo
e57c319658 konvert @AppContext 2020-06-04 11:56:03 +03:00
olegivo
e54ba826b3 konvert AndroidDirFinder
remove if-null condition cause ContextCompat.getExternalFilesDirs is @NonNull
2020-06-04 11:56:03 +03:00
olegivo
9b8784b4c4 AndroidBugReporter: more idiomatic kotlin 2020-06-04 11:56:03 +03:00
olegivo
51a7b7a7d4 konvert AndroidBugReporter 2020-06-04 11:56:03 +03:00
olegivo
d761b474cf add kotlin support for android-base project 2020-06-04 11:56:03 +03:00
olegivo
51be585b9d update AGP (4.0.0) 2020-06-04 11:14:52 +03:00
olegivo
76be5037fd update AGP (3.6.3) 2020-05-18 17:37:39 +03:00
aee0da2c64 Merge branch 'feature/kn-update' into dev 2020-05-17 14:54:36 -05:00
f1610e6603 core: Skip BaseViewTests on iOS 2020-05-17 14:54:02 -05:00
a7a1766809 core: update gradle, rename iOS framework 2020-05-17 14:44:00 -05:00
5f83314d56 Fix format method on JVM and JS 2020-05-17 13:33:22 -05:00
c784f40c55 Remove custom iosTest task; fix IosDatabase 2020-05-17 13:28:52 -05:00
6a172d135b Update to K/N 1.3.72; remove i18n classes; rewrite sprintf 2020-05-17 11:47:13 -05:00
13f4981066 Merge pull request #547 from recheej/rechee/add_notes
Add Notes to Habits.
2020-03-01 15:11:38 -05:00
Rechee Jozil
849b91dde2 delete bad unit test 2020-02-01 11:08:32 -08:00
Rechee Jozil
66b4c48d92 null check description 2020-02-01 11:08:06 -08:00
Rechee Jozil
2e64da4cac using wildcard imports 2020-02-01 11:03:14 -08:00
323ddcc11a Implement FrequencyPickerDialog 2020-01-20 06:08:29 -06:00
175000efd1 Follow current theme; implement color switching 2020-01-12 11:54:15 -06:00
6f94fc48c1 First version of EditHabitActivity 2020-01-12 10:20:36 -06:00
Rechee
18d1d0d9f7 fixed screenshot tests 2020-01-09 19:49:14 -08:00
Rechee
47edea47ae creating UI test for blank description 2020-01-08 20:35:45 -08:00
Rechee
1714cf8050 added create habit test for description 2020-01-08 20:16:43 -08:00
Rechee
7366e9a47f add blank habit test 2020-01-08 20:04:11 -08:00
Rechee
e58589cfbd adding description to test 2020-01-08 20:02:38 -08:00
Rechee
2999e0e5eb updating test 2020-01-08 19:50:17 -08:00
Rechee
fa7bc27124 add test for notes Card 2020-01-08 19:36:54 -08:00
Rechee
f5be9d3c67 introduced notes card to support refresh 2020-01-08 19:25:23 -08:00
Rechee
2c46e8909a now showing notes in show habits 2020-01-08 18:57:47 -08:00
Rechee
8b042f30dc displaying subtitle card UI in designer
squash! displaying subtitle card UI in designer
2020-01-08 18:39:30 -08:00
Rechee
46761926d2 now writing question and description to csv 2020-01-08 17:59:26 -08:00
Rechee
88d6a8e513 fix habit importers to use description instead 2020-01-08 17:52:52 -08:00
Rechee
557ae19297 setting description to be blank instead of null 2020-01-08 17:45:55 -08:00
Rechee
9c10a56dda setting question label in subtitle 2020-01-07 22:25:14 -08:00
Rechee
895b068321 fixing tests 2020-01-07 21:32:18 -08:00
Rechee
fb98c5fe9a replacing getDescription with getQuestion all over the code 2020-01-07 20:39:23 -08:00
Rechee
0ec604f21e fix formatting 2020-01-07 20:34:18 -08:00
Rechee
bcd9dd1bb5 now allowing blank for description
squash! now allowing blank for description
2020-01-07 20:33:39 -08:00
Rechee
61bcd253f8 fully implementing question & description in UI and code 2020-01-07 20:22:35 -08:00
Rechee
fb40dbdabc edit name description panel xml 2020-01-07 20:02:55 -08:00
Rechee
0990192cd6 making nullable 2020-01-06 17:46:48 -08:00
Rechee
1cf2d69534 creating migration tests 2020-01-06 17:40:11 -08:00
Rechee
0489dc39e0 updating db version and adding migration for question column 2020-01-06 12:46:05 -08:00
Rechee
88b9645be1 adding question to habit and habit record 2020-01-06 12:36:25 -08:00
717 changed files with 10929 additions and 12293 deletions

View File

@@ -1,31 +1,50 @@
name: Build & Test
on:
push:
branches:
- dev
pull_request:
branches:
- dev
on: [push, pull_request]
jobs:
build:
runs-on: macOS-latest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Check out source code
uses: actions/checkout@v1
- name: Install Java Development Kit 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Build APK & Run small tests
run: android/build.sh build
- name: Run medium tests
uses: ReactiveCircus/android-emulator-runner@v2.2.0
- name: Upload APK
uses: actions/upload-artifact@v2
with:
api-level: 29
script: android/build.sh medium-tests
- name: Upload artifacts
uses: actions/upload-artifact@v1
name: debug-apk
path: android/build/*apk
- name: Upload build outputs
uses: actions/upload-artifact@v2
with:
name: Build
name: build
path: android/uhabits-android/build/outputs/
test:
needs: build
runs-on: macOS-latest
strategy:
matrix:
api-level: [23, 24, 25, 26, 27, 28, 29]
steps:
- name: Check out source code
uses: actions/checkout@v1
- name: Download previous build folder
uses: actions/download-artifact@v2
with:
name: build
path: android/uhabits-android/build/outputs/
- name: Run medium tests
uses: ReactiveCircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
script: android/build.sh medium-tests

5
.gitignore vendored
View File

@@ -4,6 +4,7 @@
*.perspectivev3
*.swp
*~.nib
*.hprof
.DS_Store
._.DS_Store
.externalNativeBuild
@@ -16,3 +17,7 @@ captures
local.properties
node_modules
*xcuserdata*
*.sketch
/design
/releases
/screenshots

View File

@@ -1,9 +1,31 @@
# Changelog
### 2.0.0 (TBD)
* **New Features:**
* Track numerical habits (@iSoron, @namnl)
* Skip days without breaking streak (@KristianTashkov)
* Sort habits by status (@hiqua)
* Sort habits in reverse order (@iSoron)
* Add notes to habits (@recheej)
* Improve readibility of charts (@chennemann)
* Delay new day until 3am (@KristianTashkov)
* Export backups daily (@iSoron)
* **Bug fixes:**
* Reset chart offset when switching scale (@alxmjo)
* Don't show reminders from archived habits (@KristianTashkov)
* Lapses on non-daily habits decrease the score too much (@iSoron)
* Update widgets at midnight (@KristianTashkov)
* **Refactoring:**
* Convert files to Kotlin (@olegivo)
### 1.8.10 (Nov 26, 2020)
* Update translations
### 1.8.9 (Nov 18, 2020)
* Remove INTERNET permission
* Manage exceptions for when activities don't exist to handle intents (#181)
* Manage exceptions when activities don't exist to handle intents (#181)
* MemoryHabitList: Inherit parent's order (#598)
* Remove notification groups; revert to default system behavior
* Remove SyncManager and Internet permission

View File

@@ -1,3 +1,10 @@
<a href="https://github.com/iSoron/uhabits/actions?query=workflow%3A%22Build+%26+Test%22">
<img src="https://github.com/iSoron/uhabits/workflows/Build%20&%20Test/badge.svg" />
</a>
<a href="https://github.com/iSoron/uhabits/releases">
<img src="https://img.shields.io/github/v/release/iSoron/uhabits" />
</a>
# Loop Habit Tracker
Loop is a mobile app that helps you create and maintain good habits,

1
android/.gitignore vendored
View File

@@ -23,6 +23,7 @@ docs/
gen/
local.properties
crowdin.yaml
crowdin.yml
local
tmp/
secret/

View File

@@ -1,4 +1,5 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion COMPILE_SDK_VERSION as Integer
@@ -6,8 +7,8 @@ android {
defaultConfig {
minSdkVersion MIN_SDK_VERSION as Integer
targetSdkVersion TARGET_SDK_VERSION as Integer
versionCode VERSION_CODE as Integer
versionName "$VERSION_NAME"
buildConfigField 'int', 'VERSION_CODE', "$VERSION_CODE"
buildConfigField 'String', 'VERSION_NAME', "\"$VERSION_NAME\""
}
compileOptions {
@@ -28,4 +29,5 @@ dependencies {
implementation "org.apache.commons:commons-lang3:3.5"
annotationProcessor "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION"
}

View File

@@ -1,156 +0,0 @@
/*
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.androidbase;
import android.content.*;
import android.os.*;
import android.view.*;
import androidx.annotation.NonNull;
import java.io.*;
import java.text.*;
import java.util.*;
import javax.inject.*;
public class AndroidBugReporter
{
private final Context context;
@Inject
public AndroidBugReporter(@NonNull @AppContext Context context)
{
this.context = context;
}
/**
* Captures and returns a bug report. The bug report contains some device
* information and the logcat.
*
* @return a String containing the bug report.
* @throws IOException when any I/O error occur.
*/
@NonNull
public String getBugReport() throws IOException
{
String logcat = getLogcat();
String deviceInfo = getDeviceInfo();
String log = "---------- BUG REPORT BEGINS ----------\n";
log += deviceInfo + "\n" + logcat;
log += "---------- BUG REPORT ENDS ------------\n";
return log;
}
public String getDeviceInfo()
{
if (context == null) return "null context\n";
WindowManager wm =
(WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
return
String.format("App Version Name: %s\n", BuildConfig.VERSION_NAME) +
String.format("App Version Code: %s\n", BuildConfig.VERSION_CODE) +
String.format("OS Version: %s (%s)\n",
System.getProperty("os.version"), Build.VERSION.INCREMENTAL) +
String.format("OS API Level: %s\n", Build.VERSION.SDK) +
String.format("Device: %s\n", Build.DEVICE) +
String.format("Model (Product): %s (%s)\n", Build.MODEL,
Build.PRODUCT) +
String.format("Manufacturer: %s\n", Build.MANUFACTURER) +
String.format("Other tags: %s\n", Build.TAGS) +
String.format("Screen Width: %s\n",
wm.getDefaultDisplay().getWidth()) +
String.format("Screen Height: %s\n",
wm.getDefaultDisplay().getHeight()) +
String.format("External storage state: %s\n\n",
Environment.getExternalStorageState());
}
public String getLogcat() throws IOException
{
int maxLineCount = 250;
StringBuilder builder = new StringBuilder();
String[] command = new String[]{ "logcat", "-d" };
java.lang.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() > maxLineCount) log.removeFirst();
}
for (String l : log)
{
builder.append(l);
builder.append('\n');
}
return builder.toString();
}
/**
* Captures a bug report and saves it to a file in the SD card.
* <p>
* The contents of the file are generated by the method {@link
* #getBugReport()}. The file is saved in the apps's external private
* storage.
*
* @return the generated file.
* @throws IOException when I/O errors occur.
*/
@NonNull
public void dumpBugReportToFile()
{
try
{
String date =
new SimpleDateFormat("yyyy-MM-dd HHmmss", Locale.US).format(
new Date());
if (context == null) throw new IllegalStateException();
File dir = new AndroidDirFinder(context).getFilesDir("Logs");
if (dir == null)
throw new IOException("log dir should not be null");
File logFile =
new File(String.format("%s/Log %s.txt", dir.getPath(), date));
FileWriter output = new FileWriter(logFile);
output.write(getBugReport());
output.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,110 @@
/*
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.androidbase
import android.content.Context
import android.os.Build
import android.os.Environment
import android.view.WindowManager
import java.io.*
import java.text.SimpleDateFormat
import java.util.*
import javax.inject.Inject
open class AndroidBugReporter @Inject constructor(@AppContext private val context: Context) {
/**
* Captures and returns a bug report. The bug report contains some device
* information and the logcat.
*
* @return a String containing the bug report.
* @throws IOException when any I/O error occur.
*/
@Throws(IOException::class)
fun getBugReport(): String {
var log = "---------- BUG REPORT BEGINS ----------\n"
log += "${getLogcat()}\n"
log += "${getDeviceInfo()}\n"
log += "---------- BUG REPORT ENDS ------------\n"
return log
}
@Throws(IOException::class)
fun getLogcat(): String {
val maxLineCount = 250
val builder = StringBuilder()
val process = Runtime.getRuntime().exec(arrayOf("logcat", "-d"))
val inputReader = InputStreamReader(process.inputStream)
val bufferedReader = BufferedReader(inputReader)
val log = LinkedList<String>()
var line: String?
while (true) {
line = bufferedReader.readLine()
if (line == null) break;
log.addLast(line)
if (log.size > maxLineCount) log.removeFirst()
}
for (l in log) {
builder.appendln(l)
}
return builder.toString()
}
/**
* Captures a bug report and saves it to a file in the SD card.
*
* The contents of the file are generated by the method [ ][.getBugReport]. The file is saved
* in the apps's external private storage.
*
* @return the generated file.
* @throws IOException when I/O errors occur.
*/
fun dumpBugReportToFile() {
try {
val date = SimpleDateFormat("yyyy-MM-dd HHmmss", Locale.US).format(Date())
val dir = AndroidDirFinder(context).getFilesDir("Logs")
?: throw IOException("log dir should not be null")
val logFile = File(String.format("%s/Log %s.txt", dir.path, date))
val output = FileWriter(logFile)
output.write(getBugReport())
output.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
private fun getDeviceInfo(): String {
val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
return buildString {
appendln("App Version Name: ${BuildConfig.VERSION_NAME}")
appendln("App Version Code: ${BuildConfig.VERSION_CODE}")
appendln("OS Version: ${System.getProperty("os.version")} (${Build.VERSION.INCREMENTAL})")
appendln("OS API Level: ${Build.VERSION.SDK_INT}")
appendln("Device: ${Build.DEVICE}")
appendln("Model (Product): ${Build.MODEL} (${Build.PRODUCT})")
appendln("Manufacturer: ${Build.MANUFACTURER}")
appendln("Other tags: ${Build.TAGS}")
appendln("Screen Width: ${wm.defaultDisplay.width}")
appendln("Screen Height: ${wm.defaultDisplay.height}")
appendln("External storage state: ${Environment.getExternalStorageState()}")
appendln()
}
}
}

View File

@@ -1,60 +0,0 @@
/*
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.androidbase;
import android.content.*;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.*;
import android.util.*;
import org.isoron.androidbase.utils.*;
import java.io.*;
import javax.inject.*;
public class AndroidDirFinder
{
@NonNull
private Context context;
@Inject
public AndroidDirFinder(@NonNull @AppContext Context context)
{
this.context = context;
}
@Nullable
public File getFilesDir(@Nullable String relativePath)
{
File externalFilesDirs[] =
ContextCompat.getExternalFilesDirs(context, null);
if (externalFilesDirs == null)
{
Log.e("BaseSystem",
"getFilesDir: getExternalFilesDirs returned null");
return null;
}
return FileUtils.getDir(externalFilesDirs, relativePath);
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.androidbase
import android.content.Context
import androidx.core.content.ContextCompat
import org.isoron.androidbase.utils.FileUtils
import java.io.File
import javax.inject.Inject
class AndroidDirFinder @Inject constructor(@param:AppContext private val context: Context) {
fun getFilesDir(relativePath: String): File? {
return FileUtils.getDir(
ContextCompat.getExternalFilesDirs(context, null),
relativePath
)
}
}

View File

@@ -16,16 +16,11 @@
* 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.androidbase
package org.isoron.androidbase;
import java.lang.annotation.*;
import javax.inject.*;
import javax.inject.Qualifier
@Qualifier
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface AppContext
{
}
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
annotation class AppContext

View File

@@ -1,68 +0,0 @@
/*
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.androidbase;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.isoron.androidbase.activities.*;
public class BaseExceptionHandler implements Thread.UncaughtExceptionHandler
{
@Nullable
private Thread.UncaughtExceptionHandler originalHandler;
@NonNull
private BaseActivity activity;
public BaseExceptionHandler(@NonNull BaseActivity activity)
{
this.activity = activity;
originalHandler = Thread.getDefaultUncaughtExceptionHandler();
}
@Override
public void uncaughtException(@Nullable Thread thread,
@Nullable Throwable ex)
{
if (ex == null) return;
try
{
ex.printStackTrace();
new AndroidBugReporter(activity).dumpBugReportToFile();
}
catch (Exception e)
{
e.printStackTrace();
}
// if (ex.getCause() instanceof InconsistentDatabaseException)
// {
// HabitsApplication app = (HabitsApplication) activity.getApplication();
// HabitList habits = app.getComponent().getHabitList();
// habits.repair();
// System.exit(0);
// }
if (originalHandler != null)
originalHandler.uncaughtException(thread, ex);
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.androidbase
import org.isoron.androidbase.activities.BaseActivity
class BaseExceptionHandler(private val activity: BaseActivity) : Thread.UncaughtExceptionHandler {
private val originalHandler: Thread.UncaughtExceptionHandler? =
Thread.getDefaultUncaughtExceptionHandler()
override fun uncaughtException(thread: Thread?, ex: Throwable?) {
if (ex == null) return
if (thread == null) return
try {
ex.printStackTrace()
AndroidBugReporter(activity).dumpBugReportToFile()
} catch (e: Exception) {
e.printStackTrace()
}
originalHandler?.uncaughtException(thread, ex)
}
}

View File

@@ -1,70 +0,0 @@
/*
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.androidbase;
import android.content.*;
import androidx.annotation.NonNull;
import java.io.*;
import java.security.*;
import java.security.cert.Certificate;
import java.security.cert.*;
import javax.inject.*;
import javax.net.ssl.*;
public class SSLContextProvider
{
private Context context;
@Inject
public SSLContextProvider(@NonNull @AppContext Context context)
{
this.context = context;
}
public SSLContext getCACertSSLContext()
{
try
{
CertificateFactory cf = CertificateFactory.getInstance("X.509");
InputStream caInput = context.getAssets().open("cacert.pem");
Certificate ca = cf.generateCertificate(caInput);
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null, null);
ks.setCertificateEntry("ca", ca);
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ks);
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, tmf.getTrustManagers(), null);
return ctx;
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.androidbase
import android.content.Context
import java.security.KeyStore
import java.security.cert.CertificateFactory
import javax.inject.Inject
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManagerFactory
class SSLContextProvider @Inject constructor(@param:AppContext private val context: Context) {
fun getCACertSSLContext(): SSLContext {
try {
val cf = CertificateFactory.getInstance("X.509")
val ca = cf.generateCertificate(context.assets.open("cacert.pem"))
val ks = KeyStore.getInstance(KeyStore.getDefaultType()).apply {
load(null, null)
setCertificateEntry("ca", ca)
}
val alg = TrustManagerFactory.getDefaultAlgorithm()
val tmf = TrustManagerFactory.getInstance(alg).apply {
init(ks)
}
return SSLContext.getInstance("TLS").apply {
init(null, tmf.trustManagers, null)
}
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}

View File

@@ -16,16 +16,11 @@
* 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.androidbase.activities
package org.isoron.androidbase.activities;
import java.lang.annotation.*;
import javax.inject.*;
import javax.inject.*
@Qualifier
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface ActivityContext
{
}
@MustBeDocumented
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
annotation class ActivityContext

View File

@@ -16,13 +16,12 @@
* 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.androidbase.activities
package org.isoron.androidbase.activities;
import javax.inject.*;
import javax.inject.*
/**
* Scope used by objects that live as long as the activity is alive.
*/
@Scope
public @interface ActivityScope { }
annotation class ActivityScope

View File

@@ -1,143 +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.androidbase.activities;
import android.content.*;
import android.os.*;
import androidx.annotation.Nullable;
import androidx.appcompat.app.*;
import android.view.*;
import org.isoron.androidbase.*;
import static android.R.anim.fade_in;
import static android.R.anim.fade_out;
/**
* Base class for all activities in the application.
* <p>
* This class delegates the responsibilities of an Android activity to other
* classes. For example, callbacks related to menus are forwarded to a {@link
* BaseMenu}, while callbacks related to activity results are forwarded to a
* {@link BaseScreen}.
* <p>
* A BaseActivity also installs an {@link java.lang.Thread.UncaughtExceptionHandler}
* to the main thread. By default, this handler is an instance of
* BaseExceptionHandler, which logs the exception to the disk before the application
* crashes. To the default handler, you should override the method
* getExceptionHandler.
*/
abstract public class BaseActivity extends AppCompatActivity
{
@Nullable
private BaseMenu baseMenu;
@Nullable
private BaseScreen screen;
@Override
public boolean onCreateOptionsMenu(@Nullable Menu menu)
{
if (menu == null) return true;
if (baseMenu == null) return true;
baseMenu.onCreate(getMenuInflater(), menu);
return true;
}
@Override
public boolean onOptionsItemSelected(@Nullable MenuItem item)
{
if (item == null) return false;
if (baseMenu == null) return false;
return baseMenu.onItemSelected(item);
}
public void restartWithFade(Class<?> cls)
{
new Handler().postDelayed(() ->
{
finish();
overridePendingTransition(fade_in, fade_out);
startActivity(new Intent(this, cls));
}, 500); // HACK: Let the menu disappear first
}
public void setBaseMenu(@Nullable BaseMenu baseMenu)
{
this.baseMenu = baseMenu;
}
public void setScreen(@Nullable BaseScreen screen)
{
this.screen = screen;
}
public void showDialog(AppCompatDialogFragment dialog, String tag)
{
dialog.show(getSupportFragmentManager(), tag);
}
public void showDialog(AppCompatDialog dialog)
{
dialog.show();
}
@Override
protected void onActivityResult(int request, int result, Intent data)
{
if (screen == null) super.onActivityResult(request, result, data);
else screen.onResult(request, result, data);
}
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
Thread.setDefaultUncaughtExceptionHandler(getExceptionHandler());
}
protected Thread.UncaughtExceptionHandler getExceptionHandler()
{
return new BaseExceptionHandler(this);
}
@Override
protected void onResume()
{
super.onResume();
if(screen != null) screen.reattachDialogs();
}
@Override
public void startActivity(Intent intent)
{
try
{
super.startActivity(intent);
}
catch (ActivityNotFoundException e)
{
if (this.screen != null)
this.screen.showMessage(R.string.activity_not_found);
}
}
}

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.androidbase.activities
import android.R.anim
import android.content.*
import android.os.*
import android.view.*
import androidx.appcompat.app.*
import org.isoron.androidbase.*
/**
* Base class for all activities in the application.
*
* This class delegates the responsibilities of an Android activity to other classes. For example,
* callbacks related to menus are forwarded to a []BaseMenu], while callbacks related to activity
* results are forwarded to a [BaseScreen].
*
*
* A BaseActivity also installs an [java.lang.Thread.UncaughtExceptionHandler] to the main thread.
* By default, this handler is an instance of BaseExceptionHandler, which logs the exception to the
* disk before the application crashes. To the default handler, you should override the method
* getExceptionHandler.
*/
abstract class BaseActivity : AppCompatActivity() {
private var baseMenu: BaseMenu? = null
private var screen: BaseScreen? = null
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
if (menu != null) baseMenu?.onCreate(menuInflater, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
if (item == null) return false
return baseMenu?.onItemSelected(item) ?: false
}
fun restartWithFade(cls: Class<*>?) {
Handler().postDelayed({
finish()
overridePendingTransition(anim.fade_in, anim.fade_out)
startActivity(Intent(this, cls))
}, 500) // HACK: Let the menu disappear first
}
fun setBaseMenu(baseMenu: BaseMenu?) {
this.baseMenu = baseMenu
}
fun setScreen(screen: BaseScreen?) {
this.screen = screen
}
fun showDialog(dialog: AppCompatDialogFragment, tag: String?) {
dialog.show(supportFragmentManager, tag)
}
fun showDialog(dialog: AppCompatDialog) {
dialog.show()
}
override fun onActivityResult(request: Int, result: Int, data: Intent?) {
val screen = screen
if(screen == null) super.onActivityResult(request, result, data)
else screen.onResult(request, result, data)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Thread.setDefaultUncaughtExceptionHandler(getExceptionHandler())
}
private fun getExceptionHandler() = BaseExceptionHandler(this)
override fun onResume() {
super.onResume()
screen?.reattachDialogs()
}
override fun startActivity(intent: Intent?) {
try {
super.startActivity(intent)
} catch(e: ActivityNotFoundException) {
this.screen?.showMessage(R.string.activity_not_found)
}
}
}

View File

@@ -16,71 +16,50 @@
* 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.androidbase.activities
package org.isoron.androidbase.activities;
import android.view.*;
import androidx.annotation.MenuRes;
import androidx.annotation.NonNull;
import android.view.*
import androidx.annotation.*
/**
* Base class for all the menus in the application.
* <p>
*
* This class receives from BaseActivity all callbacks related to menus, such as
* menu creation and click events. It also handles some implementation details
* of creating menus in Android, such as inflating the resources.
*/
public abstract class BaseMenu
{
@NonNull
private final BaseActivity activity;
public BaseMenu(@NonNull BaseActivity activity)
{
this.activity = activity;
}
@NonNull
public BaseActivity getActivity()
{
return activity;
}
abstract class BaseMenu(private val activity: BaseActivity) {
/**
* Declare that the menu has changed, and should be recreated.
*/
public void invalidate()
{
activity.invalidateOptionsMenu();
fun invalidate() {
activity.invalidateOptionsMenu()
}
/**
* Called when the menu is first displayed.
* <p>
*
* The given menu is already inflated and ready to receive items. The
* application should override this method and add items to the menu here.
*
* @param menu the menu that is being created.
*/
public void onCreate(@NonNull Menu menu)
{
}
open fun onCreate(menu: Menu) {}
/**
* Called when the menu is first displayed.
* <p>
*
* This method should not be overridden. The application should override
* the methods onCreate(Menu) and getMenuResourceId instead.
*
* @param inflater a menu inflater, for creating the menu
* @param menu the menu that is being created.
*/
public void onCreate(@NonNull MenuInflater inflater, @NonNull Menu menu)
{
menu.clear();
inflater.inflate(getMenuResourceId(), menu);
onCreate(menu);
fun onCreate(inflater: MenuInflater, menu: Menu) {
menu.clear()
inflater.inflate(getMenuResourceId(), menu)
onCreate(menu)
}
/**
@@ -89,10 +68,7 @@ public abstract class BaseMenu
* @param item the item that was selected.
* @return true if the event was consumed, or false otherwise
*/
public boolean onItemSelected(@NonNull MenuItem item)
{
return false;
}
open fun onItemSelected(item: MenuItem): Boolean = false
/**
* Returns the id of the resource that should be used to inflate this menu.
@@ -100,5 +76,6 @@ public abstract class BaseMenu
* @return id of the menu resource.
*/
@MenuRes
protected abstract int getMenuResourceId();
}
protected abstract fun getMenuResourceId(): Int
}

View File

@@ -1,110 +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.androidbase.activities;
import android.content.*;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import android.view.*;
import android.widget.*;
import org.isoron.androidbase.*;
import org.isoron.androidbase.utils.*;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
/**
* Base class for all root views in the application.
* <p>
* A root view is an Android view that is directly attached to an activity. This
* view usually includes a toolbar and a progress bar. This abstract class hides
* some of the complexity of setting these things up, for every version of
* Android.
*/
public abstract class BaseRootView extends FrameLayout
{
@NonNull
private final Context context;
protected boolean shouldDisplayHomeAsUp = false;
@Nullable
private BaseScreen screen;
public BaseRootView(@NonNull Context context)
{
super(context);
this.context = context;
}
public boolean getDisplayHomeAsUp()
{
return shouldDisplayHomeAsUp;
}
public void setDisplayHomeAsUp(boolean b)
{
shouldDisplayHomeAsUp = b;
}
@NonNull
public Toolbar getToolbar()
{
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
if (toolbar == null) throw new RuntimeException(
"Your BaseRootView should have a " +
"toolbar with id R.id.toolbar");
return toolbar;
}
public int getToolbarColor()
{
StyledResources res = new StyledResources(context);
return res.getColor(R.attr.colorPrimary);
}
protected void initToolbar()
{
if (SDK_INT >= LOLLIPOP)
{
getToolbar().setElevation(InterfaceUtils.dpToPixels(context, 2));
View view = findViewById(R.id.toolbarShadow);
if (view != null) view.setVisibility(GONE);
view = findViewById(R.id.headerShadow);
if (view != null) view.setVisibility(GONE);
}
}
public void onAttachedToScreen(BaseScreen screen)
{
this.screen = screen;
}
@Nullable
public BaseScreen getScreen()
{
return screen;
}
}

View File

@@ -0,0 +1,63 @@
/*
* 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.androidbase.activities
import android.content.Context
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.view.View
import android.widget.FrameLayout
import androidx.appcompat.widget.Toolbar
import org.isoron.androidbase.R
import org.isoron.androidbase.utils.InterfaceUtils.dpToPixels
import org.isoron.androidbase.utils.StyledResources
/**
* Base class for all root views in the application.
*
*
* A root view is an Android view that is directly attached to an activity. This
* view usually includes a toolbar and a progress bar. This abstract class hides
* some of the complexity of setting these things up, for every version of
* Android.
*/
abstract class BaseRootView(context: Context) : FrameLayout(context) {
var displayHomeAsUp = false
var screen: BaseScreen? = null
private set
open fun getToolbar(): Toolbar {
return findViewById(R.id.toolbar)
?: throw RuntimeException("Your BaseRootView should have a toolbar with id R.id.toolbar")
}
open fun getToolbarColor(): Int = StyledResources(context).getColor(R.attr.colorPrimary)
protected open fun initToolbar() {
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
getToolbar().elevation = dpToPixels(context, 2f)
findViewById<View>(R.id.toolbarShadow)?.visibility = View.GONE
findViewById<View>(R.id.headerShadow)?.visibility = View.GONE
}
}
fun onAttachedToScreen(screen: BaseScreen?) {
this.screen = screen
}
}

View File

@@ -1,318 +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.androidbase.activities;
import android.content.*;
import android.graphics.*;
import android.graphics.drawable.*;
import android.net.*;
import android.os.*;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.content.res.*;
import androidx.appcompat.app.*;
import androidx.appcompat.view.ActionMode;
import androidx.appcompat.widget.Toolbar;
import android.view.*;
import android.widget.*;
import com.google.android.material.snackbar.Snackbar;
import org.isoron.androidbase.*;
import org.isoron.androidbase.utils.*;
import java.io.*;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static androidx.core.content.FileProvider.getUriForFile;
/**
* Base class for all screens in the application.
* <p>
* Screens are responsible for deciding what root views and what menus should be
* attached to the main window. They are also responsible for showing other
* screens and for receiving their results.
*/
public class BaseScreen
{
protected BaseActivity activity;
@Nullable
private BaseRootView rootView;
@Nullable
private BaseSelectionMenu selectionMenu;
protected Snackbar snackbar;
public BaseScreen(@NonNull BaseActivity activity)
{
this.activity = activity;
}
@Deprecated
public static int getDefaultActionBarColor(Context context)
{
if (SDK_INT < LOLLIPOP)
{
return ResourcesCompat.getColor(context.getResources(),
R.color.grey_900, context.getTheme());
}
else
{
StyledResources res = new StyledResources(context);
return res.getColor(R.attr.colorPrimary);
}
}
@Deprecated
public static void setupActionBarColor(@NonNull AppCompatActivity activity,
int color)
{
Toolbar toolbar = (Toolbar) activity.findViewById(R.id.toolbar);
if (toolbar == null) return;
activity.setSupportActionBar(toolbar);
ActionBar actionBar = activity.getSupportActionBar();
if (actionBar == null) return;
actionBar.setDisplayHomeAsUpEnabled(true);
ColorDrawable drawable = new ColorDrawable(color);
actionBar.setBackgroundDrawable(drawable);
if (SDK_INT >= LOLLIPOP)
{
int darkerColor = ColorUtils.mixColors(color, Color.BLACK, 0.75f);
activity.getWindow().setStatusBarColor(darkerColor);
toolbar.setElevation(InterfaceUtils.dpToPixels(activity, 2));
View view = activity.findViewById(R.id.toolbarShadow);
if (view != null) view.setVisibility(View.GONE);
view = activity.findViewById(R.id.headerShadow);
if (view != null) view.setVisibility(View.GONE);
}
}
/**
* Notifies the screen that its contents should be updated.
*/
public void invalidate()
{
if (rootView == null) return;
rootView.invalidate();
}
public void invalidateToolbar()
{
if (rootView == null) return;
activity.runOnUiThread(() ->
{
Toolbar toolbar = rootView.getToolbar();
activity.setSupportActionBar(toolbar);
ActionBar actionBar = activity.getSupportActionBar();
if (actionBar == null) return;
actionBar.setDisplayHomeAsUpEnabled(rootView.getDisplayHomeAsUp());
int color = rootView.getToolbarColor();
setActionBarColor(actionBar, color);
setStatusBarColor(color);
});
}
/**
* Called when another Activity has finished, and has returned some result.
*
* @param requestCode the request code originally supplied to {@link
* android.app.Activity#startActivityForResult(Intent,
* int, Bundle)}.
* @param resultCode the result code sent by the other activity.
* @param data an Intent containing extra data sent by the other
* activity.
* @see {@link android.app.Activity#onActivityResult(int, int, Intent)}
*/
public void onResult(int requestCode, int resultCode, Intent data)
{
}
/**
* Called after activity has been recreated, and the dialogs should be
* reattached to their controllers.
*/
public void reattachDialogs()
{
}
/**
* Sets the menu to be shown by this screen.
* <p>
* This menu will be visible if when there is no active selection operation.
* If the provided menu is null, then no menu will be shown.
*
* @param menu the menu to be shown.
*/
public void setMenu(@Nullable BaseMenu menu)
{
activity.setBaseMenu(menu);
}
/**
* Sets the root view for this screen.
*
* @param rootView the root view for this screen.
*/
public void setRootView(@Nullable BaseRootView rootView)
{
this.rootView = rootView;
activity.setContentView(rootView);
if (rootView == null) return;
rootView.onAttachedToScreen(this);
invalidateToolbar();
}
/**
* Sets the menu to be shown when a selection is active on the screen.
*
* @param menu the menu to be shown during a selection
*/
public void setSelectionMenu(@Nullable BaseSelectionMenu menu)
{
this.selectionMenu = menu;
}
/**
* Shows a message on the screen.
*
* @param stringId the string resource id for this message.
*/
public void showMessage(@StringRes Integer stringId)
{
if (stringId == null || rootView == null) return;
if (snackbar == null)
{
snackbar = Snackbar.make(rootView, stringId, Snackbar.LENGTH_SHORT);
int tvId = R.id.snackbar_text;
TextView tv = (TextView) snackbar.getView().findViewById(tvId);
tv.setTextColor(Color.WHITE);
}
else snackbar.setText(stringId);
snackbar.show();
}
public void showSendEmailScreen(@StringRes int toId,
@StringRes int subjectId,
String content)
{
String to = activity.getString(toId);
String subject = activity.getString(subjectId);
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.setType("message/rfc822");
intent.putExtra(Intent.EXTRA_EMAIL, new String[]{ to });
intent.putExtra(Intent.EXTRA_SUBJECT, subject);
intent.putExtra(Intent.EXTRA_TEXT, content);
activity.startActivity(intent);
}
public void showSendFileScreen(@NonNull String archiveFilename)
{
File file = new File(archiveFilename);
Uri fileUri = getUriForFile(activity, "org.isoron.uhabits", file);
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.setType("application/zip");
intent.putExtra(Intent.EXTRA_STREAM, fileUri);
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
activity.startActivity(intent);
}
/**
* Instructs the screen to start a selection.
* <p>
* If a selection menu was provided, this menu will be shown instead of the
* regular one.
*/
public void startSelection()
{
activity.startSupportActionMode(new ActionModeWrapper());
}
private void setActionBarColor(@NonNull ActionBar actionBar, int color)
{
ColorDrawable drawable = new ColorDrawable(color);
actionBar.setBackgroundDrawable(drawable);
}
private void setStatusBarColor(int baseColor)
{
if (SDK_INT < LOLLIPOP) return;
int darkerColor = ColorUtils.mixColors(baseColor, Color.BLACK, 0.75f);
activity.getWindow().setStatusBarColor(darkerColor);
}
private class ActionModeWrapper implements ActionMode.Callback
{
@Override
public boolean onActionItemClicked(@Nullable ActionMode mode,
@Nullable MenuItem item)
{
if (item == null || selectionMenu == null) return false;
return selectionMenu.onItemClicked(item);
}
@Override
public boolean onCreateActionMode(@Nullable ActionMode mode,
@Nullable Menu menu)
{
if (selectionMenu == null) return false;
if (mode == null || menu == null) return false;
selectionMenu.onCreate(activity.getMenuInflater(), mode, menu);
return true;
}
@Override
public void onDestroyActionMode(@Nullable ActionMode mode)
{
if (selectionMenu == null) return;
selectionMenu.onFinish();
}
@Override
public boolean onPrepareActionMode(@Nullable ActionMode mode,
@Nullable Menu menu)
{
if (selectionMenu == null || menu == null) return false;
return selectionMenu.onPrepare(menu);
}
}
}

View File

@@ -0,0 +1,237 @@
/*
* 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.androidbase.activities
import android.content.*
import android.graphics.*
import android.graphics.drawable.*
import android.view.*
import android.widget.*
import androidx.annotation.*
import androidx.appcompat.app.*
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.Toolbar
import androidx.core.content.*
import com.google.android.material.snackbar.*
import org.isoron.androidbase.*
import org.isoron.androidbase.utils.*
import org.isoron.androidbase.utils.ColorUtils.mixColors
import org.isoron.androidbase.utils.InterfaceUtils.dpToPixels
import java.io.*
/**
* Base class for all screens in the application.
*
* Screens are responsible for deciding what root views and what menus should be attached to the
* main window. They are also responsible for showing other screens and for receiving their results.
*/
open class BaseScreen(@JvmField protected var activity: BaseActivity) {
private var rootView: BaseRootView? = null
private var selectionMenu: BaseSelectionMenu? = null
private var snackbar: Snackbar? = null
/**
* Notifies the screen that its contents should be updated.
*/
fun invalidate() {
rootView?.invalidate()
}
fun invalidateToolbar() {
rootView?.let { root ->
activity.runOnUiThread {
val toolbar = root.getToolbar()
activity.setSupportActionBar(toolbar)
activity.supportActionBar?.let { actionBar ->
actionBar.setDisplayHomeAsUpEnabled(root.displayHomeAsUp)
val color = root.getToolbarColor()
setActionBarColor(actionBar, color)
setStatusBarColor(color)
}
}
}
}
/**
* Called when another Activity has finished, and has returned some result.
*
* @param requestCode the request code originally supplied to startActivityForResult.
* @param resultCode the result code sent by the other activity.
* @param data an Intent containing extra data sent by the other
* activity.
* @see {@link android.app.Activity.onActivityResult
*/
open fun onResult(requestCode: Int, resultCode: Int, data: Intent?) {}
/**
* Called after activity has been recreated, and the dialogs should be
* reattached to their controllers.
*/
open fun reattachDialogs() {}
/**
* Sets the menu to be shown by this screen.
*
*
* This menu will be visible if when there is no active selection operation.
* If the provided menu is null, then no menu will be shown.
*
* @param menu the menu to be shown.
*/
fun setMenu(menu: BaseMenu?) {
activity.setBaseMenu(menu)
}
/**
* Sets the root view for this screen.
*
* @param rootView the root view for this screen.
*/
fun setRootView(rootView: BaseRootView?) {
this.rootView = rootView
activity.setContentView(rootView)
rootView?.let {
it.onAttachedToScreen(this)
invalidateToolbar()
}
}
/**
* Sets the menu to be shown when a selection is active on the screen.
*
* @param menu the menu to be shown during a selection
*/
fun setSelectionMenu(menu: BaseSelectionMenu?) {
selectionMenu = menu
}
/**
* Shows a message on the screen.
*
* @param stringId the string resource id for this message.
*/
fun showMessage(@StringRes stringId: Int?, rootView: View?) {
var snackbar = this.snackbar
if (stringId == null || rootView == null) return
if (snackbar == null) {
snackbar = Snackbar.make(rootView, stringId, Snackbar.LENGTH_SHORT)
val tvId = R.id.snackbar_text
val tv = snackbar.view.findViewById<TextView>(tvId)
tv.setTextColor(Color.WHITE)
this.snackbar = snackbar
}
snackbar.setText(stringId)
snackbar.show()
}
fun showMessage(@StringRes stringId: Int?) {
showMessage(stringId, this.rootView)
}
fun showSendEmailScreen(@StringRes toId: Int, @StringRes subjectId: Int, content: String?) {
val to = activity.getString(toId)
val subject = activity.getString(subjectId)
activity.startActivity(Intent().apply {
action = Intent.ACTION_SEND
type = "message/rfc822"
putExtra(Intent.EXTRA_EMAIL, arrayOf(to))
putExtra(Intent.EXTRA_SUBJECT, subject)
putExtra(Intent.EXTRA_TEXT, content)
})
}
fun showSendFileScreen(archiveFilename: String) {
val file = File(archiveFilename)
val fileUri = FileProvider.getUriForFile(activity, "org.isoron.uhabits", file)
activity.startActivity(Intent().apply {
action = Intent.ACTION_SEND
type = "application/zip"
putExtra(Intent.EXTRA_STREAM, fileUri)
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
})
}
/**
* Instructs the screen to start a selection.
*
* If a selection menu was provided, this menu will be shown instead of the regular one.
*/
fun startSelection() {
activity.startSupportActionMode(ActionModeWrapper())
}
private fun setActionBarColor(actionBar: ActionBar, color: Int) {
val drawable = ColorDrawable(color)
actionBar.setBackgroundDrawable(drawable)
}
private fun setStatusBarColor(baseColor: Int) {
val darkerColor = mixColors(baseColor, Color.BLACK, 0.75f)
activity.window.statusBarColor = darkerColor
}
private inner class ActionModeWrapper : ActionMode.Callback {
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
val selectionMenu = selectionMenu
if (item == null || selectionMenu == null) return false
return selectionMenu.onItemClicked(item)
}
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
if (mode == null || menu == null) return false
val selectionMenu = selectionMenu ?: return false
selectionMenu.onCreate(activity.menuInflater, mode, menu)
return true
}
override fun onDestroyActionMode(mode: ActionMode?) {
selectionMenu?.onFinish()
}
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
val selectionMenu = selectionMenu
if (selectionMenu == null || menu == null) return false
return selectionMenu.onPrepare(menu)
}
}
companion object {
@JvmStatic
@Deprecated("")
fun getDefaultActionBarColor(context: Context) =
StyledResources(context).getColor(R.attr.colorPrimary)
@JvmStatic
@Deprecated("")
fun setupActionBarColor(activity: AppCompatActivity, color: Int) {
val toolbar = activity.findViewById<Toolbar>(R.id.toolbar) ?: return
activity.setSupportActionBar(toolbar)
val supportActionBar = activity.supportActionBar ?: return
supportActionBar.setDisplayHomeAsUpEnabled(true)
val drawable = ColorDrawable(color)
supportActionBar.setBackgroundDrawable(drawable)
val darkerColor = mixColors(color, Color.BLACK, 0.75f)
activity.window.statusBarColor = darkerColor
toolbar.elevation = dpToPixels(activity, 2f)
activity.findViewById<View>(R.id.toolbarShadow)?.visibility = View.GONE
activity.findViewById<View>(R.id.headerShadow)?.visibility = View.GONE
}
}
}

View File

@@ -16,50 +16,43 @@
* 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.androidbase.activities
package org.isoron.androidbase.activities;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.view.ActionMode;
import android.view.*;
import android.view.*
import androidx.appcompat.view.ActionMode
/**
* Base class for all the selection menus in the application.
* <p>
*
* A selection menu is a menu that appears when the screen starts a selection
* operation. It contains actions that modify the selected items, such as delete
* or archive. Since it replaces the toolbar, it also has a title.
* <p>
*
* This class hides many implementation details of creating such menus in
* Android. The interface is supposed to look very similar to {@link BaseMenu},
* Android. The interface is supposed to look very similar to [BaseMenu],
* with a few additional methods, such as finishing the selection operation.
* Internally, it uses an {@link ActionMode}.
* Internally, it uses an [ActionMode].
*/
public abstract class BaseSelectionMenu
{
@Nullable
private ActionMode actionMode;
abstract class BaseSelectionMenu {
private var actionMode: ActionMode? = null
/**
* Finishes the selection operation.
*/
public void finish()
{
if (actionMode != null) actionMode.finish();
fun finish() {
actionMode?.finish()
}
/**
* Declare that the menu has changed, and should be recreated.
*/
public void invalidate()
{
if (actionMode != null) actionMode.invalidate();
fun invalidate() {
actionMode?.invalidate()
}
/**
* Called when the menu is first displayed.
* <p>
*
* This method should not be overridden. The application should override
* the methods onCreate(Menu) and getMenuResourceId instead.
*
@@ -67,22 +60,16 @@ public abstract class BaseSelectionMenu
* @param mode the action mode associated with this menu.
* @param menu the menu that is being created.
*/
public void onCreate(@NonNull MenuInflater inflater,
@NonNull ActionMode mode,
@NonNull Menu menu)
{
this.actionMode = mode;
inflater.inflate(getResourceId(), menu);
onCreate(menu);
fun onCreate(inflater: MenuInflater, mode: ActionMode, menu: Menu) {
actionMode = mode
inflater.inflate(getResourceId(), menu)
onCreate(menu)
}
/**
* Called when the selection operation is about to finish.
*/
public void onFinish()
{
}
open fun onFinish() {}
/**
* Called whenever an item on the menu is selected.
@@ -90,11 +77,7 @@ public abstract class BaseSelectionMenu
* @param item the item that was selected.
* @return true if the event was consumed, or false otherwise
*/
public boolean onItemClicked(@NonNull MenuItem item)
{
return false;
}
open fun onItemClicked(item: MenuItem): Boolean = false
/**
* Called whenever the menu is invalidated.
@@ -102,29 +85,23 @@ public abstract class BaseSelectionMenu
* @param menu the menu to be refreshed
* @return true if the menu has changes, false otherwise
*/
public boolean onPrepare(@NonNull Menu menu)
{
return false;
}
open fun onPrepare(menu: Menu): Boolean = false
/**
* Sets the title of the selection menu.
*
* @param title the new title.
*/
public void setTitle(String title)
{
if (actionMode != null) actionMode.setTitle(title);
fun setTitle(title: String?) {
actionMode?.title = title
}
protected abstract int getResourceId();
protected abstract fun getResourceId(): Int
/**
* Called when the menu is first created.
*
* @param menu the menu being created
*/
protected void onCreate(@NonNull Menu menu)
{
}
}
protected fun onCreate(menu: Menu) {}
}

View File

@@ -1,66 +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.androidbase.utils;
import android.graphics.*;
public abstract class ColorUtils
{
public static int mixColors(int color1, int color2, float amount)
{
final byte ALPHA_CHANNEL = 24;
final byte RED_CHANNEL = 16;
final byte GREEN_CHANNEL = 8;
final byte BLUE_CHANNEL = 0;
final float inverseAmount = 1.0f - amount;
int a = ((int) (((float) (color1 >> ALPHA_CHANNEL & 0xff) * amount) +
((float) (color2 >> ALPHA_CHANNEL & 0xff) *
inverseAmount))) & 0xff;
int r = ((int) (((float) (color1 >> RED_CHANNEL & 0xff) * amount) +
((float) (color2 >> RED_CHANNEL & 0xff) *
inverseAmount))) & 0xff;
int g = ((int) (((float) (color1 >> GREEN_CHANNEL & 0xff) * amount) +
((float) (color2 >> GREEN_CHANNEL & 0xff) *
inverseAmount))) & 0xff;
int b = ((int) (((float) (color1 & 0xff) * amount) +
((float) (color2 & 0xff) * inverseAmount))) & 0xff;
return a << ALPHA_CHANNEL | r << RED_CHANNEL | g << GREEN_CHANNEL |
b << BLUE_CHANNEL;
}
public static int setAlpha(int color, float newAlpha)
{
int intAlpha = (int) (newAlpha * 255);
return Color.argb(intAlpha, Color.red(color), Color.green(color),
Color.blue(color));
}
public static int setMinValue(int color, float newValue)
{
float hsv[] = new float[3];
Color.colorToHSV(color, hsv);
hsv[2] = Math.max(hsv[2], newValue);
return Color.HSVToColor(hsv);
}
}

View File

@@ -0,0 +1,58 @@
/*
* 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.androidbase.utils
import android.graphics.Color
import kotlin.math.max
object ColorUtils {
private const val ALPHA_CHANNEL = 24
private const val RED_CHANNEL = 16
private const val GREEN_CHANNEL = 8
private const val BLUE_CHANNEL = 0
@JvmStatic
fun mixColors(color1: Int, color2: Int, amount: Float): Int {
val a = mixColorChannel(color1, color2, amount, ALPHA_CHANNEL)
val r = mixColorChannel(color1, color2, amount, RED_CHANNEL)
val g = mixColorChannel(color1, color2, amount, GREEN_CHANNEL)
val b = mixColorChannel(color1, color2, amount, BLUE_CHANNEL)
return a or r or g or b
}
@JvmStatic
fun setAlpha(color: Int, newAlpha: Float): Int {
val intAlpha = (newAlpha * 255).toInt()
return Color.argb(intAlpha, Color.red(color), Color.green(color), Color.blue(color))
}
@JvmStatic
fun setMinValue(color: Int, newValue: Float): Int {
val hsv = FloatArray(3)
Color.colorToHSV(color, hsv)
hsv[2] = max(hsv[2], newValue)
return Color.HSVToColor(hsv)
}
private fun mixColorChannel(color1: Int, color2: Int, amount: Float, channel: Int): Int {
val fl = (color1 shr channel and 0xff).toFloat() * amount
val f2 = (color2 shr channel and 0xff).toFloat() * (1.0f - amount)
return (fl + f2).toInt() and 0xff shl channel
}
}

View File

@@ -1,94 +0,0 @@
/*
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.androidbase.utils;
import android.os.*;
import android.util.*;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.*;
public abstract class FileUtils
{
public static void copy(File src, File dst) throws IOException
{
FileInputStream inStream = new FileInputStream(src);
FileOutputStream outStream = new FileOutputStream(dst);
copy(inStream, outStream);
}
public static void copy(InputStream inStream, File dst) throws IOException
{
FileOutputStream outStream = new FileOutputStream(dst);
copy(inStream, outStream);
}
public static void copy(InputStream in, OutputStream out) throws IOException
{
int numBytes;
byte[] buffer = new byte[1024];
while ((numBytes = in.read(buffer)) != -1)
out.write(buffer, 0, numBytes);
}
@Nullable
public static File getDir(@NonNull File potentialParentDirs[],
@Nullable String relativePath)
{
if (relativePath == null) relativePath = "";
File chosenDir = null;
for (File dir : potentialParentDirs)
{
if (dir == null || !dir.canWrite()) continue;
chosenDir = dir;
break;
}
if (chosenDir == null)
{
Log.e("FileUtils",
"getDir: all potential parents are null or non-writable");
return null;
}
File dir = new File(
String.format("%s/%s/", chosenDir.getAbsolutePath(), relativePath));
if (!dir.exists() && !dir.mkdirs())
{
Log.e("FileUtils",
"getDir: chosen dir does not exist and cannot be created");
return null;
}
return dir;
}
@Nullable
public static File getSDCardDir(@Nullable String relativePath)
{
File parents[] =
new File[]{ Environment.getExternalStorageDirectory() };
return getDir(parents, relativePath);
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.androidbase.utils
import android.os.Environment
import android.util.Log
import java.io.*
fun File.copyTo(dst: File) {
val inStream = FileInputStream(this)
val outStream = FileOutputStream(dst)
inStream.copyTo(outStream)
}
fun InputStream.copyTo(dst: File) {
val outStream = FileOutputStream(dst)
this.copyTo(outStream)
}
fun InputStream.copyTo(out: OutputStream) {
var numBytes: Int
val buffer = ByteArray(1024)
while (this.read(buffer).also { numBytes = it } != -1) {
out.write(buffer, 0, numBytes)
}
}
object FileUtils {
@JvmStatic
fun getDir(potentialParentDirs: Array<File>, relativePath: String): File? {
val chosenDir: File? = potentialParentDirs.firstOrNull { dir -> dir.canWrite() }
if (chosenDir == null) {
Log.e("FileUtils", "getDir: all potential parents are null or non-writable")
return null
}
val dir = File("${chosenDir.absolutePath}/${relativePath}/")
if (!dir.exists() && !dir.mkdirs()) {
Log.e("FileUtils", "getDir: chosen dir does not exist and cannot be created")
return null
}
return dir
}
@JvmStatic
fun getSDCardDir(relativePath: String): File? {
val parents = arrayOf(Environment.getExternalStorageDirectory())
return getDir(parents, relativePath)
}
}

View File

@@ -1,104 +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.androidbase.utils;
import android.content.*;
import android.content.res.*;
import android.graphics.*;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.*;
import android.util.*;
import android.view.*;
import android.widget.*;
public abstract class InterfaceUtils
{
private static Typeface fontAwesome;
@Nullable
private static Float fixedResolution = null;
public static void setFixedResolution(@NonNull Float f)
{
fixedResolution = f;
}
public static Typeface getFontAwesome(Context context)
{
if(fontAwesome == null) fontAwesome =
Typeface.createFromAsset(context.getAssets(),
"fontawesome-webfont.ttf");
return fontAwesome;
}
public static float dpToPixels(Context context, float dp)
{
if(fixedResolution != null) return dp * fixedResolution;
Resources resources = context.getResources();
DisplayMetrics metrics = resources.getDisplayMetrics();
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, metrics);
}
public static float spToPixels(Context context, float sp)
{
if(fixedResolution != null) return sp * fixedResolution;
Resources resources = context.getResources();
DisplayMetrics metrics = resources.getDisplayMetrics();
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, metrics);
}
public static float getDimension(Context context, int id)
{
float dim = context.getResources().getDimension(id);
if (fixedResolution == null) return dim;
else
{
DisplayMetrics dm = context.getResources().getDisplayMetrics();
float actualDensity = dm.density;
return dim / actualDensity * fixedResolution;
}
}
public static void setupEditorAction(@NonNull ViewGroup parent,
@NonNull TextView.OnEditorActionListener listener)
{
for (int i = 0; i < parent.getChildCount(); i++)
{
View child = parent.getChildAt(i);
if (child instanceof ViewGroup)
setupEditorAction((ViewGroup) child, listener);
if (child instanceof TextView)
((TextView) child).setOnEditorActionListener(listener);
}
}
public static boolean isLayoutRtl(View view)
{
return ViewCompat.getLayoutDirection(view) ==
ViewCompat.LAYOUT_DIRECTION_RTL;
}
}

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.androidbase.utils
import android.content.*
import android.graphics.*
import android.util.*
import android.view.*
import android.widget.*
import android.widget.TextView.*
import androidx.core.view.*
object InterfaceUtils {
private var fontAwesome: Typeface? = null
private var fixedResolution: Float? = null
@JvmStatic
fun setFixedResolution(f: Float) {
fixedResolution = f
}
@JvmStatic
fun getFontAwesome(context: Context): Typeface? {
if (fontAwesome == null) {
fontAwesome = Typeface.createFromAsset(context.assets, "fontawesome-webfont.ttf")
}
return fontAwesome
}
@JvmStatic
fun dpToPixels(context: Context, dp: Float): Float {
if (fixedResolution != null) return dp * fixedResolution!!
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
dp,
context.resources.displayMetrics)
}
@JvmStatic
fun spToPixels(context: Context, sp: Float): Float {
if (fixedResolution != null) return sp * fixedResolution!!
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
sp,
context.resources.displayMetrics)
}
@JvmStatic
fun getDimension(context: Context, id: Int): Float {
val dim = context.resources.getDimension(id)
if (fixedResolution != null) {
val actualDensity = context.resources.displayMetrics.density
return dim / actualDensity * fixedResolution!!
}
return dim
}
fun setupEditorAction(parent: ViewGroup,
listener: OnEditorActionListener) {
for (i in 0 until parent.childCount) {
val child = parent.getChildAt(i)
if (child is ViewGroup) setupEditorAction(child, listener)
if (child is TextView) child.setOnEditorActionListener(listener)
}
}
fun isLayoutRtl(view: View?): Boolean {
return ViewCompat.getLayoutDirection(view!!) ==
ViewCompat.LAYOUT_DIRECTION_RTL
}
}

View File

@@ -1,118 +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.androidbase.utils;
import android.content.*;
import android.content.res.*;
import android.graphics.drawable.*;
import androidx.annotation.AttrRes;
import androidx.annotation.NonNull;
import org.isoron.androidbase.*;
public class StyledResources
{
private static Integer fixedTheme;
private final Context context;
public StyledResources(@NonNull Context context)
{
this.context = context;
}
public static void setFixedTheme(Integer theme)
{
fixedTheme = theme;
}
public boolean getBoolean(@AttrRes int attrId)
{
TypedArray ta = getTypedArray(attrId);
boolean bool = ta.getBoolean(0, false);
ta.recycle();
return bool;
}
public int getDimension(@AttrRes int attrId)
{
TypedArray ta = getTypedArray(attrId);
int dim = ta.getDimensionPixelSize(0, 0);
ta.recycle();
return dim;
}
public int getColor(@AttrRes int attrId)
{
TypedArray ta = getTypedArray(attrId);
int color = ta.getColor(0, 0);
ta.recycle();
return color;
}
public Drawable getDrawable(@AttrRes int attrId)
{
TypedArray ta = getTypedArray(attrId);
Drawable drawable = ta.getDrawable(0);
ta.recycle();
return drawable;
}
public float getFloat(@AttrRes int attrId)
{
TypedArray ta = getTypedArray(attrId);
float f = ta.getFloat(0, 0);
ta.recycle();
return f;
}
public int[] getPalette()
{
int resourceId = getResource(R.attr.palette);
if (resourceId < 0) throw new RuntimeException("resource not found");
return context.getResources().getIntArray(resourceId);
}
public int getResource(@AttrRes int attrId)
{
TypedArray ta = getTypedArray(attrId);
int resourceId = ta.getResourceId(0, -1);
ta.recycle();
return resourceId;
}
private TypedArray getTypedArray(@AttrRes int attrId)
{
int[] attrs = new int[]{ attrId };
if (fixedTheme != null)
return context.getTheme().obtainStyledAttributes(fixedTheme, attrs);
return context.obtainStyledAttributes(attrs);
}
}

View File

@@ -0,0 +1,93 @@
/*
* 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.androidbase.utils
import android.content.Context
import android.content.res.TypedArray
import android.graphics.drawable.Drawable
import androidx.annotation.AttrRes
import org.isoron.androidbase.R
class StyledResources(private val context: Context) {
fun getBoolean(@AttrRes attrId: Int): Boolean {
val ta = getTypedArray(attrId)
val bool = ta.getBoolean(0, false)
ta.recycle()
return bool
}
fun getDimension(@AttrRes attrId: Int): Int {
val ta = getTypedArray(attrId)
val dim = ta.getDimensionPixelSize(0, 0)
ta.recycle()
return dim
}
fun getColor(@AttrRes attrId: Int): Int {
val ta = getTypedArray(attrId)
val color = ta.getColor(0, 0)
ta.recycle()
return color
}
fun getDrawable(@AttrRes attrId: Int): Drawable? {
val ta = getTypedArray(attrId)
val drawable = ta.getDrawable(0)
ta.recycle()
return drawable
}
fun getFloat(@AttrRes attrId: Int): Float {
val ta = getTypedArray(attrId)
val f = ta.getFloat(0, 0f)
ta.recycle()
return f
}
fun getPalette(): IntArray {
val resourceId = getResource(R.attr.palette)
if (resourceId < 0) throw RuntimeException("palette resource not found")
return context.resources.getIntArray(resourceId)
}
fun getResource(@AttrRes attrId: Int): Int {
val ta = getTypedArray(attrId)
val resourceId = ta.getResourceId(0, -1)
ta.recycle()
return resourceId
}
private fun getTypedArray(@AttrRes attrId: Int): TypedArray {
val attrs = intArrayOf(attrId)
if (fixedTheme != null) {
return context.theme.obtainStyledAttributes(fixedTheme!!, attrs)
}
return context.obtainStyledAttributes(attrs)
}
companion object {
private var fixedTheme: Int? = null
@JvmStatic
fun setFixedTheme(theme: Int?) {
fixedTheme = theme
}
}
}

View File

@@ -41,8 +41,8 @@ public class AmPmCirclesView extends View {
private final Paint mPaint = new Paint();
private int mSelectedAlpha;
private int mUnselectedColor;
private int mAmPmTextColor;
private int mSelectedColor;
protected int mAmPmTextColor = Color.WHITE;
protected int mSelectedColor = Color.BLUE;
private float mCircleRadiusMultiplier;
private float mAmPmCircleRadiusMultiplier;
private String mAmText;
@@ -73,8 +73,8 @@ public class AmPmCirclesView extends View {
Resources res = context.getResources();
mUnselectedColor = res.getColor(R.color.white);
mSelectedColor = res.getColor(R.color.blue);
mAmPmTextColor = res.getColor(R.color.ampm_text_color);
//mSelectedColor = res.getColor(R.color.blue);
//mAmPmTextColor = res.getColor(R.color.ampm_text_color);
mSelectedAlpha = SELECTED_ALPHA;
String typefaceFamily = res.getString(R.string.sans_serif);
Typeface tf = Typeface.create(typefaceFamily, Typeface.NORMAL);
@@ -105,8 +105,8 @@ public class AmPmCirclesView extends View {
mSelectedAlpha = SELECTED_ALPHA_THEME_DARK;
} else {
mUnselectedColor = res.getColor(R.color.white);
mSelectedColor = res.getColor(R.color.blue);
mAmPmTextColor = res.getColor(R.color.ampm_text_color);
//mSelectedColor = res.getColor(R.color.blue);
//mAmPmTextColor = res.getColor(R.color.ampm_text_color);
mSelectedAlpha = SELECTED_ALPHA;
}
}

View File

@@ -84,6 +84,14 @@ public class RadialPickerLayout extends FrameLayout implements OnTouchListener {
private AnimatorSet mTransition;
private Handler mHandler = new Handler();
public void setColor(int selectedColor)
{
mHourRadialSelectorView.mPaint.setColor(selectedColor);
mMinuteRadialSelectorView.mPaint.setColor(selectedColor);
mAmPmCirclesView.mSelectedColor = selectedColor;
mAmPmCirclesView.mAmPmTextColor = selectedColor;
}
public interface OnValueSelectedListener {
void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance);
}

View File

@@ -40,7 +40,7 @@ public class RadialSelectorView extends View {
// Alpha level for the line.
private static final int FULL_ALPHA = Utils.FULL_ALPHA;
private final Paint mPaint = new Paint();
protected final Paint mPaint = new Paint();
private boolean mIsInitialized;
private boolean mDrawValuesReady;
@@ -96,8 +96,6 @@ public class RadialSelectorView extends View {
Resources res = context.getResources();
int blue = res.getColor(R.color.blue);
mPaint.setColor(blue);
mPaint.setAntiAlias(true);
mSelectionAlpha = SELECTED_ALPHA;
@@ -139,15 +137,11 @@ public class RadialSelectorView extends View {
/* package */ void setTheme(Context context, boolean themeDark) {
Resources res = context.getResources();
int color;
if (themeDark) {
color = res.getColor(R.color.red);
mSelectionAlpha = SELECTED_ALPHA_THEME_DARK;
} else {
color = res.getColor(R.color.blue);
mSelectionAlpha = SELECTED_ALPHA;
}
mPaint.setColor(color);
}
/**

View File

@@ -23,7 +23,9 @@ import android.app.*;
import android.content.*;
import android.content.res.*;
import android.os.*;
import androidx.appcompat.app.*;
import android.util.*;
import android.view.*;
import android.view.View.*;
@@ -39,7 +41,8 @@ import java.util.*;
/**
* Dialog to set a time.
*/
public class TimePickerDialog extends AppCompatDialogFragment 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";
@@ -49,6 +52,7 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
private static final String KEY_IN_KB_MODE = "in_kb_mode";
private static final String KEY_TYPED_TIMES = "typed_times";
private static final String KEY_DARK_THEME = "dark_theme";
private static final String KEY_SELECTED_COLOR = "selected_color";
public static final int HOUR_INDEX = 0;
public static final int MINUTE_INDEX = 1;
@@ -108,37 +112,50 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
* The callback interface used to indicate the user is done filling in
* the time (they clicked on the 'Set' button).
*/
public interface OnTimeSetListener {
public interface OnTimeSetListener
{
/**
* @param view The view associated with this listener.
* @param view The view associated with this listener.
* @param hourOfDay The hour that was set.
* @param minute The minute that was set.
* @param minute The minute that was set.
*/
void onTimeSet(RadialPickerLayout view, int hourOfDay, int minute);
default void onTimeCleared(RadialPickerLayout view) {}
default void onTimeCleared(RadialPickerLayout view)
{
}
}
public TimePickerDialog() {
public TimePickerDialog()
{
// Empty constructor required for dialog fragment.
}
@SuppressLint("Java")
public TimePickerDialog(Context context, int theme, OnTimeSetListener callback,
int hourOfDay, int minute, boolean is24HourMode) {
int hourOfDay, int minute, boolean is24HourMode)
{
// Empty constructor required for dialog fragment.
}
public static TimePickerDialog newInstance(OnTimeSetListener callback,
int hourOfDay, int minute, boolean is24HourMode) {
int hourOfDay,
int minute,
boolean is24HourMode,
int color)
{
TimePickerDialog ret = new TimePickerDialog();
ret.initialize(callback, hourOfDay, minute, is24HourMode);
ret.initialize(callback, hourOfDay, minute, is24HourMode, color);
return ret;
}
public void initialize(OnTimeSetListener callback,
int hourOfDay, int minute, boolean is24HourMode) {
int hourOfDay,
int minute,
boolean is24HourMode,
int color)
{
mCallback = callback;
mInitialHourOfDay = hourOfDay;
@@ -146,40 +163,47 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
mIs24HourMode = is24HourMode;
mInKbMode = false;
mThemeDark = false;
mSelectedColor = color;
}
/**
* Set a dark or light theme. NOTE: this will only take effect for the next onCreateView.
*/
public void setThemeDark(boolean dark) {
public void setThemeDark(boolean dark)
{
mThemeDark = dark;
}
public boolean isThemeDark() {
public boolean isThemeDark()
{
return mThemeDark;
}
public void setOnTimeSetListener(OnTimeSetListener callback) {
public void setOnTimeSetListener(OnTimeSetListener callback)
{
mCallback = callback;
}
public void setStartTime(int hourOfDay, int minute) {
public void setStartTime(int hourOfDay, int minute)
{
mInitialHourOfDay = hourOfDay;
mInitialMinute = minute;
mInKbMode = false;
}
@Override
public void onCreate(Bundle savedInstanceState) {
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
if (savedInstanceState != null && savedInstanceState.containsKey(KEY_HOUR_OF_DAY)
&& savedInstanceState.containsKey(KEY_MINUTE)
&& savedInstanceState.containsKey(KEY_IS_24_HOUR_VIEW)) {
&& savedInstanceState.containsKey(KEY_MINUTE)
&& savedInstanceState.containsKey(KEY_IS_24_HOUR_VIEW)) {
mInitialHourOfDay = savedInstanceState.getInt(KEY_HOUR_OF_DAY);
mInitialMinute = savedInstanceState.getInt(KEY_MINUTE);
mIs24HourMode = savedInstanceState.getBoolean(KEY_IS_24_HOUR_VIEW);
mInKbMode = savedInstanceState.getBoolean(KEY_IN_KB_MODE);
mThemeDark = savedInstanceState.getBoolean(KEY_DARK_THEME);
mSelectedColor = savedInstanceState.getInt(KEY_SELECTED_COLOR);
}
}
@@ -191,7 +215,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Bundle savedInstanceState)
{
getDialog().requestWindowFeature(Window.FEATURE_NO_TITLE);
View view = inflater.inflate(R.layout.time_picker_dialog, null);
@@ -203,8 +228,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
mSelectHours = res.getString(R.string.select_hours);
mMinutePickerDescription = res.getString(R.string.minute_picker_description);
mSelectMinutes = res.getString(R.string.select_minutes);
mSelectedColor = res.getColor(mThemeDark? R.color.red : R.color.blue);
mUnselectedColor = res.getColor(mThemeDark? R.color.white : R.color.numbers_text_color);
//mSelectedColor = res.getColor(mThemeDark ? R.color.red : R.color.blue);
mUnselectedColor = res.getColor(mThemeDark ? R.color.white : R.color.numbers_text_color);
mHourView = (TextView) view.findViewById(R.id.hours);
mHourView.setOnKeyListener(keyboardListener);
@@ -223,8 +248,9 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
mTimePicker = (RadialPickerLayout) view.findViewById(R.id.time_picker);
mTimePicker.setOnValueSelectedListener(this);
mTimePicker.setOnKeyListener(keyboardListener);
mTimePicker.setColor(mSelectedColor);
mTimePicker.initialize(getActivity(), mHapticFeedbackController, mInitialHourOfDay,
mInitialMinute, mIs24HourMode);
mInitialMinute, mIs24HourMode);
int currentItemShowing = HOUR_INDEX;
if (savedInstanceState != null &&
@@ -234,25 +260,31 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
setCurrentItemShowing(currentItemShowing, false, true, true);
mTimePicker.invalidate();
mHourView.setOnClickListener(new OnClickListener() {
mHourView.setOnClickListener(new OnClickListener()
{
@Override
public void onClick(View v) {
public void onClick(View v)
{
setCurrentItemShowing(HOUR_INDEX, true, false, true);
tryVibrate();
}
});
mMinuteView.setOnClickListener(new OnClickListener() {
mMinuteView.setOnClickListener(new OnClickListener()
{
@Override
public void onClick(View v) {
public void onClick(View v)
{
setCurrentItemShowing(MINUTE_INDEX, true, false, true);
tryVibrate();
}
});
mDoneButton = (TextView) view.findViewById(R.id.done_button);
mDoneButton.setOnClickListener(new OnClickListener() {
mDoneButton.setOnClickListener(new OnClickListener()
{
@Override
public void onClick(View v) {
public void onClick(View v)
{
if (mInKbMode && isTypedTimeFullyLegal()) {
finishKbMode(false);
} else {
@@ -260,25 +292,25 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
}
if (mCallback != null) {
mCallback.onTimeSet(mTimePicker,
mTimePicker.getHours(), mTimePicker.getMinutes());
mTimePicker.getHours(), mTimePicker.getMinutes());
}
dismiss();
}
});
mDoneButton.setOnKeyListener(keyboardListener);
mClearButton = (TextView) view.findViewById(R.id.clear_button);
mClearButton.setOnClickListener(new OnClickListener()
{
@Override
public void onClick(View v)
{
if(mCallback != null) {
mCallback.onTimeCleared(mTimePicker);
}
dismiss();
}
});
{
@Override
public void onClick(View v)
{
if (mCallback != null) {
mCallback.onTimeCleared(mTimePicker);
}
dismiss();
}
});
mClearButton.setOnKeyListener(keyboardListener);
// Enable or disable the AM/PM view.
@@ -293,15 +325,17 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
separatorView.setLayoutParams(paramsSeparator);
} else {
mAmPmTextView.setVisibility(View.VISIBLE);
updateAmPmDisplay(mInitialHourOfDay < 12? AM : PM);
mAmPmHitspace.setOnClickListener(new OnClickListener() {
updateAmPmDisplay(mInitialHourOfDay < 12 ? AM : PM);
mAmPmHitspace.setOnClickListener(new OnClickListener()
{
@Override
public void onClick(View v) {
public void onClick(View v)
{
tryVibrate();
int amOrPm = mTimePicker.getIsCurrentlyAmOrPm();
if (amOrPm == AM) {
amOrPm = PM;
} else if (amOrPm == PM){
} else if (amOrPm == PM) {
amOrPm = AM;
}
updateAmPmDisplay(amOrPm);
@@ -328,56 +362,61 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
mTypedTimes = new ArrayList<Integer>();
}
// Set the theme at the end so that the initialize()s above don't counteract the theme.
mTimePicker.setTheme(getActivity().getApplicationContext(), mThemeDark);
// Prepare some palette to use.
int white = res.getColor(R.color.white);
int circleBackground = res.getColor(R.color.circle_background);
int line = res.getColor(R.color.line_background);
int timeDisplay = res.getColor(R.color.numbers_text_color);
ColorStateList doneTextColor = res.getColorStateList(R.color.done_text_color);
int doneBackground = R.drawable.done_background_color;
int darkGray = res.getColor(R.color.dark_gray);
int lightGray = res.getColor(R.color.light_gray);
int darkLine = res.getColor(R.color.line_dark);
ColorStateList darkDoneTextColor = res.getColorStateList(R.color.done_text_color_dark);
int darkDoneBackground = R.drawable.done_background_color_dark;
// // Set the theme at the end so that the initialize()s above don't counteract the theme.
// mTimePicker.setTheme(getActivity().getApplicationContext(), mThemeDark);
// // Prepare some palette to use.
// int white = res.getColor(R.color.white);
// int circleBackground = res.getColor(R.color.circle_background);
// int line = res.getColor(R.color.line_background);
// int timeDisplay = res.getColor(R.color.numbers_text_color);
// ColorStateList doneTextColor = res.getColorStateList(R.color.done_text_color);
// int doneBackground = R.drawable.done_background_color;
//
// int darkGray = res.getColor(R.color.dark_gray);
// int lightGray = res.getColor(R.color.light_gray);
// int darkLine = res.getColor(R.color.line_dark);
// ColorStateList darkDoneTextColor = res.getColorStateList(R.color.done_text_color_dark);
// int darkDoneBackground = R.drawable.done_background_color_dark;
// Set the palette for each view based on the theme.
view.findViewById(R.id.time_display_background).setBackgroundColor(mThemeDark? darkGray : white);
view.findViewById(R.id.time_display).setBackgroundColor(mThemeDark? darkGray : white);
((TextView) view.findViewById(R.id.separator)).setTextColor(mThemeDark? white : timeDisplay);
((TextView) view.findViewById(R.id.ampm_label)).setTextColor(mThemeDark? white : timeDisplay);
view.findViewById(R.id.line).setBackgroundColor(mThemeDark? darkLine : line);
mDoneButton.setTextColor(mThemeDark? darkDoneTextColor : doneTextColor);
mTimePicker.setBackgroundColor(mThemeDark? lightGray : circleBackground);
mDoneButton.setBackgroundResource(mThemeDark? darkDoneBackground : doneBackground);
// view.findViewById(R.id.time_display_background).setBackgroundColor(mThemeDark? darkGray : white);
// view.findViewById(R.id.time_display).setBackgroundColor(mThemeDark? darkGray : white);
// ((TextView) view.findViewById(R.id.separator)).setTextColor(mThemeDark? white : timeDisplay);
// ((TextView) view.findViewById(R.id.ampm_label)).setTextColor(mThemeDark? white : timeDisplay);
// view.findViewById(R.id.line).setBackgroundColor(mThemeDark? darkLine : line);
// mDoneButton.setTextColor(mThemeDark? darkDoneTextColor : doneTextColor);
// mTimePicker.setBackgroundColor(mThemeDark? lightGray : circleBackground);
// mDoneButton.setBackgroundResource(mThemeDark? darkDoneBackground : doneBackground);
return view;
}
@Override
public void onResume() {
public void onResume()
{
super.onResume();
mHapticFeedbackController.start();
}
@Override
public void onPause() {
public void onPause()
{
super.onPause();
mHapticFeedbackController.stop();
}
public void tryVibrate() {
public void tryVibrate()
{
mHapticFeedbackController.tryVibrate();
}
private void updateAmPmDisplay(int amOrPm) {
private void updateAmPmDisplay(int amOrPm)
{
if (amOrPm == AM) {
mAmPmTextView.setText(mAmText);
Utils.tryAccessibilityAnnounce(mTimePicker, mAmText);
mAmPmHitspace.setContentDescription(mAmText);
} else if (amOrPm == PM){
} else if (amOrPm == PM) {
mAmPmTextView.setText(mPmText);
Utils.tryAccessibilityAnnounce(mTimePicker, mPmText);
mAmPmHitspace.setContentDescription(mPmText);
@@ -387,7 +426,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
}
@Override
public void onSaveInstanceState(Bundle outState) {
public void onSaveInstanceState(Bundle outState)
{
if (mTimePicker != null) {
outState.putInt(KEY_HOUR_OF_DAY, mTimePicker.getHours());
outState.putInt(KEY_MINUTE, mTimePicker.getMinutes());
@@ -398,6 +438,7 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
outState.putIntegerArrayList(KEY_TYPED_TIMES, mTypedTimes);
}
outState.putBoolean(KEY_DARK_THEME, mThemeDark);
outState.putInt(KEY_SELECTED_COLOR, mSelectedColor);
}
}
@@ -405,7 +446,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
* Called by the picker for updating the header display.
*/
@Override
public void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance) {
public void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance)
{
if (pickerIndex == HOUR_INDEX) {
setHour(newValue, false);
String announcement = String.format("%d", newValue);
@@ -417,7 +459,7 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
}
Utils.tryAccessibilityAnnounce(mTimePicker, announcement);
} else if (pickerIndex == MINUTE_INDEX){
} else if (pickerIndex == MINUTE_INDEX) {
setMinute(newValue);
mTimePicker.setContentDescription(mMinutePickerDescription + ": " + newValue);
} else if (pickerIndex == AMPM_INDEX) {
@@ -430,7 +472,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
}
}
private void setHour(int value, boolean announce) {
private void setHour(int value, boolean announce)
{
String format;
if (mIs24HourMode) {
format = "%02d";
@@ -450,7 +493,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
}
}
private void setMinute(int value) {
private void setMinute(int value)
{
if (value == 60) {
value = 0;
}
@@ -462,7 +506,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
// Show either Hours or Minutes.
private void setCurrentItemShowing(int index, boolean animateCircle, boolean delayLabelAnimate,
boolean announce) {
boolean announce)
{
mTimePicker.setCurrentItemShowing(index, animateCircle);
TextView labelToAnimate;
@@ -485,8 +530,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
labelToAnimate = mMinuteView;
}
int hourColor = (index == HOUR_INDEX)? mSelectedColor : mUnselectedColor;
int minuteColor = (index == MINUTE_INDEX)? mSelectedColor : mUnselectedColor;
int hourColor = (index == HOUR_INDEX) ? mSelectedColor : mUnselectedColor;
int minuteColor = (index == MINUTE_INDEX) ? mSelectedColor : mUnselectedColor;
mHourView.setTextColor(hourColor);
mMinuteView.setTextColor(minuteColor);
@@ -499,15 +544,17 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
/**
* For keyboard mode, processes key events.
*
* @param keyCode the pressed key.
* @return true if the key was successfully processed, false otherwise.
*/
private boolean processKeyUp(int keyCode) {
private boolean processKeyUp(int keyCode)
{
if (keyCode == KeyEvent.KEYCODE_ESCAPE || keyCode == KeyEvent.KEYCODE_BACK) {
dismiss();
return true;
} else if (keyCode == KeyEvent.KEYCODE_TAB) {
if(mInKbMode) {
if (mInKbMode) {
if (isTypedTimeFullyLegal()) {
finishKbMode(true);
}
@@ -522,7 +569,7 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
}
if (mCallback != null) {
mCallback.onTimeSet(mTimePicker,
mTimePicker.getHours(), mTimePicker.getMinutes());
mTimePicker.getHours(), mTimePicker.getMinutes());
}
dismiss();
return true;
@@ -539,7 +586,7 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
deletedKeyStr = String.format("%d", getValFromKeyCode(deleted));
}
Utils.tryAccessibilityAnnounce(mTimePicker,
String.format(mDeletedKeyFormat, deletedKeyStr));
String.format(mDeletedKeyFormat, deletedKeyStr));
updateDisplay(true);
}
}
@@ -549,7 +596,7 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
|| keyCode == KeyEvent.KEYCODE_6 || keyCode == KeyEvent.KEYCODE_7
|| keyCode == KeyEvent.KEYCODE_8 || keyCode == KeyEvent.KEYCODE_9
|| (!mIs24HourMode &&
(keyCode == getAmOrPmKeyCode(AM) || keyCode == getAmOrPmKeyCode(PM)))) {
(keyCode == getAmOrPmKeyCode(AM) || keyCode == getAmOrPmKeyCode(PM)))) {
if (!mInKbMode) {
if (mTimePicker == null) {
// Something's wrong, because time picker should definitely not be null.
@@ -572,11 +619,13 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
/**
* Try to start keyboard mode with the specified key, as long as the timepicker is not in the
* middle of a touch-event.
*
* @param keyCode The key to use as the first press. Keyboard mode will not be started if the
* key is not legal to start with. Or, pass in -1 to get into keyboard mode without a starting
* key.
* key is not legal to start with. Or, pass in -1 to get into keyboard mode without a starting
* key.
*/
private void tryStartingKbMode(int keyCode) {
private void tryStartingKbMode(int keyCode)
{
if (mTimePicker.trySettingInputEnabled(false) &&
(keyCode == -1 || addKeyIfLegal(keyCode))) {
mInKbMode = true;
@@ -585,7 +634,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
}
}
private boolean addKeyIfLegal(int keyCode) {
private boolean addKeyIfLegal(int keyCode)
{
// If we're in 24hour mode, we'll need to check if the input is full. If in AM/PM mode,
// we'll need to see if AM/PM have been typed.
if ((mIs24HourMode && mTypedTimes.size() == 4) ||
@@ -617,7 +667,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
* Traverse the tree to see if the keys that have been typed so far are legal as is,
* or may become legal as more keys are typed (excluding backspace).
*/
private boolean isTypedTimeLegalSoFar() {
private boolean isTypedTimeLegalSoFar()
{
Node node = mLegalTimesTree;
for (int keyCode : mTypedTimes) {
node = node.canReach(keyCode);
@@ -631,7 +682,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
/**
* Check if the time that has been typed so far is completely legal, as is.
*/
private boolean isTypedTimeFullyLegal() {
private boolean isTypedTimeFullyLegal()
{
if (mIs24HourMode) {
// For 24-hour mode, the time is legal if the hours and minutes are each legal. Note:
// getEnteredTime() will ONLY call isTypedTimeFullyLegal() when NOT in 24hour mode.
@@ -645,7 +697,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
}
}
private int deleteLastTypedKey() {
private int deleteLastTypedKey()
{
int deleted = mTypedTimes.remove(mTypedTimes.size() - 1);
if (!isTypedTimeFullyLegal()) {
mDoneButton.setEnabled(false);
@@ -655,9 +708,11 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
/**
* Get out of keyboard mode. If there is nothing in typedTimes, revert to TimePicker's time.
*
* @param changeDisplays If true, update the displays with the relevant time.
*/
private void finishKbMode(boolean updateDisplays) {
private void finishKbMode(boolean updateDisplays)
{
mInKbMode = false;
if (!mTypedTimes.isEmpty()) {
int values[] = getEnteredTime(null);
@@ -677,29 +732,31 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
* Update the hours, minutes, and AM/PM displays with the typed times. If the typedTimes is
* empty, either show an empty display (filled with the placeholder text), or update from the
* timepicker's values.
*
* @param allowEmptyDisplay if true, then if the typedTimes is empty, use the placeholder text.
* Otherwise, revert to the timepicker's values.
* Otherwise, revert to the timepicker's values.
*/
private void updateDisplay(boolean allowEmptyDisplay) {
private void updateDisplay(boolean allowEmptyDisplay)
{
if (!allowEmptyDisplay && mTypedTimes.isEmpty()) {
int hour = mTimePicker.getHours();
int minute = mTimePicker.getMinutes();
setHour(hour, true);
setMinute(minute);
if (!mIs24HourMode) {
updateAmPmDisplay(hour < 12? AM : PM);
updateAmPmDisplay(hour < 12 ? AM : PM);
}
setCurrentItemShowing(mTimePicker.getCurrentItemShowing(), true, true, true);
mDoneButton.setEnabled(true);
} else {
Boolean[] enteredZeros = {false, false};
int[] values = getEnteredTime(enteredZeros);
String hourFormat = enteredZeros[0]? "%02d" : "%2d";
String minuteFormat = (enteredZeros[1])? "%02d" : "%2d";
String hourStr = (values[0] == -1)? mDoublePlaceholderText :
String.format(hourFormat, values[0]).replace(' ', mPlaceholderText);
String minuteStr = (values[1] == -1)? mDoublePlaceholderText :
String.format(minuteFormat, values[1]).replace(' ', mPlaceholderText);
String hourFormat = enteredZeros[0] ? "%02d" : "%2d";
String minuteFormat = (enteredZeros[1]) ? "%02d" : "%2d";
String hourStr = (values[0] == -1) ? mDoublePlaceholderText :
String.format(hourFormat, values[0]).replace(' ', mPlaceholderText);
String minuteStr = (values[1] == -1) ? mDoublePlaceholderText :
String.format(minuteFormat, values[1]).replace(' ', mPlaceholderText);
mHourView.setText(hourStr);
mHourSpaceView.setText(hourStr);
mHourView.setTextColor(mUnselectedColor);
@@ -712,7 +769,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
}
}
private static int getValFromKeyCode(int keyCode) {
private static int getValFromKeyCode(int keyCode)
{
switch (keyCode) {
case KeyEvent.KEYCODE_0:
return 0;
@@ -741,20 +799,22 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
/**
* Get the currently-entered time, as integer values of the hours and minutes typed.
*
* @param enteredZeros A size-2 boolean array, which the caller should initialize, and which
* may then be used for the caller to know whether zeros had been explicitly entered as either
* hours of minutes. This is helpful for deciding whether to show the dashes, or actual 0's.
* may then be used for the caller to know whether zeros had been explicitly entered as either
* hours of minutes. This is helpful for deciding whether to show the dashes, or actual 0's.
* @return A size-3 int array. The first value will be the hours, the second value will be the
* minutes, and the third will be either TimePickerDialog.AM or TimePickerDialog.PM.
*/
private int[] getEnteredTime(Boolean[] enteredZeros) {
private int[] getEnteredTime(Boolean[] enteredZeros)
{
int amOrPm = -1;
int startIndex = 1;
if (!mIs24HourMode && isTypedTimeFullyLegal()) {
int keyCode = mTypedTimes.get(mTypedTimes.size() - 1);
if (keyCode == getAmOrPmKeyCode(AM)) {
amOrPm = AM;
} else if (keyCode == getAmOrPmKeyCode(PM)){
} else if (keyCode == getAmOrPmKeyCode(PM)) {
amOrPm = PM;
}
startIndex = 2;
@@ -765,15 +825,15 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
int val = getValFromKeyCode(mTypedTimes.get(mTypedTimes.size() - i));
if (i == startIndex) {
minute = val;
} else if (i == startIndex+1) {
minute += 10*val;
} else if (i == startIndex + 1) {
minute += 10 * val;
if (enteredZeros != null && val == 0) {
enteredZeros[1] = true;
}
} else if (i == startIndex+2) {
} else if (i == startIndex + 2) {
hour = val;
} else if (i == startIndex+3) {
hour += 10*val;
} else if (i == startIndex + 3) {
hour += 10 * val;
if (enteredZeros != null && val == 0) {
enteredZeros[0] = true;
}
@@ -787,7 +847,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
/**
* Get the keycode value for AM and PM in the current language.
*/
private int getAmOrPmKeyCode(int amOrPm) {
private int getAmOrPmKeyCode(int amOrPm)
{
// Cache the codes.
if (mAmKeyCode == -1 || mPmKeyCode == -1) {
// Find the first character in the AM/PM text that is unique.
@@ -822,7 +883,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
/**
* Create a tree for deciding what keys can legally be typed.
*/
private void generateLegalTimesTree() {
private void generateLegalTimesTree()
{
// Create a quick cache of numbers to their keycodes.
int k0 = KeyEvent.KEYCODE_0;
int k1 = KeyEvent.KEYCODE_1;
@@ -878,7 +940,7 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
// When the first digit is 2, the second digit may be 4-5.
secondDigit = new Node(k4, k5);
firstDigit.addChild(secondDigit);
// We must now be followd by the last minute digit. E.g. 2:40, 2:53.
// We must now be followed by the last minute digit. E.g. 2:40, 2:53.
secondDigit.addChild(minuteSecondDigit);
// The first digit may be 3-9.
@@ -955,20 +1017,24 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
* mLegalKeys represents the keys that can be typed to get to the node.
* mChildren are the children that can be reached from this node.
*/
private class Node {
private class Node
{
private int[] mLegalKeys;
private ArrayList<Node> mChildren;
public Node(int... legalKeys) {
public Node(int... legalKeys)
{
mLegalKeys = legalKeys;
mChildren = new ArrayList<Node>();
}
public void addChild(Node child) {
public void addChild(Node child)
{
mChildren.add(child);
}
public boolean containsKey(int key) {
public boolean containsKey(int key)
{
for (int i = 0; i < mLegalKeys.length; i++) {
if (mLegalKeys[i] == key) {
return true;
@@ -977,7 +1043,8 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
return false;
}
public Node canReach(int key) {
public Node canReach(int key)
{
if (mChildren == null) {
return null;
}
@@ -990,9 +1057,11 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
}
}
private class KeyboardListener implements OnKeyListener {
private class KeyboardListener implements OnKeyListener
{
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
public boolean onKey(View v, int keyCode, KeyEvent event)
{
if (event.getAction() == KeyEvent.ACTION_UP) {
return processKeyUp(keyCode);
}
@@ -1000,14 +1069,16 @@ public class TimePickerDialog extends AppCompatDialogFragment implements OnValue
}
}
public void setDismissListener( DialogInterface.OnDismissListener listener ) {
public void setDismissListener(DialogInterface.OnDismissListener listener)
{
dismissListener = listener;
}
@Override
public void onDismiss(DialogInterface dialog) {
public void onDismiss(DialogInterface dialog)
{
super.onDismiss(dialog);
if( dismissListener != null )
if (dismissListener != null)
dismissListener.onDismiss(dialog);
}
}

View File

@@ -49,33 +49,33 @@
android:layout_height="1dip"
android:background="@color/line_background" />
<LinearLayout
<androidx.appcompat.widget.LinearLayoutCompat
style="?android:attr/buttonBarStyle"
android:layout_width="@dimen/date_picker_component_width"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<Button
<androidx.appcompat.widget.AppCompatButton
style="?android:attr/buttonBarButtonStyle"
android:id="@+id/clear_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/done_background_color"
android:minHeight="48dp"
android:textColor="#333"
android:text="@string/clear_label"
android:textColor="@color/done_text_color"
android:textSize="@dimen/done_label_size" />
<Button
<androidx.appcompat.widget.AppCompatButton
style="?android:attr/buttonBarButtonStyle"
android:id="@+id/done_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/done_background_color"
android:minHeight="48dp"
android:textColor="#333"
android:text="@string/done_label"
android:textColor="@color/done_text_color"
android:textSize="@dimen/done_label_size" />
</LinearLayout>
</androidx.appcompat.widget.LinearLayoutCompat>
</LinearLayout>

View File

@@ -71,12 +71,12 @@ build_apk() {
if [ ! -z $RELEASE ]; then
log_info "Building release APK"
./gradlew assembleRelease
$GRADLE assembleRelease
cp -v uhabits-android/build/outputs/apk/release/uhabits-android-release.apk build/loop-$VERSION-release.apk
fi
log_info "Building debug APK"
./gradlew assembleDebug || fail
$GRADLE assembleDebug --stacktrace || fail
cp -v uhabits-android/build/outputs/apk/debug/uhabits-android-debug.apk build/loop-$VERSION-debug.apk
}
@@ -212,6 +212,7 @@ parse_opts() {
}
remove_build_dir() {
rm -rfv .gradle
rm -rfv build
rm -rfv android-base/build
rm -rfv android-pickers/build
@@ -231,7 +232,10 @@ case "$1" in
medium-tests)
shift; parse_opts $*
run_tests medium
for attempt in {1..3}; do
(run_tests medium) && exit 0
done
exit 1
;;
large-tests)
@@ -252,7 +256,7 @@ case "$1" in
build_apk
install_apk
;;
clean)
remove_build_dir
;;

View File

@@ -1,15 +1,17 @@
VERSION_CODE = 52
VERSION_NAME = 1.8.9
VERSION_CODE = 20000
VERSION_NAME = 2.0.0-alpha
MIN_SDK_VERSION = 21
MIN_SDK_VERSION = 23
TARGET_SDK_VERSION = 29
COMPILE_SDK_VERSION = 29
DAGGER_VERSION = 2.25.4
KOTLIN_VERSION = 1.3.61
KOTLIN_VERSION = 1.4.0
KX_COROUTINES_VERSION = 1.4.2
SUPPORT_LIBRARY_VERSION = 28.0.0
AUTO_FACTORY_VERSION = 1.0-beta6
BUILD_TOOLS_VERSION = 3.5.3
BUILD_TOOLS_VERSION = 4.1.0
KTOR_VERSION=1.4.2
org.gradle.parallel=false
org.gradle.daemon=true

View File

@@ -1,5 +1,6 @@
#Sat Nov 28 09:55:24 CST 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip

View File

@@ -55,7 +55,7 @@ def get_total(report):
def get_color(total):
"""
Return color for current coverage precent
Return color for current coverage percent
"""
try:
xtotal = int(total)

View File

@@ -4,6 +4,7 @@ plugins {
id 'kotlin-android'
id 'kotlin-kapt'
id 'com.github.triplet.play' version '2.6.2'
id 'kotlin-android-extensions'
}
android {
@@ -52,6 +53,7 @@ android {
}
compileOptions {
coreLibraryDesugaringEnabled true
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_1_8
}
@@ -69,6 +71,10 @@ android {
sourceSets {
main.assets.srcDirs += '../uhabits-core/src/main/resources/'
}
buildFeatures {
viewBinding true
}
}
dependencies {
@@ -87,7 +93,19 @@ dependencies {
implementation "com.google.code.gson:gson:2.8.5"
implementation "com.google.code.findbugs:jsr305:3.0.2"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$KOTLIN_VERSION"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$KX_COROUTINES_VERSION"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$KX_COROUTINES_VERSION"
implementation "androidx.constraintlayout:constraintlayout:2.0.0-beta4"
implementation 'com.google.zxing:core:3.4.1'
implementation "io.ktor:ktor-client-core:$KTOR_VERSION"
implementation "io.ktor:ktor-client-android:$KTOR_VERSION"
implementation "io.ktor:ktor-client-json:$KTOR_VERSION"
implementation "io.ktor:ktor-client-jackson:$KTOR_VERSION"
implementation "com.google.guava:guava:30.0-android"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
compileOnly "javax.annotation:jsr250-api:1.0"
compileOnly "com.google.auto.factory:auto-factory:$AUTO_FACTORY_VERSION"
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
@@ -104,7 +122,8 @@ dependencies {
androidTestImplementation 'androidx.annotation:annotation:1.0.0'
androidTestImplementation 'androidx.test:rules:1.1.1'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation "com.google.guava:guava:24.1-android"
androidTestImplementation "io.ktor:ktor-client-mock:$KTOR_VERSION"
androidTestImplementation "io.ktor:ktor-jackson:$KTOR_VERSION"
androidTestImplementation project(":uhabits-core")
kaptAndroidTest "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
@@ -134,4 +153,4 @@ kapt {
play {
serviceAccountCredentials = file("../../.secret/gcp-key.json")
track = "alpha"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

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