├── .github └── dependabot.yml ├── .gitignore ├── .idea ├── androidTestResultsUserPreferences.xml ├── codeStyles │ └── Project.xml ├── compiler.xml ├── copyright │ ├── Default.xml │ └── profiles_settings.xml ├── detekt.xml ├── dictionaries │ └── noah.xml ├── encodings.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── jarRepositories.xml ├── libraries │ ├── support_annotations_20_0_0.xml │ └── support_v4_20_0_0.xml ├── misc.xml ├── scopes │ └── scope_settings.xml └── vcs.xml ├── .tx └── config ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── PRIVACY.md ├── README.md ├── app ├── build.gradle ├── proguard-project.txt └── src │ ├── androidTest │ └── java │ │ └── org │ │ └── yuttadhammo │ │ └── BodhiTimer │ │ ├── AdvPicker.java │ │ ├── TimerActivityTest.kt │ │ └── TimerActivityTest2.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── org │ │ └── yuttadhammo │ │ └── BodhiTimer │ │ ├── AdvNumberPicker.kt │ │ ├── Animation │ │ ├── BodhiLeaf.kt │ │ ├── CircleAnimation.kt │ │ └── TimerAnimation.kt │ │ ├── BodhiApp.kt │ │ ├── Const │ │ ├── BroadcastTypes.kt │ │ ├── SessionTypes.kt │ │ └── TimerState.kt │ │ ├── Models │ │ ├── AlarmTask.kt │ │ ├── AlarmTaskManager.kt │ │ └── TimerList.kt │ │ ├── Service │ │ ├── SoundService.kt │ │ ├── TTSService.kt │ │ └── TimerReceiver.kt │ │ ├── SettingsActivity.kt │ │ ├── SettingsFragment.kt │ │ ├── SlidingPickerDialog.kt │ │ ├── TimerActivity.kt │ │ ├── Util │ │ ├── Notifications.kt │ │ ├── Settings.kt │ │ ├── SettingsDelegate.kt │ │ ├── Sounds.kt │ │ ├── Themes.kt │ │ └── Time.kt │ │ └── Widget │ │ └── BodhiAppWidgetProvider.kt │ └── res │ ├── color │ └── gallery_item_color.xml │ ├── drawable-hdpi │ └── leaf.png │ ├── drawable-mdpi │ └── leaf.png │ ├── drawable-v24 │ └── ic_launcher_background.xml │ ├── drawable-xhdpi │ └── leaf.png │ ├── drawable-xxhdpi │ └── leaf.png │ ├── drawable │ ├── enso.xml │ ├── ic_launcher_foreground.xml │ ├── notification.xml │ ├── pause.xml │ ├── play.xml │ ├── preferences.xml │ ├── set.xml │ ├── stop.xml │ ├── widget_background_black.xml │ └── widget_background_black_square.xml │ ├── font │ ├── source_sans.xml │ ├── sourcesanspro_light.ttf │ └── sourcesanspro_regular.ttf │ ├── layout │ ├── about.xml │ ├── adv_list_item.xml │ ├── adv_number_picker.xml │ ├── appwidget.xml │ ├── gallery_item.xml │ ├── main.xml │ ├── settings_activity.xml │ ├── sliding_picker_dialog.xml │ └── timepicker_dialog.xml │ ├── mipmap-anydpi-v26 │ └── ic_launcher.xml │ ├── raw │ ├── bell.ogg │ ├── bell1.ogg │ ├── bell2.ogg │ ├── bell3.ogg │ ├── bell4.ogg │ ├── bell_1.ogg │ ├── bell_2.ogg │ ├── birds.ogg │ ├── bowl.ogg │ ├── bowl1.ogg │ ├── bowl2.ogg │ ├── bowl_low.ogg │ ├── gong.ogg │ ├── gong1.ogg │ └── gong2.ogg │ ├── values-de │ └── strings.xml │ ├── values-eo │ └── strings.xml │ ├── values-fr │ └── strings.xml │ ├── values-pl │ └── strings.xml │ ├── values-zh │ └── strings.xml │ ├── values │ ├── arrays.xml │ ├── dimens.xml │ ├── setting_keys.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ ├── backup_descriptor.xml │ ├── bodhi_appwidget_info.xml │ └── preferences.xml ├── build.gradle ├── detekt-config.yml ├── fastlane ├── Appfile ├── Fastfile ├── README.md └── metadata │ └── android │ └── en-US │ ├── changelogs │ ├── 59.txt │ ├── 60.txt │ ├── 61.txt │ ├── 62.txt │ ├── 63.txt │ ├── 65.txt │ ├── 66.txt │ ├── 67.txt │ ├── 69.txt │ ├── 70.txt │ ├── 71.txt │ ├── 72.txt │ ├── 73.txt │ ├── 74.txt │ ├── 75.txt │ ├── 76.txt │ ├── 77.txt │ ├── 78.txt │ ├── 79.txt │ ├── 80.txt │ ├── 82.txt │ ├── 86.txt │ ├── 87.txt │ ├── 88.txt │ ├── 90.txt │ ├── 91.txt │ ├── 92.txt │ ├── 93.txt │ ├── 94.txt │ ├── 95.txt │ ├── 96.txt │ ├── 97.txt │ └── 98.txt │ ├── full_description-uni.txt │ ├── full_description.txt │ ├── images │ ├── featureGraphic.png │ └── phoneScreenshots │ │ ├── 1_en-US.png │ │ ├── 2_en-US.png │ │ ├── 3_en-US.png │ │ └── 4_en-US.png │ ├── short_description.txt │ ├── title.txt │ └── video.txt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── image-sources ├── leaf-image-res.png ├── leaf-image-res.xcf ├── leaf-image.xcf ├── leaf.xcf ├── leaf2.xcf ├── leaf3.png └── leaf3.xcf └── settings.gradle /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gradle" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/androidstudio 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=androidstudio 3 | 4 | ### AndroidStudio ### 5 | # Covers files to be ignored for android development using Android Studio. 6 | 7 | # Built application files 8 | *.apk 9 | *.ap_ 10 | *.aab 11 | 12 | # Files for the ART/Dalvik VM 13 | *.dex 14 | 15 | # Java class files 16 | *.class 17 | 18 | # Generated files 19 | bin/ 20 | gen/ 21 | out/ 22 | 23 | # Gradle files 24 | .gradle 25 | .gradle/ 26 | build/ 27 | 28 | # Signing files 29 | .signing/ 30 | 31 | # Local configuration file (sdk path, etc) 32 | local.properties 33 | 34 | # Proguard folder generated by Eclipse 35 | proguard/ 36 | 37 | # Log Files 38 | *.log 39 | 40 | # Android Studio 41 | /*/build/ 42 | /*/local.properties 43 | /*/out 44 | /*/*/build 45 | /*/*/production 46 | captures/ 47 | .navigation/ 48 | *.ipr 49 | *~ 50 | *.swp 51 | 52 | # Keystore files 53 | *.jks 54 | *.keystore 55 | 56 | # Google Services (e.g. APIs or Firebase) 57 | # google-services.json 58 | 59 | # Android Patch 60 | gen-external-apklibs 61 | 62 | # External native build folder generated in Android Studio 2.2 and later 63 | .externalNativeBuild 64 | 65 | # NDK 66 | obj/ 67 | 68 | # IntelliJ IDEA 69 | *.iml 70 | *.iws 71 | /out/ 72 | 73 | # User-specific configurations 74 | .idea/caches/ 75 | .idea/libraries/ 76 | .idea/shelf/ 77 | .idea/workspace.xml 78 | .idea/tasks.xml 79 | .idea/.name 80 | .idea/compiler.xml 81 | .idea/copyright/profiles_settings.xml 82 | .idea/encodings.xml 83 | .idea/misc.xml 84 | .idea/modules.xml 85 | .idea/scopes/scope_settings.xml 86 | .idea/dictionaries 87 | .idea/vcs.xml 88 | .idea/jsLibraryMappings.xml 89 | .idea/datasources.xml 90 | .idea/dataSources.ids 91 | .idea/sqlDataSources.xml 92 | .idea/dynamic.xml 93 | .idea/uiDesigner.xml 94 | .idea/assetWizardSettings.xml 95 | .idea/gradle.xml 96 | .idea/jarRepositories.xml 97 | .idea/navEditor.xml 98 | 99 | # Legacy Eclipse project files 100 | .classpath 101 | .project 102 | .cproject 103 | .settings/ 104 | 105 | # Mobile Tools for Java (J2ME) 106 | .mtj.tmp/ 107 | 108 | # Package Files # 109 | *.war 110 | *.ear 111 | 112 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) 113 | hs_err_pid* 114 | 115 | ## Plugin-specific files: 116 | 117 | # mpeltonen/sbt-idea plugin 118 | .idea_modules/ 119 | 120 | # JIRA plugin 121 | atlassian-ide-plugin.xml 122 | 123 | # Mongo Explorer plugin 124 | .idea/mongoSettings.xml 125 | 126 | # Crashlytics plugin (for Android Studio and IntelliJ) 127 | com_crashlytics_export_strings.xml 128 | crashlytics.properties 129 | crashlytics-build.properties 130 | fabric.properties 131 | 132 | ### AndroidStudio Patch ### 133 | 134 | !/gradle/wrapper/gradle-wrapper.jar 135 | 136 | # End of https://www.toptal.com/developers/gitignore/api/androidstudio 137 | 138 | 139 | # Built application files 140 | *.aar 141 | .idea/libraries 142 | # Android Studio 3 in .gitignore file. 143 | .idea/caches 144 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 145 | # Keystore files 146 | .cxx/ 147 | 148 | # Google Services (e.g. APIs or Firebase) 149 | # google-services.json 150 | 151 | # Freeline 152 | freeline.py 153 | freeline/ 154 | freeline_project_description.json 155 | 156 | # fastlane 157 | fastlane/report.xml 158 | fastlane/Preview.html 159 | fastlane/screenshots 160 | fastlane/test_output 161 | fastlane/readme.md 162 | api.json 163 | 164 | # Version control 165 | vcs.xml 166 | 167 | # lint 168 | lint/intermediates/ 169 | lint/generated/ 170 | lint/outputs/ 171 | lint/tmp/ 172 | # lint/reports/ 173 | 174 | # Android Profiling 175 | *.hprof 176 | /key.properties 177 | /.idea/deploymentTargetDropDown.xml 178 | -------------------------------------------------------------------------------- /.idea/androidTestResultsUserPreferences.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 36 | 37 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | xmlns:android 14 | 15 | ^$ 16 | 17 | 18 | 19 |
20 |
21 | 22 | 23 | 24 | xmlns:.* 25 | 26 | ^$ 27 | 28 | 29 | BY_NAME 30 | 31 |
32 |
33 | 34 | 35 | 36 | .*:id 37 | 38 | http://schemas.android.com/apk/res/android 39 | 40 | 41 | 42 |
43 |
44 | 45 | 46 | 47 | .*:name 48 | 49 | http://schemas.android.com/apk/res/android 50 | 51 | 52 | 53 |
54 |
55 | 56 | 57 | 58 | name 59 | 60 | ^$ 61 | 62 | 63 | 64 |
65 |
66 | 67 | 68 | 69 | style 70 | 71 | ^$ 72 | 73 | 74 | 75 |
76 |
77 | 78 | 79 | 80 | .* 81 | 82 | ^$ 83 | 84 | 85 | BY_NAME 86 | 87 |
88 |
89 | 90 | 91 | 92 | .* 93 | 94 | http://schemas.android.com/apk/res/android 95 | 96 | 97 | ANDROID_ATTRIBUTE_ORDER 98 | 99 |
100 |
101 | 102 | 103 | 104 | .* 105 | 106 | .* 107 | 108 | 109 | BY_NAME 110 | 111 |
112 |
113 |
114 |
115 |
116 |
-------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.idea/copyright/Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/detekt.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/dictionaries/noah.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | 39 | 40 | 44 | 45 | 49 | 50 | 54 | 55 | 59 | 60 | -------------------------------------------------------------------------------- /.idea/libraries/support_annotations_20_0_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/libraries/support_v4_20_0_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/scopes/scope_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.com 3 | 4 | [o:bodhi-timer:p:bodhi-timer-app:r:strings_xml] 5 | file_filter = app/src/main/res/values-/strings.xml 6 | source_file = app/src/main/res/values/strings.xml 7 | type = ANDROID 8 | minimum_perc = 0 9 | resource_name = strings.xml 10 | 11 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" 4 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.3) 5 | addressable (2.8.0) 6 | public_suffix (>= 2.0.2, < 5.0) 7 | atomos (0.1.3) 8 | aws-eventstream (1.1.0) 9 | aws-partitions (1.416.0) 10 | aws-sdk-core (3.111.1) 11 | aws-eventstream (~> 1, >= 1.0.2) 12 | aws-partitions (~> 1, >= 1.239.0) 13 | aws-sigv4 (~> 1.1) 14 | jmespath (~> 1.0) 15 | aws-sdk-kms (1.41.0) 16 | aws-sdk-core (~> 3, >= 3.109.0) 17 | aws-sigv4 (~> 1.1) 18 | aws-sdk-s3 (1.87.0) 19 | aws-sdk-core (~> 3, >= 3.109.0) 20 | aws-sdk-kms (~> 1) 21 | aws-sigv4 (~> 1.1) 22 | aws-sigv4 (1.2.2) 23 | aws-eventstream (~> 1, >= 1.0.2) 24 | babosa (1.0.4) 25 | claide (1.0.3) 26 | colored (1.2) 27 | colored2 (3.1.2) 28 | commander-fastlane (4.4.6) 29 | highline (~> 1.7.2) 30 | declarative (0.0.20) 31 | declarative-option (0.1.0) 32 | digest-crc (0.6.3) 33 | rake (>= 12.0.0, < 14.0.0) 34 | domain_name (0.5.20190701) 35 | unf (>= 0.0.5, < 1.0.0) 36 | dotenv (2.7.6) 37 | emoji_regex (3.2.1) 38 | excon (0.78.1) 39 | faraday (1.3.0) 40 | faraday-net_http (~> 1.0) 41 | multipart-post (>= 1.2, < 3) 42 | ruby2_keywords 43 | faraday-cookie_jar (0.0.7) 44 | faraday (>= 0.8.0) 45 | http-cookie (~> 1.0.0) 46 | faraday-net_http (1.0.1) 47 | faraday_middleware (1.0.0) 48 | faraday (~> 1.0) 49 | fastimage (2.2.1) 50 | fastlane (2.171.0) 51 | CFPropertyList (>= 2.3, < 4.0.0) 52 | addressable (>= 2.3, < 3.0.0) 53 | aws-sdk-s3 (~> 1.0) 54 | babosa (>= 1.0.3, < 2.0.0) 55 | bundler (>= 1.12.0, < 3.0.0) 56 | colored 57 | commander-fastlane (>= 4.4.6, < 5.0.0) 58 | dotenv (>= 2.1.1, < 3.0.0) 59 | emoji_regex (>= 0.1, < 4.0) 60 | excon (>= 0.71.0, < 1.0.0) 61 | faraday (~> 1.0) 62 | faraday-cookie_jar (~> 0.0.6) 63 | faraday_middleware (~> 1.0) 64 | fastimage (>= 2.1.0, < 3.0.0) 65 | gh_inspector (>= 1.1.2, < 2.0.0) 66 | google-api-client (>= 0.37.0, < 0.39.0) 67 | google-cloud-storage (>= 1.15.0, < 2.0.0) 68 | highline (>= 1.7.2, < 2.0.0) 69 | json (< 3.0.0) 70 | jwt (>= 2.1.0, < 3) 71 | mini_magick (>= 4.9.4, < 5.0.0) 72 | multipart-post (~> 2.0.0) 73 | plist (>= 3.1.0, < 4.0.0) 74 | rubyzip (>= 2.0.0, < 3.0.0) 75 | security (= 0.1.3) 76 | simctl (~> 1.6.3) 77 | slack-notifier (>= 2.0.0, < 3.0.0) 78 | terminal-notifier (>= 2.0.0, < 3.0.0) 79 | terminal-table (>= 1.4.5, < 2.0.0) 80 | tty-screen (>= 0.6.3, < 1.0.0) 81 | tty-spinner (>= 0.8.0, < 1.0.0) 82 | word_wrap (~> 1.0.0) 83 | xcodeproj (>= 1.13.0, < 2.0.0) 84 | xcpretty (~> 0.3.0) 85 | xcpretty-travis-formatter (>= 0.0.3) 86 | gh_inspector (1.1.3) 87 | google-api-client (0.38.0) 88 | addressable (~> 2.5, >= 2.5.1) 89 | googleauth (~> 0.9) 90 | httpclient (>= 2.8.1, < 3.0) 91 | mini_mime (~> 1.0) 92 | representable (~> 3.0) 93 | retriable (>= 2.0, < 4.0) 94 | signet (~> 0.12) 95 | google-apis-core (0.2.0) 96 | addressable (~> 2.5, >= 2.5.1) 97 | googleauth (~> 0.14) 98 | httpclient (>= 2.8.1, < 3.0) 99 | mini_mime (~> 1.0) 100 | representable (~> 3.0) 101 | retriable (>= 2.0, < 4.0) 102 | rexml 103 | signet (~> 0.14) 104 | google-apis-iamcredentials_v1 (0.1.0) 105 | google-apis-core (~> 0.1) 106 | google-apis-storage_v1 (0.1.0) 107 | google-apis-core (~> 0.1) 108 | google-cloud-core (1.5.0) 109 | google-cloud-env (~> 1.0) 110 | google-cloud-errors (~> 1.0) 111 | google-cloud-env (1.4.0) 112 | faraday (>= 0.17.3, < 2.0) 113 | google-cloud-errors (1.0.1) 114 | google-cloud-storage (1.30.0) 115 | addressable (~> 2.5) 116 | digest-crc (~> 0.4) 117 | google-apis-iamcredentials_v1 (~> 0.1) 118 | google-apis-storage_v1 (~> 0.1) 119 | google-cloud-core (~> 1.2) 120 | googleauth (~> 0.9) 121 | mini_mime (~> 1.0) 122 | googleauth (0.14.0) 123 | faraday (>= 0.17.3, < 2.0) 124 | jwt (>= 1.4, < 3.0) 125 | memoist (~> 0.16) 126 | multi_json (~> 1.11) 127 | os (>= 0.9, < 2.0) 128 | signet (~> 0.14) 129 | highline (1.7.10) 130 | http-cookie (1.0.3) 131 | domain_name (~> 0.5) 132 | httpclient (2.8.3) 133 | jmespath (1.6.1) 134 | json (2.5.1) 135 | jwt (2.2.2) 136 | memoist (0.16.2) 137 | mini_magick (4.11.0) 138 | mini_mime (1.0.2) 139 | multi_json (1.15.0) 140 | multipart-post (2.0.0) 141 | nanaimo (0.3.0) 142 | naturally (2.2.0) 143 | os (1.1.1) 144 | plist (3.6.0) 145 | public_suffix (4.0.6) 146 | rake (13.0.3) 147 | representable (3.0.4) 148 | declarative (< 0.1.0) 149 | declarative-option (< 0.2.0) 150 | uber (< 0.2.0) 151 | retriable (3.1.2) 152 | rexml (3.2.5) 153 | rouge (2.0.7) 154 | ruby2_keywords (0.0.2) 155 | rubyzip (2.3.0) 156 | security (0.1.3) 157 | signet (0.14.0) 158 | addressable (~> 2.3) 159 | faraday (>= 0.17.3, < 2.0) 160 | jwt (>= 1.5, < 3.0) 161 | multi_json (~> 1.10) 162 | simctl (1.6.8) 163 | CFPropertyList 164 | naturally 165 | slack-notifier (2.3.2) 166 | terminal-notifier (2.0.0) 167 | terminal-table (1.8.0) 168 | unicode-display_width (~> 1.1, >= 1.1.1) 169 | tty-cursor (0.7.1) 170 | tty-screen (0.8.1) 171 | tty-spinner (0.9.3) 172 | tty-cursor (~> 0.7) 173 | uber (0.1.0) 174 | unf (0.1.4) 175 | unf_ext 176 | unf_ext (0.0.7.7) 177 | unicode-display_width (1.7.0) 178 | word_wrap (1.0.0) 179 | xcodeproj (1.19.0) 180 | CFPropertyList (>= 2.3.3, < 4.0) 181 | atomos (~> 0.1.3) 182 | claide (>= 1.0.2, < 2.0) 183 | colored2 (~> 3.1) 184 | nanaimo (~> 0.3.0) 185 | xcpretty (0.3.0) 186 | rouge (~> 2.0.7) 187 | xcpretty-travis-formatter (1.0.1) 188 | xcpretty (~> 0.2, >= 0.0.7) 189 | 190 | PLATFORMS 191 | x86_64-linux 192 | 193 | DEPENDENCIES 194 | fastlane 195 | 196 | BUNDLED WITH 197 | 2.2.4 198 | -------------------------------------------------------------------------------- /PRIVACY.md: -------------------------------------------------------------------------------- 1 | 2 | ## Privacy policy 3 | 4 | **Bodhi Timer does not collect, log or share your personal information.** 5 | 6 | 7 | The timer stores your settings on the device, they are never uploaded or shared anywhere. 8 | 9 | The Google Play Store collects data on crashes, you can opt-out of this in the Play Store settings. These crash reports are anonymized and contain no identifiable information. 10 | 11 | If you have any questions or concerns about this privacy notice, or our practices with regards to your personal information, please contact us at bodhitimer@riseup.net. 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | Bodhi Timer is an elegant, minimalist countdown timer. 3 | It is designed mainly for use as a meditation timer but can easily be used for any similar purpose. 4 | This app is free and developed as [open-source](https://github.com/yuttadhammo/BodhiTimer) software. 5 | It collects no data and uses the minimal permissions necessary. 6 | 7 | **Want to help with translating the app? [It's easy](https://www.transifex.com/bodhi-timer/bodhi-timer-app/)** 8 | 9 | # Install 10 | [Get it on F-Droid](https://f-droid.org/en/packages/org.yuttadhammo.BodhiTimer/) 11 | 12 | # Screenshots 13 | 14 | | Timer      | Setting up timer | Customization | Settings | 15 | | :--: | :--: | :--: | :--: | 16 | | ![Timer](/fastlane/metadata/android/en-US/images/phoneScreenshots/1_en-US.png) | ![Setting up timer](/fastlane/metadata/android/en-US/images/phoneScreenshots/2_en-US.png) | ![Customization](/fastlane/metadata/android/en-US/images/phoneScreenshots/3_en-US.png) | ![Settings](/fastlane/metadata/android/en-US/images/phoneScreenshots/4_en-US.png) | 17 | 18 | ## How to use 19 | 20 | Set the time via the clock icon in the top left. You may set presets by choosing a time then holding down on one of the three preset buttons. 21 | 22 | Pause / resume via the button in the bottom left, and stop the timer via the button in the bottom right. The top right button is the preference button. 23 | 24 | Animation may be toggled between an image and one of four circle animations, chosen from the preferences screen. 25 | 26 | It uses Android's built-in notification system to trigger the alarm, which means it works even when your device is asleep. 27 | 28 | ## Features 29 | 30 | - minimalist full screen UI, no clutter 31 | - uses scroll and fling gestures to set the time 32 | - set up to three presets on the time chooser 33 | 34 | - displays two animation types: fade in static image (defaults to Bodhi leaf) and animated Zen Enso (brush circle) 35 | - option to use custom image for fade in 36 | 37 | - option for timer auto-restarting 38 | - option to set multiple consecutive timers via the "adv" button 39 | - speech recognition via long-press on clock button (set multiple timers separated by the word "again") 40 | 41 | - includes different meditation timer sounds (Burmese bell, Tibetan bell, Tibetan singing bowls, Zen gong, and bird song) 42 | - option to use any ring tone as timer sound 43 | - option to use custom sound file as timer sound 44 | - other apps can start an alarm using a deep link like `bodhi://timer?times=60,120` 45 | 46 | - licensed under the [GPL 3+](https://www.gnu.org/licenses/gpl.html) 47 | 48 | ## Credits 49 | 50 | Some code is based on the free and open source [TeaTimer by Ralph Gootee](https://play.google.com/store/apps/details?id=goo.TeaTimer). 51 | 52 | The Enso image was drawn by Ryōnen Gensō (1646-1711). 53 | Next to the Enso she has written: 54 | "When you do understand yourself fully, 55 | there is not one thing." 56 | 57 | Singing Bowl Low sound 58 | [Recorded by juskiddink](https://freesound.org/people/juskiddink/sounds/122647/) 59 | Licensed under CC-BY-SA 3.0 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | 4 | def keystoreProperties = new Properties() 5 | def keystorePropertiesFile = rootProject.file('key.properties') 6 | if (keystorePropertiesFile.exists()) { 7 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 8 | } 9 | 10 | android { 11 | compileSdkVersion 33 12 | 13 | defaultConfig { 14 | applicationId "org.yuttadhammo.BodhiTimer" 15 | minSdkVersion 23 16 | targetSdkVersion 33 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | } 19 | 20 | signingConfigs { 21 | release { 22 | keyAlias keystoreProperties['keyAlias'] 23 | keyPassword keystoreProperties['keyPassword'] 24 | storeFile file(keystoreProperties['storeFile']) 25 | storePassword keystoreProperties['storePassword'] 26 | } 27 | } 28 | 29 | buildTypes { 30 | release { 31 | signingConfig signingConfigs.release 32 | minifyEnabled true 33 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-project.txt' 34 | } 35 | } 36 | compileOptions { 37 | sourceCompatibility JavaVersion.VERSION_1_8 38 | targetCompatibility JavaVersion.VERSION_1_8 39 | } 40 | namespace 'org.yuttadhammo.BodhiTimer' 41 | 42 | } 43 | 44 | dependencies { 45 | def lifecycle_version = '2.5.1' 46 | 47 | implementation 'androidx.vectordrawable:vectordrawable:1.1.0' 48 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" 49 | implementation "com.jakewharton.timber:timber:5.0.1" 50 | implementation 'androidx.preference:preference-ktx:1.2.0' 51 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 52 | implementation 'com.google.android.material:material:1.7.0' 53 | androidTestImplementation 'tools.fastlane:screengrab:2.1.1' 54 | implementation "androidx.core:core-ktx:1.9.0" 55 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 56 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0' 57 | androidTestImplementation 'androidx.test:runner:1.5.1' 58 | androidTestImplementation 'androidx.test:rules:1.5.0' 59 | androidTestImplementation 'tools.fastlane:screengrab:2.1.1' 60 | } 61 | 62 | repositories { 63 | mavenCentral() 64 | } 65 | -------------------------------------------------------------------------------- /app/proguard-project.txt: -------------------------------------------------------------------------------- 1 | # To enable ProGuard in your project, edit project.properties 2 | # to define the proguard.config property as described in that file. 3 | # 4 | # Add project specific ProGuard rules here. 5 | # By default, the flags in this file are appended to flags specified 6 | # in ${sdk.dir}/tools/proguard/proguard-android.txt 7 | # You can edit the include path and order by changing the ProGuard 8 | # include property in project.properties. 9 | # 10 | # For more details, see 11 | # http://developer.android.com/guide/developing/tools/proguard.html 12 | 13 | # Add any project specific keep options here: 14 | 15 | # If your project uses WebView with JS, uncomment the following 16 | # and specify the fully qualified class name to the JavaScript interface 17 | # class: 18 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 19 | # public *; 20 | #} 21 | -------------------------------------------------------------------------------- /app/src/androidTest/java/org/yuttadhammo/BodhiTimer/TimerActivityTest.kt: -------------------------------------------------------------------------------- 1 | package org.yuttadhammo.BodhiTimer 2 | 3 | 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.test.espresso.Espresso.onView 7 | import androidx.test.espresso.action.ViewActions.click 8 | import androidx.test.espresso.action.ViewActions.scrollTo 9 | import androidx.test.espresso.assertion.ViewAssertions.matches 10 | import androidx.test.espresso.matcher.ViewMatchers.* 11 | import androidx.test.filters.LargeTest 12 | import androidx.test.rule.ActivityTestRule 13 | import androidx.test.runner.AndroidJUnit4 14 | import org.hamcrest.Description 15 | import org.hamcrest.Matcher 16 | import org.hamcrest.Matchers.allOf 17 | import org.hamcrest.TypeSafeMatcher 18 | import org.junit.Rule 19 | import org.junit.Test 20 | import org.junit.runner.RunWith 21 | import tools.fastlane.screengrab.Screengrab 22 | 23 | @LargeTest 24 | @RunWith(AndroidJUnit4::class) 25 | class TimerActivityTest { 26 | 27 | @Rule 28 | @JvmField 29 | var mActivityTestRule = ActivityTestRule(TimerActivity::class.java) 30 | 31 | @Test 32 | fun timerActivityTest() { 33 | Screengrab.screenshot("main") 34 | val appCompatImageButton = onView( 35 | allOf( 36 | withId(R.id.setButton), withContentDescription("Set"), 37 | childAtPosition( 38 | allOf( 39 | withId(R.id.mainLayout), 40 | childAtPosition( 41 | withId(android.R.id.content), 42 | 0 43 | ) 44 | ), 45 | 4 46 | ), 47 | isDisplayed() 48 | ) 49 | ) 50 | appCompatImageButton.perform(click()) 51 | 52 | //val gallery = onView() 53 | 54 | val button = onView( 55 | allOf( 56 | withId(R.id.btnOk), withText("OK"), 57 | childAtPosition( 58 | allOf( 59 | withId(R.id.button_cont), 60 | childAtPosition( 61 | withId(R.id.container), 62 | 6 63 | ) 64 | ), 65 | 2 66 | ) 67 | ) 68 | ) 69 | button.perform(scrollTo(), click()) 70 | 71 | val textView = onView( 72 | allOf( 73 | withId(R.id.text_top), 74 | withParent( 75 | allOf( 76 | withId(R.id.mainLayout), 77 | withParent(withId(android.R.id.content)) 78 | ) 79 | ), 80 | isDisplayed() 81 | ) 82 | ) 83 | textView.check(matches(withText("0"))) 84 | 85 | } 86 | 87 | private fun childAtPosition( 88 | parentMatcher: Matcher, position: Int 89 | ): Matcher { 90 | 91 | return object : TypeSafeMatcher() { 92 | override fun describeTo(description: Description) { 93 | description.appendText("Child at position $position in parent ") 94 | parentMatcher.describeTo(description) 95 | } 96 | 97 | public override fun matchesSafely(view: View): Boolean { 98 | val parent = view.parent 99 | return parent is ViewGroup && parentMatcher.matches(parent) 100 | && view == parent.getChildAt(position) 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /app/src/androidTest/java/org/yuttadhammo/BodhiTimer/TimerActivityTest2.java: -------------------------------------------------------------------------------- 1 | package org.yuttadhammo.BodhiTimer; 2 | 3 | 4 | import static androidx.test.espresso.Espresso.onView; 5 | import static androidx.test.espresso.action.ViewActions.click; 6 | import static androidx.test.espresso.action.ViewActions.scrollTo; 7 | import static androidx.test.espresso.assertion.ViewAssertions.matches; 8 | import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; 9 | import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription; 10 | import static androidx.test.espresso.matcher.ViewMatchers.withId; 11 | import static androidx.test.espresso.matcher.ViewMatchers.withParent; 12 | import static androidx.test.espresso.matcher.ViewMatchers.withText; 13 | import static org.hamcrest.Matchers.allOf; 14 | 15 | import android.view.View; 16 | import android.view.ViewGroup; 17 | import android.view.ViewParent; 18 | 19 | import androidx.test.espresso.ViewInteraction; 20 | import androidx.test.filters.LargeTest; 21 | import androidx.test.rule.ActivityTestRule; 22 | import androidx.test.runner.AndroidJUnit4; 23 | 24 | import org.hamcrest.Description; 25 | import org.hamcrest.Matcher; 26 | import org.hamcrest.TypeSafeMatcher; 27 | import org.junit.Rule; 28 | import org.junit.Test; 29 | import org.junit.runner.RunWith; 30 | 31 | import tools.fastlane.screengrab.Screengrab; 32 | 33 | @LargeTest 34 | @RunWith(AndroidJUnit4.class) 35 | public class TimerActivityTest2 { 36 | 37 | @Rule 38 | public ActivityTestRule mActivityTestRule = new ActivityTestRule<>(TimerActivity.class); 39 | 40 | private static Matcher childAtPosition( 41 | final Matcher parentMatcher, final int position) { 42 | 43 | return new TypeSafeMatcher() { 44 | @Override 45 | public void describeTo(Description description) { 46 | description.appendText("Child at position " + position + " in parent "); 47 | parentMatcher.describeTo(description); 48 | } 49 | 50 | @Override 51 | public boolean matchesSafely(View view) { 52 | ViewParent parent = view.getParent(); 53 | return parent instanceof ViewGroup && parentMatcher.matches(parent) 54 | && view.equals(((ViewGroup) parent).getChildAt(position)); 55 | } 56 | }; 57 | } 58 | 59 | @Test 60 | public void timerActivityTest2() { 61 | Screengrab.screenshot("main"); 62 | ViewInteraction appCompatImageButton = onView( 63 | allOf(withId(R.id.setButton), withContentDescription("Set"), 64 | childAtPosition( 65 | allOf(withId(R.id.mainLayout), 66 | childAtPosition( 67 | withId(android.R.id.content), 68 | 0)), 69 | 4), 70 | isDisplayed())); 71 | appCompatImageButton.perform(click()); 72 | 73 | ViewInteraction button = onView( 74 | allOf(withId(R.id.btnOk), withText("OK"), 75 | childAtPosition( 76 | allOf(withId(R.id.button_cont), 77 | childAtPosition( 78 | withId(R.id.container), 79 | 6)), 80 | 2))); 81 | button.perform(scrollTo(), click()); 82 | 83 | ViewInteraction textView = onView( 84 | allOf(withId(R.id.text_top), 85 | withParent(allOf(withId(R.id.mainLayout), 86 | withParent(withId(android.R.id.content)))), 87 | isDisplayed())); 88 | textView.check(matches(withText("0"))); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 20 | 21 | 27 | 32 | 33 | 34 | 35 | 36 | 37 | 40 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 57 | 58 | 59 | 64 | 65 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /app/src/main/java/org/yuttadhammo/BodhiTimer/Animation/BodhiLeaf.kt: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of Bodhi Timer. 3 | 4 | Bodhi Timer is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Bodhi Timer is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with Bodhi Timer. If not, see . 16 | */ 17 | package org.yuttadhammo.BodhiTimer.Animation 18 | 19 | import android.content.Context 20 | import android.graphics.Bitmap 21 | import android.graphics.BitmapFactory 22 | import android.graphics.Canvas 23 | import android.graphics.Color 24 | import android.graphics.Paint 25 | import android.graphics.PorterDuff 26 | import android.graphics.PorterDuffXfermode 27 | import android.graphics.Rect 28 | import android.net.Uri 29 | import android.os.ParcelFileDescriptor 30 | import org.yuttadhammo.BodhiTimer.Animation.TimerAnimation.TimerDrawing 31 | import org.yuttadhammo.BodhiTimer.R 32 | import org.yuttadhammo.BodhiTimer.Util.Settings 33 | import timber.log.Timber 34 | import java.io.IOException 35 | 36 | 37 | internal class BodhiLeaf(context: Context) : TimerDrawing { 38 | private var mBitmap: Bitmap? = null 39 | private val mWidth: Int 40 | private val mHeight: Int 41 | private val mProgressPaint: Paint = Paint() 42 | private var isCustom: Boolean = false 43 | 44 | init { 45 | mProgressPaint.color = Color.BLACK 46 | mProgressPaint.alpha = 255 47 | mProgressPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN) 48 | 49 | // Get custom bitmap 50 | mBitmap = if (!Settings.customBmp || Settings.bmpUri.isEmpty()) { 51 | isCustom = false 52 | BitmapFactory.decodeResource(context.resources, R.drawable.leaf) 53 | } else { 54 | isCustom = true 55 | val bmpUrl = Settings.bmpUri 56 | val selectedImage = Uri.parse(bmpUrl) 57 | val resolver = context.contentResolver 58 | val readOnlyMode = "r" 59 | var file: ParcelFileDescriptor? = null 60 | try { 61 | file = resolver.openFileDescriptor(selectedImage, readOnlyMode) 62 | BitmapFactory.decodeFileDescriptor(file?.fileDescriptor) 63 | } catch (e: IOException) { 64 | Timber.e(e) 65 | BitmapFactory.decodeResource(context.resources, R.drawable.leaf) 66 | } finally { 67 | file?.close() 68 | } 69 | } 70 | mHeight = mBitmap!!.height 71 | mWidth = mBitmap!!.width 72 | } 73 | 74 | /** 75 | * Updates the image to be in sync with the current time 76 | * 77 | * @param time in milliseconds 78 | * @param max the original time set in milliseconds 79 | */ 80 | override fun updateImage(canvas: Canvas, time: Int, max: Int) { 81 | canvas.save() 82 | val w = canvas.clipBounds.width() 83 | val h = canvas.clipBounds.height() 84 | val rs = Rect(0, 0, mWidth, mHeight) 85 | val rd: Rect 86 | if (mHeight / mWidth > h / w) { // image skinnier than canvas 87 | val nWidth = (mWidth * (h.toFloat() / mHeight.toFloat())).toInt() 88 | val shift = (w - nWidth) / 2 89 | rd = Rect(shift, 0, nWidth + shift, h) 90 | } else { // image fatter than or equal to canvas 91 | val nHeight = (mHeight * (w.toFloat() / mWidth.toFloat())).toInt() 92 | var shift = (h - nHeight) / 2 93 | // Special tweak to visually center the leaf image: 94 | if (!isCustom) shift -= 90 95 | rd = Rect(0, shift, w, nHeight + shift) 96 | } 97 | 98 | val p = if (max != 0) (time / max.toFloat()) else 0F 99 | val alpha = (255 - 255 * p).toInt() 100 | val color = Paint() 101 | color.alpha = alpha 102 | canvas.drawBitmap(mBitmap!!, rs, rd, color) 103 | canvas.restore() 104 | } 105 | 106 | override fun configure(isEditMode: Boolean) { 107 | // Void 108 | } 109 | } -------------------------------------------------------------------------------- /app/src/main/java/org/yuttadhammo/BodhiTimer/Animation/TimerAnimation.kt: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of Bodhi Timer. 3 | 4 | Bodhi Timer is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Bodhi Timer is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with Bodhi Timer. If not, see . 16 | */ 17 | package org.yuttadhammo.BodhiTimer.Animation 18 | 19 | import android.content.Context 20 | import android.content.SharedPreferences 21 | import android.graphics.Canvas 22 | import android.util.AttributeSet 23 | import androidx.appcompat.widget.AppCompatImageView 24 | import timber.log.Timber 25 | import java.io.FileNotFoundException 26 | import java.util.Vector 27 | 28 | class TimerAnimation : AppCompatImageView { 29 | var mDrawings = Vector() 30 | var mIndex = 1 31 | var mLastTime = 0 32 | var mLastMax = 0 33 | var prefs: SharedPreferences? = null 34 | private val mContext: Context 35 | 36 | interface TimerDrawing { 37 | /** 38 | * Updates the image to be in sync with the current time 39 | * 40 | * @param time in milliseconds 41 | * @param max the original time set in milliseconds 42 | */ 43 | fun updateImage(canvas: Canvas, time: Int, max: Int) 44 | fun configure(isEditMode: Boolean) 45 | } 46 | 47 | constructor(context: Context) : super(context) { 48 | mContext = context 49 | createDrawings(mContext) 50 | } 51 | 52 | constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { 53 | mContext = context 54 | createDrawings(mContext) 55 | } 56 | 57 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( 58 | context, 59 | attrs, 60 | defStyleAttr 61 | ) { 62 | mContext = context 63 | createDrawings(mContext) 64 | } 65 | 66 | private fun createDrawings(mContext: Context) { 67 | mDrawings = Vector() 68 | mDrawings.add(BodhiLeaf(mContext)) 69 | mDrawings.add(CircleAnimation(mContext)) 70 | } 71 | 72 | @set:Throws(FileNotFoundException::class) 73 | var index: Int 74 | get() = mIndex 75 | set(i) { 76 | Timber.d("Setting animation index to $i") 77 | mIndex = i.coerceAtLeast(0).coerceAtMost(mDrawings.size) 78 | invalidate() 79 | } 80 | 81 | fun updateImage(time: Int, max: Int) { 82 | mLastTime = time 83 | mLastMax = max 84 | invalidate() 85 | } 86 | 87 | public override fun onDraw(canvas: Canvas) { 88 | if (mIndex < 0 || mIndex >= mDrawings.size) mIndex = 0 89 | if (isInEditMode) { 90 | createDrawings(context) 91 | configure() 92 | } 93 | val drawing = mDrawings[mIndex] 94 | drawing.updateImage(canvas, mLastTime, mLastMax) 95 | } 96 | 97 | fun configure() { 98 | for (drawing in mDrawings) { 99 | drawing.configure(isInEditMode) 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /app/src/main/java/org/yuttadhammo/BodhiTimer/BodhiApp.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * BodhiApp.kt 3 | * Copyright (C) 2014-2022 BodhiTimer developers 4 | * 5 | * Distributed under terms of the GNU GPLv3 license. 6 | */ 7 | 8 | package org.yuttadhammo.BodhiTimer 9 | 10 | import android.app.Application 11 | import android.content.BroadcastReceiver 12 | import android.content.Context 13 | import android.content.Intent 14 | import android.content.IntentFilter 15 | import android.os.Build 16 | import android.os.StrictMode 17 | import android.os.StrictMode.ThreadPolicy 18 | import android.os.StrictMode.VmPolicy 19 | import org.yuttadhammo.BodhiTimer.Const.BroadcastTypes 20 | import org.yuttadhammo.BodhiTimer.Models.AlarmTaskManager 21 | import timber.log.Timber 22 | import timber.log.Timber.DebugTree 23 | 24 | /** 25 | * The Main class of the Application 26 | */ 27 | 28 | class BodhiApp : Application() { 29 | 30 | var alarmTaskManager: AlarmTaskManager? = null 31 | private var initiated: Boolean = false 32 | 33 | init { 34 | instance = this 35 | if (BuildConfig.DEBUG) { 36 | StrictMode.setThreadPolicy(ThreadPolicy.Builder().detectAll().penaltyLog().build()) 37 | StrictMode.setVmPolicy(VmPolicy.Builder().detectAllExceptSocket().penaltyLog().build()) 38 | } 39 | } 40 | 41 | 42 | override fun onCreate() { 43 | initiated = true 44 | super.onCreate() 45 | 46 | if (BuildConfig.DEBUG) { 47 | Timber.plant(DebugTree()) 48 | } 49 | 50 | Timber.d("onCreate called") 51 | 52 | alarmTaskManager = AlarmTaskManager(this) 53 | 54 | val filter = IntentFilter() 55 | filter.addAction(BroadcastTypes.BROADCAST_END) 56 | registerReceiver(alarmEndReceiver, filter) 57 | } 58 | 59 | 60 | // Should move to Manager.... 61 | // receiver to get restart 62 | private val alarmEndReceiver: BroadcastReceiver = object : BroadcastReceiver() { 63 | override fun onReceive(context: Context, intent: Intent) { 64 | Timber.v("Received app alarm callback in App scope") 65 | Timber.d("id " + intent.getIntExtra("id", -1)) 66 | alarmTaskManager!!.onAlarmEnd(intent.getIntExtra("id", -1)) 67 | } 68 | } 69 | 70 | companion object { 71 | var instance: BodhiApp? = null 72 | 73 | fun applicationContext(): Context { 74 | return instance!!.applicationContext 75 | } 76 | } 77 | } 78 | 79 | private fun VmPolicy.Builder.detectAllExceptSocket(): VmPolicy.Builder { 80 | detectLeakedSqlLiteObjects() 81 | detectActivityLeaks() 82 | detectLeakedClosableObjects() 83 | detectLeakedRegistrationObjects() 84 | detectFileUriExposure() 85 | 86 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 87 | detectContentUriWithoutPermission() 88 | } 89 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 90 | detectCredentialProtectedWhileLocked() 91 | } 92 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 93 | detectUnsafeIntentLaunch() 94 | detectIncorrectContextUse() 95 | } 96 | return this 97 | } 98 | -------------------------------------------------------------------------------- /app/src/main/java/org/yuttadhammo/BodhiTimer/Const/BroadcastTypes.kt: -------------------------------------------------------------------------------- 1 | package org.yuttadhammo.BodhiTimer.Const 2 | 3 | object BroadcastTypes { 4 | const val BROADCAST_UPDATE = "org.yuttadhammo.BodhiTimer.ACTION_CLOCK_UPDATE" 5 | const val BROADCAST_STOP = "org.yuttadhammo.BodhiTimer.ACTION_CLOCK_CANCEL" 6 | const val BROADCAST_PLAY = "org.yuttadhammo.BodhiTimer.ACTION_PLAY" 7 | const val BROADCAST_RESET = "org.yuttadhammo.BodhiTimer.RESTART" 8 | const val BROADCAST_END = "org.yuttadhammo.BodhiTimer.ALARMEND" 9 | } -------------------------------------------------------------------------------- /app/src/main/java/org/yuttadhammo/BodhiTimer/Const/SessionTypes.kt: -------------------------------------------------------------------------------- 1 | package org.yuttadhammo.BodhiTimer.Const 2 | 3 | enum class SessionTypes { 4 | REAL, PREPARATION, INVALID 5 | } -------------------------------------------------------------------------------- /app/src/main/java/org/yuttadhammo/BodhiTimer/Const/TimerState.kt: -------------------------------------------------------------------------------- 1 | package org.yuttadhammo.BodhiTimer.Const 2 | 3 | object TimerState { 4 | const val RUNNING = 0 5 | const val STOPPED = 1 6 | const val PAUSED = 2 7 | 8 | fun getText(number: Int): String { 9 | return when (number) { 10 | 0 -> "RUNNING" 11 | 1 -> "STOPPED" 12 | 2 -> "PAUSED" 13 | else -> "UNDEFINED" 14 | } 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/java/org/yuttadhammo/BodhiTimer/Models/AlarmTask.kt: -------------------------------------------------------------------------------- 1 | package org.yuttadhammo.BodhiTimer.Models 2 | 3 | import android.app.AlarmManager 4 | import android.app.AlarmManager.AlarmClockInfo 5 | import android.app.PendingIntent 6 | import android.content.Context 7 | import android.content.Intent 8 | import android.net.Uri 9 | import android.os.Build 10 | import android.provider.Settings 11 | import androidx.core.content.ContextCompat 12 | import org.yuttadhammo.BodhiTimer.Const.BroadcastTypes 13 | import org.yuttadhammo.BodhiTimer.Const.SessionTypes 14 | import org.yuttadhammo.BodhiTimer.Service.TimerReceiver 15 | import timber.log.Timber 16 | 17 | 18 | data class AlarmTask(val context: Context, val offset: Int, val duration: Int) { 19 | 20 | // The Android system alarm manager 21 | private var mAlarmMgr: AlarmManager = 22 | context.getSystemService(Context.ALARM_SERVICE) as AlarmManager 23 | private var mPendingIntent: PendingIntent? = null 24 | 25 | var sessionType: SessionTypes = SessionTypes.INVALID 26 | var uri: String = "" 27 | var id: Int = 0 28 | 29 | fun run() { 30 | if (!ensureNecessaryPermission()) return 31 | val intent = Intent(context, TimerReceiver::class.java) 32 | intent.putExtra("offset", offset) 33 | intent.putExtra("duration", duration) 34 | intent.putExtra("uri", uri) 35 | intent.putExtra("id", id) 36 | intent.action = BroadcastTypes.BROADCAST_END 37 | val time = duration + offset 38 | Timber.i( 39 | "Running new alarm task $id, " + 40 | "uri: $uri, type: $sessionType " + 41 | "due in ${time / 1000}, duration $duration" 42 | ) 43 | 44 | mPendingIntent = PendingIntent.getBroadcast( 45 | context, 46 | id, 47 | intent, 48 | PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE 49 | ) 50 | 51 | val alarmInfoIntent = Intent(context, TimerReceiver::class.java) 52 | val pendingAlarmInfo = PendingIntent.getBroadcast(context, 53 | id + 1000, alarmInfoIntent, PendingIntent.FLAG_IMMUTABLE) 54 | val info = AlarmClockInfo(System.currentTimeMillis() + time, pendingAlarmInfo) 55 | mAlarmMgr.setAlarmClock(info, mPendingIntent) 56 | } 57 | 58 | private fun ensureNecessaryPermission(): Boolean { 59 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 60 | val alarmManager = ContextCompat.getSystemService(context, AlarmManager::class.java) 61 | if (alarmManager?.canScheduleExactAlarms() == false) { 62 | Intent().also { intent -> 63 | intent.action = Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM 64 | intent.data = Uri.fromParts("package", context.packageName, null) 65 | intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or 66 | Intent.FLAG_ACTIVITY_CLEAR_TASK 67 | context.startActivity(intent) 68 | } 69 | return false 70 | } 71 | } 72 | return true 73 | } 74 | 75 | fun cancel() { 76 | if (mPendingIntent != null) mAlarmMgr.cancel(mPendingIntent) 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/java/org/yuttadhammo/BodhiTimer/Models/TimerList.kt: -------------------------------------------------------------------------------- 1 | package org.yuttadhammo.BodhiTimer.Models 2 | 3 | import android.text.TextUtils 4 | import org.yuttadhammo.BodhiTimer.Const.SessionTypes 5 | import timber.log.Timber 6 | 7 | class TimerList { 8 | class Timer { 9 | val duration: Int 10 | val uri: String 11 | val sessionType: SessionTypes 12 | 13 | constructor(mDuration: Int, mUri: String) : super() { 14 | duration = mDuration 15 | uri = mUri 16 | sessionType = SessionTypes.REAL 17 | } 18 | 19 | constructor(mDuration: Int, mUri: String, mSessionType: SessionTypes) : super() { 20 | duration = mDuration 21 | uri = mUri 22 | sessionType = mSessionType 23 | } 24 | } 25 | 26 | val timers: ArrayList 27 | 28 | constructor(advTimeString: String) { 29 | timers = timeStringToList(advTimeString) 30 | } 31 | 32 | constructor() { 33 | timers = ArrayList() 34 | } 35 | 36 | val string: String 37 | get() = listToTimeString(timers) 38 | 39 | companion object { 40 | fun timeStringToList(advTimeString: String): ArrayList { 41 | val list = ArrayList() 42 | val advTime = advTimeString.split("^").toTypedArray() 43 | for (s in advTime) { 44 | // advTime[n] will be of format timeInMs#pathToSound 45 | val thisAdvTime = s.split("#").toTypedArray() 46 | var duration: Int 47 | try { 48 | duration = thisAdvTime[0].toInt() 49 | val timer = Timer(duration, thisAdvTime[1]) 50 | list.add(timer) 51 | } catch (e: Exception) { 52 | Timber.e(e) 53 | } 54 | } 55 | return list 56 | } 57 | 58 | fun listToTimeString(list: ArrayList): String { 59 | val stringArray = ArrayList() 60 | for (timer in list) { 61 | stringArray.add(timer.duration.toString() + "#" + timer.uri + "#" + timer.sessionType) 62 | } 63 | return TextUtils.join("^", stringArray) 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /app/src/main/java/org/yuttadhammo/BodhiTimer/Service/SoundService.kt: -------------------------------------------------------------------------------- 1 | package org.yuttadhammo.BodhiTimer.Service 2 | 3 | import android.app.Service 4 | import android.content.BroadcastReceiver 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.content.IntentFilter 8 | import android.media.MediaPlayer 9 | import android.os.Binder 10 | import android.os.IBinder 11 | import org.yuttadhammo.BodhiTimer.Const.BroadcastTypes 12 | import org.yuttadhammo.BodhiTimer.Const.BroadcastTypes.BROADCAST_PLAY 13 | import org.yuttadhammo.BodhiTimer.Util.Notifications.getServiceNotification 14 | import org.yuttadhammo.BodhiTimer.Util.Settings 15 | import org.yuttadhammo.BodhiTimer.Util.Sounds 16 | import timber.log.Timber 17 | import java.lang.ref.WeakReference 18 | 19 | class SoundService : Service() { 20 | 21 | private var lastMediaPlayer: MediaPlayer? = null 22 | private var stop: Boolean = false 23 | private var lastStamp: Long = 0L 24 | private var active: Int = 0 25 | 26 | private lateinit var soundManager: Sounds 27 | 28 | // Create the instance on the service. 29 | private val binder = LocalBinder() 30 | 31 | override fun onCreate() { 32 | super.onCreate() 33 | startForeground(1312, getServiceNotification(this)) 34 | Timber.v("here") 35 | } 36 | 37 | override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { 38 | startForeground(1312, getServiceNotification(this)) 39 | 40 | soundManager = Sounds(applicationContext) 41 | 42 | val action = intent.action 43 | 44 | if (BROADCAST_PLAY == action) { 45 | Timber.v("Received Play Start") 46 | playIntent(intent) 47 | } 48 | 49 | val filter = IntentFilter() 50 | filter.addAction(BroadcastTypes.BROADCAST_END) 51 | registerReceiver(alarmEndReceiver, filter) 52 | 53 | val filter2 = IntentFilter() 54 | filter2.addAction(BroadcastTypes.BROADCAST_STOP) 55 | registerReceiver(stopReceiver, filter2) 56 | 57 | return START_NOT_STICKY 58 | } 59 | 60 | fun playIntent(intent: Intent) { 61 | val volume = Settings.toneVolume 62 | val stamp = intent.getLongExtra("stamp", 0L) 63 | val uri = intent.getStringExtra("uri") 64 | 65 | if (uri != null && stamp != lastStamp) { 66 | lastStamp = stamp 67 | active++ 68 | 69 | lastMediaPlayer = soundManager.play(uri, volume) 70 | 71 | lastMediaPlayer?.setOnCompletionListener { mp -> 72 | Timber.v("Resetting media player...") 73 | mp.reset() 74 | mp.release() 75 | active-- 76 | 77 | if (stop && active < 1) { 78 | Timber.v("Stopping service") 79 | stopSelf() 80 | } 81 | } 82 | 83 | } else { 84 | Timber.v("Skipping play") 85 | } 86 | } 87 | 88 | override fun onBind(intent: Intent): IBinder { 89 | binder.onBind(this) 90 | return binder 91 | } 92 | 93 | override fun onDestroy() { 94 | super.onDestroy() 95 | unregisterReceiver(alarmEndReceiver) 96 | unregisterReceiver(stopReceiver) 97 | } 98 | 99 | private val alarmEndReceiver: BroadcastReceiver = object : BroadcastReceiver() { 100 | override fun onReceive(context: Context, intent: Intent) { 101 | Timber.v("Received Broadcast") 102 | playIntent(intent) 103 | } 104 | } 105 | 106 | private val stopReceiver: BroadcastReceiver = object : BroadcastReceiver() { 107 | override fun onReceive(context: Context, intent: Intent) { 108 | Timber.e("Received Stop Broadcast, active = $active") 109 | 110 | if (active == 0) { 111 | Timber.v("Stopping service") 112 | stopSelf() 113 | } 114 | stop = true 115 | } 116 | } 117 | 118 | class LocalBinder : Binder() { 119 | private var weakService: WeakReference? = null 120 | 121 | // Inject service instance to weak reference. 122 | fun onBind(service: SoundService) { 123 | weakService = WeakReference(service) 124 | } 125 | 126 | fun getService(): SoundService? { 127 | return weakService?.get() 128 | } 129 | } 130 | } -------------------------------------------------------------------------------- /app/src/main/java/org/yuttadhammo/BodhiTimer/Service/TTSService.kt: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of Bodhi Timer. 3 | 4 | Bodhi Timer is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Bodhi Timer is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with Bodhi Timer. If not, see . 16 | */ 17 | package org.yuttadhammo.BodhiTimer.Service 18 | 19 | import android.app.Service 20 | import android.content.Intent 21 | import android.os.IBinder 22 | import android.speech.tts.TextToSpeech 23 | import android.speech.tts.TextToSpeech.OnInitListener 24 | import timber.log.Timber 25 | 26 | class TTSService : Service(), OnInitListener { 27 | private var mTts: TextToSpeech? = null 28 | private var spokenText: String? = null 29 | override fun onCreate() { 30 | mTts = TextToSpeech(this, this) 31 | } 32 | 33 | override fun onInit(status: Int) { 34 | Timber.i("initializing TTSService") 35 | if (status == TextToSpeech.SUCCESS) { 36 | val hashAudio = HashMap() 37 | hashAudio[TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID] = "english" 38 | Timber.i("speaking: $spokenText") 39 | mTts!!.setOnUtteranceCompletedListener { s: String? -> 40 | Timber.d("utterance completed") 41 | stopSelf() 42 | } 43 | mTts!!.speak(spokenText, TextToSpeech.QUEUE_FLUSH, hashAudio) 44 | } else Timber.e("error initializing TTSService") 45 | } 46 | 47 | override fun onDestroy() { 48 | if (mTts != null) { 49 | mTts!!.stop() 50 | mTts!!.shutdown() 51 | } 52 | super.onDestroy() 53 | } 54 | 55 | override fun onBind(arg0: Intent): IBinder? { 56 | return null 57 | } 58 | 59 | override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { 60 | spokenText = intent.getStringExtra("spoken_text") 61 | Timber.d(spokenText!!) 62 | return START_STICKY 63 | } 64 | } -------------------------------------------------------------------------------- /app/src/main/java/org/yuttadhammo/BodhiTimer/Service/TimerReceiver.kt: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of Bodhi Timer. 3 | 4 | Bodhi Timer is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Bodhi Timer is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with Bodhi Timer. If not, see . 16 | */ 17 | package org.yuttadhammo.BodhiTimer.Service 18 | 19 | import android.content.BroadcastReceiver 20 | import android.content.Context 21 | import android.content.Intent 22 | import androidx.preference.PreferenceManager 23 | import org.yuttadhammo.BodhiTimer.Const.BroadcastTypes.BROADCAST_END 24 | import org.yuttadhammo.BodhiTimer.Const.BroadcastTypes.BROADCAST_PLAY 25 | import org.yuttadhammo.BodhiTimer.Util.Notifications.show 26 | import org.yuttadhammo.BodhiTimer.Util.Sounds 27 | import timber.log.Timber 28 | 29 | 30 | // This class handles the alarm callback 31 | class TimerReceiver : BroadcastReceiver() { 32 | private var notificationUri: String? = null 33 | private var stamp: Long = 0 34 | private var volume: Int = 100 35 | lateinit var mContext: Context 36 | 37 | override fun onReceive(context: Context, mIntent: Intent) { 38 | Timber.v("Received system alarm callback ") 39 | 40 | stamp = System.currentTimeMillis() 41 | mContext = context 42 | 43 | // Send Broadcast to main activity 44 | // This will be only received if the app is not stopped (or destroyed)... 45 | val broadcast = Intent() 46 | broadcast.putExtra("duration", mIntent.getIntExtra("duration", 0)) 47 | broadcast.putExtra("id", mIntent.getIntExtra("id", 0)) 48 | broadcast.putExtra("uri", mIntent.getStringExtra("uri")) 49 | broadcast.putExtra("stamp", stamp) 50 | broadcast.action = BROADCAST_END 51 | mContext.sendBroadcast(broadcast) 52 | 53 | // Show notification 54 | notificationUri = mIntent.getStringExtra("uri") 55 | val duration = mIntent.getIntExtra("duration", 0) 56 | val prefs = PreferenceManager.getDefaultSharedPreferences(mContext) 57 | val alwaysShow = prefs.getBoolean("showAlwaysNotifications", false) 58 | 59 | if (alwaysShow || notificationUri == null) { 60 | show(mContext, duration) 61 | } 62 | 63 | volume = prefs.getInt("tone_volume", 0) 64 | 65 | if (notificationUri == null) return 66 | 67 | if (!prefs.getBoolean("useOldNotification", false)) { 68 | val playIntent = getServiceIntent(mContext) 69 | 70 | try { 71 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { 72 | mContext.startForegroundService(playIntent) 73 | } else { 74 | mContext.startService(playIntent) 75 | } 76 | } catch (e: Exception) { 77 | Timber.e("Could not start service") 78 | Sounds(mContext).play(notificationUri!!, volume) 79 | } 80 | } else { 81 | Sounds(mContext).play(notificationUri!!, volume) 82 | } 83 | } 84 | 85 | 86 | private fun getServiceIntent(mContext: Context): Intent { 87 | val playIntent = Intent(mContext, SoundService::class.java) 88 | playIntent.action = BROADCAST_PLAY 89 | playIntent.putExtra("uri", notificationUri) 90 | playIntent.putExtra("volume", volume) 91 | playIntent.putExtra("stamp", stamp) 92 | return playIntent 93 | } 94 | 95 | } -------------------------------------------------------------------------------- /app/src/main/java/org/yuttadhammo/BodhiTimer/SettingsActivity.kt: -------------------------------------------------------------------------------- 1 | package org.yuttadhammo.BodhiTimer 2 | 3 | import android.content.Intent 4 | import android.content.SharedPreferences 5 | import android.media.RingtoneManager 6 | import android.net.Uri 7 | import android.os.Bundle 8 | import android.view.View 9 | import androidx.appcompat.app.AppCompatActivity 10 | import androidx.appcompat.widget.Toolbar 11 | import androidx.preference.PreferenceManager 12 | import org.yuttadhammo.BodhiTimer.Util.Settings 13 | import org.yuttadhammo.BodhiTimer.Util.Themes 14 | import timber.log.Timber 15 | 16 | class SettingsActivity : AppCompatActivity() { 17 | private var prefs: SharedPreferences? = null 18 | 19 | override fun onCreate(savedInstanceState: Bundle?) { 20 | super.onCreate(savedInstanceState) 21 | Themes.applyTheme(this) 22 | setContentView(R.layout.settings_activity) 23 | val toolbar = findViewById(R.id.toolbar) 24 | setSupportActionBar(toolbar) 25 | val actionBar = supportActionBar 26 | if (actionBar != null) { 27 | actionBar.setDisplayHomeAsUpEnabled(true) 28 | actionBar.title = getString(R.string.preferences) 29 | toolbar.setNavigationOnClickListener { v: View? -> onBackPressed() } 30 | } 31 | supportFragmentManager 32 | .beginTransaction() 33 | .replace(R.id.settings, SettingsFragment()) 34 | .commit() 35 | prefs = PreferenceManager.getDefaultSharedPreferences(baseContext) 36 | } 37 | 38 | 39 | public override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) { 40 | super.onActivityResult(requestCode, resultCode, intent) 41 | if (resultCode == RESULT_OK) { 42 | var uri = intent!!.data 43 | val uriString = intent.dataString 44 | when (requestCode) { 45 | SELECT_RINGTONE -> { 46 | uri = intent.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI) 47 | if (uri != null) { 48 | Timber.i("Got ringtone $uri") 49 | Settings.systemUri = uri.toString() 50 | } 51 | } 52 | SELECT_PRE_RINGTONE -> { 53 | uri = intent.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI) 54 | if (uri != null) { 55 | Timber.i("Got ringtone $uri") 56 | Settings.preSystemUri = uri.toString() 57 | } 58 | } 59 | SELECT_FILE -> // Get the Uri of the selected file 60 | if (uriString != null) { 61 | getPersistablePermission(uri) 62 | Timber.i("File Path: " + uri.toString()) 63 | Settings.fileUri = uri.toString() 64 | } 65 | SELECT_PRE_FILE -> // Get the Uri of the selected file 66 | if (uriString != null) { 67 | getPersistablePermission(uri) 68 | Timber.i("File Path: " + uri.toString()) 69 | Settings.preFileUri = uri.toString() 70 | } 71 | SELECT_PHOTO -> if (uri != null) { 72 | getPersistablePermission(uri) 73 | Settings.bmpUri = uri.toString() 74 | } 75 | } 76 | } 77 | } 78 | 79 | private fun getPersistablePermission(uri: Uri?) { 80 | try { 81 | contentResolver.takePersistableUriPermission( 82 | uri!!, 83 | Intent.FLAG_GRANT_READ_URI_PERMISSION 84 | ) 85 | } catch (e: Exception) { 86 | Timber.e(e.toString()) 87 | } 88 | } 89 | 90 | 91 | companion object { 92 | private const val SELECT_RINGTONE = 0 93 | private const val SELECT_FILE = 1 94 | private const val SELECT_PRE_RINGTONE = 2 95 | private const val SELECT_PRE_FILE = 3 96 | private const val SELECT_PHOTO = 4 97 | 98 | } 99 | } -------------------------------------------------------------------------------- /app/src/main/java/org/yuttadhammo/BodhiTimer/Util/Notifications.kt: -------------------------------------------------------------------------------- 1 | package org.yuttadhammo.BodhiTimer.Util 2 | 3 | import android.app.Notification 4 | import android.app.NotificationChannel 5 | import android.app.NotificationManager 6 | import android.app.PendingIntent 7 | import android.content.Context 8 | import android.content.Intent 9 | import android.content.SharedPreferences 10 | import android.os.Build 11 | import androidx.core.app.NotificationCompat 12 | import androidx.preference.PreferenceManager 13 | import org.yuttadhammo.BodhiTimer.R 14 | import org.yuttadhammo.BodhiTimer.TimerActivity 15 | import timber.log.Timber 16 | 17 | 18 | object Notifications { 19 | 20 | private const val ALARM_CHANNEL_ID = "ALARMS" 21 | private const val SERVICE_CHANNEL_ID = "SERVICE" 22 | 23 | 24 | fun show(context: Context, time: Int) { 25 | Timber.v("Showing notification... $time") 26 | 27 | // Get Notification Manager & Prefs 28 | val mNotificationManager = 29 | context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 30 | val prefs = PreferenceManager.getDefaultSharedPreferences(context) 31 | 32 | 33 | // Cancel any previous notifications 34 | mNotificationManager.cancelAll() 35 | 36 | 37 | // Construct strings 38 | val setTimeStr = Time.time2humanStr(context, time) 39 | val text = context.getText(R.string.Notification) 40 | val textLatest: CharSequence = 41 | String.format(context.getString(R.string.timer_for_x), setTimeStr) 42 | 43 | 44 | // Create the notification 45 | val mBuilder = NotificationCompat.Builder(context.applicationContext, ALARM_CHANNEL_ID) 46 | .setSmallIcon(R.drawable.notification) 47 | .setContentTitle(text) 48 | .setContentText(textLatest) 49 | .setPriority(NotificationCompat.PRIORITY_HIGH) 50 | 51 | // Handle light and vibrate in older devices 52 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { 53 | legacyHandler(mBuilder, prefs) 54 | } 55 | 56 | mNotificationManager.notify(0, mBuilder.build()) 57 | } 58 | 59 | fun getServiceNotification(context: Context): Notification { 60 | 61 | // Create pending intent to be triggered when user clicks on notification 62 | val contentIntent = PendingIntent.getActivity( 63 | context, 0, 64 | Intent(context, TimerActivity::class.java), PendingIntent.FLAG_IMMUTABLE 65 | ) 66 | 67 | return NotificationCompat.Builder(context, SERVICE_CHANNEL_ID) 68 | .setContentTitle(context.getText(R.string.app_name)) 69 | .setContentText(context.getText(R.string.service_text)) 70 | .setSmallIcon(R.drawable.notification) 71 | .setContentIntent(contentIntent) 72 | .setTicker(context.getText(R.string.service_text)) 73 | .build() 74 | } 75 | 76 | private fun legacyHandler(mBuilder: NotificationCompat.Builder, prefs: SharedPreferences) { 77 | val vibrate = prefs.getBoolean("Vibrate", true) 78 | val led = prefs.getBoolean("LED", false) 79 | 80 | // Vibrate 81 | if (vibrate) { 82 | mBuilder.setDefaults(Notification.DEFAULT_VIBRATE) 83 | } 84 | 85 | // Have a light 86 | if (led) { 87 | mBuilder.setLights(-0xff0100, 300, 1000) 88 | } 89 | } 90 | 91 | fun createNotificationChannel(context: Context) { 92 | // Get Notification Manager & Prefs 93 | val mNotificationManager = 94 | context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 95 | val prefs = PreferenceManager.getDefaultSharedPreferences(context) 96 | 97 | // Create the NotificationChannel, but only on API 26+ because 98 | // the NotificationChannel class is new and not in the support library 99 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 100 | var name: CharSequence = context.getString(R.string.alarm_channel_name) 101 | var description = context.getString(R.string.alarm_channel_description) 102 | var importance = NotificationManager.IMPORTANCE_HIGH 103 | 104 | val alarmChannel = NotificationChannel(ALARM_CHANNEL_ID, name, importance) 105 | alarmChannel.description = description 106 | 107 | // Customize 108 | val vibrate = prefs.getBoolean("Vibrate", true) 109 | val led = prefs.getBoolean("LED", false) 110 | 111 | // Vibrate 112 | if (vibrate) { 113 | val pattern = longArrayOf(0, 400, 200, 400) 114 | alarmChannel.vibrationPattern = pattern 115 | alarmChannel.enableVibration(true) 116 | } 117 | 118 | // Have a light 119 | if (led) { 120 | alarmChannel.lightColor = -0xff0100 121 | alarmChannel.enableLights(true) 122 | } 123 | 124 | // We are playing the sound ourselves, 125 | // because notification channels don't allow changing sounds. 126 | alarmChannel.setSound(null, null) 127 | 128 | // Register the channel with the system; you can't change the importance 129 | // or other notification behaviors after this 130 | mNotificationManager.createNotificationChannel(alarmChannel) 131 | 132 | name = context.getString(R.string.service_channel_name) 133 | description = context.getString(R.string.service_channel_description) 134 | importance = NotificationManager.IMPORTANCE_LOW 135 | 136 | val serviceChannel = NotificationChannel(SERVICE_CHANNEL_ID, name, importance) 137 | serviceChannel.description = description 138 | mNotificationManager.createNotificationChannel(serviceChannel) 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /app/src/main/java/org/yuttadhammo/BodhiTimer/Util/Settings.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Settings.kt 3 | * Copyright (C) 2009-2022 Ultrasonic developers 4 | * 5 | * Distributed under terms of the GNU GPLv3 license. 6 | */ 7 | 8 | package org.yuttadhammo.BodhiTimer.Util 9 | 10 | import android.content.Context 11 | import android.content.SharedPreferences 12 | import androidx.preference.PreferenceManager 13 | import org.yuttadhammo.BodhiTimer.BodhiApp 14 | import org.yuttadhammo.BodhiTimer.R 15 | 16 | /** 17 | * Contains convenience functions for reading and writing preferences 18 | */ 19 | const val DEFAULT_DURATION = 120000 20 | 21 | object Settings { 22 | 23 | val toneVolume by IntSetting( 24 | "tone_volume", 25 | 90 26 | ) 27 | 28 | val customBmp by BooleanSetting( 29 | "custom_bmp", 30 | false 31 | ) 32 | var bmpUri by StringSetting( 33 | "bmp_url" 34 | ) 35 | val doNotDisturb by BooleanSetting( 36 | "doNotDisturb", 37 | false 38 | ) 39 | val hideTime by BooleanSetting( 40 | "hideTime", 41 | false 42 | ) 43 | val switchTimeMode by BooleanSetting( 44 | "SwitchTimeMode", 45 | false 46 | ) 47 | var preFileUri by StringSetting( 48 | "PreFileUri", 49 | "" 50 | ) 51 | var preSystemUri by StringSetting( 52 | "PreSystemUri", 53 | "" 54 | ) 55 | val preparationTime by IntSetting( 56 | "preparationTime", 57 | 0 58 | ) 59 | 60 | val preSoundUri by StringSetting( 61 | "PreSoundUri", "" 62 | ) 63 | val notificationUri by StringSetting( 64 | "NotificationUri", Sounds.DEFAULT_SOUND 65 | ) 66 | var systemUri by StringSetting( 67 | "SystemUri", Sounds.DEFAULT_SOUND 68 | ) 69 | var fileUri by StringSetting( 70 | "FileUri", Sounds.DEFAULT_SOUND 71 | ) 72 | val speakTime by BooleanSetting( 73 | "SpeakTime", false 74 | ) 75 | val wakeLock by BooleanSetting( 76 | "WakeLock", 77 | false 78 | ) 79 | 80 | var lastSimpleTime by IntSetting( 81 | "LastSimpleTime", 82 | DEFAULT_DURATION 83 | ) 84 | 85 | @JvmStatic 86 | val preferences: SharedPreferences 87 | get() = PreferenceManager.getDefaultSharedPreferences(appContext) 88 | 89 | 90 | @JvmStatic 91 | var theme by StringSetting( 92 | getKey(R.string.setting_key_theme), 93 | getKey(R.string.setting_key_theme_day_night) 94 | ) 95 | 96 | val isDarkTheme: Boolean 97 | get() = (theme == getKey(R.string.setting_key_theme_dark) || 98 | theme == getKey(R.string.setting_key_theme_black)) 99 | 100 | var drawingIndex by IntSetting( 101 | "DrawingIndex", 102 | 1 103 | ) 104 | 105 | var preset1 by StringSetting( 106 | "pre1" 107 | ) 108 | var preset2 by StringSetting( 109 | "pre2" 110 | ) 111 | var preset3 by StringSetting( 112 | "pre3" 113 | ) 114 | var preset4 by StringSetting( 115 | "pre4" 116 | ) 117 | 118 | var fullscreen by BooleanSetting( 119 | "FULLSCREEN", 120 | false 121 | ) 122 | 123 | var advTimeString by StringSetting( 124 | "advTimeString", 125 | "" 126 | ) 127 | 128 | var timeString by StringSetting( 129 | "timeString", 130 | "" 131 | ) 132 | 133 | var lastWasSimple by BooleanSetting( 134 | "LastWasSimple", 135 | true 136 | ) 137 | 138 | var autoRestart by BooleanSetting( 139 | "AutoRestart", 140 | false 141 | ) 142 | 143 | fun hasKey(key: String): Boolean { 144 | return preferences.contains(key) 145 | } 146 | 147 | private fun getKey(key: Int): String { 148 | return appContext.getString(key) 149 | } 150 | 151 | fun getAllKeys(): List { 152 | val prefs = PreferenceManager.getDefaultSharedPreferences(BodhiApp.applicationContext()) 153 | return prefs.all.keys.toList() 154 | } 155 | 156 | private val appContext: Context 157 | get() = BodhiApp.applicationContext() 158 | 159 | } 160 | -------------------------------------------------------------------------------- /app/src/main/java/org/yuttadhammo/BodhiTimer/Util/SettingsDelegate.kt: -------------------------------------------------------------------------------- 1 | package org.yuttadhammo.BodhiTimer.Util 2 | 3 | import android.content.SharedPreferences 4 | import androidx.core.content.edit 5 | import androidx.preference.PreferenceManager 6 | import org.yuttadhammo.BodhiTimer.BodhiApp 7 | import kotlin.properties.ReadWriteProperty 8 | import kotlin.reflect.KProperty 9 | 10 | /** 11 | * Yet another implementation of Shared Preferences using Delegated Properties 12 | * 13 | * Check out https://medium.com/@FrostRocketInc/delegated-shared-preferences-in-kotlin-45b82d6e52d0 14 | * for a detailed walkthrough. 15 | * 16 | * @author Matthew Groves 17 | */ 18 | 19 | abstract class SettingsDelegate : ReadWriteProperty { 20 | protected val sharedPreferences: SharedPreferences by lazy { 21 | PreferenceManager.getDefaultSharedPreferences(BodhiApp.applicationContext()) 22 | } 23 | } 24 | 25 | class StringSetting(private val key: String, private val defaultValue: String = "") : 26 | SettingsDelegate() { 27 | override fun getValue(thisRef: Any, property: KProperty<*>) = 28 | sharedPreferences.getString(key, defaultValue)!! 29 | 30 | override fun setValue(thisRef: Any, property: KProperty<*>, value: String) = 31 | sharedPreferences.edit { putString(key, value) } 32 | } 33 | 34 | class IntSetting(private val key: String, private val defaultValue: Int = 0) : 35 | SettingsDelegate() { 36 | override fun getValue(thisRef: Any, property: KProperty<*>) = 37 | sharedPreferences.getInt(key, defaultValue) 38 | 39 | override fun setValue(thisRef: Any, property: KProperty<*>, value: Int) = 40 | sharedPreferences.edit { putInt(key, value) } 41 | } 42 | 43 | class StringIntSetting(private val key: String, private val defaultValue: Int = 0) : 44 | SettingsDelegate() { 45 | override fun getValue(thisRef: Any, property: KProperty<*>) = 46 | sharedPreferences.getString(key, defaultValue.toString())!!.toInt() 47 | 48 | override fun setValue(thisRef: Any, property: KProperty<*>, value: Int) = 49 | sharedPreferences.edit { putString(key, value.toString()) } 50 | } 51 | 52 | class LongSetting(private val key: String, private val defaultValue: Long = 0.toLong()) : 53 | SettingsDelegate() { 54 | override fun getValue(thisRef: Any, property: KProperty<*>) = 55 | sharedPreferences.getLong(key, defaultValue) 56 | 57 | override fun setValue(thisRef: Any, property: KProperty<*>, value: Long) = 58 | sharedPreferences.edit { putLong(key, value) } 59 | } 60 | 61 | class FloatSetting( 62 | private val key: String, 63 | private val defaultValue: Float = 0.toFloat() 64 | ) : SettingsDelegate() { 65 | override fun getValue(thisRef: Any, property: KProperty<*>) = 66 | sharedPreferences.getFloat(key, defaultValue) 67 | 68 | override fun setValue(thisRef: Any, property: KProperty<*>, value: Float) = 69 | sharedPreferences.edit { putFloat(key, value) } 70 | } 71 | 72 | class BooleanSetting(private val key: String, private val defaultValue: Boolean = false) : 73 | SettingsDelegate() { 74 | override fun getValue(thisRef: Any, property: KProperty<*>) = 75 | sharedPreferences.getBoolean(key, defaultValue) 76 | 77 | override fun setValue(thisRef: Any, property: KProperty<*>, value: Boolean) = 78 | sharedPreferences.edit { putBoolean(key, value) } 79 | 80 | constructor(stringId: Int, defaultValue: Boolean = false) : this( 81 | BodhiApp.applicationContext().getString(stringId), defaultValue 82 | ) 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/org/yuttadhammo/BodhiTimer/Util/Sounds.kt: -------------------------------------------------------------------------------- 1 | package org.yuttadhammo.BodhiTimer.Util 2 | 3 | import android.content.Context 4 | import android.media.MediaPlayer 5 | import android.net.Uri 6 | import android.os.PowerManager 7 | import androidx.preference.PreferenceManager 8 | import org.yuttadhammo.BodhiTimer.R 9 | import timber.log.Timber 10 | import kotlin.math.ln 11 | 12 | 13 | class Sounds(private val mContext: Context) { 14 | 15 | private val flags: Int = PowerManager.PARTIAL_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP 16 | 17 | private fun play(mUri: Uri, volume: Int): MediaPlayer? { 18 | 19 | try { 20 | val mediaPlayer = MediaPlayer() 21 | 22 | mediaPlayer.setDataSource(mContext, mUri) 23 | mediaPlayer.prepare() 24 | 25 | //Timber.v("Volume: " + volume) 26 | if (volume != 0) { 27 | val log1 = (ln((100 - volume).toDouble()) / ln(100.0)).toFloat() 28 | mediaPlayer.setVolume(1 - log1, 1 - log1) 29 | //Timber.v("Volume: " + (1 -log1)) 30 | } 31 | 32 | 33 | mediaPlayer.isLooping = false 34 | 35 | mediaPlayer.setOnCompletionListener { mp -> 36 | Timber.v("Resetting media player...") 37 | mp.reset() 38 | mp.release() 39 | } 40 | 41 | mediaPlayer.setOnErrorListener { _, what, extra -> 42 | Timber.e("what:" + what + " extra:" + extra) 43 | true 44 | } 45 | 46 | mediaPlayer.setOnInfoListener { _, what, extra -> 47 | Timber.e("what:" + what + " extra:" + extra) 48 | true 49 | } 50 | 51 | mediaPlayer.setWakeMode(mContext, flags) 52 | mediaPlayer.start() 53 | 54 | Timber.v("Playing sound") 55 | 56 | return mediaPlayer 57 | } catch (e: Exception) { 58 | Timber.w("Problem playing sound, uri: $mUri") 59 | e.printStackTrace() 60 | //throw (e) 61 | } 62 | 63 | return null 64 | } 65 | 66 | 67 | fun play(mUri: String, volume: Int): MediaPlayer? { 68 | val uri = resolveUri(mUri, mContext) 69 | 70 | if (uri != "") { 71 | return play(Uri.parse(uri), volume) 72 | } 73 | 74 | return null 75 | } 76 | 77 | companion object { 78 | const val DEFAULT_SOUND = "android.resource://org.yuttadhammo.BodhiTimer/${R.raw.bowl1}" 79 | 80 | fun resolveUri(mUri: String, mContext: Context): String { 81 | val prefs = PreferenceManager.getDefaultSharedPreferences(mContext) 82 | var result = "" 83 | 84 | result = mUri 85 | 86 | if (result == "sys_def") { 87 | result = prefs.getString("NotificationUri", "").toString() 88 | } 89 | 90 | when (result) { 91 | "system" -> result = prefs.getString("SystemUri", "")!! 92 | "file" -> result = prefs.getString("FileUri", "")!! 93 | } 94 | 95 | return result 96 | 97 | } 98 | } 99 | 100 | 101 | } -------------------------------------------------------------------------------- /app/src/main/java/org/yuttadhammo/BodhiTimer/Util/Themes.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Themes.kt 3 | * Copyright (C) 2014-2022 BodhiTimer developers 4 | * 5 | * Distributed under terms of the GNU GPLv3 license. 6 | */ 7 | 8 | package org.yuttadhammo.BodhiTimer.Util 9 | 10 | import android.content.Context 11 | import org.yuttadhammo.BodhiTimer.R 12 | 13 | object Themes { 14 | fun applyTheme(context: Context?) { 15 | if (context == null) return 16 | val style = getStyleFromSettings(context) 17 | // First set the theme (light, dark, etc.) 18 | context.setTheme(style) 19 | // Then set an overlay controlling the status bar behaviour etc. 20 | context.setTheme(R.style.BodhiTheme_Base) 21 | } 22 | 23 | private fun getStyleFromSettings(context: Context): Int { 24 | return when (Settings.theme.lowercase()) { 25 | context.getString(R.string.setting_key_theme_dark) -> { 26 | R.style.BodhiTheme_Dark 27 | } 28 | context.getString(R.string.setting_key_theme_light) -> { 29 | R.style.BodhiTheme_Light 30 | } 31 | context.getString(R.string.setting_key_theme_black) -> { 32 | R.style.BodhiTheme_Black 33 | } 34 | else -> { 35 | R.style.BodhiTheme_DayNight 36 | } 37 | } 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /app/src/main/java/org/yuttadhammo/BodhiTimer/Util/Time.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Time.kt 3 | * Copyright (C) 2014-2022 BodhiTimer developers 4 | * 5 | * Distributed under terms of the GNU GPLv3 license. 6 | */ 7 | 8 | package org.yuttadhammo.BodhiTimer.Util 9 | 10 | import android.content.Context 11 | import android.text.TextUtils 12 | import androidx.appcompat.app.AppCompatActivity 13 | import org.yuttadhammo.BodhiTimer.R 14 | import timber.log.Timber 15 | import java.util.regex.Pattern 16 | 17 | object Time { 18 | const val TIME_SEPARATOR = " again " 19 | 20 | private fun msFromNumbers(hour: Int, minutes: Int, seconds: Int): Int { 21 | return hour * 60 * 60 * 1000 + minutes * 60 * 1000 + seconds * 1000 22 | } 23 | 24 | @JvmStatic 25 | fun msFromArray(numbers: IntArray): Int { 26 | return msFromNumbers(numbers[0], numbers[1], numbers[2]) 27 | } 28 | 29 | private fun padWithZeroes(number: Int): String { 30 | return if (number > 9) { 31 | number.toString() 32 | } else { 33 | "0$number" 34 | } 35 | } 36 | 37 | /** 38 | * Converts a millisecond time to a string time 39 | * Not meant to be pretty, but fast.. 40 | * 41 | * @param ms the time in milliseconds 42 | * @return the formatted string 43 | */ 44 | private fun ms2Str(ms: Int): String { 45 | val time = time2Array(ms) 46 | return if (time[0] == 0 && time[1] == 0) { 47 | time[2].toString() 48 | } else if (time[0] == 0) { 49 | time[1].toString() + ":" + padWithZeroes( 50 | time[2] 51 | ) 52 | } else { 53 | time[0].toString() + ":" + padWithZeroes( 54 | time[1] 55 | ) + ":" + padWithZeroes(time[2]) 56 | } 57 | } 58 | 59 | /** 60 | * Creates a time vector 61 | * 62 | * @param time the time in milliseconds 63 | * @return [hour, minutes, seconds, ms] 64 | */ 65 | @JvmStatic 66 | fun time2Array(time: Int): IntArray { 67 | val ms = time % 1000 68 | var seconds = time / 1000 // 3550000 / 1000 = 3550 69 | var minutes = seconds / 60 // 59.16666 70 | var hours = minutes / 60 // 0.9 71 | if (hours > 60) hours = 60 72 | minutes %= 60 73 | seconds %= 60 74 | val timeVec = IntArray(4) 75 | timeVec[0] = hours 76 | timeVec[1] = minutes 77 | timeVec[2] = seconds 78 | timeVec[3] = ms 79 | return timeVec 80 | } 81 | 82 | @JvmStatic 83 | fun time2humanStr(context: Context, time: Int): String { 84 | val timeVec = time2Array(time) 85 | val hour = timeVec[0] 86 | val minutes = timeVec[1] 87 | val seconds = timeVec[2] 88 | val strList = ArrayList() 89 | val res = context.resources 90 | 91 | // string formatting 92 | if (hour != 0) { 93 | strList.add(res.getQuantityString(R.plurals.x_hours, hour, hour)) 94 | } 95 | if (minutes != 0) { 96 | strList.add(res.getQuantityString(R.plurals.x_mins, minutes, minutes)) 97 | } 98 | if (seconds != 0 || seconds >= 0 && minutes == 0 && hour == 0) { 99 | strList.add(res.getQuantityString(R.plurals.x_secs, seconds, seconds)) 100 | } 101 | return TextUtils.join(", ", strList) 102 | } 103 | 104 | fun time2hms(time: Int): String { 105 | return ms2Str(time) 106 | } 107 | 108 | @JvmStatic 109 | fun str2complexTimeString(activity: AppCompatActivity, numberString: String): String { 110 | val out: String 111 | val stringArray = ArrayList() 112 | val strings = numberString.split(TIME_SEPARATOR).toTypedArray() 113 | for (string in strings) { 114 | val atime = str2timeString(activity, string) 115 | if (atime > 0) stringArray.add(atime.toString() + "#sys_def#" + activity.getString(R.string.sys_def)) 116 | } 117 | out = TextUtils.join("^", stringArray) 118 | return out 119 | } 120 | 121 | @JvmStatic 122 | fun str2timeString(activity: AppCompatActivity, numberString: String): Int { 123 | val res = activity.resources 124 | val numbers = res.getStringArray(R.array.numbers) 125 | var newString = numberString 126 | 127 | for ((position, number) in numbers.withIndex()) { 128 | val num = 60 - position 129 | newString = newString.replace(number.toRegex(), num.toString()) 130 | } 131 | 132 | val HOUR = Pattern.compile("([0-9]+) " + activity.getString(R.string.hour)) 133 | val MINUTE = Pattern.compile("([0-9]+) " + activity.getString(R.string.minute)) 134 | val SECOND = Pattern.compile("([0-9]+) " + activity.getString(R.string.second)) 135 | 136 | var hours = 0 137 | var minutes = 0 138 | var seconds = 0 139 | var m = HOUR.matcher(newString) 140 | while (m.find()) { 141 | val match = m.group(1) 142 | hours += match?.toInt() ?: 0 143 | } 144 | m = MINUTE.matcher(newString) 145 | while (m.find()) { 146 | val match = m.group(1) 147 | minutes += match?.toInt() ?: 0 148 | } 149 | m = SECOND.matcher(newString) 150 | while (m.find()) { 151 | val match = m.group(1) 152 | seconds += match?.toInt() ?: 0 153 | } 154 | Timber.d("Got numbers: $hours hours, $minutes minutes, $seconds seconds") 155 | var total = hours * 60 * 60 * 1000 + minutes * 60 * 1000 + seconds * 1000 156 | if (total > 60 * 60 * 60 * 1000 + 59 * 60 * 1000 + 59 * 1000) total = 157 | 60 * 60 * 60 * 1000 + 59 * 60 * 1000 + 59 * 1000 158 | return total 159 | } 160 | } -------------------------------------------------------------------------------- /app/src/main/java/org/yuttadhammo/BodhiTimer/Widget/BodhiAppWidgetProvider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of Bodhi Timer. 3 | 4 | Bodhi Timer is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | Bodhi Timer is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with Bodhi Timer. If not, see . 16 | */ 17 | package org.yuttadhammo.BodhiTimer.Widget 18 | 19 | import android.app.PendingIntent 20 | import android.appwidget.AppWidgetManager 21 | import android.appwidget.AppWidgetProvider 22 | import android.content.ComponentName 23 | import android.content.Context 24 | import android.content.Intent 25 | import android.content.IntentFilter 26 | import android.widget.RemoteViews 27 | import org.yuttadhammo.BodhiTimer.R 28 | import org.yuttadhammo.BodhiTimer.TimerActivity 29 | import timber.log.Timber 30 | 31 | 32 | class BodhiAppWidgetProvider : AppWidgetProvider() { 33 | private var isRegistered = false 34 | 35 | override fun onUpdate( 36 | context: Context, 37 | appWidgetManager: AppWidgetManager, 38 | appWidgetIds: IntArray 39 | ) { 40 | Timber.i("onUpdate") 41 | if (!isRegistered) { 42 | context.applicationContext.registerReceiver(this, IntentFilter(Intent.ACTION_SCREEN_ON)) 43 | context.applicationContext.registerReceiver( 44 | this, 45 | IntentFilter(Intent.ACTION_SCREEN_OFF) 46 | ) 47 | isRegistered = true 48 | } 49 | context.sendBroadcast(Intent(ACTION_CLOCK_UPDATE)) 50 | } 51 | 52 | override fun onEnabled(context: Context) { 53 | super.onEnabled(context) 54 | Timber.i("onEnabled") 55 | if (!isRegistered) { 56 | context.applicationContext.registerReceiver(this, IntentFilter(Intent.ACTION_SCREEN_ON)) 57 | context.applicationContext.registerReceiver( 58 | this, 59 | IntentFilter(Intent.ACTION_SCREEN_OFF) 60 | ) 61 | isRegistered = true 62 | } 63 | context.sendBroadcast(Intent(ACTION_CLOCK_UPDATE)) 64 | } 65 | 66 | override fun onDisabled(context: Context) { 67 | Timber.i("onDisabled") 68 | super.onDisabled(context) 69 | } 70 | 71 | override fun onDeleted(context: Context, appWidgetIds: IntArray) { 72 | Timber.d("onDeleted") 73 | } 74 | 75 | override fun onReceive(context: Context, i: Intent) { 76 | super.onReceive(context, i) 77 | val action = i.action 78 | 79 | //stopTicking = action.equals(BROADCAST_STOP) || action.equals(Intent.ACTION_SCREEN_OFF); 80 | doUpdate(context) 81 | } 82 | 83 | private fun doUpdate(context: Context) { 84 | Timber.i("updating") 85 | if (views == null) views = RemoteViews(context.packageName, R.layout.appwidget) 86 | val intent = Intent(context, TimerActivity::class.java) 87 | intent.putExtra("set", "true") 88 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 89 | intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) 90 | val pendingIntent = PendingIntent.getActivity( 91 | context, 92 | 0, 93 | intent, 94 | PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE 95 | ) 96 | val resources = context.resources 97 | appWidgetManager = AppWidgetManager.getInstance(context) 98 | val appWidgets = ComponentName( 99 | context.packageName, 100 | "org.yuttadhammo.BodhiTimer.widget.BodhiAppWidgetProvider" 101 | ) 102 | val widgetIds = appWidgetManager!!.getAppWidgetIds(appWidgets) 103 | val backgrounds = HashMap() 104 | if (widgetIds.isNotEmpty()) { 105 | for (widgetId in widgetIds) { 106 | 107 | 108 | // Get the layout for the App Widget and attach an on-click listener 109 | // to the button 110 | views!!.setOnClickPendingIntent(R.id.mainImage, pendingIntent) 111 | views!!.setImageViewResource(R.id.mainImage, R.drawable.leaf); 112 | 113 | appWidgetManager?.updateAppWidget(widgetId, views) 114 | } 115 | } 116 | } 117 | 118 | companion object { 119 | private var appWidgetManager: AppWidgetManager? = null 120 | const val ACTION_CLOCK_UPDATE = "org.yuttadhammo.BodhiTimer.ACTION_CLOCK_UPDATE" 121 | private var views: RemoteViews? = null 122 | } 123 | } -------------------------------------------------------------------------------- /app/src/main/res/color/gallery_item_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/leaf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuttadhammo/BodhiTimer/8c23e0441b4a7ef60b9a18e2895bf6519e027144/app/src/main/res/drawable-hdpi/leaf.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/leaf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuttadhammo/BodhiTimer/8c23e0441b4a7ef60b9a18e2895bf6519e027144/app/src/main/res/drawable-mdpi/leaf.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 14 | 18 | 22 | 23 | 24 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/leaf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuttadhammo/BodhiTimer/8c23e0441b4a7ef60b9a18e2895bf6519e027144/app/src/main/res/drawable-xhdpi/leaf.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/leaf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuttadhammo/BodhiTimer/8c23e0441b4a7ef60b9a18e2895bf6519e027144/app/src/main/res/drawable-xxhdpi/leaf.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 8 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/notification.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/pause.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 17 | 21 | 25 | 29 | 33 | 37 | 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/play.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/preferences.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 17 | 21 | 25 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/set.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 17 | 21 | 25 | 29 | 33 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/stop.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/widget_background_black.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/widget_background_black_square.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/font/source_sans.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/font/sourcesanspro_light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuttadhammo/BodhiTimer/8c23e0441b4a7ef60b9a18e2895bf6519e027144/app/src/main/res/font/sourcesanspro_light.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/sourcesanspro_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuttadhammo/BodhiTimer/8c23e0441b4a7ef60b9a18e2895bf6519e027144/app/src/main/res/font/sourcesanspro_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/layout/about.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/layout/adv_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 16 | 17 | 26 | 27 |