Repository: NeoApplications/Neo-Wellbeing Branch: master Commit: d37a3d8d5b53 Files: 141 Total size: 366.7 KB Directory structure: gitextract_hno7ecyi/ ├── .gitignore ├── LICENSE ├── NeoWellbeingOverlay/ │ ├── .gitignore │ ├── AndroidManifest.xml │ ├── Makefile │ ├── README │ ├── overlay.apk │ └── res/ │ └── values/ │ └── strings.xml ├── README.md ├── app/ │ ├── .gitignore │ ├── build.gradle.kts │ ├── customize.sh │ ├── proguard-rules.pro │ ├── schemas/ │ │ └── org.eu.droid_ng.wellbeing.shared.StatDb/ │ │ └── 1.json │ ├── src/ │ │ └── main/ │ │ ├── Android.bp │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── org/ │ │ │ └── eu/ │ │ │ └── droid_ng/ │ │ │ └── wellbeing/ │ │ │ ├── Wellbeing.kt │ │ │ ├── broadcast/ │ │ │ │ ├── AlarmFiresBroadcastReceiver.kt │ │ │ │ ├── AppTimersBroadcastReceiver.kt │ │ │ │ ├── BootReceiver.kt │ │ │ │ ├── ManuallyUnsuspendBroadcastReceiver.kt │ │ │ │ ├── NextAlarmChangedReceiver.kt │ │ │ │ └── NotificationBroadcastReceiver.kt │ │ │ ├── ext.kt │ │ │ ├── lib/ │ │ │ │ ├── AlarmCoordinator.kt │ │ │ │ ├── QSTiles.kt │ │ │ │ ├── ScheduleUtils.kt │ │ │ │ ├── State.kt │ │ │ │ ├── Utils.kt │ │ │ │ ├── WellbeingAirplaneState.kt │ │ │ │ ├── WellbeingService.kt │ │ │ │ └── WellbeingStateUtil.kt │ │ │ ├── prefs/ │ │ │ │ ├── AppTimers.kt │ │ │ │ ├── BedtimeMode.kt │ │ │ │ ├── DayPicker.kt │ │ │ │ ├── FocusModeActivity.kt │ │ │ │ ├── ManualSuspendActivity.kt │ │ │ │ ├── PackageRecyclerViewAdapter.kt │ │ │ │ ├── ScheduleActivity.kt │ │ │ │ ├── ScheduleCardView.kt │ │ │ │ ├── SettingsActivity.kt │ │ │ │ └── TimeSettingView.kt │ │ │ ├── shared/ │ │ │ │ ├── Database.kt │ │ │ │ └── WellbeingFrameworkClient.kt │ │ │ ├── ui/ │ │ │ │ ├── DashboardActivity.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── MainPreferenceFragment.kt │ │ │ │ ├── ShowSuspendedAppDetails.kt │ │ │ │ └── TakeBreakDialogActivity.kt │ │ │ └── widget/ │ │ │ └── ScreenTimeAppWidget.kt │ │ ├── privapp-permissions-wellbeing.xml │ │ └── res/ │ │ ├── drawable/ │ │ │ ├── appwidget_background.xml │ │ │ ├── appwidget_screen_time_bg.xml │ │ │ ├── baseline_airplanemode_active_24.xml │ │ │ ├── baseline_alarm_24.xml │ │ │ ├── baseline_arrow_drop_down_24.xml │ │ │ ├── baseline_battery_charging_full_24.xml │ │ │ ├── baseline_bedtime_24.xml │ │ │ ├── baseline_cancel_24.xml │ │ │ ├── baseline_delete_24.xml │ │ │ ├── baseline_exit_to_app_24.xml │ │ │ ├── baseline_gradient_24.xml │ │ │ ├── baseline_schedule_24.xml │ │ │ ├── dpicker_background.xml │ │ │ ├── dpicker_outline_oval.xml │ │ │ ├── dpicker_shape_oval.xml │ │ │ ├── dpicker_text_color.xml │ │ │ ├── ic_baseline_access_time_24dp.xml │ │ │ ├── ic_baseline_app_blocking_24.xml │ │ │ ├── ic_baseline_bug_report_24.xml │ │ │ ├── ic_baseline_dashboard_24dp.xml │ │ │ ├── ic_baseline_king_bed_24dp.xml │ │ │ ├── ic_baseline_person_24.xml │ │ │ ├── ic_baseline_person_24dp.xml │ │ │ ├── ic_baseline_settings_24dp.xml │ │ │ ├── ic_launcher_foreground.xml │ │ │ ├── ic_plus_24.xml │ │ │ ├── ic_settings.xml │ │ │ └── outline_badge_24.xml │ │ ├── drawable-anydpi/ │ │ │ ├── ic_focus_mode.xml │ │ │ ├── ic_stat_name.xml │ │ │ └── ic_take_break.xml │ │ ├── layout/ │ │ │ ├── activity_app_timers.xml │ │ │ ├── activity_bedtime_mode.xml │ │ │ ├── activity_dashboard.xml │ │ │ ├── activity_focusmode.xml │ │ │ ├── activity_manual_suspend.xml │ │ │ ├── activity_schedule.xml │ │ │ ├── activity_show_suspended_app_details.xml │ │ │ ├── appitem.xml │ │ │ ├── appwidget_screen_time.xml │ │ │ ├── dpicker.xml │ │ │ ├── preference_material_switch.xml │ │ │ ├── schedule_card.xml │ │ │ ├── settings_activity.xml │ │ │ └── take_a_break_activity.xml │ │ ├── mipmap-anydpi/ │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ ├── values/ │ │ │ ├── arrays.xml │ │ │ ├── dimens.xml │ │ │ ├── ic_launcher_background.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ ├── values-fil/ │ │ │ └── strings.xml │ │ ├── values-sw360dp/ │ │ │ └── values-preference.xml │ │ ├── values-v31/ │ │ │ └── dimens.xml │ │ └── xml/ │ │ ├── appwidget_screen_time.xml │ │ ├── backup_rules.xml │ │ ├── data_extraction_rules.xml │ │ ├── main_preferences.xml │ │ └── root_preferences.xml │ └── update-binary ├── build.gradle.kts ├── framework/ │ ├── .gitignore │ ├── build.gradle.kts │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── Android.bp │ ├── AndroidManifest.xml │ ├── java/ │ │ └── org/ │ │ └── eu/ │ │ └── droid_ng/ │ │ └── wellbeing/ │ │ └── framework/ │ │ ├── Framework.kt │ │ ├── WellbeingBootReceiver.kt │ │ ├── WellbeingFrameworkService.kt │ │ └── WellbeingFrameworkServiceImpl.kt │ └── res/ │ └── values/ │ └── strings.xml ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── shared/ ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src/ └── main/ ├── Android.bp ├── AndroidManifest.xml ├── aidl/ │ └── org/ │ └── eu/ │ └── droid_ng/ │ └── wellbeing/ │ └── framework/ │ └── IWellbeingFrameworkService.aidl ├── java/ │ └── org/ │ └── eu/ │ └── droid_ng/ │ └── wellbeing/ │ └── shared/ │ └── BugUtils.kt ├── java_magisk/ │ └── org/ │ └── eu/ │ └── droid_ng/ │ └── wellbeing/ │ └── shim/ │ ├── PackageManagerDelegate.java │ └── UserHandlerShim.java └── java_real/ └── org/ └── eu/ └── droid_ng/ └── wellbeing/ └── shim/ ├── PackageManagerDelegate.java └── UserHandlerShim.java ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.iml .gradle .kotlin /local.properties .idea .DS_Store /build /captures .externalNativeBuild .cxx local.properties ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. ================================================ FILE: NeoWellbeingOverlay/.gitignore ================================================ overlay.apk.us overlay.apk.uz ================================================ FILE: NeoWellbeingOverlay/AndroidManifest.xml ================================================ ================================================ FILE: NeoWellbeingOverlay/Makefile ================================================ KEY := android KEYSTORE := ~/.local/publish.keystore DEFAULT: overlay.apk clean: rm -f overlay.apk overlay.apk.uz overlay.apk.us overlay.apk.us: res AndroidManifest.xml aapt p -M AndroidManifest.xml -S res -I $$ANDROID_SDK_ROOT/platforms/android-29/android.jar -F overlay.apk.us overlay.apk.uz: overlay.apk.us jarsigner -keystore $(KEYSTORE) -signedjar overlay.apk.uz overlay.apk.us $(KEY) overlay.apk: overlay.apk.uz zipalign 4 overlay.apk.uz overlay.apk ================================================ FILE: NeoWellbeingOverlay/README ================================================ Creation process of overlay.apk: make KEY=android KEYSTORE=my.keystore ================================================ FILE: NeoWellbeingOverlay/res/values/strings.xml ================================================ org.eu.droid_ng.wellbeing org.eu.droid_ng.wellbeing ================================================ FILE: README.md ================================================ Work in progress. Will eat your cat. ================================================ FILE: app/.gitignore ================================================ /build ================================================ FILE: app/build.gradle.kts ================================================ import java.nio.file.Files plugins { id("com.android.application") id("org.jetbrains.kotlin.android") id("com.google.devtools.ksp") } android { namespace = "org.eu.droid_ng.wellbeing" compileSdk = 35 defaultConfig { applicationId = "org.eu.droid_ng.wellbeing" minSdk = 29 //noinspection OldTargetApi TODO targetSdk = 33 versionCode = 4 versionName = "0.2.2" javaCompileOptions { annotationProcessorOptions { arguments += mapOf("room.schemaLocation" to "$projectDir/schemas") } } } signingConfigs { register("release") { if (project.hasProperty("RELEASE_KEY_ALIAS")) { storeFile = file(project.properties["RELEASE_STORE_FILE"].toString()) storePassword = project.properties["RELEASE_STORE_PASSWORD"].toString() keyAlias = project.properties["RELEASE_KEY_ALIAS"].toString() keyPassword = project.properties["RELEASE_KEY_PASSWORD"].toString() } } } buildTypes { release { isMinifyEnabled = true setProguardFiles(listOf(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")) if (project.hasProperty("RELEASE_KEY_ALIAS")) { signingConfig = signingConfigs.getByName("release") } else { logger.warn("Using debug signing configs!") signingConfig = signingConfigs.getByName("debug") } } debug { if (project.hasProperty("RELEASE_KEY_ALIAS")) { signingConfig = signingConfigs.getByName("release") } else { logger.warn("Using debug signing configs!") signingConfig = signingConfigs.getByName("debug") } } } sourceSets { getByName("main") { java.srcDirs("src/main/java_magisk") kotlin.srcDirs("src/main/java_magisk") } } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } kotlin { jvmToolchain(17) } } ksp { arg("room.schemaLocation", "$projectDir/schemas") } dependencies { implementation(project(":shared")) val roomVersion = "2.4.0-alpha05" // Android 13 (https://cs.android.com/android/platform/superproject/+/android-13.0.0_r31:prebuilts/sdk/current/androidx/m2repository/androidx/room/room-runtime/;bpv=1) //noinspection GradleDependency implementation("androidx.room:room-runtime:$roomVersion") //noinspection GradleDependency annotationProcessor("androidx.room:room-compiler:$roomVersion") //noinspection GradleDependency ksp("androidx.room:room-compiler:$roomVersion") implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4") implementation("androidx.recyclerview:recyclerview:1.3.2") implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("androidx.preference:preference-ktx:1.2.1") implementation("androidx.appcompat:appcompat:1.7.0") implementation("com.google.android.material:material:1.12.0") implementation("com.github.AppDevNext:AndroidChart:3.1.0.15") } val outDir = rootProject.layout.buildDirectory.asFile.get() val magiskDir = File("$outDir/magisk_module") val zipName = "NeoWellbeing-${android.defaultConfig.versionName}.zip" val zipFile = File(outDir, zipName) val magiskModuleProp = mapOf( "id" to "neo_wellbeing", "name" to "Neo Wellbeing systemless", "version" to android.defaultConfig.versionName, "versionCode" to android.defaultConfig.versionCode, "minApi" to android.defaultConfig.minSdk, "support" to "https://github.com/NeoApplications/Neo-Wellbeing", "config" to "org.eu.droid_ng.wellbeing", "author" to "nift4", "description" to "Neo Wellbeing is an open source reimplementation of Wellbeing" ) tasks.register("assembleMagiskModule", Task::class) { val root = rootDir val magisk = magiskDir var modulePropText = "" val appApk = file("$root/app/build/outputs/apk/release/app-release.apk") val frameworkApk = file("$root/framework/build/outputs/apk/release/framework-release.apk") magiskModuleProp.forEach { (k, v) -> modulePropText += "$k=$v\n" } dependsOn(":app:assembleRelease") dependsOn(":framework:assembleRelease") doLast { magisk.deleteRecursively() magisk.mkdirs() File("$magisk/module.prop").writeText(modulePropText) File("${magisk.path}/system/priv-app/NeoWellbeing").mkdirs() Files.copy(appApk.toPath(), File("${magisk.path}/system/priv-app/NeoWellbeing/NeoWellbeing.apk").toPath()) File("${magisk.path}/system/priv-app/NeoWellbeingFramework").mkdirs() Files.copy(frameworkApk.toPath(), File("${magisk.path}/system/priv-app/NeoWellbeingFramework/NeoWellbeingFramework.apk").toPath()) File("${magisk.path}/system/product/overlay/NeoWellbeingOverlay").mkdirs() Files.copy(File("$root/NeoWellbeingOverlay/overlay.apk").toPath(), File("${magisk.path}/system/product/overlay/NeoWellbeingOverlay/NeoWellbeingOverlay.apk").toPath()) File("${magisk.path}/system/etc/permissions").mkdirs() Files.copy(File("$root/app/src/main/privapp-permissions-wellbeing.xml").toPath(), File("${magisk.path}/system/etc/permissions/privapp-permissions-wellbeing.xml").toPath()) File("${magisk.path}/META-INF/com/google/android").mkdirs() File("${magisk.path}/META-INF/com/google/android/updater-script").writeText("#MAGISK") Files.copy(File("$root/app/update-binary").toPath(), File("${magisk.path}/META-INF/com/google/android/update-binary").toPath()) Files.copy(File("$root/app/customize.sh").toPath(), File("${magisk.path}/customize.sh").toPath()) } } tasks.register("zipMagiskModule", Zip::class) { from(magiskDir) archiveFileName = zipName destinationDirectory = outDir dependsOn(":app:assembleMagiskModule") } tasks.register("pushMagiskModule", Exec::class) { commandLine("adb", "push", zipFile.absolutePath, "/data/local/tmp/$zipName") dependsOn(":app:zipMagiskModule") } tasks.register("testMagiskModule", Exec::class) { setIgnoreExitValue(true) commandLine("adb", "shell", "su", "-c", "magisk --install-module /data/local/tmp/" + zipName + " && (/system/bin/svc power reboot || /system/bin/reboot)") dependsOn(":app:pushMagiskModule") } ================================================ FILE: app/customize.sh ================================================ #!/bin/sh if [ "$API" -lt 29 ]; then abort "! Neo Wellbeing requires Android 10 or later" fi ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. -keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile -dontobfuscate ================================================ FILE: app/schemas/org.eu.droid_ng.wellbeing.shared.StatDb/1.json ================================================ { "formatVersion": 1, "database": { "version": 1, "identityHash": "cbf447b404076250e80614e02b0d462e", "entities": [ { "tableName": "StatEntry", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`date` INTEGER NOT NULL, `unit` TEXT NOT NULL, `type` TEXT NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`date`, `unit`, `type`))", "fields": [ { "fieldPath": "date", "columnName": "date", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "unit", "columnName": "unit", "affinity": "TEXT", "notNull": true }, { "fieldPath": "type", "columnName": "type", "affinity": "TEXT", "notNull": true }, { "fieldPath": "count", "columnName": "count", "affinity": "INTEGER", "notNull": true } ], "primaryKey": { "columnNames": [ "date", "unit", "type" ], "autoGenerate": false }, "indices": [], "foreignKeys": [] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'cbf447b404076250e80614e02b0d462e')" ] } } ================================================ FILE: app/src/main/Android.bp ================================================ android_app { name: "NeoWellbeing", defaults: ["platform_app_defaults"], static_libs: [ "NeoWellbeing-shared", "androidx.annotation_annotation", "androidx.core_core", "androidx.recyclerview_recyclerview", "androidx-constraintlayout_constraintlayout", "androidx.lifecycle_lifecycle-runtime", "androidx.preference_preference", "androidx.recyclerview_recyclerview", "androidx.preference_preference", "androidx.appcompat_appcompat", "com.google.android.material_material", ], //TODO: needs rework resource_dirs: ["res"], srcs: [ "java/**/*.java", "java/**/*.kt", ], platform_apis: true, privileged: true, certificate: "platform", required: ["privapp-permissions-wellbeing.xml"], } prebuilt_etc { name: "privapp-permissions-wellbeing.xml", src: "privapp-permissions-wellbeing.xml", sub_dir: "permissions", } ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/java/org/eu/droid_ng/wellbeing/Wellbeing.kt ================================================ package org.eu.droid_ng.wellbeing import android.app.Application import org.eu.droid_ng.wellbeing.shared.BugUtils import org.eu.droid_ng.wellbeing.lib.WellbeingService import kotlin.system.exitProcess class Wellbeing : Application() { companion object { private lateinit var application: Wellbeing fun getService(): WellbeingService { return application.getServiceInternal() } } private lateinit var service: WellbeingService override fun onCreate() { super.onCreate() application = this BugUtils.maybeInit(this) Thread.setDefaultUncaughtExceptionHandler { _, paramThrowable -> BugUtils.get()?.onBugAdded(paramThrowable, System.currentTimeMillis()) exitProcess(2) } service = WellbeingService(this) } private fun getServiceInternal(): WellbeingService { return service } } ================================================ FILE: app/src/main/java/org/eu/droid_ng/wellbeing/broadcast/AlarmFiresBroadcastReceiver.kt ================================================ package org.eu.droid_ng.wellbeing.broadcast import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import org.eu.droid_ng.wellbeing.lib.WellbeingService class AlarmFiresBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { intent?.identifier?.let { WellbeingService.get().onAlarmFired(it) } } } ================================================ FILE: app/src/main/java/org/eu/droid_ng/wellbeing/broadcast/AppTimersBroadcastReceiver.kt ================================================ package org.eu.droid_ng.wellbeing.broadcast import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import org.eu.droid_ng.wellbeing.lib.WellbeingService class AppTimersBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { // Looks weird, but we don't want to crash if someone feeds us junk intent.getStringExtra("uniqueObserverId")?.let { WellbeingService.get().onAppTimerExpired( intent.getIntExtra("observerId", -1), it ) } } } ================================================ FILE: app/src/main/java/org/eu/droid_ng/wellbeing/broadcast/BootReceiver.kt ================================================ package org.eu.droid_ng.wellbeing.broadcast import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import org.eu.droid_ng.wellbeing.lib.WellbeingService class BootReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { if ("android.intent.action.BOOT_COMPLETED" != intent.action) { /* Make sure no one is trying to fool us */ return } WellbeingService.get().onBootCompleted() } } ================================================ FILE: app/src/main/java/org/eu/droid_ng/wellbeing/broadcast/ManuallyUnsuspendBroadcastReceiver.kt ================================================ package org.eu.droid_ng.wellbeing.broadcast import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.widget.Toast import org.eu.droid_ng.wellbeing.shared.BugUtils.Companion.BUG import org.eu.droid_ng.wellbeing.lib.WellbeingService class ManuallyUnsuspendBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { if ("android.intent.action.PACKAGE_UNSUSPENDED_MANUALLY" != intent.action) { /* Make sure no one is trying to fool us */ return } val packageName = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME) if (packageName == null) { /* Make sure we have a package name */ Toast.makeText( context, "Assertion failure (0xAC): packageName is null. Please report this to the developers!", Toast.LENGTH_LONG ).show() BUG("packageName == null (0xAC)") return } WellbeingService.get().onManuallyUnsuspended(packageName) } } ================================================ FILE: app/src/main/java/org/eu/droid_ng/wellbeing/broadcast/NextAlarmChangedReceiver.kt ================================================ package org.eu.droid_ng.wellbeing.broadcast import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import org.eu.droid_ng.wellbeing.lib.AlarmCoordinator class NextAlarmChangedReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent?) { if ("android.app.action.NEXT_ALARM_CLOCK_CHANGED" != intent?.action) { /* Make sure no one is trying to fool us */ return } AlarmCoordinator(context).updateState() } } ================================================ FILE: app/src/main/java/org/eu/droid_ng/wellbeing/broadcast/NotificationBroadcastReceiver.kt ================================================ package org.eu.droid_ng.wellbeing.broadcast import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import org.eu.droid_ng.wellbeing.lib.WellbeingService class NotificationBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { // Looks weird, but we don't want to crash if someone feeds us junk intent.action?.let { WellbeingService.get().onNotificationActionClick(it) } } } ================================================ FILE: app/src/main/java/org/eu/droid_ng/wellbeing/ext.kt ================================================ package org.eu.droid_ng.wellbeing fun String.Companion.join(delimiter: String, strings: Iterable): String { return java.lang.String.join(delimiter, strings) } fun String.Companion.join(delimiter: String, strings: Array): String { return String.join(delimiter, strings.toList()) } ================================================ FILE: app/src/main/java/org/eu/droid_ng/wellbeing/lib/AlarmCoordinator.kt ================================================ package org.eu.droid_ng.wellbeing.lib import android.app.AlarmManager import android.content.Context import java.time.Instant import java.time.LocalDateTime import java.time.ZoneId class AlarmCoordinator(private val context: Context) { fun updateState() { val am = context.getSystemService(AlarmManager::class.java) val next = am.nextAlarmClock if (next == null) { ScheduleUtils.dropAlarm(context, "alc", am) } else { ScheduleUtils.setAlarm(context, "alc", LocalDateTime.ofInstant(Instant.ofEpochMilli(next.triggerTime), ZoneId.systemDefault()), am) } } fun fired() { WellbeingService.get().doTrigger(true) { it is TimeChargerTriggerCondition && it.endOnAlarm } } } ================================================ FILE: app/src/main/java/org/eu/droid_ng/wellbeing/lib/QSTiles.kt ================================================ package org.eu.droid_ng.wellbeing.lib import android.service.quicksettings.Tile.STATE_ACTIVE import android.service.quicksettings.Tile.STATE_INACTIVE import android.service.quicksettings.TileService import org.eu.droid_ng.wellbeing.R class FocusModeQSTile : TileService() { override fun onStartListening() { super.onStartListening() val tw = WellbeingService.get() val state = tw.getState() val tile = qsTile tile.state = if (state.isFocusModeEnabled()) STATE_ACTIVE else STATE_INACTIVE tile.subtitle = getString(if (state.isFocusModeEnabled()) R.string.on else R.string.off) tile.updateTile() } override fun onClick() { super.onClick() val tw = WellbeingService.get() val state = tw.getState() if (state.isFocusModeEnabled()) tw.disableFocusMode() else tw.enableFocusMode() } } class BedtimeModeQSTile : TileService() { override fun onStartListening() { super.onStartListening() val tw = WellbeingService.get() val state = tw.getState() val tile = qsTile tile.state = if (state.isBedtimeModeEnabled()) STATE_ACTIVE else STATE_INACTIVE tile.subtitle = getString(if (state.isBedtimeModeEnabled()) R.string.on else R.string.off) tile.updateTile() } override fun onClick() { super.onClick() val tw = WellbeingService.get() val state = tw.getState() tw.setBedtimeMode(!state.isBedtimeModeEnabled()) } } ================================================ FILE: app/src/main/java/org/eu/droid_ng/wellbeing/lib/ScheduleUtils.kt ================================================ package org.eu.droid_ng.wellbeing.lib import android.app.AlarmManager import android.app.PendingIntent import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetProvider import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.BatteryManager import android.os.Handler import org.eu.droid_ng.wellbeing.broadcast.AlarmFiresBroadcastReceiver import java.time.DayOfWeek import java.time.LocalDateTime import java.time.ZoneId import java.time.temporal.TemporalAdjusters class ScheduleUtils { companion object { private fun getPintentForId(context: Context, id: String): PendingIntent { return PendingIntent.getBroadcast( context, 0, Intent(context, AlarmFiresBroadcastReceiver::class.java).addFlags(Intent.FLAG_RECEIVER_FOREGROUND).setIdentifier(id), PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT ) } fun dropAlarm(context: Context, id: String, alarmManager: AlarmManager? = null, pintent: PendingIntent? = null) { (alarmManager ?: context.getSystemService(AlarmManager::class.java)) .cancel(pintent ?: getPintentForId(context, id)) } fun setAlarm(context: Context, id: String, time: LocalDateTime, alarmManager: AlarmManager? = null, pintent: PendingIntent? = null) { val am = alarmManager ?: context.getSystemService(AlarmManager::class.java) val pi = pintent ?: getPintentForId(context, id) dropAlarm(context, id, am, pi) am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, time.withSecond(0).atZone(ZoneId.systemDefault()).toEpochSecond() * 1000L, pi) } fun ensureWidgetAlarmSet(context: Context, handler: Handler, intervalSec: Long, widget: Class) { val millis = intervalSec * 1000L val am = context.getSystemService(AlarmManager::class.java) as AlarmManager val awm = AppWidgetManager.getInstance(context) val rawIntent = Intent(context, widget) rawIntent.action = "org.eu.droid_ng.wellbeing.APPWIDGET_UPDATE" val intent = PendingIntent.getBroadcast(context, widget.hashCode(), rawIntent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE) if (awm.getAppWidgetIds(ComponentName(context, widget)).isNotEmpty()) { /* widget exists */ // inexact + no wakeup + repeating (=android batching) alarm to save battery am.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, 0, millis, intent) // handler for while wellbeing running handler.postDelayed(object : Runnable { override fun run() { context.sendBroadcast(rawIntent) handler.postDelayed(this, millis) } }, millis) } else { am.cancel(intent) } } fun ensureStatProcessorAlarmSet(context: Context, handler: Handler) { val am = context.getSystemService(AlarmManager::class.java) as AlarmManager val millis = 12 * 60 * 60 * 1000L // 12 hours val intent = getPintentForId(context, "__STATS") // inexact + no wakeup + repeating (=android batching) alarm to save battery am.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, 0, millis, intent) // handler for while wellbeing running handler.postDelayed(object : Runnable { override fun run() { WellbeingService.get().bgHandler.post { WellbeingService.get().onProcessStats(true) } handler.postDelayed(this, millis) } }, millis) } } } interface Trigger { val id: String val iid: String val enabled: Boolean fun setup(applicationContext: Context) fun dispose(applicationContext: Context) } interface Condition { val id: String fun isFulfilled(applicationContext: Context): Boolean } class TimeChargerTriggerCondition( override val id: String, override val iid: String, override val enabled: Boolean, val startHour: Int, val startMinute: Int, val endHour: Int, val endMinute: Int, val weekdays: BooleanArray, // length = 7, 0 = monday, 6 = sunday val needCharger: Boolean, val endOnAlarm: Boolean ) : Trigger, Condition { override fun setup(applicationContext: Context) { if (!weekdays.any { it }) return // bail if no weekday is enabled if (!enabled) return val now = LocalDateTime.now().withNano(0) val cwd = if (!weekdays[now.dayOfWeek.ordinal]) { val offset = now.dayOfWeek.ordinal var r = now for (i in 0..6) { val j = (i + offset) % 7 if (weekdays[j]) { r = now.with(TemporalAdjusters.next(DayOfWeek.of(j + 1))) break } } if (r == now) { throw IllegalStateException("this cannot happen, r == now") } r } else now var offset = cwd.dayOfWeek.ordinal var nwd = cwd for (i in 1..7) { val j = (i + offset) % 7 if (weekdays[j]) { nwd = cwd.with(TemporalAdjusters.next(DayOfWeek.of(j + 1))) break } } if (nwd == cwd) { throw IllegalStateException("this cannot happen, nwd == cwd") } val start = cwd.withSecond(0).withHour(startHour).withMinute(startMinute).let { if (now.isEqual(it) || now.isAfter(it)) { nwd.withSecond(0).withHour(startHour).withMinute(startMinute) } else { it } } nwd = start offset = start.dayOfWeek.ordinal for (i in 1..7) { val j = (i + offset) % 7 if (weekdays[j]) { nwd = start.with(TemporalAdjusters.next(DayOfWeek.of(j + 1))) break } } if (nwd == start) { throw IllegalStateException("this cannot happen, nwd == start") } val end = cwd.withSecond(0).withHour(endHour).withMinute(endMinute).let { if (now.isEqual(it) || now.isAfter(it)) { nwd.withSecond(0).withHour(endHour).withMinute(endMinute) } else { it } } ScheduleUtils.setAlarm(applicationContext, iid, start) ScheduleUtils.setAlarm(applicationContext, "expire::$iid", end) } override fun dispose(applicationContext: Context) { ScheduleUtils.dropAlarm(applicationContext, iid) ScheduleUtils.dropAlarm(applicationContext, "expire::$iid") } override fun isFulfilled(applicationContext: Context): Boolean { val now = LocalDateTime.now().withNano(0) return (enabled && weekdays[now.dayOfWeek.ordinal] && run { val end = now.withSecond(0).withHour(endHour).withMinute(endMinute) val start = now.withSecond(0).withHour(startHour).withMinute(startMinute).let { if (it.isAfter(end)) { it.minusDays(1) } else { it } } (now.isAfter(start) || now.isEqual(start)) && now.isBefore(end) }) && (!needCharger || run { val batteryStatus: Intent? = IntentFilter(Intent.ACTION_BATTERY_CHANGED).let { applicationContext.registerReceiver(null, it) } val chargePlug: Int = batteryStatus?.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) ?: -1 chargePlug == BatteryManager.BATTERY_PLUGGED_USB || chargePlug == BatteryManager.BATTERY_PLUGGED_AC }) } } ================================================ FILE: app/src/main/java/org/eu/droid_ng/wellbeing/lib/State.kt ================================================ package org.eu.droid_ng.wellbeing.lib class State(private val value: Int) { companion object { /* Update of State (partially) failed */ const val STATE_UPDATE_FAILURE = 1 /* Focus mode currently: Enabled (also set if break is taken at the moment) */ const val STATE_FOCUS_MODE_ENABLED = 2 /* Focus mode currently: Break (Global) */ const val STATE_FOCUS_MODE_GLOBAL_BREAK = 4 /* Focus mode currently: Break (Per-App) */ const val STATE_FOCUS_MODE_APP_BREAK = 8 /* Manual suspension */ const val STATE_MANUAL_SUSPEND = 16 /* App timer set */ const val STATE_APP_TIMER_SET = 32 /* App timer expired */ const val STATE_APP_TIMER_EXPIRED = 64 /* App timer break */ const val STATE_APP_TIMER_BREAK = 128 /* Bedtime mode enabled */ const val STATE_BED_MODE = 256 } private fun isPresent(bitmask: Int): Boolean { return (value and bitmask) > 0 } fun toInt(): Int { return value } fun hasUpdateFailed(): Boolean { return isPresent(STATE_UPDATE_FAILURE) } fun isFocusModeEnabled(): Boolean { return isPresent(STATE_FOCUS_MODE_ENABLED) } fun isOnFocusModeBreakGlobal(): Boolean { return isPresent(STATE_FOCUS_MODE_GLOBAL_BREAK) } fun isOnFocusModeBreakPartial(): Boolean { return isPresent(STATE_FOCUS_MODE_APP_BREAK) } fun isSuspendedManually(): Boolean { return isPresent(STATE_MANUAL_SUSPEND) } fun isAppTimerSet(): Boolean { return isPresent(STATE_APP_TIMER_SET) } fun isAppTimerExpired(): Boolean { return isPresent(STATE_APP_TIMER_EXPIRED) } fun isAppTimerBreak(): Boolean { return isPresent(STATE_APP_TIMER_BREAK) } fun isBedtimeModeEnabled(): Boolean { return isPresent(STATE_BED_MODE) } } ================================================ FILE: app/src/main/java/org/eu/droid_ng/wellbeing/lib/Utils.kt ================================================ package org.eu.droid_ng.wellbeing.lib import android.annotation.SuppressLint import android.app.usage.UsageEvents import android.app.usage.UsageEvents.Event import android.app.usage.UsageStatsManager import android.content.ComponentName import android.content.Intent import android.content.pm.PackageManager import android.content.res.Resources import android.util.Log import org.eu.droid_ng.wellbeing.shim.PackageManagerDelegate import java.time.* import java.util.* object Utils { private const val MOST_USED_PKG_CACHE_SIZE: Int = 3 private const val MOST_USED_PKG_MIN_USAGE_MINS: Long = 5 private var calculatedUsageStats: Map? = null private var calculatedScreenTime: Duration? = null private var mostUsedPackages: Array? = null const val PACKAGE_MANAGER_MATCH_INSTANT = 0x00800000 val blackListedPackages: HashSet = HashSet() val restrictedPackages: HashSet = HashSet() private fun eventsStr(events: Iterable): String { val b = StringBuilder() b.append("[") for (element in events) { b.append("el(t=").append(element.eventType).append("), ") } b.replace(b.length - 2, b.length - 1, "]") return b.toString() } fun clearUsageStatsCache(usm: UsageStatsManager?, pm: PackageManager?, pmd: PackageManagerDelegate?, recalculate: Boolean) { calculatedUsageStats = null calculatedScreenTime = null mostUsedPackages = null if (recalculate) { updateApplicationBlackLists(pm!!, pmd!!) checkInitializeCache(usm!!) } } fun getTimeUsed(usm: UsageStatsManager, packageName: String?): Duration { checkInitializeCache(usm) return calculatedUsageStats!!.getOrDefault(packageName, Duration.ZERO) } fun getTimeUsed(usm: UsageStatsManager, packageNames: Array): Duration { checkInitializeCache(usm) var d = Duration.ZERO for (packageName in packageNames) { d = d.plus(calculatedUsageStats!!.getOrDefault(packageName, Duration.ZERO)) } return if (d.isNegative) Duration.ZERO else d } fun getScreenTime(usm: UsageStatsManager): Duration { checkInitializeCache(usm) return calculatedScreenTime!! } fun getMostUsedPackages(usm: UsageStatsManager): Array { checkInitializeCache(usm) return mostUsedPackages!! } private fun checkInitializeCache(usm: UsageStatsManager) { if (calculatedUsageStats != null) return // Cache not available. Calculate it once and keep it. val z = ZoneId.systemDefault() val startTime = LocalDateTime.now().with(LocalTime.MIN) // Start of day .atZone(z).toEpochSecond() * 1000 val result = calculateUsageStats(usm, startTime, System.currentTimeMillis()) calculatedScreenTime = result.first calculatedUsageStats = result.second.first mostUsedPackages = result.second.second } fun calculateUsageStats(usm: UsageStatsManager, startTimeMillis: Long, endTimeMillis: Long): Pair, Array>> { val usageEvents: UsageEvents = usm.queryEvents(startTimeMillis, endTimeMillis) var currentEvent: Event val e = HashMap>() while (usageEvents.hasNextEvent()) { currentEvent = Event() // TODO don't allocate all of them usageEvents.getNextEvent(currentEvent) e.computeIfAbsent(currentEvent.packageName) { ArrayList() }.add(currentEvent) } // Calculate usageStats val myCalculatedUsageStats = HashMap() e.forEach { (pkgName: String, events: ArrayList) -> val openActivities = hashMapOf() for (event in events.sortedWith { a, b -> val c = a.timeStamp.compareTo(b.timeStamp) if (c != 0) return@sortedWith c val d = a.eventType == Event.ACTIVITY_RESUMED || a.eventType == 4 /* CONTINUE_PREVIOUS_DAY */ val f = b.eventType == Event.ACTIVITY_RESUMED || b.eventType == 4 /* CONTINUE_PREVIOUS_DAY */ return@sortedWith d.compareTo(f) }) { when (event.eventType) { Event.ACTIVITY_PAUSED -> { val start = openActivities.remove(event.className) if (start != null) myCalculatedUsageStats[pkgName] = myCalculatedUsageStats .getOrDefault(pkgName, Duration.ZERO).plus( Duration.ofMillis(event.timeStamp - start)) else Log.w("WellbeingUtils", "got ACTIVITY_PAUSED for ${event.className} at ${event.timeStamp} but didn't remember it starting") } Event.DEVICE_SHUTDOWN, 3 /* END_OF_DAY */ -> { while (openActivities.isNotEmpty()) { myCalculatedUsageStats[pkgName] = myCalculatedUsageStats .getOrDefault(pkgName, Duration.ZERO).plus( Duration.ofMillis(event.timeStamp - openActivities.remove(openActivities.keys.first())!! )) } } Event.ACTIVITY_RESUMED, 4 /* CONTINUE_PREVIOUS_DAY */ -> openActivities[event.className] = event.timeStamp } } } // Calculate screenTime + mostUsedPackages var screenTimeTmp: Duration = Duration.ZERO val mostUsedPackagesTmp = arrayOfNulls(MOST_USED_PKG_CACHE_SIZE) val mostUsedPackageTime = Array(MOST_USED_PKG_CACHE_SIZE) { MOST_USED_PKG_MIN_USAGE_MINS } myCalculatedUsageStats.forEach { (pkgName: String, duration: Duration) -> val seconds: Long if (!blackListedPackages.contains(pkgName)) { screenTimeTmp = screenTimeTmp.plus(duration) seconds = duration.seconds } else seconds = 0 if (!restrictedPackages.contains(pkgName) && seconds > mostUsedPackageTime[MOST_USED_PKG_CACHE_SIZE - 1]) { var index = 0 while (seconds <= mostUsedPackageTime[index]) { index++ } System.arraycopy(mostUsedPackagesTmp, index, mostUsedPackagesTmp, index + 1, (MOST_USED_PKG_CACHE_SIZE - 1) - index) System.arraycopy(mostUsedPackageTime, index, mostUsedPackageTime, index + 1, (MOST_USED_PKG_CACHE_SIZE - 1) - index) mostUsedPackagesTmp[index] = pkgName mostUsedPackageTime[index] = seconds } } val myMostUsedPackages: Array if (mostUsedPackagesTmp[MOST_USED_PKG_CACHE_SIZE - 1] != null) { @Suppress("UNCHECKED_CAST") myMostUsedPackages = mostUsedPackagesTmp as Array } else if (mostUsedPackagesTmp[0] == null) { myMostUsedPackages = emptyArray() } else { var arraySize = MOST_USED_PKG_CACHE_SIZE while (arraySize --> 0) { if (mostUsedPackagesTmp[arraySize] != null) { arraySize + 1 break } } @Suppress("UNCHECKED_CAST") myMostUsedPackages = mostUsedPackagesTmp.copyOf(arraySize) as Array } return Pair(screenTimeTmp, Pair(myCalculatedUsageStats, myMostUsedPackages)) } @SuppressLint("DiscouragedApi") private fun updateApplicationBlackLists(pm: PackageManager, pmd: PackageManagerDelegate) { blackListedPackages.clear() restrictedPackages.clear() blackListedPackages.add("com.android.systemui") val resId = Resources.getSystem().getIdentifier( "config_recentsComponentName", "string", "android") if (resId != 0) { val recentsComponent = ComponentName.unflattenFromString( Resources.getSystem().getString(resId)) if (recentsComponent != null) restrictedPackages.add(recentsComponent.packageName) } var intent = Intent(Intent.ACTION_MAIN) intent.addCategory(Intent.CATEGORY_HOME) addDefaultHandlersToBlacklist(pm, intent, restrictedPackages) restrictedPackages.addAll(blackListedPackages) restrictedPackages.add("com.android.settings") // Add every system dialer to the blacklist intent = Intent(Intent.ACTION_DIAL) intent.addCategory(Intent.CATEGORY_DEFAULT) addDefaultHandlersToBlacklist(pm, intent, restrictedPackages) restrictedPackages.add("org.eu.droid_ng.wellbeing") //Log.d("Utils", "Hard Blacklisted packages: $blackListedPackages") //Log.d("Utils", "Soft Blacklisted packages: $restrictedPackages") val packages = pm.getInstalledApplications(PackageManager.GET_META_DATA).map { it.packageName }.toTypedArray() restrictedPackages.addAll(pmd.getUnsuspendablePackages(packages)) } private fun addDefaultHandlersToBlacklist(pm: PackageManager, intent: Intent, blacklist: HashSet) { // Add the system handlers to the blacklist val resolveInfoList = pm.queryIntentActivities(intent, PackageManager.MATCH_SYSTEM_ONLY) if (resolveInfoList.isNotEmpty()) { for (resolveInfo in resolveInfoList) { blacklist.add(resolveInfo.activityInfo.packageName) } } // Add the default handler to the blacklist val resolveInfo = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) if (resolveInfo != null) { blacklist.add(resolveInfo.activityInfo.packageName) } } } ================================================ FILE: app/src/main/java/org/eu/droid_ng/wellbeing/lib/WellbeingAirplaneState.kt ================================================ package org.eu.droid_ng.wellbeing.lib import android.content.Context import android.provider.Settings enum class WellbeingAirplaneState(val airplaneModeState: Boolean, val systemAirplaneModeState: Boolean, val wellbeingAirplaneModeState: Boolean) { DISABLED_BY_SYSTEM(false, false, false) { override fun onReceiveAirplaneEnabled(): WellbeingAirplaneState { return ENABLED_BY_SYSTEM } override fun onEnableAirplaneByWellbeing(): WellbeingAirplaneState { return ENABLED_DESPITE_SYSTEM } }, ENABLED_BY_SYSTEM(true, true, false) { override fun onReceiveAirplaneDisabled(): WellbeingAirplaneState { return DISABLED_BY_SYSTEM } override fun onEnableAirplaneByWellbeing(): WellbeingAirplaneState { return ENABLED_WITH_SYSTEM } }, ENABLED_WITH_SYSTEM(true, true, true) { override fun onReceiveAirplaneDisabled(): WellbeingAirplaneState { return DISABLED_DESPITE_WELLBEING_SYSTEM } override fun onDisableAirplaneByWellbeing(): WellbeingAirplaneState { return ENABLED_BY_SYSTEM } }, ENABLED_DESPITE_SYSTEM(true, false, true) { override fun onReceiveAirplaneDisabled(): WellbeingAirplaneState { return DISABLED_DESPITE_WELLBEING } override fun onDisableAirplaneByWellbeing(): WellbeingAirplaneState { return DISABLED_BY_SYSTEM } }, DISABLED_DESPITE_WELLBEING_SYSTEM(false, false, true) { override fun onReceiveAirplaneEnabled(): WellbeingAirplaneState { return ENABLED_WITH_SYSTEM } override fun onDisableAirplaneByWellbeing(): WellbeingAirplaneState { return DISABLED_BY_SYSTEM } }, DISABLED_DESPITE_WELLBEING(false, false, true) { override fun onReceiveAirplaneEnabled(): WellbeingAirplaneState { return ENABLED_DESPITE_SYSTEM } override fun onDisableAirplaneByWellbeing(): WellbeingAirplaneState { return DISABLED_BY_SYSTEM } }; open fun onDisableAirplaneByWellbeing(): WellbeingAirplaneState { return this } open fun onEnableAirplaneByWellbeing(): WellbeingAirplaneState { return this } open fun onReceiveAirplaneDisabled(): WellbeingAirplaneState { return this } open fun onReceiveAirplaneEnabled(): WellbeingAirplaneState { return this } open fun shouldRestoreAirplaneMode(): Boolean { return this.airplaneModeState != this.systemAirplaneModeState } companion object { fun isAirplaneModeOn(context: Context): Boolean { return Settings.Global.getInt(context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0) != 0 } } } ================================================ FILE: app/src/main/java/org/eu/droid_ng/wellbeing/lib/WellbeingService.kt ================================================ package org.eu.droid_ng.wellbeing.lib import android.app.* import android.app.usage.UsageStatsManager import android.appwidget.AppWidgetProvider import android.content.* import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.os.BatteryManager import android.os.Build import android.os.Handler import android.os.HandlerThread import android.service.quicksettings.TileService import android.util.Log import android.widget.Toast import androidx.appcompat.app.AlertDialog import org.eu.droid_ng.wellbeing.R import org.eu.droid_ng.wellbeing.Wellbeing import org.eu.droid_ng.wellbeing.broadcast.AppTimersBroadcastReceiver import org.eu.droid_ng.wellbeing.broadcast.NotificationBroadcastReceiver import org.eu.droid_ng.wellbeing.join import org.eu.droid_ng.wellbeing.shared.BugUtils.Companion.BUG import org.eu.droid_ng.wellbeing.lib.Utils.getTimeUsed import org.eu.droid_ng.wellbeing.shared.Database import org.eu.droid_ng.wellbeing.shared.ExactTime import org.eu.droid_ng.wellbeing.shared.TimeDimension import org.eu.droid_ng.wellbeing.shared.WellbeingFrameworkClient import org.eu.droid_ng.wellbeing.shim.PackageManagerDelegate import org.eu.droid_ng.wellbeing.shim.PackageManagerDelegate.SuspendDialogInfo import org.eu.droid_ng.wellbeing.ui.MainActivity import org.eu.droid_ng.wellbeing.ui.TakeBreakDialogActivity import org.eu.droid_ng.wellbeing.widget.ScreenTimeAppWidget import java.time.Duration import java.time.LocalDateTime import java.time.temporal.ChronoUnit import java.util.* import java.util.concurrent.TimeUnit import java.util.function.Consumer import java.util.stream.Collectors class WellbeingService(private val context: Context) : WellbeingFrameworkClient.ConnectionCallback { private var host: WellbeingStateHost? = null // systemApp should always be true, only used for development purposes. private val systemApp: Boolean = (context.applicationInfo.flags and (ApplicationInfo.FLAG_UPDATED_SYSTEM_APP or ApplicationInfo.FLAG_SYSTEM)) > 1 private val frameworkService: WellbeingFrameworkClient = WellbeingFrameworkClient(context, this) fun bindToHost(newhost: WellbeingStateHost?) { host = newhost if (host != null) { onServiceStartedCallbacks.toTypedArray().forEach { it.run() onServiceStartedCallbacks.remove(it) } } } private val stateCallbacks: ArrayList> = ArrayList() fun addStateCallback(callback: Consumer) { stateCallbacks.add(callback) } fun removeStateCallback(callback: Consumer) { stateCallbacks.remove(callback) } private fun onStateChanged() { updateServiceStatus() stateCallbacks.forEach { it.accept(this) } //Log.i("WellbeingImpl", "found " + frameworkService.getEventCount("unlock", TimeDimension.MONTH, LocalDateTime.now().minusMonths(1), LocalDateTime.now()) + " unlocks") TODO } private val onServiceStartedCallbacks: ArrayList = ArrayList() private fun startService(lateNotify: Boolean = false) { if (host != null) { return } val client = WellbeingStateClient(context) client.startService(lateNotify) } private fun startServiceAnd(lateNotify: Boolean = false, callback: Runnable? = null) { if (host != null) { callback?.run() return } if (callback != null) { onServiceStartedCallbacks.add(callback) } startService(lateNotify) } private fun stopService() { host?.stop() } fun getInstalledApplications(flags: Int = 0): List { val newflags = (when(systemApp) { true -> Utils.PACKAGE_MANAGER_MATCH_INSTANT false -> 0 } or flags) return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { pm.getInstalledApplications(PackageManager.ApplicationInfoFlags.of(newflags.toLong())) } else { pm.getInstalledApplications(newflags) } } @Throws(PackageManager.NameNotFoundException::class) fun getApplicationInfo(packageName: String, matchUninstalled: Boolean = true, flags: Int = 0): ApplicationInfo { val newflags = when(matchUninstalled) { true -> PackageManager.MATCH_UNINSTALLED_PACKAGES false -> 0 } or when(systemApp) { true -> Utils.PACKAGE_MANAGER_MATCH_INSTANT false -> 0 } or PackageManager.MATCH_ALL or flags return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { pm.getApplicationInfo(packageName, PackageManager.ApplicationInfoFlags.of(newflags.toLong())) } else { pm.getApplicationInfo(packageName, newflags) } } @Throws(PackageManager.NameNotFoundException::class) fun getApplicationLabel(packageName: String, matchUninstalled: Boolean = true): CharSequence { return pm.getApplicationLabel(getApplicationInfo(packageName, matchUninstalled)) } companion object { fun get(): WellbeingService { return Wellbeing.getService() } /* **** main part of service starts here **** */ const val INTENT_ACTION_TAKE_BREAK = "org.eu.droid_ng.wellbeing.TAKE_BREAK" const val INTENT_ACTION_QUIT_BREAK = "org.eu.droid_ng.wellbeing.QUIT_BREAK" const val INTENT_ACTION_QUIT_BED = "org.eu.droid_ng.wellbeing.QUIT_BED" const val INTENT_ACTION_QUIT_FOCUS = "org.eu.droid_ng.wellbeing.QUIT_FOCUS" const val INTENT_ACTION_UNSUSPEND_ALL = "org.eu.droid_ng.wellbeing.UNSUSPEND_ALL" val breakTimeOptions = intArrayOf(1, 3, 5, 10, 15) // keep in sync with getUseAppForString } private val handler = Handler.createAsync(context.mainLooper) private val pm = context.packageManager val pmd = PackageManagerDelegate(pm) val cdm = PackageManagerDelegate.getColorDisplayManager(context) val usm = context.getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager private val alc = AlarmCoordinator(context) private val notificationManager = context.getSystemService(NotificationManager::class.java) as NotificationManager private val bgThread = HandlerThread("WellbeingService").also { it.start() } val bgHandler = Handler(bgThread.looper) private val db = Database(context, bgHandler, 0) private var airplaneState: WellbeingAirplaneState private var airplaneStateLogical: Boolean = false private var triggers: Set = HashSet() private val oidMap = context.getSharedPreferences("AppTimersInternal", 0) private val config = context.getSharedPreferences("appTimers", 0) private val sched = context.getSharedPreferences("sched", 0) var focusModeAllApps = true private var focusModeInvertSelection = false private var focusModeBreakTimeDialog = -1 private var focusModeBreakTimeNotification = -1 private var manualSuspendDialog = false private var manualSuspendAllApps = false private var appTimerDialogBreakTime = -1 private var bedtimeGreyscale = true private var bedtimeAirplaneMode = true private var reminderMin = -1 private var lastStatsProcessed = 0L private fun loadSettings() { val prefs = context.getSharedPreferences("service", 0) val bedmode = context.getSharedPreferences("bedtime_mode", 0) focusModeBreakTimeNotification = Integer.parseInt(prefs.getString("focus_notification", focusModeBreakTimeNotification.toString()) ?: focusModeBreakTimeNotification.toString()) focusModeBreakTimeDialog = Integer.parseInt(prefs.getString("focus_dialog", focusModeBreakTimeDialog.toString()) ?: focusModeBreakTimeDialog.toString()) manualSuspendDialog = prefs.getBoolean("manual_dialog", manualSuspendDialog) manualSuspendAllApps = prefs.getBoolean("manual_all", manualSuspendAllApps) focusModeAllApps = prefs.getBoolean("focus_all", focusModeAllApps) focusModeInvertSelection = prefs.getBoolean("focus_whitelist", focusModeInvertSelection) appTimerDialogBreakTime = Integer.parseInt(prefs.getString("app_timer_dialog", appTimerDialogBreakTime.toString()) ?: appTimerDialogBreakTime.toString()) reminderMin = Integer.parseInt(prefs.getString("app_timer_reminder", reminderMin.toString()) ?: reminderMin.toString()) bedtimeGreyscale = bedmode.getBoolean("greyscale", bedtimeGreyscale) bedtimeAirplaneMode = bedmode.getBoolean("airplane_mode", bedtimeAirplaneMode) alc.updateState() } private fun loadSchedcfg() { sched.getStringSet("triggers", HashSet())?.stream()?.map { raw -> val values = raw.split(";;") if (values.size < 2) throw IllegalStateException("invalid value $raw") return@map when (values[0]) { "time" -> { val bools = BooleanArray(7); for (i in bools.indices) if (values[8].toInt() and (1 shl i) != 0) bools[i] = true // bitmask -> boolean[] TimeChargerTriggerCondition(values[1], values[2], values[3].toBooleanStrict(), values[4].toInt(), values[5].toInt(), values[6].toInt(), values[7].toInt(), bools, values[9].toBooleanStrict(), values[10].toBooleanStrict()) } else -> { throw IllegalStateException("invalid trigger type ${values[0]}") } } }?.collect(Collectors.toSet())?.let { triggers = it } ensureSchedSetup() } private fun writeSchedcfg() { val s = sched.edit().clear() s.putStringSet("triggers", triggers.stream().map { when (it) { is TimeChargerTriggerCondition -> { var bits = 0; for (i in 0 until it.weekdays.size) if (it.weekdays[i]) bits = bits or (1 shl i) // boolean[] -> bitmask "time;;${it.id};;${it.iid};;${it.enabled};;${it.startHour};;${it.startMinute};;${it.endHour};;${it.endMinute};;${bits};;${it.needCharger};;${it.endOnAlarm}" } else -> throw IllegalStateException("unknown trigger ${it::class.qualifiedName}") } }.collect(Collectors.toSet())) s.apply() } private fun updateWidget(widget: Class) { val intent = Intent(context, widget) intent.action = "org.eu.droid_ng.wellbeing.APPWIDGET_UPDATE" context.sendBroadcast(intent) } private var bedtimeModeEnabled = false private var isFocusModeEnabled = false private var isFocusModeBreak /* global break */ = false private val perAppState: HashMap = HashMap() init { Utils.clearUsageStatsCache(usm, pm, pmd, true) airplaneState = when (WellbeingAirplaneState.isAirplaneModeOn(context)) { true -> WellbeingAirplaneState.ENABLED_BY_SYSTEM false -> WellbeingAirplaneState.DISABLED_BY_SYSTEM } onStateChanged() // includes loadSettings() ScheduleUtils.ensureWidgetAlarmSet(context, handler, 60, ScreenTimeAppWidget::class.java) ScheduleUtils.ensureStatProcessorAlarmSet(context, handler) val channel = NotificationChannel( "reminder", context.getString(R.string.channel2_name), NotificationManager.IMPORTANCE_HIGH ) channel.description = context.getString(R.string.channel2_description) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { channel.isBlockable = true } notificationManager.createNotificationChannel(channel) context.registerReceiver(object : BroadcastReceiver() { override fun onReceive(p0: Context?, p1: Intent?) { onUpdatePowerConnection() } }, IntentFilter(Intent.ACTION_BATTERY_CHANGED)) context.registerReceiver(object : BroadcastReceiver() { override fun onReceive(p0: Context?, p1: Intent?) { airplaneState = if (WellbeingAirplaneState.isAirplaneModeOn(context)) { airplaneState.onReceiveAirplaneEnabled() } else { airplaneState.onReceiveAirplaneDisabled() } } }, IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED)) frameworkService.tryConnect() } override fun onWellbeingFrameworkConnected(initial: Boolean) { if (hasWellbeingAirplaneModeCapabilities()) { if (airplaneState.wellbeingAirplaneModeState != airplaneStateLogical) { setWellbeingAirplaneMode(airplaneStateLogical) } else if (initial) { val prefs = context.getSharedPreferences("restore_state", 0) if (prefs.getBoolean("restore_airplane_mode", false) && !airplaneState.wellbeingAirplaneModeState) { prefs.edit().remove("restore_airplane_mode").apply() frameworkService.setAirplaneMode(false) } } } } override fun onWellbeingFrameworkDisconnected() {} // Service / Global state. Do not confuse with per-app state, that's using the same values. fun getState(includeAppState: Boolean = true): State { val value = (if (bedtimeModeEnabled) State.STATE_BED_MODE else 0) or (if (isFocusModeEnabled) State.STATE_FOCUS_MODE_ENABLED else 0) or (if (isFocusModeBreak) State.STATE_FOCUS_MODE_GLOBAL_BREAK else 0) or (if (includeAppState && (perAppState.entries.stream().filter { (it.value and State.STATE_FOCUS_MODE_APP_BREAK) > 0 }.findAny().isPresent)) State.STATE_FOCUS_MODE_APP_BREAK else 0) or (if (includeAppState && (perAppState.entries.stream().filter { (it.value and State.STATE_MANUAL_SUSPEND) > 0 }.findAny().isPresent)) State.STATE_MANUAL_SUSPEND else 0) or (if (includeAppState && (perAppState.entries.stream().filter { (it.value and State.STATE_APP_TIMER_SET) > 0 }.findAny().isPresent)) State.STATE_APP_TIMER_SET else 0) or (if (includeAppState && (perAppState.entries.stream().filter { (it.value and State.STATE_APP_TIMER_EXPIRED) > 0 }.findAny().isPresent)) State.STATE_APP_TIMER_EXPIRED else 0) or (if (includeAppState && (perAppState.entries.stream().filter { (it.value and State.STATE_APP_TIMER_BREAK) > 0 }.findAny().isPresent)) State.STATE_APP_TIMER_BREAK else 0) return State(value) } fun getAppState(packageName: String): State { var value = perAppState.getOrDefault(packageName, 0) /* apply matching global flags */ val global = getState(false).toInt() if ((value and State.STATE_FOCUS_MODE_ENABLED) > 0) { value = value or (global and State.STATE_FOCUS_MODE_GLOBAL_BREAK) } /* apply app timer flags */ if (config.getInt(packageName, -1) > 0) { value = value or State.STATE_APP_TIMER_SET } if ((value and State.STATE_APP_TIMER_SET) > 0 && Duration.ofMinutes(config.getInt(packageName, 0).toLong()).minus(getTimeUsed(usm, packageName)).toMinutes() <= 0) { value = value or State.STATE_APP_TIMER_EXPIRED } if ((value and State.STATE_APP_TIMER_SET) > 0 && oidMap.contains(ParsedUoid("AppBreak", 0, arrayOf(packageName)).toString())) { value = value or State.STATE_APP_TIMER_BREAK } return State(value) } fun setBedtimeMode(enable: Boolean) { loadSettings() bedtimeModeEnabled = enable if (bedtimeGreyscale) { cdm.setSaturationLevel(if (enable) 0 else 100) } setWellbeingAirplaneMode(enable && bedtimeAirplaneMode) onStateChanged() doUpdateTile(BedtimeModeQSTile::class.java) } private fun hasWellbeingAirplaneModeCapabilities(): Boolean { return frameworkService.versionCode() >= 1 } fun setWellbeingAirplaneMode(enable: Boolean) { airplaneStateLogical = enable val oldState = airplaneState if (!hasWellbeingAirplaneModeCapabilities()) { // Allow partial update when in state that // previously had airplane mode capabilities if (oldState.wellbeingAirplaneModeState && !enable) { airplaneState = airplaneState.onDisableAirplaneByWellbeing() if (oldState.shouldRestoreAirplaneMode() != airplaneState.shouldRestoreAirplaneMode()) { val prefs = context.getSharedPreferences("restore_state", 0) prefs.edit().putBoolean("restore_airplane_mode", airplaneState.shouldRestoreAirplaneMode()).apply() } } return } airplaneState = when(enable) { true -> airplaneState.onEnableAirplaneByWellbeing() false -> airplaneState.onDisableAirplaneByWellbeing() } if (airplaneState.airplaneModeState != oldState.airplaneModeState) { frameworkService.setAirplaneMode( airplaneState.airplaneModeState) } if (airplaneState.shouldRestoreAirplaneMode() != oldState.shouldRestoreAirplaneMode()) { val prefs = context.getSharedPreferences("restore_state", 0) prefs.edit().putBoolean("restore_airplane_mode", airplaneState.shouldRestoreAirplaneMode()).apply() } } fun onAppTimerExpired(observerId: Int, uniqueObserverId: String) { var msg: String var uoid: String = uniqueObserverId if (oidMap.getInt(uoid, -2) != observerId) { msg = "Warning: unknown oid/uoid - $observerId / $uoid - this might be an bug? Trying to recover." Toast.makeText(context, msg, Toast.LENGTH_LONG).show() BUG(msg) uoid = oidMap.all.entries.stream().filter { a -> observerId == a.value } .findAny().get().key } val parsed = ParsedUoid.from(uoid) msg = "AppTimersInternal: success oid:" + observerId + " action:" + parsed.action + " timeMillis:" + parsed.timeMillis + " pkgs:" + String.join(",", parsed.pkgs) Log.i("AppTimersInternal", msg) when (parsed.action) { "AppTimer", "AppLimit" -> { dropAppTimer(parsed) parsed.pkgs.forEach { if (it == null) return@forEach updateSuspendStatusForApp(it) } } "Reminder" -> { dropAppTimer(parsed) parsed.pkgs.forEach { if (it == null) return@forEach val text = context.resources.getQuantityString( R.plurals.app_timer_reminder_title, reminderMin, reminderMin ) val n = Notification.Builder(context, "reminder") .setWhen(System.currentTimeMillis()) .setSmallIcon(R.drawable.ic_focus_mode) .setOnlyAlertOnce(true) .setContentTitle(text) .setTicker(text) .setContentText( context.getString( R.string.app_timer_reminder, getApplicationLabel(it) ) ) notificationManager.notify( (System.currentTimeMillis() / 1000).toInt(), n.build() ) } } "AppBreak" -> endBreak(parsed.pkgs) else -> { Toast.makeText(context, msg, Toast.LENGTH_LONG).show() BUG(msg) dropAppTimer(parsed) } } } private fun endBreak(pkgs: Array) { val u = ParsedUoid("AppBreak", 0, pkgs) if (!oidMap.contains(u.toString())) return dropAppTimer(u) pkgs.forEach { if (it == null) return@forEach updateSuspendStatusForApp(it) } } private fun loadAppTimer(packageName: String) { val s = arrayOf(packageName) val i = config.getInt(packageName, -1) val m = Duration.ofMinutes(i.toLong()).minus(getTimeUsed(usm, s)) if (i > 0 && m.toMinutes() > 0) setAppTimer(s, m, getTimeUsed(usm, s)) updateSuspendStatusForApp(packageName) } private fun takeAppTimerBreak(packageNames: Array, breakMins: Int) { val u = ParsedUoid("AppBreak", 0, packageNames).toString() if (!oidMap.contains(u)) { updatePrefs(u, makeOid()) } setAppTimerInternal(u, packageNames, Duration.ofMinutes(breakMins.toLong()), getTimeUsed(usm, packageNames)) packageNames.forEach { if (it != null) { updateSuspendStatusForApp(it) } } } fun takeAppTimerBreakWithDialog(activityContext: Activity, endActivity: Boolean, packageNames: Array) { val optionsS: Array = Arrays.stream(breakTimeOptions).mapToObj { i -> context.resources.getQuantityString(R.plurals.break_mins, i, i) }.toArray { arrayOfNulls(it) } val b = AlertDialog.Builder(activityContext) .setTitle(R.string.focus_mode_break) .setNegativeButton(R.string.cancel) { d, _ -> d.dismiss() } .setItems(optionsS) { _, i -> val breakMins = breakTimeOptions[i] takeAppTimerBreak(packageNames, breakMins) if (endActivity) activityContext.finish() } b.show() } private fun loadAppTimers() { oidMap.edit().clear().apply() for (pkg in config.all.keys) { loadAppTimer(pkg) } } fun onUpdateAppTimerPreference(pkgName: String, oldLimit: Duration) { val s = arrayOf(pkgName) var u = ParsedUoid("AppTimer", oldLimit.toMillis(), s) if (oidMap.contains(u.toString())) dropAppTimer(u) u = ParsedUoid("AppLimit", oldLimit.toMillis(), s) if (oidMap.contains(u.toString())) dropAppTimer(u) u = ParsedUoid("AppBreak", 0, s) if (oidMap.contains(u.toString())) dropAppTimer(u) u = ParsedUoid("Reminder", 0, s) if (oidMap.contains(u.toString())) dropAppTimer(u) loadAppTimer(pkgName) } fun onBootCompleted() { // Try to reconnect to frameworkService if the first connection failed. frameworkService.tryConnect() loadAppTimers() doUpdateTile(FocusModeQSTile::class.java) doUpdateTile(BedtimeModeQSTile::class.java) onStateChanged() } @Suppress("unchecked_cast") // AIDL fun getBugs(): MutableMap { return frameworkService.bugs as MutableMap } private fun doUpdateTile(tile: Class) { TileService.requestListeningState(context, ComponentName(context, tile)) } private fun updateServiceStatus() { loadSettings() loadSchedcfg() updateWidget(ScreenTimeAppWidget::class.java) val state = getState() val needServiceRunning = state.isFocusModeEnabled() || state.isSuspendedManually() || state.isBedtimeModeEnabled() val next = { if (state.isFocusModeEnabled()) { if (state.isOnFocusModeBreakGlobal()) { host?.updateNotification( R.string.focus_mode, R.string.notification_focus_mode_break, R.drawable.outline_badge_24, arrayOf( host?.buildAction( R.string.focus_mode_break_end, R.drawable.ic_take_break, Intent( context, NotificationBroadcastReceiver::class.java ).setAction(INTENT_ACTION_QUIT_BREAK), true ), host?.buildAction( R.string.focus_mode_off, R.drawable.baseline_cancel_24, Intent( context, NotificationBroadcastReceiver::class.java ).setAction(INTENT_ACTION_QUIT_FOCUS), true ) ), Intent(context, MainActivity::class.java) ) } else { host?.updateNotification( R.string.focus_mode, R.string.notification_focus_mode, R.drawable.outline_badge_24, arrayOf( if (focusModeBreakTimeNotification == -1) host?.buildAction( R.string.focus_mode_break, R.drawable.ic_take_break, Intent( context, TakeBreakDialogActivity::class.java ), false ) else host?.buildAction( R.string.focus_mode_break, R.drawable.ic_take_break, Intent( context, NotificationBroadcastReceiver::class.java ).setAction(INTENT_ACTION_TAKE_BREAK), true ), host?.buildAction( R.string.focus_mode_off, R.drawable.baseline_cancel_24, Intent( context, NotificationBroadcastReceiver::class.java ).setAction(INTENT_ACTION_QUIT_FOCUS), true ) ), Intent(context, MainActivity::class.java) ) } } else if (state.isSuspendedManually()) { host?.updateNotification( R.string.notification_title, R.string.notification_manual, R.drawable.ic_baseline_person_24, arrayOf( host?.buildAction( R.string.unsuspend_all, R.drawable.baseline_exit_to_app_24, Intent( context, NotificationBroadcastReceiver::class.java ).setAction(INTENT_ACTION_UNSUSPEND_ALL), true ) ), Intent(context, MainActivity::class.java) ) } else if (state.isBedtimeModeEnabled()) { host?.updateNotification( R.string.bedtime_mode, R.string.bedtime_desc, R.drawable.baseline_bedtime_24, arrayOf( host?.buildAction( R.string.disable, R.drawable.baseline_cancel_24, Intent( context, NotificationBroadcastReceiver::class.java ).setAction(INTENT_ACTION_QUIT_BED), true ) ), Intent(context, MainActivity::class.java) ) } else { host?.updateDefaultNotification() } } if (needServiceRunning) { if (host == null) { startServiceAnd { next() } } else { next() } } else { if (host != null) { stopService() } next() } } fun onManuallyUnsuspended(packageName: String) { val state = getAppState(packageName) if (state.isFocusModeEnabled() && !(state.isOnFocusModeBreakGlobal() || state.isOnFocusModeBreakPartial())) { if (focusModeAllApps) { takeFocusModeBreak(focusModeBreakTimeDialog) } else { takeFocusModeBreak(arrayOf(packageName), focusModeBreakTimeDialog) } } else if (state.isSuspendedManually()) { if (manualSuspendAllApps) { manualUnsuspend(null) // unsuspend all } else { manualUnsuspend(arrayOf(packageName)) } } else { BUG("Unable to handle manual unsuspend") } } private fun getUseAppForString(time: Int): Int { // keep in sync with breakTimeOptions return when (time) { 1 -> R.string.break_dialog_1 3 -> R.string.break_dialog_3 5 -> R.string.break_dialog_5 10 -> R.string.break_dialog_10 15 -> R.string.break_dialog_15 else -> { throw IllegalArgumentException("$time needs to be in breakTimeOptions list") } } } fun onNotificationActionClick(action: String) { when (action) { INTENT_ACTION_UNSUSPEND_ALL -> { manualUnsuspend(null) } INTENT_ACTION_TAKE_BREAK -> { takeFocusModeBreak(focusModeBreakTimeNotification) } INTENT_ACTION_QUIT_BREAK -> { endFocusModeBreak() } INTENT_ACTION_QUIT_FOCUS -> { disableFocusMode() } INTENT_ACTION_QUIT_BED -> { setBedtimeMode(false) } else -> { BUG("invalid notification action: $action") } } } private fun updateSuspendStatusForApp(packageName: String) { val state = getAppState(packageName) val f: Array = if (state.isFocusModeEnabled() && !(state.isOnFocusModeBreakGlobal() || state.isOnFocusModeBreakPartial())) { val label: CharSequence = try { getApplicationLabel(packageName, false) } catch (e: PackageManager.NameNotFoundException) { BUG("tried to suspend nonexistant app: $packageName") return } val di = SuspendDialogInfo.Builder() .setTitle(R.string.focus_mode_enabled) .setMessage(context.getString(R.string.focus_mode_dialog, label)) .setIcon(R.drawable.ic_focus_mode) .setNeutralButtonText(if (focusModeBreakTimeDialog == -1) R.string.dialog_btn_settings else getUseAppForString(focusModeBreakTimeDialog)) .setNeutralButtonAction(if (focusModeBreakTimeDialog == -1) SuspendDialogInfo.BUTTON_ACTION_MORE_DETAILS else SuspendDialogInfo.BUTTON_ACTION_UNSUSPEND) .build() pmd.setPackagesSuspended(arrayOf(packageName), true, null, null, di) } else if (state.isSuspendedManually()) { val di = SuspendDialogInfo.Builder() .setTitle(R.string.dialog_title) .setMessage(R.string.dialog_message) .setIcon(R.drawable.ic_baseline_app_blocking_24) .setNeutralButtonText(if (!manualSuspendDialog) R.string.dialog_btn_settings else (if (manualSuspendAllApps) R.string.unsuspend_all else R.string.unsuspend)) .setNeutralButtonAction(if (!manualSuspendDialog) SuspendDialogInfo.BUTTON_ACTION_MORE_DETAILS else SuspendDialogInfo.BUTTON_ACTION_UNSUSPEND) .build() pmd.setPackagesSuspended(arrayOf(packageName), true, null, null, di) } else if (state.isAppTimerExpired() && !state.isAppTimerBreak()) { pmd.setPackagesSuspended( arrayOf(packageName), true, null, null, SuspendDialogInfo.Builder() .setTitle(R.string.app_timers) .setMessage(context.getString(R.string.app_timer_exceed_f, getApplicationLabel(packageName))) .setNeutralButtonText(if (appTimerDialogBreakTime == -1) R.string.dialog_btn_settings else getUseAppForString(appTimerDialogBreakTime)) .setNeutralButtonAction(if (appTimerDialogBreakTime == -1) SuspendDialogInfo.BUTTON_ACTION_MORE_DETAILS else SuspendDialogInfo.BUTTON_ACTION_UNSUSPEND) .setIcon(R.drawable.ic_focus_mode).build() ) } else { pmd.setPackagesSuspended(arrayOf(packageName), false, null, null, null) } for (s in f) { BUG("Failed to (un)suspend package: $s") } } private fun setFocusModeStateForPkgInternal(s: String, suspend: Boolean, forBreak: Boolean, forAppBreak: Boolean) { if (suspend) { perAppState[s] = (perAppState.getOrDefault(s, 0) or State.STATE_FOCUS_MODE_ENABLED) and State.STATE_FOCUS_MODE_APP_BREAK.inv() } else { if (forBreak) { if (forAppBreak) { perAppState[s] = perAppState.getOrDefault(s, 0) or (State.STATE_FOCUS_MODE_APP_BREAK) } } else { perAppState[s] = perAppState.getOrDefault(s, 0) and (State.STATE_FOCUS_MODE_ENABLED.inv() and State.STATE_FOCUS_MODE_APP_BREAK.inv()) } } updateSuspendStatusForApp(s) } private fun isValidFocusPkg(packageName: String): Boolean { return !Utils.blackListedPackages.contains(packageName) && !Utils.restrictedPackages.contains(packageName) } fun enableFocusMode() { loadSettings() val spref = context.getSharedPreferences("appLists", 0) val st = spref.getStringSet("focus_mode", null) if (st == null) { BUG("st == null") return } isFocusModeEnabled = true isFocusModeBreak = false for (s in getInstalledApplications(PackageManager.GET_META_DATA)) if (((!focusModeInvertSelection && st.contains(s.packageName)) || (focusModeInvertSelection && !st.contains(s.packageName))) && isValidFocusPkg(s.packageName)) setFocusModeStateForPkgInternal(s.packageName, suspend = true, forBreak = false, forAppBreak = false) onStateChanged() doUpdateTile(FocusModeQSTile::class.java) } fun disableFocusMode() { loadSettings() val spref = context.getSharedPreferences("appLists", 0) val st = spref.getStringSet("focus_mode", null) if (st == null) { BUG("st == null") return } if (isFocusModeBreak) { handler.removeCallbacks(breakEndedCallback) } oneAppUnsuspendCallbacks.forEach { handler.removeCallbacks(it) } oneAppUnsuspendCallbacks.clear() isFocusModeEnabled = false isFocusModeBreak = false for (s in getInstalledApplications(PackageManager.GET_META_DATA)) if (((!focusModeInvertSelection && st.contains(s.packageName)) || (focusModeInvertSelection && !st.contains(s.packageName))) && isValidFocusPkg(s.packageName)) setFocusModeStateForPkgInternal(s.packageName, suspend = false, forBreak = false, forAppBreak = false) onStateChanged() doUpdateTile(FocusModeQSTile::class.java) } // Runs every 12 hours fun onProcessStats(inBackground: Boolean) { val knownKeys = HashSet() knownKeys.add("usage") // Data saved for ~10 days. We make sure we don't delete correct data, so even if there is no data, it's OK. val absStart = ExactTime.plus(LocalDateTime.now(), TimeDimension.DAY, if (!inBackground) -3 else -10) val absEnd = ExactTime.plus(LocalDateTime.now(), TimeDimension.DAY, 1) val dimension = TimeDimension.HOUR var startTime = absStart var endTime = ExactTime.plus(startTime, dimension, 1) while ((startTime.isAfter(absStart) || startTime.isEqual(absStart)) && startTime.isBefore(absEnd) && endTime.isAfter(absStart) && (endTime.isBefore(absEnd) || endTime.isEqual(absEnd))) { val result = Utils.calculateUsageStats( usm, ExactTime.of(startTime, dimension) * 1000L, (ExactTime.of(endTime, dimension) - 1L) * 1000L ) val calculatedScreenTime = result.first val calculatedUsageStats = result.second.first val curValue = db.getCountFor("usage", dimension, startTime, ExactTime.plus(startTime, dimension, 1)) val newValue = calculatedScreenTime.toMinutes() if (newValue > curValue) db.insert("usage", startTime, dimension, calculatedScreenTime.toMinutes()) calculatedUsageStats.forEach { val key = "usage_${it.key}" val curVal = db.getCountFor(key, dimension, startTime, ExactTime.plus(startTime, dimension, 1)) val newVal = it.value.toMinutes() if (newVal > curVal) db.insert(key, startTime, dimension, it.value.toMinutes()) knownKeys.add(key) } startTime = endTime endTime = ExactTime.plus(startTime, dimension, 1) } knownKeys.forEach { db.consolidate(it, true) } lastStatsProcessed = System.currentTimeMillis() } fun getEventStatsByType(type: String, dimension: TimeDimension, from: LocalDateTime, to: LocalDateTime): Long { return db.getCountFor(type, dimension, from, to) } fun getEventStatsByPrefix(prefix: String, dimension: TimeDimension, from: LocalDateTime, to: LocalDateTime): Map { return db.getTypesForPrefix(prefix, dimension, from, to) } fun onFocusModePreferenceChanged(packageName: String) { loadSettings() val spref = context.getSharedPreferences("appLists", 0) val st = spref.getStringSet("focus_mode", null) if (st == null) { BUG("st == null") return } setFocusModeStateForPkgInternal(packageName, isFocusModeEnabled && isValidFocusPkg(packageName) && ((!focusModeInvertSelection && st.contains(packageName)) || (focusModeInvertSelection && !st.contains(packageName))) && !isFocusModeBreak, isFocusModeEnabled && isFocusModeBreak, false) } fun takeFocusModeBreakWithDialog(activityContext: Activity, endActivity: Boolean, packageNames: Array?) { loadSettings() val optionsS: Array = Arrays.stream(breakTimeOptions).mapToObj { i -> context.resources.getQuantityString(R.plurals.break_mins, i, i) }.toArray { arrayOfNulls(it) } val b = AlertDialog.Builder(activityContext) .setTitle(R.string.focus_mode_break) .setNegativeButton(R.string.cancel) { d, _ -> d.dismiss() } .setItems(optionsS) { _, i -> val breakMins = breakTimeOptions[i] takeFocusModeBreak(packageNames, breakMins) if (endActivity) activityContext.finish() } b.show() } private val breakEndedCallback = Runnable { endFocusModeBreak(false) } fun endFocusModeBreak(needCancel: Boolean = true) { loadSettings() if (!isFocusModeEnabled) { BUG("Focus mode not active") return } if (!isFocusModeBreak) { BUG("No focus mode break active") return } if (needCancel) { handler.removeCallbacks(breakEndedCallback) } isFocusModeBreak = false val spref = context.getSharedPreferences("appLists", 0) val st = spref.getStringSet("focus_mode", null) if (st == null) { BUG("st == null") return } for (packageName in st) { setFocusModeStateForPkgInternal(packageName, suspend = true, forBreak = true, forAppBreak = false) } onStateChanged() } private val oneAppUnsuspendCallbacks = ArrayList() private fun takeFocusModeBreak(packageNames: Array?, breakMins: Int) { loadSettings() if (packageNames == null) { takeFocusModeBreak(breakMins) return } if (!isFocusModeEnabled) { BUG("Focus mode not active") return } if (isFocusModeBreak) { BUG("Focus mode break active") return } for (packageName in packageNames) { setFocusModeStateForPkgInternal(packageName, suspend = false, forBreak = true, forAppBreak = true) } val r = object : Runnable { override fun run() { oneAppUnsuspendCallbacks.remove(this) for (packageName in packageNames) { setFocusModeStateForPkgInternal(packageName, isFocusModeEnabled, isFocusModeEnabled, true) } } } oneAppUnsuspendCallbacks.add(r) handler.postDelayed(r, breakMins * 60 * 1000L) onStateChanged() } fun takeFocusModeBreak(breakMins: Int) { loadSettings() if (!isFocusModeEnabled) { BUG("Focus mode not active") return } if (isFocusModeBreak) { BUG("Focus mode break active") return } val spref = context.getSharedPreferences("appLists", 0) val st = spref.getStringSet("focus_mode", null) if (st == null) { BUG("st == null") return } isFocusModeBreak = true for (packageName in st) { setFocusModeStateForPkgInternal(packageName, suspend = false, forBreak = true, forAppBreak = false) } handler.postDelayed(breakEndedCallback, breakMins * 60 * 1000L) onStateChanged() } fun manualSuspend(packageNamesI: Array?) { loadSettings() val packageNames: Array = if (packageNamesI == null) { val spref = context.getSharedPreferences("appLists", 0) val packageNamesT = spref.getStringSet("manual_suspend", null) if (packageNamesT == null) { BUG("packagesNames == null") return } packageNamesT.toTypedArray() } else packageNamesI for (s in packageNames) { perAppState[s] = perAppState.getOrDefault(s, 0) or State.STATE_MANUAL_SUSPEND updateSuspendStatusForApp(s) } onStateChanged() } fun manualUnsuspend(packageNamesI: Array?) { loadSettings() val packageNames: Array = if (packageNamesI == null) { val spref = context.getSharedPreferences("appLists", 0) val packageNamesT = spref.getStringSet("manual_suspend", null) if (packageNamesT == null) { BUG("packagesNames == null") return } packageNamesT.toTypedArray() } else packageNamesI for (s in packageNames) { perAppState[s] = perAppState.getOrDefault(s, 0) and State.STATE_MANUAL_SUSPEND.inv() updateSuspendStatusForApp(s) } onStateChanged() } // start time limit core private fun updatePrefs(key: String, value: Int) { if (value < 0) { oidMap.edit().remove(key).apply() } else { oidMap.edit().putInt(key, value).apply() } } private fun makeOid(): Int { val vals: Collection<*> = oidMap.all.values // try to save time by starting at size value for (i in vals.size..999) { if (!vals.contains(i)) return i } // if all high values are used up, try all values for (i in 0..999) { if (!vals.contains(i)) return i } throw IllegalStateException("more than 1000 observers registered") } private class ParsedUoid(val action: String, val timeMillis: Long, val pkgs: Array) { override fun toString(): String { return action + ":" + timeMillis + "//" + java.lang.String.join(":", *pkgs) } companion object { fun from(uoid: String): ParsedUoid { val l = uoid.indexOf(":") val ll = uoid.indexOf("//") val action = uoid.substring(0, l) val timeMillis = uoid.substring(l + 1, ll).toLong() val pkgs: Array = uoid.substring(ll + 2).split(":".toRegex()).dropLastWhile { it.isEmpty() } .toTypedArray() return ParsedUoid(action, timeMillis, pkgs) } } } private fun setUnhintedAppTimerInternal( oid: Int, uoid: String, toObserve: Array, timeLimit: Duration ) { val i = Intent(context, AppTimersBroadcastReceiver::class.java) i.putExtra("observerId", oid) i.putExtra("uniqueObserverId", uoid) val pintent: PendingIntent = PendingIntent.getBroadcast(context, oid, i, PendingIntent.FLAG_IMMUTABLE) PackageManagerDelegate.registerAppUsageObserver( usm, oid, toObserve, timeLimit.toMillis(), TimeUnit.MILLISECONDS, pintent ) } private fun setHintedAppTimerInternal( oid: Int, uoid: String, toObserve: Array, timeLimit: Duration, timeUsed: Duration ) { val i = Intent(context, AppTimersBroadcastReceiver::class.java) i.putExtra("observerId", oid) i.putExtra("uniqueObserverId", uoid) val pIntent = PendingIntent.getBroadcast(context, oid, i, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT) PackageManagerDelegate.registerAppUsageLimitObserver( usm, oid, toObserve, timeLimit, timeUsed, pIntent ) } private fun setAppTimerInternal( uoid: String, toObserve: Array, timeLimit: Duration, timeUsed: Duration? ) { val oid: Int = oidMap.getInt(uoid, -1) if (timeUsed == null) { setUnhintedAppTimerInternal(oid, uoid, toObserve, timeLimit) } else { setHintedAppTimerInternal(oid, uoid, toObserve, timeLimit, timeUsed) } } private fun dropAppTimer(parsedUoid: ParsedUoid) { val uoid = parsedUoid.toString() updatePrefs(uoid, -1) //delete pref if (parsedUoid.action != "AppLimit") { PackageManagerDelegate.unregisterAppUsageLimitObserver(usm, oidMap.getInt(uoid, -1)) } else { PackageManagerDelegate.unregisterAppUsageObserver(usm, oidMap.getInt(uoid, -1)) } } private fun setAppTimer( toObserve: Array, timeLimit: Duration, timeUsed: Duration? ) { // AppLimit: do not provide info to launcher, use registerAppUsageObserver // AppTimer: provide info to launcher, use registerAppUsageLimitObserver val uoid = ParsedUoid( if (timeUsed == null) "AppLimit" else "AppTimer", timeLimit.toMillis(), toObserve ).toString() var timeLimitInternal = timeLimit if (timeUsed != null) { timeLimitInternal = timeLimitInternal.minus(timeUsed) } if (!oidMap.contains(uoid)) { updatePrefs(uoid, makeOid()) } if (reminderMin > 0 && timeLimitInternal.toMinutes() > reminderMin) { val u = ParsedUoid("Reminder", 0, toObserve).toString() if (!oidMap.contains(u)) { updatePrefs(u, makeOid()) } setAppTimerInternal(u, toObserve, timeLimitInternal.minus(reminderMin.toLong(), ChronoUnit.MINUTES), null) } setAppTimerInternal(uoid, toObserve, timeLimitInternal, timeUsed) } // end time limit core fun onUpdatePowerConnection() { val batteryStatus: Intent? = IntentFilter(Intent.ACTION_BATTERY_CHANGED).let { context.registerReceiver(null, it) } val chargePlug: Int = batteryStatus?.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) ?: -1 val charging = chargePlug == BatteryManager.BATTERY_PLUGGED_USB || chargePlug == BatteryManager.BATTERY_PLUGGED_AC doTrigger(!charging) { it is TimeChargerTriggerCondition && it.needCharger } } private fun ensureSchedSetup() { triggers.forEach { it.setup(context) } } private fun triggerFired(expire: Boolean, trigger: Trigger) { when (trigger.id) { "bedtime_mode" -> { if (expire && bedtimeModeEnabled) { setBedtimeMode(false) } else if (!expire) { setBedtimeMode(true) } } "focus_mode" -> { if (expire && isFocusModeEnabled) { disableFocusMode() } else if (!expire) { enableFocusMode() } } else -> { BUG("invalid trigger id ${trigger.id} expire=$expire") } } } fun doTrigger(expire: Boolean, condition: (Trigger) -> Boolean) { triggers.forEach { fired -> if (condition(fired) && // is this the trigger we're searching for? (expire || // is this an deactivation request? (fired !is Condition) || // if this trigger is an condition, it needs to be fulfilled fired.isFulfilled(context))) { triggerFired(expire, fired) } } } fun onAlarmFired(id: String) { if ("alc" == id) { alc.fired() return } if ("__STATS" == id) { bgHandler.post { onProcessStats(true) } return } var t = false val nid = if (id.startsWith("expire::")) { t = true id.substring(8) } else id doTrigger(t) { it is TimeChargerTriggerCondition && it.iid == nid } } fun setTriggersForId(id: String, triggersIn: Array) { triggers.filter { id == it.id }.forEach { it.dispose(context) } triggers = triggers.filterNot { id == it.id }.toSet().plus(triggersIn) writeSchedcfg() ensureSchedSetup() } fun getTriggersForId(id: String): List { return triggers.filter { id == it.id } } } ================================================ FILE: app/src/main/java/org/eu/droid_ng/wellbeing/lib/WellbeingStateUtil.kt ================================================ package org.eu.droid_ng.wellbeing.lib import android.app.* import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection import android.graphics.drawable.Icon import android.os.Binder import android.os.Build import android.os.IBinder import android.util.Log import android.widget.Toast import org.eu.droid_ng.wellbeing.R import org.eu.droid_ng.wellbeing.shared.BugUtils.Companion.BUG import org.eu.droid_ng.wellbeing.lib.WellbeingStateHost.LocalBinder import org.eu.droid_ng.wellbeing.ui.MainActivity import java.util.function.Consumer // Helper to connect to WellbeingStateHost class WellbeingStateClient(context: Context) { // Our context private val context = context.applicationContext // Don't attempt to unbind from the service unless the client has received some // information about the service's state. private var mShouldUnbind = false // To invoke the bound service, first make sure that this value // is not null. private var mBoundService: WellbeingStateHost? = null // Callback when service is connected private var callback: Consumer? = null // Connection callback utility private val mConnection: ServiceConnection = object : ServiceConnection { override fun onServiceConnected(className: ComponentName, service: IBinder) { mBoundService = try { (service as LocalBinder).service } catch (e: ClassCastException) { Log.e("WellbeingStateClient", Log.getStackTraceString(e)) Toast.makeText( context, "Assertion failure (0xAE): Service is in another process. Please report this to the developers!", Toast.LENGTH_SHORT ).show() BUG("0xAE: ${Log.getStackTraceString(e)}") return } callback!!.accept(mBoundService!!.state) } override fun onServiceDisconnected(className: ComponentName) { mBoundService = null } /*override fun onNullBinding(className: ComponentName) { Toast.makeText(context, "Assertion failure (0xAF): Service is null. Please report this to the developers!", Toast.LENGTH_SHORT).show() BUG("service is null (0xAF)") }*/ } //backward compatibility does what we want, so ignore warning @Suppress("deprecation") fun isServiceRunning(): Boolean { val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager for (service in manager.getRunningServices(Int.MAX_VALUE)) { if (WellbeingStateHost::class.java.name == service.service.className) { return true } } return false } fun doBindService( callback: Consumer, canHandleFailure: Boolean, maybeStartService: Boolean = false, lateNotify: Boolean = false ): Boolean { this.callback = callback if (mBoundService != null) { callback.accept(mBoundService!!.state) return true } return if (isServiceRunning() && context.bindService( Intent(context, WellbeingStateHost::class.java), mConnection, Context.BIND_IMPORTANT ) ) { mShouldUnbind = true true } else { if (maybeStartService) { startService(lateNotify) if (doBindService(callback, canHandleFailure = true, maybeStartService = false, lateNotify)) { return true } else if (!canHandleFailure) { Toast.makeText( context, "Assertion failure (0xAA): Failed to start service. Please report this to the developers!", Toast.LENGTH_SHORT ).show() BUG("didn't start service (0xAA)") } } else if (!canHandleFailure) { Toast.makeText( context, "Assertion failure (0xAD): Failed to find service. Please report this to the developers!", Toast.LENGTH_SHORT ).show() BUG("no service (0xAD)") } false } } fun startService(lateNotify: Boolean = false) { val i = Intent(context, WellbeingStateHost::class.java) i.putExtra("lateNotify", lateNotify) context.startForegroundService(i) } fun killService() { context.stopService(Intent(context, WellbeingStateHost::class.java)) } } // Fancy class holding WellbeingService & a notification class WellbeingStateHost : Service() { var state: WellbeingService? = null private var lateNotify = false private var mStopped = false // Unique Identification Number for the Notification. private val notificationId = 325563 private val channelId = "service_notif" /** * Class for clients to access. Because we know this service always * runs in the same process as its clients, we don't need to deal with * IPC. */ inner class LocalBinder : Binder() { val service: WellbeingStateHost get() = this@WellbeingStateHost } override fun onCreate() { state = WellbeingService.get() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val notificationManager = getSystemService( NotificationManager::class.java ) val channel = NotificationChannel( channelId, getString(R.string.channel_name), NotificationManager.IMPORTANCE_LOW ) channel.description = getString(R.string.channel_description) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { channel.isBlockable = true } notificationManager.createNotificationChannel(channel) if (intent != null) { lateNotify = intent.getBooleanExtra("lateNotify", lateNotify) } val n = buildDefaultNotification() // Notification ID cannot be 0. startForeground(notificationId, n) state?.bindToHost(this) return START_STICKY } fun buildAction( actionText: Int, actionIcon: Int, actionIntent: Intent?, isBroadcast: Boolean ): Notification.Action { val pendingIntent = if (isBroadcast) { PendingIntent.getBroadcast(this, 0, actionIntent!!, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT) } else { PendingIntent.getActivity(this, 0, actionIntent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT) } val builder = Notification.Action.Builder( Icon.createWithResource(applicationContext, actionIcon), getText(actionText), pendingIntent ) .setAllowGeneratedReplies(false).setContextual(true) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { builder.setAuthenticationRequired(true) } return builder.build() } private fun buildNotification( title: Int, text: String, icon: Int, actions: Array, notificationIntent: Intent ): Notification { val pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE) val b = Notification.Builder(this, channelId) .setSmallIcon(icon) // the status icon .setTicker(text) // the status text .setWhen(System.currentTimeMillis()) // the time stamp .setContentTitle(getText(title)) // the label of the entry .setContentText(text) // the contents of the entry .setContentIntent(pendingIntent) // The intent to send when the entry is clicked .setOnlyAlertOnce(true) // don't headsup/bling twice if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !lateNotify) { b.setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE) // do not wait with showing the notification } if (lateNotify) lateNotify = false for (action in actions) { if (action != null) b.addAction(action) } return b.build() } private fun buildDefaultNotification(): Notification { val text = R.string.notification_desc val title = R.string.notification_title val icon = R.drawable.ic_stat_name val notificationIntent = Intent(this, MainActivity::class.java) return buildNotification(title, getString(text), icon, arrayOf(), notificationIntent) } private fun updateNotification(n: Notification) { if (mStopped) return getSystemService(NotificationManager::class.java).notify(notificationId, n) } private fun updateNotification( title: Int, text: String, icon: Int, actions: Array, notificationIntent: Intent ) { updateNotification(buildNotification(title, text, icon, actions, notificationIntent)) } fun updateNotification( title: Int, text: Int, icon: Int, actions: Array, notificationIntent: Intent ) { updateNotification(title, getString(text), icon, actions, notificationIntent) } fun updateDefaultNotification() { updateNotification(buildDefaultNotification()) } fun stop() { mStopped = true stopForeground(STOP_FOREGROUND_REMOVE) stopSelf() } override fun onDestroy() { super.onDestroy() state?.bindToHost(null) } override fun onBind(intent: Intent): IBinder { return mBinder } // This is the object that receives interactions from clients. See // RemoteService for a more complete example. private val mBinder: IBinder = LocalBinder() } ================================================ FILE: app/src/main/java/org/eu/droid_ng/wellbeing/prefs/AppTimers.kt ================================================ package org.eu.droid_ng.wellbeing.prefs import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.content.pm.ResolveInfo import android.graphics.drawable.Drawable import android.os.Bundle import android.os.Handler import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.NumberPicker import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.widget.AppCompatImageButton import androidx.appcompat.widget.AppCompatImageView import androidx.appcompat.widget.AppCompatTextView import androidx.recyclerview.widget.RecyclerView import com.google.android.material.checkbox.MaterialCheckBox import org.eu.droid_ng.wellbeing.R import org.eu.droid_ng.wellbeing.lib.Utils import org.eu.droid_ng.wellbeing.lib.Utils.clearUsageStatsCache import org.eu.droid_ng.wellbeing.lib.Utils.getTimeUsed import org.eu.droid_ng.wellbeing.lib.WellbeingService import org.eu.droid_ng.wellbeing.lib.WellbeingService.Companion.get import org.eu.droid_ng.wellbeing.prefs.AppTimers.AppTimersRecyclerViewAdapter.AppTimerViewHolder import java.text.Collator import java.time.Duration import java.util.stream.Collectors class AppTimers : AppCompatActivity() { private var ati: WellbeingService? = null private var h: Handler? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) h = Handler(mainLooper) ati = get() setContentView(R.layout.activity_app_timers) setSupportActionBar(findViewById(R.id.topbar)) val actionBar = checkNotNull(supportActionBar) actionBar.setDisplayHomeAsUpEnabled(true) val r = findViewById(R.id.appTimerPkgs) Thread { val a = AppTimersRecyclerViewAdapter( this, ati!!.getInstalledApplications(PackageManager.GET_META_DATA) ) h!!.post { findViewById(R.id.appTimerLoading).visibility = View.GONE r.adapter = a r.visibility = View.VISIBLE } }.start() } override fun onSupportNavigateUp(): Boolean { finish() return true } inner class AppTimersRecyclerViewAdapter(context: Context, mData: List) : RecyclerView.Adapter() { private val inflater: LayoutInflater = LayoutInflater.from(context) private val mData: List private val pm: PackageManager = context.packageManager val prefs: SharedPreferences = context.getSharedPreferences("appTimers", 0) val enabledMap: MutableMap = HashMap() init { prefs.all.forEach { (k: String, v: Any?) -> if (v !is Int) { Log.e("OpenWellbeing", "Failed to parse $k") return@forEach } enabledMap[k] = v } val collator = Collator.getInstance() // Sort alphabetically by display name val nc = Comparator { a, b -> val durationA = getTimeUsed( ati!!.usm, a.packageName ) val durationB = getTimeUsed(ati!!.usm, b.packageName) val x = durationA.compareTo(durationB) if (x != 0) return@Comparator -x val displayA: CharSequence = getAppNameForPkgName(a.packageName) val displayB: CharSequence = getAppNameForPkgName(b.packageName) collator.compare(displayA, displayB) } val mainIntent = Intent(Intent.ACTION_MAIN, null) .addCategory(Intent.CATEGORY_LAUNCHER) // We already force include user apps, so let's only iterate over system apps val hasLauncherIcon = pm.queryIntentActivities(mainIntent, PackageManager.MATCH_SYSTEM_ONLY) .stream().map { a: ResolveInfo -> a.activityInfo.packageName } .collect(Collectors.toList()) this.mData = mData.stream().filter { // Filter out system apps without launcher icon and Default Launcher val isUser = (it.flags and (ApplicationInfo.FLAG_UPDATED_SYSTEM_APP or ApplicationInfo.FLAG_SYSTEM)) < 1 !Utils.blackListedPackages.contains(it.packageName) && (isUser || hasLauncherIcon.contains( it.packageName )) }.sorted { a, b -> // Enabled goes first val hasA = enabledMap.getOrDefault(a.packageName, 0) != 0 val hasB = enabledMap.getOrDefault(b.packageName, 0) != 0 if (hasA && hasB) return@sorted nc.compare(a, b) else if (hasA) return@sorted -1 else if (hasB) return@sorted 1 else return@sorted nc.compare(a, b) }.collect(Collectors.toList()) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppTimerViewHolder { val view = inflater.inflate(R.layout.appitem, parent, false) return AppTimerViewHolder(view) } override fun getItemCount(): Int { return mData.size } override fun onBindViewHolder(holder: AppTimerViewHolder, position: Int) { val i = mData[position] val mins = prefs.getInt(i.packageName, 0) holder.apply(i, mins) } inner class AppTimerViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { private val container: ViewGroup = itemView.findViewById(R.id.container) private val appIcon: AppCompatImageView = itemView.findViewById(R.id.appIcon) private val appName: AppCompatTextView = itemView.findViewById(R.id.appName2) private val appTimerInfo: AppCompatTextView = itemView.findViewById(R.id.pkgName) private val actionButton = AppCompatImageButton(itemView.context) init { actionButton.setImageDrawable( AppCompatResources.getDrawable( itemView.context, R.drawable.ic_focus_mode ) ) actionButton.background = null val checkBox = itemView.findViewById(R.id.isChecked) val parent = checkBox.parent as ViewGroup val idx = parent.indexOfChild(checkBox) parent.removeView(checkBox) parent.addView(actionButton, idx) } fun apply(info: ApplicationInfo, mins: Int) { val restricted = Utils.restrictedPackages.contains(info.packageName) appIcon.setImageDrawable(getAppIconForPkgName(info.packageName)) appName.text = getAppNameForPkgName(info.packageName) applyText( mins, Math.toIntExact( getTimeUsed( ati!!.usm, info.packageName ).toMinutes() ) ) actionButton.isEnabled = !restricted container.setOnClickListener { if (restricted) return@setOnClickListener val realmins = enabledMap.getOrDefault(info.packageName, 0) val numberPicker = NumberPicker(this@AppTimers) numberPicker.minValue = 0 numberPicker.maxValue = 9999 //i mean why not numberPicker.value = realmins AlertDialog.Builder(this@AppTimers) .setTitle(pm.getApplicationLabel(info)) .setView(numberPicker) .setNegativeButton(R.string.cancel) { _, _ -> } .setPositiveButton(R.string.ok) { _, _ -> updateMins(info.packageName, realmins, numberPicker.value) } .show() } } private fun updateMins(pkgName: String, oldmins: Int, mins: Int) { enabledMap[pkgName] = mins prefs.edit().putInt(pkgName, mins).apply() applyText( mins, Math.toIntExact( getTimeUsed( ati!!.usm, pkgName ).toMinutes() ) ) Thread { clearUsageStatsCache(ati!!.usm, pm, get().pmd, true) h!!.post { applyText( mins, Math.toIntExact( getTimeUsed( ati!!.usm, pkgName ).toMinutes() ) ) ati!!.onUpdateAppTimerPreference( pkgName, Duration.ofMinutes(oldmins.toLong()) ) } }.start() } private fun applyText(mins: Int, mins2: Int) { appTimerInfo.text = itemView.context.getString( R.string.desc_container, if (mins == 0) itemView.context.getString(R.string.no_timer) else itemView.context.resources.getQuantityString( R.plurals.break_mins, mins, mins ), itemView.context.resources.getQuantityString(R.plurals.break_mins, mins2, mins2) ) } } } fun getAppNameForPkgName(tag: String): String { return appNames.computeIfAbsent(tag) { packageName -> val pm = packageManager try { val i = pm.getApplicationInfo(packageName, 0) return@computeIfAbsent pm.getApplicationLabel(i).toString() } catch (e: PackageManager.NameNotFoundException) { return@computeIfAbsent packageName } } } fun getAppIconForPkgName(tag: String): Drawable { return appIcons.computeIfAbsent(tag) { packageName -> val pm = packageManager try { return@computeIfAbsent pm.getApplicationIcon(packageName) } catch (e: PackageManager.NameNotFoundException) { return@computeIfAbsent AppCompatResources.getDrawable( this@AppTimers, android.R.drawable.sym_def_app_icon )!! } } } private val appIcons = hashMapOf() private val appNames = hashMapOf() } ================================================ FILE: app/src/main/java/org/eu/droid_ng/wellbeing/prefs/BedtimeMode.kt ================================================ package org.eu.droid_ng.wellbeing.prefs import android.content.Intent import android.os.Bundle import android.view.View import androidx.appcompat.app.AppCompatActivity import com.google.android.material.checkbox.MaterialCheckBox import com.google.android.material.materialswitch.MaterialSwitch import org.eu.droid_ng.wellbeing.R import org.eu.droid_ng.wellbeing.lib.WellbeingService import org.eu.droid_ng.wellbeing.lib.WellbeingService.Companion.get import java.util.function.Consumer class BedtimeMode : AppCompatActivity() { private val sc = Consumer { tw: WellbeingService -> val bt = findViewById(R.id.topsw) bt.isChecked = tw.getState(false).isBedtimeModeEnabled() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_bedtime_mode) setSupportActionBar(findViewById(R.id.topbar)) val actionBar = checkNotNull(supportActionBar) actionBar.setDisplayHomeAsUpEnabled(true) val tw = get() val prefs = getSharedPreferences("bedtime_mode", 0) val bt = findViewById(R.id.topsw) findViewById(R.id.topsc).setOnClickListener { v: View? -> val b = !tw.getState(false).isBedtimeModeEnabled() tw.setBedtimeMode(b) bt.isChecked = b } bt.isChecked = tw.getState(false).isBedtimeModeEnabled() val checkBox2 = findViewById(R.id.checkBox2) checkBox2.isChecked = prefs.getBoolean("greyscale", false) findViewById(R.id.greyscaleCheckbox).setOnClickListener { v: View? -> val b = !prefs.getBoolean("greyscale", false) checkBox2.isChecked = b val g = tw.getState(false).isBedtimeModeEnabled() prefs.edit().putBoolean("greyscale", b).apply() if (g) { tw.cdm.setSaturationLevel(if (b) 0 else 100) } } val checkBox3 = findViewById(R.id.checkBox3) checkBox3.isChecked = prefs.getBoolean("airplane_mode", false) findViewById(R.id.airplaneModeCheckbox).setOnClickListener { v: View? -> val b = !prefs.getBoolean("airplane_mode", false) checkBox3.isChecked = b val g = tw.getState(false).isBedtimeModeEnabled() prefs.edit().putBoolean("airplane_mode", b).apply() if (g) { tw.setWellbeingAirplaneMode(b) } } findViewById(R.id.schedule).setOnClickListener { v: View? -> startActivity( Intent( this, ScheduleActivity::class.java ).putExtra("type", "bedtime_mode") .putExtra("name", getString(R.string.bedtime_mode)) ) } //TODO: do not disturb //TODO: disable AOD(A11) //TODO: dim the wallpaper(A13) //TODO: dark theme(A13) tw.addStateCallback(sc) } override fun onDestroy() { super.onDestroy() val tw = get() tw.removeStateCallback(sc) } override fun onSupportNavigateUp(): Boolean { finish() return true } } ================================================ FILE: app/src/main/java/org/eu/droid_ng/wellbeing/prefs/DayPicker.kt ================================================ package org.eu.droid_ng.wellbeing.prefs import android.content.Context import android.util.AttributeSet import android.widget.FrameLayout import androidx.appcompat.widget.AppCompatToggleButton import org.eu.droid_ng.wellbeing.R import java.util.Calendar import java.util.function.Consumer class DayPicker(context: Context, attrs: AttributeSet?, defStyle: Int) : FrameLayout(context, attrs, defStyle) { private var views: Array var values = BooleanArray(7) // Monday -> Sunday like Java DayOfWeek set(value) { field = value for (i in 0..6) { val v = views[i] val javaDayOfWeek = Math.floorMod((i + firstDayOfWeek), 7) v.isChecked = field[javaDayOfWeek] } } private var firstDayOfWeek = 0 private var onValuesChangeListener: Consumer? = null constructor(context: Context, attrs: AttributeSet?) : this( context, attrs, 0 ) constructor(context: Context) : this(context, null) init { inflate(context, R.layout.dpicker, this) val day1 = findViewById(R.id.dayPickerDay1) val day2 = findViewById(R.id.dayPickerDay2) val day3 = findViewById(R.id.dayPickerDay3) val day4 = findViewById(R.id.dayPickerDay4) val day5 = findViewById(R.id.dayPickerDay5) val day6 = findViewById(R.id.dayPickerDay6) val day7 = findViewById(R.id.dayPickerDay7) views = arrayOf(day1, day2, day3, day4, day5, day6, day7) firstDayOfWeek = (Calendar.getInstance().firstDayOfWeek - 2) % 7 for (i in 0..6) { val v = views[i] var textToSet: Int val javaDayOfWeek = Math.floorMod((i + firstDayOfWeek), 7) textToSet = when (javaDayOfWeek + 2) { Calendar.MONDAY -> R.string.dpicker_monday Calendar.TUESDAY -> R.string.dpicker_tuesday Calendar.WEDNESDAY -> R.string.dpicker_wednesday Calendar.THURSDAY -> R.string.dpicker_thursday Calendar.FRIDAY -> R.string.dpicker_friday Calendar.SATURDAY -> R.string.dpicker_saturday Calendar.SUNDAY -> R.string.dpicker_sunday else -> R.string.dpicker_sunday } val textToSet2: CharSequence = context.getString(textToSet) v.textOn = textToSet2 v.textOff = textToSet2 v.setOnCheckedChangeListener { _, isChecked -> values[javaDayOfWeek] = isChecked if (onValuesChangeListener != null) { onValuesChangeListener!!.accept(values) } } } this.values = values // call setter } fun setOnValuesChangeListener(onValuesChangeListener: Consumer?) { this.onValuesChangeListener = onValuesChangeListener } } ================================================ FILE: app/src/main/java/org/eu/droid_ng/wellbeing/prefs/FocusModeActivity.kt ================================================ package org.eu.droid_ng.wellbeing.prefs import android.animation.LayoutTransition import android.content.Intent import android.content.pm.PackageManager import android.os.Bundle import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.AppCompatTextView import androidx.appcompat.widget.LinearLayoutCompat import androidx.recyclerview.widget.RecyclerView import com.google.android.material.materialswitch.MaterialSwitch import org.eu.droid_ng.wellbeing.R import org.eu.droid_ng.wellbeing.lib.WellbeingService import org.eu.droid_ng.wellbeing.lib.WellbeingService.Companion.get import java.util.function.Consumer class FocusModeActivity : AppCompatActivity() { private val sc = Consumer { updateUi() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_focusmode) setSupportActionBar(findViewById(R.id.topbar)) val actionBar = checkNotNull(supportActionBar) actionBar.setDisplayHomeAsUpEnabled(true) val layoutTransition = (findViewById(R.id.focusModeRoot) as LinearLayoutCompat).layoutTransition layoutTransition.enableTransitionType(LayoutTransition.CHANGING) findViewById(R.id.schedule).setOnClickListener { startActivity( Intent( this, ScheduleActivity::class.java ).putExtra("type", "focus_mode").putExtra("name", getString(R.string.focus_mode)) ) } val tw = get() tw.addStateCallback(sc) val r = findViewById(R.id.focusModePkgs) r.adapter = PackageRecyclerViewAdapter( this, tw.getInstalledApplications(PackageManager.GET_META_DATA), "focus_mode" ) { packageName: String? -> tw.onFocusModePreferenceChanged( packageName!! ) } updateUi() } override fun onDestroy() { super.onDestroy() val tw = get() tw.removeStateCallback(sc) } private fun updateUi() { val tw = get() val state = tw.getState() val toggle = findViewById(R.id.topsw) toggle.isChecked = state.isFocusModeEnabled() findViewById(R.id.topsc).setOnClickListener { if (state.isFocusModeEnabled()) { tw.disableFocusMode() } else { tw.enableFocusMode() } } val takeBreak = findViewById(R.id.takeBreak) (findViewById(R.id.title) as AppCompatTextView).setText(if (state.isOnFocusModeBreakGlobal()) R.string.focus_mode_break_end else R.string.focus_mode_break) takeBreak.setOnClickListener { if (state.isOnFocusModeBreakGlobal()) { tw.endFocusModeBreak() } else { tw.takeFocusModeBreakWithDialog(this@FocusModeActivity, false, null) } } takeBreak.visibility = if (state.isFocusModeEnabled()) View.VISIBLE else View.GONE } override fun onSupportNavigateUp(): Boolean { finish() return true } } ================================================ FILE: app/src/main/java/org/eu/droid_ng/wellbeing/prefs/ManualSuspendActivity.kt ================================================ package org.eu.droid_ng.wellbeing.prefs import android.content.pm.PackageManager import android.os.Bundle import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.RecyclerView import com.google.android.material.button.MaterialButton import org.eu.droid_ng.wellbeing.R import org.eu.droid_ng.wellbeing.lib.WellbeingService.Companion.get class ManualSuspendActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_manual_suspend) setSupportActionBar(findViewById(R.id.topbar)) val actionBar = checkNotNull(supportActionBar) actionBar.setDisplayHomeAsUpEnabled(true) val suspendbtn = findViewById(R.id.suspendbtn) val unsuspendbtn = findViewById(R.id.desuspendbtn) val pkgList = findViewById(R.id.pkgList) val a: PackageRecyclerViewAdapter pkgList.adapter = PackageRecyclerViewAdapter( this, packageManager.getInstalledApplications(PackageManager.GET_META_DATA), "manual_suspend", null ).also { a = it } val tw = get() suspendbtn.setOnClickListener { v: View? -> tw.manualSuspend(null) } unsuspendbtn.setOnClickListener { v: View? -> tw.manualUnsuspend( a.prefs.getStringSet("manual_suspend", HashSet())!!.toTypedArray() ) } } override fun onSupportNavigateUp(): Boolean { finish() return true } } ================================================ FILE: app/src/main/java/org/eu/droid_ng/wellbeing/prefs/PackageRecyclerViewAdapter.kt ================================================ package org.eu.droid_ng.wellbeing.prefs import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.content.pm.ResolveInfo import android.graphics.drawable.Drawable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.CheckBox import android.widget.ImageView import android.widget.TextView import androidx.appcompat.content.res.AppCompatResources import androidx.recyclerview.widget.RecyclerView import org.eu.droid_ng.wellbeing.R import org.eu.droid_ng.wellbeing.lib.Utils import org.eu.droid_ng.wellbeing.prefs.PackageRecyclerViewAdapter.PackageNameViewHolder import java.text.Collator import java.util.function.Consumer import java.util.stream.Collectors internal class PackageRecyclerViewAdapter( private val mContext: Context, mData: List, private val settingsKey: String, private val callback: Consumer? ) : RecyclerView.Adapter() { private val inflater: LayoutInflater = LayoutInflater.from(mContext) private val mData: List private val enabledArr: MutableList private val pm: PackageManager = mContext.packageManager val prefs: SharedPreferences = mContext.getSharedPreferences("appLists", 0) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PackageNameViewHolder { val view = inflater.inflate(R.layout.appitem, parent, false) return PackageNameViewHolder(view) } override fun getItemCount(): Int { return mData.size } override fun onBindViewHolder(holder: PackageNameViewHolder, position: Int) { holder.apply(mData[position].packageName) } inner class PackageNameViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { private val container: View = itemView.findViewById(R.id.container) private val appIcon: ImageView = itemView.findViewById(R.id.appIcon) private val appName: TextView = itemView.findViewById(R.id.appName2) private val pkgName: TextView = itemView.findViewById(R.id.pkgName) private val checkBox: CheckBox = itemView.findViewById(R.id.isChecked) @SuppressLint("ApplySharedPref") fun apply(packageName: String?) { appIcon.setImageDrawable(getAppIconForPkgName(packageName)) appName.text = getAppNameForPkgName(packageName) pkgName.text = packageName checkBox.isChecked = enabledArr.contains(packageName) container.setOnClickListener { var enabled = enabledArr.contains(packageName) enabled = !enabled checkBox.isChecked = enabled if (enabled) { enabledArr.add(packageName) } else { enabledArr.remove(packageName) } prefs.edit().putStringSet(settingsKey, HashSet(enabledArr)).commit() callback?.accept(packageName) } } } fun getAppNameForPkgName(tag: String?): String { return appNames.computeIfAbsent(tag) { packageName -> try { val i = pm.getApplicationInfo(packageName!!, 0) return@computeIfAbsent pm.getApplicationLabel(i).toString() } catch (e: PackageManager.NameNotFoundException) { return@computeIfAbsent packageName!! } } } fun getAppIconForPkgName(tag: String?): Drawable { return appIcons.computeIfAbsent(tag) { packageName: String? -> try { return@computeIfAbsent pm.getApplicationIcon(packageName!!) } catch (e: PackageManager.NameNotFoundException) { return@computeIfAbsent AppCompatResources.getDrawable( mContext, android.R.drawable.sym_def_app_icon )!! } } } private val appIcons: HashMap = HashMap() private val appNames: HashMap = HashMap() init { val focusAppsS = prefs.getStringSet(this.settingsKey, HashSet())!! enabledArr = ArrayList(focusAppsS) val collator = Collator.getInstance() // Sort alphabetically by display name val nc = java.util.Comparator { a: ApplicationInfo, b: ApplicationInfo -> val displayA: CharSequence = getAppNameForPkgName(a.packageName) val displayB: CharSequence = getAppNameForPkgName(b.packageName) collator.compare(displayA, displayB) } val mainIntent = Intent(Intent.ACTION_MAIN, null) .addCategory(Intent.CATEGORY_LAUNCHER) // We already force include user apps, so let's only iterate over system apps val hasLauncherIcon = pm.queryIntentActivities(mainIntent, PackageManager.MATCH_SYSTEM_ONLY) .stream().map { a: ResolveInfo -> a.activityInfo.packageName } .collect(Collectors.toList()) this.mData = mData.stream().filter { i: ApplicationInfo -> // Filter out system apps without launcher icon and Settings, Dialer and Wellbeing val isUser = (i.flags and (ApplicationInfo.FLAG_UPDATED_SYSTEM_APP or ApplicationInfo.FLAG_SYSTEM)) < 1 !Utils.restrictedPackages.contains(i.packageName) && (isUser || hasLauncherIcon.contains( i.packageName )) }.sorted { a: ApplicationInfo, b: ApplicationInfo -> // Enabled goes first val hasA = enabledArr.contains(a.packageName) val hasB = enabledArr.contains(b.packageName) if (hasA && hasB) return@sorted nc.compare(a, b) else if (hasA) return@sorted -1 else if (hasB) return@sorted 1 else return@sorted nc.compare(a, b) }.collect(Collectors.toList()) } } ================================================ FILE: app/src/main/java/org/eu/droid_ng/wellbeing/prefs/ScheduleActivity.kt ================================================ package org.eu.droid_ng.wellbeing.prefs import android.os.Bundle import android.util.Log import android.util.TypedValue import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.AppCompatTextView import androidx.appcompat.widget.LinearLayoutCompat import org.eu.droid_ng.wellbeing.R import org.eu.droid_ng.wellbeing.shared.BugUtils.Companion.BUG import org.eu.droid_ng.wellbeing.lib.TimeChargerTriggerCondition import org.eu.droid_ng.wellbeing.lib.Trigger import org.eu.droid_ng.wellbeing.lib.WellbeingService.Companion.get import java.util.function.Consumer class ScheduleActivity : AppCompatActivity() { private var type: String? = null private lateinit var data: MutableList private var cardHost: LinearLayoutCompat? = null private var noCardNotification: AppCompatTextView? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val intent = intent type = null if (intent != null && intent.hasExtra("type")) { type = intent.getStringExtra("type") } if (type == null) { Log.e("ScheduleActivity", "intent or type is null") finish() return } setContentView(R.layout.activity_schedule) setSupportActionBar(findViewById(R.id.topbar)) val actionBar = checkNotNull(supportActionBar) actionBar.setDisplayHomeAsUpEnabled(true) if (intent!!.hasExtra("name")) { actionBar.title = intent.getStringExtra("name") } findViewById(R.id.floating_action_button).setOnClickListener { data.add( TimeChargerTriggerCondition( type!!, System.currentTimeMillis().toString(), true, 7, 0, 18, 0, booleanArrayOf(true, true, true, true, true, true, true), needCharger = false, endOnAlarm = false ) ) updateUi() updateServiceStatus() } cardHost = findViewById(R.id.cardHost) noCardNotification = AppCompatTextView(this) noCardNotification!!.setText(R.string.add_schedule_info) val tw = get() data = tw.getTriggersForId(type!!).toMutableList() updateUi() } private fun updateUi() { cardHost!!.removeAllViews() for (e in data) { if (e is TimeChargerTriggerCondition) { val scv = ScheduleCardView(this) scv.timeData = e scv.onValuesChangedCallback = Consumer { iid -> data.replaceAll { if (it.iid == iid) scv.timeData else it } updateUi() updateServiceStatus() } scv.onDeleteCardCallback = Consumer { iid -> data.removeIf { it.iid == iid } updateUi() updateServiceStatus() } cardHost!!.addView(scv) val m = LinearLayoutCompat.LayoutParams(scv.layoutParams) m.setMargins( 0, TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 10f, resources.displayMetrics ).toInt(), 0, 0 ) scv.layoutParams = m } else { BUG("Cannot display " + e.javaClass.canonicalName) } } if (data.size < 1) { cardHost!!.addView(noCardNotification) } } private fun updateServiceStatus() { val tw = get() tw.setTriggersForId(type!!, data.toTypedArray()) } override fun onSupportNavigateUp(): Boolean { finish() return true } } ================================================ FILE: app/src/main/java/org/eu/droid_ng/wellbeing/prefs/ScheduleCardView.kt ================================================ package org.eu.droid_ng.wellbeing.prefs import android.content.Context import android.util.AttributeSet import android.view.View import android.widget.FrameLayout import androidx.appcompat.widget.AppCompatCheckBox import com.google.android.material.materialswitch.MaterialSwitch import org.eu.droid_ng.wellbeing.R import org.eu.droid_ng.wellbeing.lib.TimeChargerTriggerCondition import java.time.LocalTime import java.util.function.Consumer class ScheduleCardView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : FrameLayout(context, attrs, defStyleAttr) { constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context) : this(context, null) private var startTime: TimeSettingView private var endTime: TimeSettingView private var daypicker: DayPicker private var enable: MaterialSwitch private var charger: AppCompatCheckBox private var alarm: AppCompatCheckBox var onValuesChangedCallback: Consumer? = null var onDeleteCardCallback: Consumer? = null var id: String? = null var iid: String? = null init { inflate(context, R.layout.schedule_card, this) startTime = findViewById(R.id.startTime) endTime = findViewById(R.id.endTime) daypicker = findViewById(R.id.dayPicker) enable = findViewById(R.id.enableCheckBox) charger = findViewById(R.id.chargerCheckBox) alarm = findViewById(R.id.alarmCheckBox) daypicker.values = booleanArrayOf(true, true, true, true, true, true, true) startTime.setData(LocalTime.of(7, 0)) endTime.setData(LocalTime.of(18, 0)) startTime.setOnTimeChangedListener { if (onValuesChangedCallback != null) { onValuesChangedCallback!!.accept(iid) } } endTime.setOnTimeChangedListener { if (onValuesChangedCallback != null) { onValuesChangedCallback!!.accept(iid) } } daypicker.setOnValuesChangeListener { if (onValuesChangedCallback != null) { onValuesChangedCallback!!.accept(iid) } } enable.setOnCheckedChangeListener { _, _ -> if (onValuesChangedCallback != null) { onValuesChangedCallback!!.accept(iid) } } findViewById(R.id.chargerLayout).setOnClickListener { charger.isChecked = !charger.isChecked if (onValuesChangedCallback != null) { onValuesChangedCallback!!.accept(iid) } } findViewById(R.id.alarmLayout).setOnClickListener { alarm.isChecked = !alarm.isChecked if (onValuesChangedCallback != null) { onValuesChangedCallback!!.accept(iid) } } findViewById(R.id.delete).setOnClickListener { if (onDeleteCardCallback != null) { onDeleteCardCallback!!.accept(iid) } } } var timeData: TimeChargerTriggerCondition get() { val s = startTime.getData() val e = endTime.getData() return TimeChargerTriggerCondition( id!!, iid!!, enable.isChecked, s.hour, s.minute, e.hour, e.minute, daypicker.values, charger.isChecked, alarm.isChecked ) } set(t) { id = t.id iid = t.iid enable.isChecked = t.enabled startTime.setData(LocalTime.of(t.startHour, t.startMinute)) endTime.setData(LocalTime.of(t.endHour, t.endMinute)) daypicker.values = t.weekdays charger.isChecked = t.needCharger alarm.isChecked = t.endOnAlarm } } ================================================ FILE: app/src/main/java/org/eu/droid_ng/wellbeing/prefs/SettingsActivity.kt ================================================ package org.eu.droid_ng.wellbeing.prefs import android.content.ClipData import android.content.ClipboardManager import android.content.Intent import android.os.Build import android.os.Bundle import android.widget.ArrayAdapter import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import org.eu.droid_ng.wellbeing.R import org.eu.droid_ng.wellbeing.lib.WellbeingService import org.eu.droid_ng.wellbeing.shared.BugUtils.Companion.formatDateForRender import org.eu.droid_ng.wellbeing.shared.BugUtils.Companion.get import org.eu.droid_ng.wellbeing.shim.PackageManagerDelegate import java.util.Objects class SettingsActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.settings_activity) if (savedInstanceState == null) { supportFragmentManager .beginTransaction() .replace(R.id.settings, SettingsFragment()) .commit() } setSupportActionBar(findViewById(R.id.topbar)) val actionBar = checkNotNull(supportActionBar) actionBar.setDisplayHomeAsUpEnabled(true) } override fun onSupportNavigateUp(): Boolean { finish() return true } class SettingsFragment : PreferenceFragmentCompat() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { preferenceManager.sharedPreferencesName = "service" setPreferencesFromResource(R.xml.root_preferences, rootKey) if (!PackageManagerDelegate.canSetNeutralButtonAction()) { (Objects.requireNonNull(findPreference("manual_dialog")) as Preference).isEnabled = false (Objects.requireNonNull(findPreference("focus_dialog")) as Preference).isEnabled = false } val bugs = get()!!.getBugs() + WellbeingService.get().getBugs() if (bugs.isNotEmpty()) { val bugMap = bugs.toList().sortedBy { it.first }.map { Pair(formatDateForRender(it.first), it.second) } val a = bugMap.map { it.first }.toTypedArray() val bp = findPreference("bugs")!! bp.isVisible = true bp.onPreferenceClickListener = Preference.OnPreferenceClickListener { AlertDialog.Builder(requireActivity()) .setTitle(R.string.bug_viewer) .setAdapter( ArrayAdapter( requireActivity(), android.R.layout.simple_list_item_1, a ) ) { _, pos -> val key = a[pos] val value = bugMap[pos].second AlertDialog.Builder(requireActivity()) .setTitle(key) .setMessage(value) .setPositiveButton(R.string.share) { _, _ -> val sendIntent = Intent() sendIntent.setAction(Intent.ACTION_SEND) sendIntent.putExtra(Intent.EXTRA_TEXT, value) sendIntent.setType("text/plain") val shareIntent = Intent.createChooser(sendIntent, null) startActivity(shareIntent) } .setNeutralButton(R.string.copy_to_clipboard) { _, _ -> val clipboard = requireActivity().getSystemService( CLIPBOARD_SERVICE ) as ClipboardManager val clip = ClipData.newPlainText("Bug report", value) clipboard.setPrimaryClip(clip) if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { // T+ have built in indicator Toast.makeText( activity, R.string.copied, Toast.LENGTH_LONG ).show() } } .setNegativeButton(R.string.cancel) { _, _ -> } .show() } .setNegativeButton(R.string.cancel) { _, _ -> } .show() true } } } } } ================================================ FILE: app/src/main/java/org/eu/droid_ng/wellbeing/prefs/TimeSettingView.kt ================================================ package org.eu.droid_ng.wellbeing.prefs import android.app.TimePickerDialog import android.content.Context import android.text.SpannableString import android.text.format.DateFormat import android.text.style.RelativeSizeSpan import android.util.AttributeSet import android.view.View import android.widget.TimePicker import androidx.appcompat.widget.AppCompatTextView import java.time.LocalTime import java.util.Locale import java.util.function.Consumer class TimeSettingView : AppCompatTextView { constructor(context: Context?) : super(context!!) { initView() } constructor(context: Context?, attrs: AttributeSet?) : super( context!!, attrs ) { initView() } constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super( context!!, attrs, defStyleAttr ) { initView() } private var data: LocalTime = LocalTime.of(0, 0) private var extraText = "" private val use24h = DateFormat.is24HourFormat(context) private var onTimeChangedListener: Consumer? = null private fun initView() { updateText() setOnClickListener { v: View? -> TimePickerDialog(context, { tp: TimePicker?, h: Int, m: Int -> data = LocalTime.of(h, m) updateText() if (onTimeChangedListener != null) { onTimeChangedListener!!.accept(data) } }, data.hour, data.minute, use24h).show() } } private fun updateText() { var hour: Int val minute: Int val amPmSymbol: String val o = extraText.length + 1 if (use24h) { hour = data.hour minute = data.minute amPmSymbol = "" } else { hour = data.hour % 12 if (hour == 0) { hour = 12 } minute = data.minute amPmSymbol = " " + (if (data.hour < 12) "AM" else "PM") } val s = extraText + " " + String.format(Locale.ROOT, "%02d", hour) + ":" + String.format( Locale.ROOT, "%02d", minute ) + amPmSymbol val spannableString = SpannableString(s) spannableString.setSpan(RelativeSizeSpan(1.5f), o, o + 2, 0) // hour spannableString.setSpan(RelativeSizeSpan(1.5f), o + 3, o + 5, 0) // minute text = spannableString } fun setExtraText(extraText: String) { this.extraText = extraText updateText() } fun setData(data: LocalTime) { this.data = data updateText() } fun getData(): LocalTime { return data } fun setOnTimeChangedListener(onTimeChangedListener: Consumer?) { this.onTimeChangedListener = onTimeChangedListener } } ================================================ FILE: app/src/main/java/org/eu/droid_ng/wellbeing/shared/Database.kt ================================================ package org.eu.droid_ng.wellbeing.shared import android.content.Context import android.os.Handler import android.util.Log import androidx.room.Database import androidx.room.Dao import androidx.room.Delete import androidx.room.Entity import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.Update import org.eu.droid_ng.wellbeing.shared.BugUtils.Companion.BUG import java.time.Instant import java.time.LocalDateTime import java.time.LocalTime import java.time.ZoneId @Entity(primaryKeys = ["date", "unit", "type"]) private data class StatEntry( // Unix time because yes val date: Long, val unit: TimeDimension = TimeDimension.ERROR, val type: String, val count: Long, ) { override fun toString(): String { return "StatEntry{date=$date, unit=$unit, count=$count}" } } enum class TimeDimension { YEAR, MONTH, DAY, HOUR, ERROR } object ExactTime { private fun ofHour(date: LocalDateTime): LocalDateTime { return date.withMinute(0).withSecond(0).withNano(0) } private fun ofDay(date: LocalDateTime): LocalDateTime { return ofHour(date.with(LocalTime.MIN)) } private fun ofMonth(date: LocalDateTime): LocalDateTime { return ofDay(date.withDayOfMonth(1)) } private fun ofYear(date: LocalDateTime): LocalDateTime { return ofMonth(date.withMonth(1)) } fun ofUnit(date: LocalDateTime, dimension: TimeDimension): LocalDateTime { return when(dimension) { TimeDimension.YEAR -> ofYear(date) TimeDimension.MONTH -> ofMonth(date) TimeDimension.DAY -> ofDay(date) TimeDimension.HOUR -> ofHour(date) else -> LocalDateTime.now() } } fun of(date: LocalDateTime, dimension: TimeDimension): Long { return ofUnit(date, dimension).atZone(ZoneId.systemDefault()).toEpochSecond() } fun plus(old: LocalDateTime, dimension: TimeDimension?, count: Int): LocalDateTime { when (dimension) { TimeDimension.YEAR -> return old.plusYears(count.toLong()) TimeDimension.MONTH -> return old.plusMonths(count.toLong()) TimeDimension.DAY -> return old.plusDays(count.toLong()) TimeDimension.HOUR -> return old.plusHours(count.toLong()) else -> {} } throw IllegalArgumentException() } } @Dao private abstract class StatDao { @Insert(onConflict = OnConflictStrategy.ABORT) abstract fun insert(stat: StatEntry) @Insert(onConflict = OnConflictStrategy.REPLACE) abstract fun replace(stat: StatEntry) @Update abstract fun update(stat: StatEntry) @Delete abstract fun delete(stat: StatEntry) @Query( "SELECT MIN(statentry.date) FROM statentry WHERE statentry.type = :type" ) abstract fun getEarliest(type: String): Long @Query( "SELECT * FROM statentry WHERE statentry.type LIKE :prefix || '%' AND statentry.unit = :unit AND " + "statentry.date >= :min AND statentry.date < :max" ) abstract fun findStatsOfPrefixBetween(prefix: String, unit: TimeDimension, min: Long, max: Long): List @Query( "SELECT * FROM statentry WHERE statentry.type = :type AND statentry.unit = :unit AND " + "statentry.date >= :min AND statentry.date < :max" ) abstract fun findStatsOfTypeBetween(type: String, unit: TimeDimension, min: Long, max: Long): List @Query( "SELECT * FROM statentry WHERE statentry.type = :type AND statentry.unit = :unit AND " + "statentry.date = :date" ) abstract fun findStatsOfTypeWhere(type: String, unit: TimeDimension, date: Long): List fun insert(type: String, date: LocalDateTime, dimension: TimeDimension, count: Long) { insert(StatEntry(ExactTime.of(date, dimension), dimension, type, count)) } fun replace(type: String, date: LocalDateTime, dimension: TimeDimension, count: Long) { replace(StatEntry(ExactTime.of(date, dimension), dimension, type, count)) } fun increment(type: String, date: LocalDateTime, dimension: TimeDimension) { val results = findStatsOfTypeWhere(type, dimension, ExactTime.of(date, dimension)) if (results.size > 1) { // Should never happen BUG("FATAL, destroying invalid data! results.size(${results.size}) > 1, ${results.joinToString(";; ")}") Log.e("WellbeingDatabase", "FATAL, destroying invalid data! results.size(${results.size}) > 1") for (i in 1.. { while (lastConsolidate == -1L) { Thread.sleep(100) } val tfrom = ExactTime.of(from, dimension) val tto = ExactTime.of(to, dimension) val results = dao.findStatsOfPrefixBetween(prefix, dimension, tfrom, tto) if (results.isEmpty()) { val newdim = TimeDimension.entries[dimension.ordinal + 1] if (newdim == TimeDimension.ERROR) return hashMapOf() return getTypesForPrefix(prefix, newdim, from, to) } maybeConsolidate(prefix) val fresult = HashMap() results.forEach { fresult.merge(it.type, it.count) { old, new -> old + new } } return fresult } private fun maybeConsolidate(type: String) { if (consolidateDelay > 0) { bgHandler.postDelayed({ if (lastConsolidate + consolidateDelay > System.currentTimeMillis()) { return@postDelayed } consolidate(type) }, consolidateDelay / 2L) } } fun consolidate(type: String, every: Boolean = false) { if (lastConsolidate == -1L) return lastConsolidate = -1L val earliest = LocalDateTime.ofInstant( Instant.ofEpochSecond(dao.getEarliest(type)), ZoneId.systemDefault()) consolidateUnit(type, TimeDimension.DAY, { it.minusDays(1) }, false, earliest, LocalDateTime.now().minusDays(7)) // hour -> day. store hourly stats for 7 days consolidateUnit(type, TimeDimension.MONTH, { it.minusMonths(1) }, false, earliest, LocalDateTime.now().minusMonths(3)) // day -> month. store daily stats for 3 months consolidateUnit(type, TimeDimension.YEAR, { it.minusYears(1) }, every, earliest, LocalDateTime.now().minusYears(10)) // month -> year. store monthly stats for 10 years lastConsolidate = System.currentTimeMillis() } private fun consolidateUnit(type: String, dimension: TimeDimension, genFrom: (LocalDateTime) -> LocalDateTime, every: Boolean, earliest: LocalDateTime, last: LocalDateTime = LocalDateTime.now()) { if (!ExactTime.ofUnit(last, dimension).isAfter(earliest)) return val from = genFrom(last) val to = ExactTime.of(last, dimension) val newdim = TimeDimension.entries[dimension.ordinal + 1] if (newdim == TimeDimension.ERROR) return val results = dao.findStatsOfTypeBetween(type, newdim, ExactTime.of(from, dimension), to) var count = 0L results.forEach { count += it.count } if (count > 0) { dao.insert(type, from, dimension, count) results.forEach { dao.delete(it) } } if (every || results.isNotEmpty()) { consolidateUnit(type, dimension, genFrom, every, earliest, from) } } } ================================================ FILE: app/src/main/java/org/eu/droid_ng/wellbeing/shared/WellbeingFrameworkClient.kt ================================================ package org.eu.droid_ng.wellbeing.shared import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection import android.os.Handler import android.os.IBinder import android.os.Looper import android.os.RemoteException import android.util.Log import org.eu.droid_ng.wellbeing.framework.IWellbeingFrameworkService class WellbeingFrameworkClient( private val context: Context, private val wellbeingService: ConnectionCallback ) : IWellbeingFrameworkService { private val serviceConnection: ServiceConnection private var wellbeingFrameworkService: IWellbeingFrameworkService? = null private var binder: IBinder? = null private var versionCode = 0 private var initial = true init { serviceConnection = object : ServiceConnection { override fun onServiceConnected(name: ComponentName, service: IBinder) { wellbeingFrameworkService = IWellbeingFrameworkService.Stub.asInterface( service.also { binder = it }) try { versionCode = wellbeingFrameworkService!!.versionCode() } catch (e: Exception) { Log.e("WellbeingFrameworkService", "Failed to get framework version", e) invalidateConnection() context.unbindService(this) } if (binder != null || initial) { notifyWellbeingService() } } override fun onServiceDisconnected(name: ComponentName) { invalidateConnection() if (versionCode > -2) HANDLER.post { tryConnect() } } override fun onBindingDied(name: ComponentName) { invalidateConnection() } override fun onNullBinding(name: ComponentName) { invalidateConnection() } } } private fun invalidateConnection() { wellbeingFrameworkService = DEFAULT versionCode = 0 binder = null wellbeingService.onWellbeingFrameworkDisconnected() } private fun notifyWellbeingService() { initial.let { initial = false wellbeingService.onWellbeingFrameworkConnected(it) } } fun tryConnect() { if (versionCode() < 0) return if (binder == null || !(binder!!.isBinderAlive && binder!!.pingBinder())) { versionCode = -1 try { context.bindService( FRAMEWORK_SERVICE_INTENT, serviceConnection, Context.BIND_NOT_FOREGROUND or Context.BIND_ALLOW_OOM_MANAGEMENT or Context.BIND_WAIVE_PRIORITY or Context.BIND_ABOVE_CLIENT ) } catch (e: Exception) { Log.e("WellbeingFrameworkService", "Failed to bind framework service", e) if (versionCode == -1) { versionCode = 0 if (initial) { notifyWellbeingService() } } } } } fun tryDisconnect() { if (versionCode() < 1) return if (binder != null && binder!!.isBinderAlive && binder!!.pingBinder()) { versionCode = -2 context.unbindService(serviceConnection) } } // since 1 override fun versionCode(): Int { if (binder != null && !binder!!.isBinderAlive) { invalidateConnection() } return versionCode } // since 1 @Throws(RemoteException::class) override fun setAirplaneMode(value: Boolean) { if (versionCode() < 1) return wellbeingFrameworkService!!.setAirplaneMode(value) } // only in 2 @Deprecated("superseded by UsageEvents") @Throws(RemoteException::class) override fun onNotificationPosted(packageName: String) { throw IllegalArgumentException("no longer supported") } // only in 2 @Deprecated("superseded by UsageEvents") @Throws(RemoteException::class) override fun getEventCount(type: String?, dimension: Int, from: Long, to: Long): Long { throw IllegalArgumentException("no longer supported") } // only in 2 @Deprecated("superseded by UsageEvents") @Throws(RemoteException::class) override fun getTypesForPrefix(prefix: String?, dimension: Int, from: Long, to: Long): MutableMap { throw IllegalArgumentException("no longer supported") } // since 3 override fun getBugs(): MutableMap { if (versionCode() < 3) return mutableMapOf() return wellbeingFrameworkService!!.getBugs() } override fun asBinder(): IBinder { return binder!! } companion object { private val HANDLER = Handler(Looper.getMainLooper()) private val FRAMEWORK_SERVICE_INTENT = Intent("org.eu.droid_ng.wellbeing.framework.FRAMEWORK_SERVICE") .setPackage("org.eu.droid_ng.wellbeing.framework") private val DEFAULT: IWellbeingFrameworkService = IWellbeingFrameworkService.Default() init { IWellbeingFrameworkService.Stub.setDefaultImpl(DEFAULT) } } interface ConnectionCallback { fun onWellbeingFrameworkConnected(initial: Boolean) fun onWellbeingFrameworkDisconnected() } } ================================================ FILE: app/src/main/java/org/eu/droid_ng/wellbeing/ui/DashboardActivity.kt ================================================ package org.eu.droid_ng.wellbeing.ui import android.content.Context import android.content.pm.PackageManager import android.graphics.drawable.Drawable import android.icu.text.SimpleDateFormat import android.os.Bundle import android.os.Handler import android.os.HandlerThread import android.text.format.DateFormat import android.util.ArrayMap import android.util.Pair import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.annotation.ColorInt import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.widget.AppCompatImageButton import androidx.appcompat.widget.AppCompatImageView import androidx.appcompat.widget.AppCompatTextView import androidx.recyclerview.widget.RecyclerView import com.github.mikephil.charting.charts.BarChart import com.github.mikephil.charting.charts.PieChart import com.github.mikephil.charting.data.BarData import com.github.mikephil.charting.data.BarDataSet import com.github.mikephil.charting.data.BarEntry import com.github.mikephil.charting.data.PieData import com.github.mikephil.charting.data.PieDataSet import com.github.mikephil.charting.data.PieEntry import com.github.mikephil.charting.utils.ColorTemplate import com.google.android.material.checkbox.MaterialCheckBox import com.google.android.material.chip.Chip import com.google.android.material.datepicker.MaterialDatePicker import com.google.android.material.timepicker.MaterialTimePicker import com.google.android.material.timepicker.TimeFormat import org.eu.droid_ng.wellbeing.R import org.eu.droid_ng.wellbeing.lib.WellbeingService.Companion.get import org.eu.droid_ng.wellbeing.shared.ExactTime.ofUnit import org.eu.droid_ng.wellbeing.shared.ExactTime.plus import org.eu.droid_ng.wellbeing.shared.TimeDimension import org.eu.droid_ng.wellbeing.ui.DashboardActivity.DashboardRecyclerViewAdapter.DashboardViewHolder import java.text.Collator import java.time.Instant import java.time.LocalDateTime import java.time.ZoneId import java.util.Date import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicLong import java.util.function.BiFunction import java.util.function.Consumer import java.util.function.Function import java.util.function.Supplier class DashboardActivity : AppCompatActivity() { private lateinit var ht: HandlerThread private lateinit var bgHandler: Handler private val appIcons = hashMapOf() private val appNames = hashMapOf() private lateinit var whatStrings: Array private lateinit var whenStrings: Array private lateinit var thisStrings: Array private var whatValue: Int = WhatStat.SCREEN_TIME.ordinal private var whenValue: Int = TimeDimension.DAY.ordinal private var mStart: LocalDateTime? = null private lateinit var chipWhen: Chip private lateinit var chipWhat: Chip private lateinit var chipStart: Chip private var didProcessTime = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ht = HandlerThread("DashboardActivity") ht.start() bgHandler = Handler(ht.looper) setContentView(R.layout.activity_dashboard) setSupportActionBar(findViewById(R.id.topbar)) val actionBar = checkNotNull(supportActionBar) actionBar.setDisplayHomeAsUpEnabled(true) whatStrings = resources.getStringArray(R.array.chip_what_entries) whenStrings = resources.getStringArray(R.array.chip_when_entries) thisStrings = resources.getStringArray(R.array.chip_this_entries) chipWhen = findViewById(R.id.chip_when) chipWhat = findViewById(R.id.chip_what) chipStart = findViewById(R.id.chip_start) chipWhen.setOnClickListener { showChipDialog(whenStrings, R.string.time_dimension_to_display, whenValue) { i: Int -> whenValue = i refresh(true) } } chipWhat.setOnClickListener { showChipDialog(whatStrings, R.string.stat_to_display, whatValue) { i: Int -> whatValue = i refresh(false) } } chipStart.setOnClickListener { val `when` = TimeDimension.entries[whenValue] val dp = MaterialDatePicker.Builder.datePicker() .setTitleText(R.string.select_date) .setSelection( mStart!!.withHour(12).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() ) .build() dp.addOnPositiveButtonClickListener { date: Long? -> if (`when` == TimeDimension.HOUR) { val tp = MaterialTimePicker.Builder() .setTitleText(R.string.select_hour) .setTimeFormat(if (DateFormat.is24HourFormat(this)) TimeFormat.CLOCK_24H else TimeFormat.CLOCK_12H) .setInputMode(MaterialTimePicker.INPUT_MODE_CLOCK) .setHour(mStart!!.hour) .setMinute(mStart!!.minute) .build() tp.addOnPositiveButtonClickListener { mStart = ofUnit( LocalDateTime.ofInstant( Instant.ofEpochMilli( date!! ), ZoneId.systemDefault() ) .withHour(tp.hour).withMinute(tp.minute), `when` ) refresh(false) } tp.show(supportFragmentManager, "chipStartTime") } else { mStart = ofUnit( LocalDateTime.ofInstant( Instant.ofEpochMilli( date!! ), ZoneId.systemDefault() ), `when` ) refresh(false) } } dp.show(supportFragmentManager, "chipStartDate") } refresh(true) } private fun refresh(resetDate: Boolean) { bgHandler.post { if (resetDate) mStart = ofUnit(LocalDateTime.now(), TimeDimension.entries[whenValue]) updateLabels() showData { if (whatValue == WhatStat.SCREEN_TIME.ordinal && !didProcessTime) { get().onProcessStats(false) // takes a long time didProcessTime = true } } } } override fun onDestroy() { super.onDestroy() ht.quitSafely() } private fun updateLabels() { runOnUiThread { chipWhen.text = whenStrings[whenValue] chipWhat.text = whatStrings[whatValue] chipStart.text = fancyDate(mStart) } } private fun showChipDialog( values: Array?, title: Int, currentValue: Int, newValueConsumer: Consumer ) { val atom = AtomicInteger(currentValue) AlertDialog.Builder(this) .setSingleChoiceItems( values, currentValue ) { _, which -> atom.set(which) } .setTitle(title) .setNeutralButton(R.string.cancel) { _, _ -> } .setPositiveButton(R.string.ok) { _, _ -> newValueConsumer.accept( atom.get() ) } .show() } private fun showData(preProcess: Runnable) { val what = WhatStat.entries[whatValue] val `when` = TimeDimension.entries[whenValue] showData( preProcess, what.isRemote, if (`when` != TimeDimension.HOUR) what.tName else null, what.prefix, `when`, mStart, plus( mStart!!, `when`, 1 ), getString(R.string.stat_view_name, whatStrings[whatValue], whenStrings[whenValue]), what.getSubtitleGenerator( this ) ) } private fun showData( preProcess: Runnable, remote: Boolean, id: String?, prefix: String?, dimension: TimeDimension, start: LocalDateTime?, end: LocalDateTime?, name: String?, subtitleGenerator: BiFunction? ) { bgHandler.post { showData(preProcess, if (prefix == null) null else Supplier> { if (remote) /*get().getRemoteEventStatsByPrefix(prefix, dimension, start!!, end!!) TODO*/ hashMapOf() else get().getEventStatsByPrefix(prefix, dimension, start!!, end!!) }, if (id == null) null else Supplier> { val count: MutableMap = ArrayMap() val myDimension = TimeDimension.entries[dimension.ordinal + 1] var newStart = start var newEnd = plus(newStart!!, myDimension, 1) var count2 = if (myDimension == TimeDimension.HOUR) 0 else 1 // hours start at 0 (midnight); days and months at 1 while ((newStart!!.isAfter(start) || newStart.isEqual(start)) && newStart.isBefore( end ) && newEnd.isAfter(start) && (newEnd.isBefore(end) || newEnd.isEqual(end)) ) { count[count2++] = if (remote) /*get().getRemoteEventStatsByType( id, myDimension, newStart, newEnd ) TODO */ 0 else get().getEventStatsByType(id, myDimension, newStart, newEnd) newStart = plus(newStart, myDimension, 1) newEnd = plus(newEnd, myDimension, 1) } count }, name, if (prefix == null) null else Function { tag: String -> tag.substring(prefix.length) }, subtitleGenerator ) } } private fun showData( preProcess: Runnable, rawDataGenerator: Supplier>?, rawData2Generator: Supplier>?, desc: String?, packageNameGenerator: Function?, subtitleGenerator: BiFunction? ) { runOnUiThread { findViewById(R.id.dashboardLoading).visibility = View.VISIBLE findViewById(R.id.dashboardContainer).visibility = View.GONE } preProcess.run() val bar = findViewById(R.id.chart) val pie = findViewById(R.id.chart2) val rawData: Map? = rawDataGenerator?.get() val rawData2: Map? = rawData2Generator?.get() val pieEntries: MutableList = ArrayList() rawData?.forEach { (tag, count) -> pieEntries.add( PieEntry( count.toFloat(), getAppNameForPkgName( packageNameGenerator!!.apply(tag) ) ) ) } pieEntries.sortWith { a, b -> b.value.compareTo(a.value) } if (pieEntries.size > 4) { val count = AtomicLong() val others = pieEntries.subList(4, pieEntries.size) others.forEach(Consumer { p: PieEntry -> count.addAndGet(p.value.toLong()) }) others.clear() pieEntries.add(PieEntry(count.get().toFloat(), "Other")) } runOnUiThread { val set = PieDataSet(pieEntries, "") set.setColors(*ColorTemplate.JOYFUL_COLORS) val data = PieData(set) pie.data = data pie.description.text = desc pie.data.setValueTextColor(getAttrColor(com.google.android.material.R.attr.colorOnSurface)) pie.setEntryLabelColor(getAttrColor(com.google.android.material.R.attr.colorOnSurface)) //pie.getDescription().setTextSize(getTextSize(com.google.android.material.R.attr.tabTextAppearance)); pie.description.textColor = getAttrColor(com.google.android.material.R.attr.colorOnSurface) pie.setHoleColor(getAttrColor(com.google.android.material.R.attr.colorSurface)) pie.legend.textColor = getAttrColor(com.google.android.material.R.attr.colorOnSurface) pie.legend.isWordWrapEnabled = true pie.invalidate() // refresh } val barEntries: MutableList = ArrayList() val total2 = AtomicLong() rawData2?.forEach { (tag: Int, count: Long) -> barEntries.add(BarEntry(tag.toFloat(), count.toFloat())) total2.addAndGet(count) } runOnUiThread { val set = BarDataSet(barEntries, "") set.setColors(*ColorTemplate.MATERIAL_COLORS) val data = BarData(set) bar.data = data bar.description.text = getString(R.string.total, total2.get()) bar.xAxis.textColor = getAttrColor(com.google.android.material.R.attr.colorOnSurface) bar.axisLeft.textColor = getAttrColor(com.google.android.material.R.attr.colorOnSurface) bar.axisRight.textColor = getAttrColor(com.google.android.material.R.attr.colorOnSurface) bar.data.setValueTextColor(getAttrColor(com.google.android.material.R.attr.colorOnSurface)) //bar.getDescription().setTextSize(getTextSize(com.google.android.material.R.attr.tabTextAppearance)); bar.description.textColor = getAttrColor(com.google.android.material.R.attr.colorOnSurface) bar.legend.textColor = getAttrColor(com.google.android.material.R.attr.colorOnSurface) bar.legend.isWordWrapEnabled = true bar.invalidate() // refresh } val r = findViewById(R.id.dashboardPkgs) val adapter: RecyclerView.Adapter<*>? = if (rawData != null) { DashboardRecyclerViewAdapter(this, rawData, packageNameGenerator, subtitleGenerator) } else { null } runOnUiThread { if (rawData != null) { pie.visibility = View.VISIBLE } else { pie.visibility = View.GONE } if (rawData2 != null) { bar.visibility = View.VISIBLE } else { bar.visibility = View.GONE } if (pieEntries.isEmpty() && barEntries.isEmpty()) { findViewById(R.id.noData).visibility = View.VISIBLE } else { findViewById(R.id.noData).visibility = View.GONE } r.adapter = adapter findViewById(R.id.dashboardLoading).visibility = View.GONE findViewById(R.id.dashboardContainer).visibility = View.VISIBLE } } inner class DashboardRecyclerViewAdapter( context: Context?, data: Map, packageNameGenerator: Function?, private val subtitleGenerator: BiFunction? ) : RecyclerView.Adapter() { private val inflater: LayoutInflater = LayoutInflater.from(context) private val mData: MutableList> = ArrayList() init { val collator = Collator.getInstance() data.forEach { (i, j) -> val include = j > 0 if (include) mData.add( Pair( packageNameGenerator!!.apply(i), j ) ) } mData.sortWith { a, b -> val countA = a.second val countB = b.second val x = if (countA == null || countB == null) 0 else countA.compareTo(countB) if (x != 0) return@sortWith -x val displayA: CharSequence = getAppNameForPkgName(a.first) val displayB: CharSequence = getAppNameForPkgName(b.first) collator.compare(displayA, displayB) } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DashboardViewHolder { val view = inflater.inflate(R.layout.appitem, parent, false) return DashboardViewHolder(view) } override fun getItemCount(): Int { return mData.size } override fun onBindViewHolder(holder: DashboardViewHolder, position: Int) { val i = mData[position] holder.apply(i) } inner class DashboardViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { private val appIcon: AppCompatImageView = itemView.findViewById(R.id.appIcon) private val appName: AppCompatTextView = itemView.findViewById(R.id.appName2) private val subtitle: AppCompatTextView = itemView.findViewById(R.id.pkgName) init { val actionButton = AppCompatImageButton(itemView.context) actionButton.setImageDrawable( AppCompatResources.getDrawable( itemView.context, R.drawable.ic_focus_mode ) ) actionButton.background = null val checkBox = itemView.findViewById(R.id.isChecked) val parent = checkBox.parent as ViewGroup //val idx = parent.indexOfChild(checkBox) parent.removeView(checkBox) //parent.addView(actionButton, idx) } fun apply(info: Pair) { appIcon.setImageDrawable(getAppIconForPkgName(info.first)) appName.text = getAppNameForPkgName(info.first) subtitle.text = subtitleGenerator!!.apply(info.first, info.second) } } } fun getAppNameForPkgName(tag: String): String { return appNames.computeIfAbsent(tag) { packageName -> val pm = packageManager try { val i = pm.getApplicationInfo(packageName, 0) return@computeIfAbsent pm.getApplicationLabel(i).toString() } catch (e: PackageManager.NameNotFoundException) { return@computeIfAbsent packageName } } } fun getAppIconForPkgName(tag: String): Drawable { return appIcons.computeIfAbsent(tag) { packageName -> val pm = packageManager try { return@computeIfAbsent pm.getApplicationIcon(packageName) } catch (e: PackageManager.NameNotFoundException) { return@computeIfAbsent AppCompatResources.getDrawable( this@DashboardActivity, android.R.drawable.sym_def_app_icon )!! } } } private fun getAttrColor(attr: Int): Int { val typedValue = TypedValue() val theme = theme theme.resolveAttribute(attr, typedValue, true) @ColorInt val color = typedValue.data return color } private fun getTextSize(size: Int): Int { val typedValue = TypedValue() theme.resolveAttribute(size, typedValue, true) val textSizeAttr = intArrayOf(android.R.attr.textSize) val a = obtainStyledAttributes(typedValue.data, textSizeAttr) val textSize = a.getDimensionPixelSize(0, -1) a.recycle() return textSize } enum class WhatStat( val tName: String, val prefix: String?, val isRemote: Boolean, private val subtitleGeneratorGenerator: (Context) -> BiFunction? ) { SCREEN_TIME( "usage", "usage_", false, { ctx -> BiFunction { _, count -> ctx.resources.getQuantityString( R.plurals.break_mins, count.toInt(), count.toInt() ) } }), NOTIFICATIONS( "notif", "notif_", true, { ctx -> BiFunction { _, count -> ctx.resources.getQuantityString( R.plurals.notifications_count, count.toInt(), count ) } }), UNLOCK("unlock", null, true, { null }); fun getSubtitleGenerator(context: Context): BiFunction? { return subtitleGeneratorGenerator(context) } } // "Today"/"This week"/etc or locale-sensible date private fun fancyDate(start: LocalDateTime?): String { if (plus( LocalDateTime.now(), TimeDimension.entries[whenValue], -1 ).isBefore(start) && !start!!.isAfter( LocalDateTime.now() ) ) { return thisStrings[whenValue] } if (whenValue == TimeDimension.HOUR.ordinal) { return SimpleDateFormat.getDateTimeInstance().format( Date( start!!.atZone(ZoneId.systemDefault()).toEpochSecond() * 1000L ) ) } return SimpleDateFormat.getDateInstance() .format(Date(start!!.atZone(ZoneId.systemDefault()).toEpochSecond() * 1000L)) } } ================================================ FILE: app/src/main/java/org/eu/droid_ng/wellbeing/ui/MainActivity.kt ================================================ package org.eu.droid_ng.wellbeing.ui import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.google.android.material.appbar.MaterialToolbar import org.eu.droid_ng.wellbeing.R class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.settings_activity) // Set the toolbar as support action so we can access it from fragments. val topAppBar = findViewById(R.id.topbar) setSupportActionBar(topAppBar) supportActionBar?.setDisplayHomeAsUpEnabled(true) // Launch settings preference fragment if (savedInstanceState == null) { supportFragmentManager .beginTransaction() .replace(R.id.settings, MainPreferenceFragment()) .commit() } } } ================================================ FILE: app/src/main/java/org/eu/droid_ng/wellbeing/ui/MainPreferenceFragment.kt ================================================ package org.eu.droid_ng.wellbeing.ui import android.os.Bundle import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import org.eu.droid_ng.wellbeing.R import org.eu.droid_ng.wellbeing.lib.WellbeingService import java.util.function.Consumer class MainPreferenceFragment : PreferenceFragmentCompat() { private val service: WellbeingService by lazy { WellbeingService.get() } private val stateCallback = Consumer { _: WellbeingService -> // Update summary on new state updateSummary() } override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.main_preferences, rootKey) findPreference("manual")?.apply { val show = requireActivity().getSharedPreferences("service", 0) .getBoolean("manual", false) isVisible = show } // Update the summary of the preferences based on the state updateSummary() // Add state callback service.addStateCallback(stateCallback) } override fun onDestroy() { super.onDestroy() // Remove state callback service.removeStateCallback(stateCallback) } private fun updateSummary() { val state = service.getState(false) findPreference("bedtime_mode")?.apply { val isBedTimeModeEnabled = state.isBedtimeModeEnabled() summary = if (isBedTimeModeEnabled) getString(R.string.on) else getString(R.string.off) } findPreference("timers")?.apply { val isAppTimerSet = state.isAppTimerSet() summary = if (isAppTimerSet) getString(R.string.on) else getString(R.string.off) } findPreference("manual")?.apply { val isManuallySuspended = state.isSuspendedManually() summary = if (isManuallySuspended) getString(R.string.on) else getString(R.string.off) } } } ================================================ FILE: app/src/main/java/org/eu/droid_ng/wellbeing/ui/ShowSuspendedAppDetails.kt ================================================ package org.eu.droid_ng.wellbeing.ui import android.annotation.SuppressLint import android.content.Intent import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.graphics.drawable.Drawable import android.os.Bundle import android.view.View import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.AppCompatImageView import androidx.appcompat.widget.AppCompatTextView import com.google.android.material.card.MaterialCardView import org.eu.droid_ng.wellbeing.R import org.eu.droid_ng.wellbeing.shared.BugUtils.Companion.BUG import org.eu.droid_ng.wellbeing.lib.WellbeingService import org.eu.droid_ng.wellbeing.lib.WellbeingService.Companion.get import org.eu.droid_ng.wellbeing.shim.PackageManagerDelegate class ShowSuspendedAppDetails : AppCompatActivity() { private var tw: WellbeingService? = null private var pmd: PackageManagerDelegate? = null @SuppressLint("SetTextI18n") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val packageName = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME) if (packageName == null) { Toast.makeText( this@ShowSuspendedAppDetails, "Assertion failure (0xAB): packageName is null. Please report this to the developers!", Toast.LENGTH_LONG ).show() BUG("packageName == null (0xAB)") finish() return } tw = get() val pm = packageManager pmd = PackageManagerDelegate(pm) setContentView(R.layout.activity_show_suspended_app_details) setSupportActionBar(findViewById(R.id.topbar)) val actionBar = checkNotNull(supportActionBar) actionBar.setDisplayHomeAsUpEnabled(true) val iconView = findViewById(R.id.appIcon) val nameView = findViewById(R.id.appName) var appInfo: ApplicationInfo? = null var icon: Drawable? = null var name: CharSequence? = null try { appInfo = tw!!.getApplicationInfo(packageName, false) icon = pm.getApplicationIcon(appInfo) name = pm.getApplicationLabel(appInfo) } catch (ignored: PackageManager.NameNotFoundException) { } if (appInfo != null && icon != null && name != null) { iconView.setImageDrawable(icon) nameView.text = name } val reason = tw!!.getAppState(packageName) var container: MaterialCardView var hasReason = 0 if (reason.isAppTimerExpired() && !reason.isAppTimerBreak()) { hasReason++ container = findViewById(R.id.apptimer) findViewById(R.id.takeabreakbtn2).setOnClickListener { v: View? -> tw!!.takeAppTimerBreakWithDialog( this@ShowSuspendedAppDetails, true, arrayOf(packageName) ) } container.visibility = View.VISIBLE } if (reason.isFocusModeEnabled() && !(reason.isOnFocusModeBreakGlobal() || reason.isOnFocusModeBreakPartial())) { hasReason++ container = findViewById(R.id.focusMode) findViewById(R.id.takeabreakbtn).setOnClickListener { v: View? -> tw!!.takeFocusModeBreakWithDialog( this@ShowSuspendedAppDetails, true, if (tw!!.focusModeAllApps) null else arrayOf(packageName) ) } findViewById(R.id.disablefocusmode).setOnClickListener { v: View? -> tw!!.disableFocusMode() this@ShowSuspendedAppDetails.finish() } container.visibility = View.VISIBLE } if (reason.isSuspendedManually()) { hasReason++ container = findViewById(R.id.manually) findViewById(R.id.unsuspendbtn2).setOnClickListener { v: View? -> tw!!.manualUnsuspend(arrayOf(packageName)) this@ShowSuspendedAppDetails.finish() } findViewById(R.id.unsuspendallbtn).setOnClickListener { v: View? -> tw!!.manualUnsuspend(null) this@ShowSuspendedAppDetails.finish() } container.visibility = View.VISIBLE } if (hasReason < 1 || reason.hasUpdateFailed()) { container = findViewById(R.id.unknown) findViewById(R.id.unsuspendbtn).setOnClickListener { v: View? -> BUG("Used unknown unsuspend!!") pmd!!.setPackagesSuspended(arrayOf(packageName), false, null, null, null) this@ShowSuspendedAppDetails.finish() } container.visibility = View.VISIBLE } } override fun onSupportNavigateUp(): Boolean { finish() return true } } ================================================ FILE: app/src/main/java/org/eu/droid_ng/wellbeing/ui/TakeBreakDialogActivity.kt ================================================ package org.eu.droid_ng.wellbeing.ui import android.os.Bundle import android.view.View import android.view.ViewGroup import android.widget.ArrayAdapter import android.widget.ListView import androidx.appcompat.app.AppCompatActivity import org.eu.droid_ng.wellbeing.R import org.eu.droid_ng.wellbeing.lib.WellbeingService import org.eu.droid_ng.wellbeing.lib.WellbeingService.Companion.get class TakeBreakDialogActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.take_a_break_activity) setSupportActionBar(findViewById(R.id.topbar)) val actionBar = checkNotNull(supportActionBar) actionBar.setDisplayHomeAsUpEnabled(true) val tw = get() val optionsS = WellbeingService.breakTimeOptions .map { i -> resources.getQuantityString(R.plurals.break_mins, i, i) } .toTypedArray() val a: ArrayAdapter = object : ArrayAdapter(this, android.R.layout.simple_list_item_1, optionsS) { override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { val v = super.getView(position, convertView, parent) v.setOnClickListener { tw.takeFocusModeBreak(WellbeingService.breakTimeOptions[position]) this@TakeBreakDialogActivity.finish() } return v } } val lv = findViewById(R.id.listView) lv.adapter = a } override fun onSupportNavigateUp(): Boolean { finish() return true } } ================================================ FILE: app/src/main/java/org/eu/droid_ng/wellbeing/widget/ScreenTimeAppWidget.kt ================================================ package org.eu.droid_ng.wellbeing.widget import android.app.PendingIntent import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetProvider import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.util.Log import android.view.View import android.widget.RemoteViews import org.eu.droid_ng.wellbeing.R import org.eu.droid_ng.wellbeing.lib.Utils.clearUsageStatsCache import org.eu.droid_ng.wellbeing.lib.Utils.getMostUsedPackages import org.eu.droid_ng.wellbeing.lib.Utils.getScreenTime import org.eu.droid_ng.wellbeing.lib.Utils.getTimeUsed import org.eu.droid_ng.wellbeing.lib.WellbeingService.Companion.get import java.time.Duration class ScreenTimeAppWidget : AppWidgetProvider() { private var pendingIntent: PendingIntent? = null override fun onReceive(context: Context, intent: Intent) { super.onReceive(context, intent) if ("org.eu.droid_ng.wellbeing.APPWIDGET_UPDATE" == intent.action) { val awm = AppWidgetManager.getInstance(context) onUpdate( context, awm, awm.getAppWidgetIds(ComponentName(context, ScreenTimeAppWidget::class.java)) ) } } private fun checkInitialize(context: Context) { if (pendingIntent == null) { val intent = Intent("com.android.settings.action.IA_SETTINGS") intent.setPackage(context.packageName) pendingIntent = PendingIntent.getActivity( context, this.hashCode(), intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT ) } } override fun onEnabled(context: Context) { checkInitialize(context) } override fun onUpdate( context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray ) { checkInitialize(context) clearUsageStatsCache(get().usm, context.packageManager, get().pmd, true) for (appWidgetId in appWidgetIds) { appWidgetManager.updateAppWidget( appWidgetId, updateLayout(context) ) } } override fun onDeleted(context: Context, appWidgetIds: IntArray) { } override fun onDisabled(context: Context) { } private fun updateLayout( context: Context ): RemoteViews { val usm = get().usm val remoteViews = RemoteViews( context.packageName, R.layout.appwidget_screen_time ) remoteViews.setOnClickPendingIntent(R.id.appwidget_root, pendingIntent) remoteViews.setTextViewText( R.id.appwidget_screen_time, formatDuration(getScreenTime(usm)) ) val mostUsedPackages = getMostUsedPackages(usm) for (i in appViewIds.indices) { if (i >= mostUsedPackages.size) { remoteViews.setViewVisibility(appView3Ids[i], View.GONE) remoteViews.setViewVisibility(appViewIds[i], View.GONE) } else { remoteViews.setViewVisibility(appViewIds[i], View.VISIBLE) remoteViews.setViewVisibility(appView3Ids[i], View.VISIBLE) val packageName = mostUsedPackages[i] var packageLabel = packageName try { packageLabel = get() .getApplicationLabel(packageName).toString() } catch (e: PackageManager.NameNotFoundException) { Log.e("ScreenTimeAppWidget", "Failed to get app label!") } remoteViews.setTextViewText(appViewIds[i], packageLabel) remoteViews.setTextViewText( appView2Ids[i], formatDuration(getTimeUsed(usm, packageName)) ) } } return remoteViews } companion object { private val appViewIds = intArrayOf( R.id.appwidget_app1_n, R.id.appwidget_app2_n, R.id.appwidget_app3_n ) private val appView2Ids = intArrayOf( R.id.appwidget_app1_t, R.id.appwidget_app2_t, R.id.appwidget_app3_t ) private val appView3Ids = intArrayOf( R.id.appwidget_app1_l, R.id.appwidget_app2_l, R.id.appwidget_app3_l ) private fun formatDuration(duration: Duration): String { val hours = duration.toHours() var minutes = duration.toMinutes() minutes -= (hours * 60) return if (hours == 0L) { minutes.toString() + "m" } else if (minutes == 0L) { hours.toString() + "h" } else { hours.toString() + "h " + minutes + "m" } } } } ================================================ FILE: app/src/main/privapp-permissions-wellbeing.xml ================================================ ================================================ FILE: app/src/main/res/drawable/appwidget_background.xml ================================================ ================================================ FILE: app/src/main/res/drawable/appwidget_screen_time_bg.xml ================================================ ================================================ FILE: app/src/main/res/drawable/baseline_airplanemode_active_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/baseline_alarm_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/baseline_arrow_drop_down_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/baseline_battery_charging_full_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/baseline_bedtime_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/baseline_cancel_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/baseline_delete_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/baseline_exit_to_app_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/baseline_gradient_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/baseline_schedule_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/dpicker_background.xml ================================================ ================================================ FILE: app/src/main/res/drawable/dpicker_outline_oval.xml ================================================ ================================================ FILE: app/src/main/res/drawable/dpicker_shape_oval.xml ================================================ ================================================ FILE: app/src/main/res/drawable/dpicker_text_color.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_access_time_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_app_blocking_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_bug_report_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_dashboard_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_king_bed_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_person_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_person_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_settings_24dp.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_launcher_foreground.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_plus_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_settings.xml ================================================ ================================================ FILE: app/src/main/res/drawable/outline_badge_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable-anydpi/ic_focus_mode.xml ================================================ ================================================ FILE: app/src/main/res/drawable-anydpi/ic_stat_name.xml ================================================ ================================================ FILE: app/src/main/res/drawable-anydpi/ic_take_break.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_app_timers.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_bedtime_mode.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_dashboard.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_focusmode.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_manual_suspend.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_schedule.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_show_suspended_app_details.xml ================================================ ================================================ FILE: app/src/main/res/layout/appitem.xml ================================================ ================================================ FILE: app/src/main/res/layout/appwidget_screen_time.xml ================================================ ================================================ FILE: app/src/main/res/layout/dpicker.xml ================================================ ================================================ FILE: app/src/main/res/layout/preference_material_switch.xml ================================================ ================================================ FILE: app/src/main/res/layout/schedule_card.xml ================================================ ================================================ FILE: app/src/main/res/layout/settings_activity.xml ================================================ ================================================ FILE: app/src/main/res/layout/take_a_break_activity.xml ================================================ ================================================ FILE: app/src/main/res/mipmap-anydpi/ic_launcher.xml ================================================ ================================================ FILE: app/src/main/res/mipmap-anydpi/ic_launcher_round.xml ================================================ ================================================ FILE: app/src/main/res/values/arrays.xml ================================================ @string/always_ask @string/min_1 @string/min_3 @string/min_5 @string/min_10 @string/min_15 -1 1 3 5 10 15 @string/disabled @string/min_1 @string/min_3 @string/min_5 @string/min_10 @string/min_15 @string/screen_time @string/notifications @string/unlocks @string/when_year @string/when_month @string/when_day @string/when_hour @string/this_year @string/this_month @string/this_day @string/this_hour ================================================ FILE: app/src/main/res/values/dimens.xml ================================================ 16dp ================================================ FILE: app/src/main/res/values/ic_launcher_background.xml ================================================ #FEFCE9 ================================================ FILE: app/src/main/res/values/strings.xml ================================================ Neo Wellbeing Wellbeing settings @string/other Tap to set up Loading… Focus mode, bedtime mode, app timers App is blocked Unfortunately, this app is currently not available. More details Contains notifications like \"Focus mode is enabled\" Service notifications Contains notifications like \"The app timer for this app is about to run out\" Reminders The app timer for %s is about to run out Disabled Application icon App Focus mode Focus mode is enabled Focus mode is currently enabled! Distracting apps are currently disabled. Take a break Unknown The app is suspended for an unknown reason. This probably is a bug! Unsuspend The app was manually suspended. Manual suspension Wellbeing is suspending some applications. Apps suspended Disable You are taking a break right now! End break Cancel Some apps were manually suspended. Unsuspend all %d minute %d minutes Use app for 1 minute Use app for 3 minutes Use app for 5 minutes Use app for 10 minutes Use app for 15 minutes 1 minute 3 minutes 5 minutes 10 minutes 15 minutes Settings App com.android.app Enable Disable Default break time (dialog) Default break time (notification) It has less features Use flow non-interrupting dialog Only unsuspend the app you tried to open Unsuspend all apps when you are unsuspending one Unsuspend all apps %s and other distracting apps are currently disabled. Suspend Schedule Bedtime mode You should go to sleep now! No timer set App timers Screen time OK %1$s (used %2$s) The app timer for this app has elapsed. The app timer for %s has elapsed. About Neo Wellbeing A project by the Neo Applications Collective Open bug viewer That you found this hidden entry means that you found an bug in the program. Please click here for more details. Thank you for using Neo Wellbeing :) Share Copy Copied to clipboard Bug viewer Greyscale Airplane mode Only while charging M T W T F S S Add schedule Delete Nothing here yet, try to add an schedule End with alarm Application 23h 59m - On Off %d minute left %d minutes left Remind when time is running out Always ask Whitelist mode Suspend all apps that you selected Suspend all apps except those you selected Other Dashboard Notifications %d notification %d notifications Unlocks This year This month Today This hour By year By month By day By hour %s - %s Total: %d Select hour Select date Statistic to display Time dimension to display No data Notifies you when Neo Wellbeing is processing events in the background. Feel free to turn off these notifications. Event processing Open framework bug viewer ================================================ FILE: app/src/main/res/values/styles.xml ================================================ ================================================ FILE: app/src/main/res/values-fil/strings.xml ================================================ Ikinakarga… Mode sa pag-focus, mode sa pagtulog, at orasan sa app Naharang ang app Paumanhin, hindi available sa kasalukuyan ang app na ito. Higit pang detalye Mga notipikasyon tulad ng \"Nakabukas ang mode sa pag-focus\" Mga notipikasyon ng serbisyo Mga notipikasyon tulad ng \"Mauubos na ang oras para sa app na ito\" Mga paalala Mauubos na ang oras para sa %s Nakasara Icon ng aplikasyon App Mode sa pag-focus Binuksan ang mode sa pag-focus Kasalukuyang nakabukas ang mode sa pag-focus! Kasalukuyang hinaharang ang mga nakakaabalang app. Magpahinga Hindi alam Sinuspinde ang app sa hindi malamang dahilan. Maaaring isa itong bug! Itigil ang suspensyon Manu-manong sinuspinde ang app. Manu-manong pagsuspinde May mga sinususpindeng aplikasyon ang Wellbeing. Mga suspendidong app Isara Kasalukuyan kang nagpapahinga! Tapusin ang pagpapahinga Kanselahin Manu-manong sinuspinde ang ilang mga app. Itigil ang pagsuspinde sa lahat %d minuto %d (na) minuto Gamitin ang app nang 1 minuto Gamitin ang app nang 3 minuto Gamitin ang app nang 5 minuto Gamitin ang app nang 10 minuto Gamitin ang app nang 15 minuto 1 minuto 3 minuto 5 minuto 10 minuto 15 minuto Mga Setting App Buksan Isara Default na oras sa pahinga (diyalogo) Default na oras sa pahinga (notipikasyon) Mas kaunti ang mga feature nito Gumamit ng diyalogong hindi nakakaistorbo sa daloy Itigil ang suspensyon ng app lang na gusto mong buksan Itigil ang pagsuspinde sa lahat ng mga app kung itinigil mo ang pagsuspinde sa isa man lang sa kanila Itigil ang pagsuspinde sa lahat ng mga app Kasalukuyang naka-disable ang %s at iba pang mga nakakaabalang app. Suspindehin Iskedyul Mode sa pagtulog Oras na ng tulog mo! Walang nakatakdang orasan Orasan sa app Oras sa screen Sige %1$s (ginamit %2$s) Naubos na ang oras para sa app na ito. Naubos na ang oras para sa %s. Tungkol Neo Wellbeing Isang proyekto ng Neo Applications Collective Buksan ang pangtingin sa mga bug Nahanap mo ang nakatagong entry na ito na nangangahulugang nakadiskubre ka ng bug sa programa. Pumindot dito para sa higit pang detalye. Salamat sa paggamit ng Neo Wellbeing :) Ibahagi Kopyahin Nakopya sa clipboard Pangtingin sa mga bug Grayscale Mode pang-eroplano Kapag nagcha-charge lang Lu Ma Mi Hu Bi Sa Li Magdagdag ng iskedyul Tanggalin Wala pang nandito, subukang magdagdag ng iskedyul Wakasan nang may alarm Aplikasyon 23o 59m Bukas Sarado Magpaalala kung paubos na ang oras Laging magtanong Allowlist mode Suspindehin lahat ng mga pinili mong app Suspindehin lahat maliban sa mga pinili mong app Iba pa Dashboard ================================================ FILE: app/src/main/res/values-sw360dp/values-preference.xml ================================================ false ================================================ FILE: app/src/main/res/values-v31/dimens.xml ================================================ @android:dimen/system_app_widget_background_radius ================================================ FILE: app/src/main/res/xml/appwidget_screen_time.xml ================================================ ================================================ FILE: app/src/main/res/xml/backup_rules.xml ================================================ ================================================ FILE: app/src/main/res/xml/data_extraction_rules.xml ================================================ ================================================ FILE: app/src/main/res/xml/main_preferences.xml ================================================ ================================================ FILE: app/src/main/res/xml/root_preferences.xml ================================================ ================================================ FILE: app/update-binary ================================================ #!/sbin/sh ################# # Initialization ################# umask 022 # echo before loading util_functions ui_print() { echo "$1"; } require_new_magisk() { ui_print "*******************************" ui_print " Please install Magisk v20.4+! " ui_print "*******************************" exit 1 } ######################### # Load util_functions.sh ######################### export OUTFD="$2" export ZIPFILE="$3" mount /data 2>/dev/null [ -f /data/adb/magisk/util_functions.sh ] || require_new_magisk . /data/adb/magisk/util_functions.sh [ "$MAGISK_VER_CODE" -lt 20400 ] && require_new_magisk install_module exit 0 ================================================ FILE: build.gradle.kts ================================================ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { val agpVersion = "8.6.0-rc01" id("com.android.application") version agpVersion apply false id("com.android.library") version agpVersion apply false val kotlinVersion = "2.0.20" id("org.jetbrains.kotlin.android") version kotlinVersion apply false id("com.google.devtools.ksp") version "$kotlinVersion-1.0.24" apply false } tasks.withType(JavaCompile::class.java) { options.compilerArgs.add("-Xlint:all") } val dir = rootProject.layout.buildDirectory.get().asFile tasks.register("clean", type = Delete::class) { delete(dir) } ================================================ FILE: framework/.gitignore ================================================ /build ================================================ FILE: framework/build.gradle.kts ================================================ plugins { id("com.android.application") id("org.jetbrains.kotlin.android") } android { namespace = "org.eu.droid_ng.wellbeing.framework" compileSdk = 35 defaultConfig { applicationId = "org.eu.droid_ng.wellbeing.framework" minSdk = 29 targetSdk = 35 versionCode = 33 versionName = "13.0" } signingConfigs { register("release") { if (project.hasProperty("RELEASE_KEY_ALIAS")) { storeFile = file(project.properties["RELEASE_STORE_FILE"].toString()) storePassword = project.properties["RELEASE_STORE_PASSWORD"].toString() keyAlias = project.properties["RELEASE_KEY_ALIAS"].toString() keyPassword = project.properties["RELEASE_KEY_PASSWORD"].toString() } } } buildTypes { release { isMinifyEnabled = true setProguardFiles(listOf(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")) if (project.hasProperty("RELEASE_KEY_ALIAS")) { signingConfig = signingConfigs.getByName("release") } else { logger.warn("Using debug signing configs!") signingConfig = signingConfigs.getByName("debug") } } } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } kotlin { jvmToolchain(17) } } dependencies { implementation(project(":shared")) } ================================================ FILE: framework/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. -keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile -dontobfuscate ================================================ FILE: framework/src/main/Android.bp ================================================ android_app { name: "NeoWellbeingFramework", defaults: ["platform_app_defaults"], static_libs: [ "androidx.room_room-runtime", "NeoWellbeing-shared", ], plugins: ["androidx.room_room-compiler-plugin"], resource_dirs: ["res"], srcs: [ "java/**/*.java", "java/**/*.kt", ], platform_apis: true, privileged: true, certificate: "platform", } ================================================ FILE: framework/src/main/AndroidManifest.xml ================================================ ================================================ FILE: framework/src/main/java/org/eu/droid_ng/wellbeing/framework/Framework.kt ================================================ package org.eu.droid_ng.wellbeing.framework import android.app.Application import android.util.Log import org.eu.droid_ng.wellbeing.shared.BugUtils class Framework : Application() { companion object { private const val TAG = "WellbeingFramework" private lateinit var application: Framework fun setService(service: WellbeingFrameworkServiceImpl?) { return application.setServiceInternal(service) } fun getService(): WellbeingFrameworkServiceImpl? { return application.getServiceInternal() } } private var service: WellbeingFrameworkServiceImpl? = null init { // While it's... quite bad if we get uncaught exceptions, it's even worse if we crash. // If Android can't keep us alive, the device gets thrown into a boot loop. // This is also why this app should be kept as simple as possible. Thread.setDefaultUncaughtExceptionHandler { _, e -> Log.e(TAG, Log.getStackTraceString(e)) BugUtils.get()?.onBugAdded(e, System.currentTimeMillis()) } } override fun onCreate() { super.onCreate() application = this BugUtils.maybeInit(this) Thread { // maybe it'll be useful in the future val prefs = getSharedPreferences("framework", 0) if (prefs.getInt("version", 0) != WellbeingFrameworkServiceImpl.VERSION_CODE) { prefs.edit().putInt("version", WellbeingFrameworkServiceImpl.VERSION_CODE).apply() } // == temp migration code start == try { filesDir.listFiles()?.forEach { it.deleteRecursively() } } catch (e: Exception) { Log.e(TAG, Log.getStackTraceString(e)) } // == temp migration code end == }.start() } private fun getServiceInternal(): WellbeingFrameworkServiceImpl? { return service } private fun setServiceInternal(service: WellbeingFrameworkServiceImpl?) { this.service = service } } ================================================ FILE: framework/src/main/java/org/eu/droid_ng/wellbeing/framework/WellbeingBootReceiver.kt ================================================ package org.eu.droid_ng.wellbeing.framework import android.app.admin.DevicePolicyManager import android.content.BroadcastReceiver import android.content.Context import android.content.Intent class WellbeingBootReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent?) { if (intent?.action != "android.intent.action.BOOT_COMPLETED") return if (!isInWorkProfile(context)) context.startService(Intent(context, WellbeingFrameworkService::class.java)) } private fun isInWorkProfile(context: Context): Boolean { val devicePolicyManager = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager val activeAdmins = devicePolicyManager.activeAdmins if (activeAdmins != null) { for (admin in activeAdmins) { if (devicePolicyManager.isProfileOwnerApp(admin.packageName)) return true } } return false } } ================================================ FILE: framework/src/main/java/org/eu/droid_ng/wellbeing/framework/WellbeingFrameworkService.kt ================================================ package org.eu.droid_ng.wellbeing.framework import android.app.Service import android.content.Context import android.content.Intent import android.os.Handler import android.os.HandlerThread import android.os.IBinder class WellbeingFrameworkService : Service() { override fun onBind(intent: Intent): IBinder? { return if ("org.eu.droid_ng.wellbeing.framework.FRAMEWORK_SERVICE" == intent.action) Framework.getService() else null } override fun onCreate() { super.onCreate() if (Framework.getService() == null) Framework.setService(WellbeingFrameworkServiceImpl(this)) val ws = Framework.getService()!! ws.bgThread.start() ws.start() } override fun onDestroy() { val ws = Framework.getService() ws?.stop() ws?.bgThread?.quitSafely() ws?.bgThread?.join() super.onDestroy() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { return START_STICKY } abstract class BaseWellbeingFrameworkService(protected val context: Context) : IWellbeingFrameworkService.Stub() { companion object { @JvmStatic protected val TAG = "WellbeingFrameworkService" } internal val bgThread = HandlerThread(TAG) protected val bgHandler by lazy { Handler(bgThread.looper ?: throw IllegalStateException("used bgHandler before start() was called")) } abstract fun start() abstract fun stop() } } ================================================ FILE: framework/src/main/java/org/eu/droid_ng/wellbeing/framework/WellbeingFrameworkServiceImpl.kt ================================================ package org.eu.droid_ng.wellbeing.framework import android.content.Context import android.content.Intent import android.os.RemoteException import android.provider.Settings import org.eu.droid_ng.wellbeing.shared.BugUtils import org.eu.droid_ng.wellbeing.shim.UserHandlerShim class WellbeingFrameworkServiceImpl(context: Context) : WellbeingFrameworkService.BaseWellbeingFrameworkService(context) { companion object { const val VERSION_CODE = 3 } override fun start() { // do nothing } override fun stop() { // do nothing } // since 1 @Throws(RemoteException::class) override fun versionCode(): Int { return VERSION_CODE } // since 1 @Throws(RemoteException::class) override fun setAirplaneMode(value: Boolean) { bgHandler.post { Settings.Global.putInt( context.contentResolver, Settings.Global.AIRPLANE_MODE_ON, if (value) 1 else 0 ) context.sendBroadcastAsUser( Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED) .putExtra("state", value), UserHandlerShim.ALL ) } } // only in 2 @Throws(RemoteException::class) override fun onNotificationPosted(packageName: String) { throw IllegalArgumentException("no longer supported") } // only in 2 @Throws(RemoteException::class) override fun getEventCount(type: String, dimension: Int, from: Long, to: Long): Long { throw IllegalArgumentException("no longer supported") } // only in 2 @Throws(RemoteException::class) override fun getTypesForPrefix(type: String, dimension: Int, from: Long, to: Long): Map { throw IllegalArgumentException("no longer supported") } // since 3 override fun getBugs(): MutableMap { return BugUtils.get()?.getBugs()?.toMutableMap() ?: mutableMapOf() } } ================================================ FILE: framework/src/main/res/values/strings.xml ================================================ Neo Wellbeing Framework ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionSha256Sum=a4b4158601f8636cdeeab09bd76afb640030bb5b144aafe261a5e8af027dc612 distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ # Project-wide Gradle settings. # IDE (e.g. Android Studio) users: # Gradle settings configured through the IDE *will override* # any settings specified in this file. # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -XX:+UseParallelGC # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects org.gradle.parallel=true # AndroidX package structure to make it clearer which packages are bundled with the # Android operating system, and which are packaged with your app's APK # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true # Generate compile-time only R class for app modules android.enableAppCompileTimeRClass=true # Only keep the single relevant constructor for types mentioned in XML files # instead of using a parameter wildcard which keeps them all android.useMinimalKeepRules=true # Enable resource optimizations for release build android.enableResourceOptimizations=true # Disable Jetifier as we don't use legacy libraries android.enableJetifier=false # Gradle org.gradle.caching=true org.gradle.configureondemand=true org.gradle.configuration-cache=true ================================================ FILE: gradlew ================================================ #!/bin/sh # # Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java if ! command -v java >/dev/null 2>&1 then die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ org.gradle.wrapper.GradleWrapperMain \ "$@" # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then die "xargs is not available" fi # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute echo. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! set EXIT_CODE=%ERRORLEVEL% if %EXIT_CODE% equ 0 set EXIT_CODE=1 if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: settings.gradle.kts ================================================ @file:Suppress("UnstableApiUsage") pluginManagement { repositories { gradlePluginPortal() google() mavenCentral() } } dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() maven("https://jitpack.io") } } rootProject.name = "NeoWellbeing" include(":shared", ":framework", ":app") ================================================ FILE: shared/.gitignore ================================================ /build ================================================ FILE: shared/build.gradle.kts ================================================ plugins { id("com.android.library") id("org.jetbrains.kotlin.android") } android { namespace = "org.eu.droid_ng.wellbeing.shared" compileSdk = 35 defaultConfig { minSdk = 29 lint { targetSdk = 35 } consumerProguardFiles("consumer-rules.pro") } buildFeatures { aidl = true } sourceSets { getByName("main") { java.srcDir("src/main/java_magisk") kotlin.srcDir("src/main/java_magisk") } } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } kotlin { jvmToolchain(17) } } dependencies { implementation("androidx.annotation:annotation:1.8.2") // For gradle builds only implementation("org.lsposed.hiddenapibypass:hiddenapibypass:4.3") } ================================================ FILE: shared/consumer-rules.pro ================================================ ================================================ FILE: shared/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. -keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile -dontobfuscate ================================================ FILE: shared/src/main/Android.bp ================================================ android_library { name: "NeoWellbeing-shared", resource_dirs: ["res"], static_libs: [ "androidx.annotation_annotation", "androidx.room_room-runtime", ], plugins: ["androidx.room_room-compiler-plugin"], srcs: [ "java/**/*.java", "java/**/*.kt", "aidl/**/*.aidl", "java_real/**/*.java", ], platform_apis: true, } ================================================ FILE: shared/src/main/AndroidManifest.xml ================================================ ================================================ FILE: shared/src/main/aidl/org/eu/droid_ng/wellbeing/framework/IWellbeingFrameworkService.aidl ================================================ // IWellbeingFrameworkService.aidl package org.eu.droid_ng.wellbeing.framework; // Declare any non-default types here with import statements @JavaDefault interface IWellbeingFrameworkService { // since 1 int versionCode() = 0; void setAirplaneMode(boolean value) = 1; // only in 2 void onNotificationPosted(String packageName) = 2; long getEventCount(String type, int dimension, long from, long to) = 3; Map getTypesForPrefix(String prefix, int dimension, long from, long to) = 4; // since 3 Map getBugs() = 5; } ================================================ FILE: shared/src/main/java/org/eu/droid_ng/wellbeing/shared/BugUtils.kt ================================================ package org.eu.droid_ng.wellbeing.shared import android.content.Context import android.util.ArrayMap import android.util.Log import android.util.SparseArray import android.util.SparseLongArray import java.io.File import java.nio.charset.Charset import java.time.Instant import java.time.LocalDateTime import java.time.ZoneId import java.time.format.DateTimeFormatter class BugUtils(private val bugFolder: File, private val pkg: String) { companion object { private var utils: BugUtils? = null fun maybeInit(context: Context) { if (utils == null) { val f = File(context.cacheDir, "bugutils") f.mkdirs() utils = BugUtils(f, context.packageName) } } @Suppress("FunctionName") fun BUG(message: String) { if (utils != null) { utils?.onBugAdded(RuntimeException(message), System.currentTimeMillis()) Log.e("Wellbeing:BugUtils", "BUG \"$message\"") } else { Log.e("Wellbeing:BugUtils", "had to drop BUG \"$message\"") } } fun get(): BugUtils? { return utils } fun formatDateForRender(epochMillis: Long): String { return DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss").format(LocalDateTime.ofInstant(Instant.ofEpochMilli(epochMillis), ZoneId.systemDefault())) } } private fun cleanup() { bugFolder.list()?.let { val l = it.asList().sorted() if (l.size > 20) { l.subList(0, l.size - 21).forEach { name -> File(bugFolder, name).delete() } } } } fun onBugAdded(message: Throwable, date: Long) { val l = "$pkg\n${Log.getStackTraceString(message)}" val o = File(bugFolder, date.toString()).outputStream() o.write(l.toByteArray(Charset.defaultCharset())) o.close() cleanup() } fun hasBugs(): Boolean { return (bugFolder.list()?.size ?: 0) > 0 } fun getBugs(): Map { val m = hashMapOf() bugFolder.list()?.let { val l = it.asList().sorted() l.forEach { date -> val content = File(bugFolder, date).readText(Charset.defaultCharset()) m[date.toLong()] = content } } return m } } ================================================ FILE: shared/src/main/java_magisk/org/eu/droid_ng/wellbeing/shim/PackageManagerDelegate.java ================================================ package org.eu.droid_ng.wellbeing.shim; import android.annotation.SuppressLint; import android.app.PendingIntent; import android.app.usage.UsageStatsManager; import android.content.Context; import android.content.pm.PackageManager; import android.os.PersistableBundle; import android.util.Log; import androidx.annotation.DrawableRes; import androidx.annotation.IntDef; import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import org.lsposed.hiddenapibypass.HiddenApiBypass; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.time.Duration; import java.util.concurrent.TimeUnit; /** @noinspection ALL*/ /* both PackageManager stub for building in Android Studio for UI stuff * and reflective delegate for magisk module, only used in debug builds. * * Note: The class must not fail or crash if a reference is missing. * */ @SuppressLint("PrivateApi") @SuppressWarnings({"unused", "JavaReflectionMemberAccess", "ConstantConditions"}) public class PackageManagerDelegate { private static boolean success; private static Method realSuspendDialogInfoBuilderBuild; private static Method setPackagesSuspended; private static Method getUnsuspendablePackages; private static Constructor realSuspendDialogInfoCst; private static Constructor realSuspendDialogInfoBuilderCst; private static Method getIconResId; private static Method setIconResId; private static Method getTitleResId; private static Method setTitleResId; private static Method getDialogMessageResId; private static Method setDialogMessageResId; private static Method getDialogMessage; private static Method setDialogMessage; private static Method getNeutralButtonTextResId; private static Method setNeutralButtonTextResId; private static Method getNeutralButtonAction; private static Method setNeutralButtonAction; private static Method usmCall; private static Method usmCall2; private static Method usmCalla; private static Method usmCalla2; private static Class realDisplayColorManager; static { HiddenApiBypass.addHiddenApiExemptions(""); // Help in some cases. try { realDisplayColorManager = Class.forName("android.hardware.display.ColorDisplayManager"); Class realUsageStatsManager = Class.forName("android.app.usage.UsageStatsManager"); usmCall = realUsageStatsManager.getMethod("registerAppUsageObserver", int.class, String[].class, long.class, TimeUnit.class, PendingIntent.class); usmCall2 = realUsageStatsManager.getMethod("registerAppUsageLimitObserver", int.class, String[].class, Duration.class, Duration.class, PendingIntent.class); usmCalla = realUsageStatsManager.getMethod("unregisterAppUsageObserver", int.class); usmCalla2 = realUsageStatsManager.getMethod("unregisterAppUsageLimitObserver", int.class); Class realSuspendDialogInfo = Class.forName("android.content.pm.SuspendDialogInfo"); Class realSuspendDialogInfoBuilder = Class.forName("android.content.pm.SuspendDialogInfo$Builder"); setPackagesSuspended = PackageManager.class.getDeclaredMethod("setPackagesSuspended", String[].class, boolean.class, PersistableBundle.class, PersistableBundle.class, realSuspendDialogInfo); getUnsuspendablePackages = PackageManager.class.getDeclaredMethod("getUnsuspendablePackages", String[].class); try { realSuspendDialogInfoBuilderBuild = realSuspendDialogInfoBuilder.getMethod("build"); if (realSuspendDialogInfoBuilderBuild.getReturnType() != realSuspendDialogInfo) { realSuspendDialogInfoBuilderBuild = null; } } catch (ReflectiveOperationException e) { realSuspendDialogInfoBuilderBuild = null; } if (realSuspendDialogInfoBuilderBuild == null) { realSuspendDialogInfoCst = realSuspendDialogInfo.getConstructor(realSuspendDialogInfoBuilder); } realSuspendDialogInfoBuilderCst = realSuspendDialogInfoBuilder.getConstructor(); getIconResId = realSuspendDialogInfo.getMethod("getIconResId"); setIconResId = realSuspendDialogInfoBuilder.getMethod("setIcon", int.class); getTitleResId = realSuspendDialogInfo.getMethod("getTitleResId"); setTitleResId = realSuspendDialogInfoBuilder.getMethod("setTitle", int.class); getDialogMessageResId = realSuspendDialogInfo.getMethod("getDialogMessageResId"); setDialogMessageResId = realSuspendDialogInfoBuilder.getMethod("setMessage", int.class); getDialogMessage = realSuspendDialogInfo.getMethod("getDialogMessage"); setDialogMessage = realSuspendDialogInfoBuilder.getMethod("setMessage", String.class); getNeutralButtonTextResId = realSuspendDialogInfo.getMethod("getNeutralButtonTextResId"); setNeutralButtonTextResId = realSuspendDialogInfoBuilder.getMethod("setNeutralButtonText", int.class); try { getNeutralButtonAction = realSuspendDialogInfo.getMethod("getNeutralButtonAction"); setNeutralButtonAction = realSuspendDialogInfoBuilder.getMethod("setNeutralButtonAction", int.class); } catch (ReflectiveOperationException e) { getNeutralButtonAction = null; setNeutralButtonAction = null; } success = true; } catch (ReflectiveOperationException e) { Log.e("PackageManagerDelegate", // Log why it's crashing "This would not occur if the app was built-in into the ROM:", e); success = false; } } public static void registerAppUsageObserver(UsageStatsManager m, int observerId, @NonNull String[] observedEntities, long timeLimit, @NonNull TimeUnit timeUnit, @NonNull PendingIntent callbackIntent) { try { usmCall.invoke(m, observerId, observedEntities, timeLimit, timeUnit, callbackIntent); } catch (ReflectiveOperationException | NullPointerException | ClassCastException e) { Log.e("UsageStatsManager", // Log why it's crashing "This would not occur if the app was built-in into the ROM:", e); } } public static void registerAppUsageLimitObserver(UsageStatsManager m, int observerId, @NonNull String[] observedEntities, Duration timeLimit, Duration timeUsed, @NonNull PendingIntent callbackIntent) { try { usmCall2.invoke(m, observerId, observedEntities, timeLimit, timeUsed, callbackIntent); } catch (ReflectiveOperationException | NullPointerException | ClassCastException e) { Log.e("UsageStatsManager", // Log why it's crashing "This would not occur if the app was built-in into the ROM:", e); } } public static void unregisterAppUsageObserver(UsageStatsManager m, int observerId) { try { usmCalla.invoke(m, observerId); } catch (ReflectiveOperationException | NullPointerException | ClassCastException e) { Log.e("UsageStatsManager", // Log why it's crashing "This would not occur if the app was built-in into the ROM:", e); } } public static void unregisterAppUsageLimitObserver(UsageStatsManager m, int observerId) { try { usmCalla2.invoke(m, observerId); } catch (ReflectiveOperationException | NullPointerException | ClassCastException e) { Log.e("UsageStatsManager", // Log why it's crashing "This would not occur if the app was built-in into the ROM:", e); } } /** @noinspection BooleanMethodIsAlwaysInverted*/ /* Does not belong here, but for one class im not creating a new delegate */ public interface IColorDisplayManager { /** * Returns whether the device has a wide color gamut display. */ boolean isDeviceColorManaged(); /** * Set the level of color saturation to apply to the display. * * @param saturationLevel 0-100 (inclusive), where 100 is full saturation * @return whether the saturation level change was applied successfully */ boolean setSaturationLevel(@IntRange(from = 0, to = 100) int saturationLevel); /** * Set the level of color saturation to apply to a specific app. * * @param packageName the package name of the app whose windows should be desaturated * @param saturationLevel 0-100 (inclusive), where 100 is full saturation * @return whether the saturation level change was applied successfully */ boolean setAppSaturationLevel(@NonNull String packageName, @IntRange(from = 0, to = 100) int saturationLevel); /** * Returns {@code true} if Night Display is supported by the device. */ boolean isNightDisplayAvailable(Context context); /** * Returns {@code true} if display white balance is supported by the device. */ boolean isDisplayWhiteBalanceAvailable(Context context); } @NonNull public static IColorDisplayManager getColorDisplayManager(Context ctx) { Object cdm; Method isDeviceColorManaged, setSaturationLevel, setAppSaturationLevel, isNightDisplayAvailable, isDisplayWhiteBalanceAvailable; try { cdm = ctx.getSystemService(realDisplayColorManager); isDeviceColorManaged = realDisplayColorManager.getDeclaredMethod("isDeviceColorManaged"); setSaturationLevel = realDisplayColorManager.getDeclaredMethod("setSaturationLevel", int.class); setAppSaturationLevel = realDisplayColorManager.getDeclaredMethod("setAppSaturationLevel", String.class, int.class); isNightDisplayAvailable = realDisplayColorManager.getDeclaredMethod("isNightDisplayAvailable", Context.class); isDisplayWhiteBalanceAvailable = realDisplayColorManager.getDeclaredMethod("isDisplayWhiteBalanceAvailable", Context.class); } catch (Exception e) { Log.e("PackageManagerDelegate", // Log why it's crashing "This would not occur if the app was built-in into the ROM:", e); return new IColorDisplayManager() { // Return stub then. @Override public boolean isDeviceColorManaged() { return false; } @Override public boolean setSaturationLevel(int saturationLevel) { return false; } @Override public boolean setAppSaturationLevel(@NonNull String packageName, int saturationLevel) { return false; } @Override public boolean isNightDisplayAvailable(Context context) { return false; } @Override public boolean isDisplayWhiteBalanceAvailable(Context context) { return false; } }; } return new IColorDisplayManager() { @Override public boolean isDeviceColorManaged() { try { return (Boolean) isDeviceColorManaged.invoke(cdm); } catch (ReflectiveOperationException | NullPointerException | ClassCastException e) { Log.e("IColorDisplayManager", // Log why it's crashing "This would not occur if the app was built-in into the ROM:", e); return false; } } @Override public boolean setSaturationLevel(int saturationLevel) { try { return (Boolean) setSaturationLevel.invoke(cdm, saturationLevel); } catch (ReflectiveOperationException | NullPointerException | ClassCastException e) { Log.e("IColorDisplayManager", // Log why it's crashing "This would not occur if the app was built-in into the ROM:", e); return false; } } @Override public boolean setAppSaturationLevel(@NonNull String packageName, int saturationLevel) { try { return (Boolean) setAppSaturationLevel.invoke(cdm, packageName, saturationLevel); } catch (ReflectiveOperationException | NullPointerException | ClassCastException e) { Log.e("IColorDisplayManager", // Log why it's crashing "This would not occur if the app was built-in into the ROM:", e); return false; } } @Override public boolean isNightDisplayAvailable(Context context) { try { return (Boolean) isNightDisplayAvailable.invoke(null, context); } catch (ReflectiveOperationException | NullPointerException | ClassCastException e) { Log.e("IColorDisplayManager", // Log why it's crashing "This would not occur if the app was built-in into the ROM:", e); return false; } } @Override public boolean isDisplayWhiteBalanceAvailable(Context context) { try { return (Boolean) isDisplayWhiteBalanceAvailable.invoke(null, context); } catch (ReflectiveOperationException | NullPointerException | ClassCastException e) { Log.e("IColorDisplayManager", // Log why it's crashing "This would not occur if the app was built-in into the ROM:", e); return false; } } }; } private final PackageManager pm; public PackageManagerDelegate(PackageManager pm) { this.pm = pm; } public String[] setPackagesSuspended(@Nullable String[] packageNames, boolean suspend, @Nullable PersistableBundle appExtras, @Nullable PersistableBundle launcherExtras, @Nullable SuspendDialogInfo dialogInfo) { if (success && (dialogInfo == null || dialogInfo.real != null)) { try { return (String[]) setPackagesSuspended.invoke(this.pm, packageNames, suspend, appExtras, launcherExtras, dialogInfo == null ? null : dialogInfo.real); } catch (ReflectiveOperationException ignored) {} } /* stub */ return new String[]{}; } public String[] getUnsuspendablePackages(String[] packageNames) { if (success) { try { return (String[]) getUnsuspendablePackages.invoke(this.pm, (Object) packageNames); } catch (ReflectiveOperationException ignored) {} } /* stub */ return new String[]{}; } public static class SuspendDialogInfo { Object real; // Instance used by reflection /** * Used with {@link Builder#setNeutralButtonAction(int)} to create a neutral button that * starts the Intent#ACTION_SHOW_SUSPENDED_APP_DETAILS activity. * @see Builder#setNeutralButtonAction(int) */ public static final int BUTTON_ACTION_MORE_DETAILS = 0; /** * Used with {@link Builder#setNeutralButtonAction(int)} to create a neutral button that * unsuspends the app that the user was trying to launch and continues with the launch. The * system also sends the broadcast * Intent#ACTION_PACKAGE_UNSUSPENDED_MANUALLY to the suspending app * when this happens. * @see Builder#setNeutralButtonAction(int) * see ACTION_PACKAGE_UNSUSPENDED_MANUALLY */ public static final int BUTTON_ACTION_UNSUSPEND = 1; /** * Button actions to specify what happens when the user taps on the neutral button. * To be used with {@link Builder#setNeutralButtonAction(int)}. * * @see Builder#setNeutralButtonAction(int) */ @IntDef(flag = true, value = { BUTTON_ACTION_MORE_DETAILS, BUTTON_ACTION_UNSUSPEND }) @Retention(RetentionPolicy.SOURCE) public @interface ButtonAction { } /** * @return the resource id of the icon to be used with the dialog */ @DrawableRes public int getIconResId() { if (success && real != null) { try { return (Integer) getIconResId.invoke(real); } catch (ReflectiveOperationException ignored) {} } return 0; } /** * @return the resource id of the title to be used with the dialog */ @StringRes public int getTitleResId() { if (success && real != null) { try { return (Integer) getTitleResId.invoke(real); } catch (ReflectiveOperationException ignored) {} } return 0; } /** * @return the resource id of the text to be shown in the dialog's body */ @StringRes public int getDialogMessageResId() { if (success && real != null) { try { return (Integer) getDialogMessageResId.invoke(real); } catch (ReflectiveOperationException ignored) {} } return 0; } /** * @return the text to be shown in the dialog's body. Returns {@code null} if {@link * #getDialogMessageResId()} returns a valid resource id */ @Nullable public String getDialogMessage() { if (success && real != null) { try { return (String) getDialogMessage.invoke(real); } catch (ReflectiveOperationException ignored) {} } return ""; } /** * @return the text to be shown */ @StringRes public int getNeutralButtonTextResId() { if (success && real != null) { try { return (Integer) getNeutralButtonTextResId.invoke(real); } catch (ReflectiveOperationException ignored) {} } return 0; } /** * @return The {@link ButtonAction} that happens on tapping this button */ @ButtonAction public int getNeutralButtonAction() { if (success && real != null && getNeutralButtonAction != null) { try { return (Integer) getNeutralButtonAction.invoke(real); } catch (ReflectiveOperationException ignored) {} } return 0; } @Override public int hashCode() { return 0; } SuspendDialogInfo(Builder b) { if (success && b != null && b.realB != null) { try { if (realSuspendDialogInfoBuilderBuild != null) { this.real = realSuspendDialogInfoBuilderBuild.invoke(b.realB); } else { this.real = realSuspendDialogInfoCst.newInstance(b.realB); } } catch (ReflectiveOperationException ignored) {} } } /** * Builder to build a {@link SuspendDialogInfo} object. */ public static final class Builder { Object realB; // Instance used by reflection public Builder() { if (success) { try { this.realB = realSuspendDialogInfoBuilderCst.newInstance(); } catch (ReflectiveOperationException ignored) {} } } /** * Set the resource id of the icon to be used. If not provided, no icon will be shown. * * @param resId The resource id of the icon. * @return this builder object. */ @NonNull public Builder setIcon(@DrawableRes int resId) { if (success && realB != null) { try { setIconResId.invoke(realB, resId); } catch (ReflectiveOperationException ignored) {} } return this; } /** * Set the resource id of the title text to be displayed. If this is not provided, the * system will use a default title. * * @param resId The resource id of the title. * @return this builder object. */ @NonNull public Builder setTitle(@StringRes int resId) { if (success && realB != null) { try { setTitleResId.invoke(realB, resId); } catch (ReflectiveOperationException ignored) {} } return this; } /** * Set the text to show in the body of the dialog. Ignored if a resource id is set via * {@link #setMessage(int)}. *

* The system will use String#format(Locale, String, Object...) to * insert the suspended app name into the message, so an example format string could be * {@code "The app %1$s is currently suspended"}. This is optional - if the string passed in * {@code message} does not accept an argument, it will be used as is. * * @param message The dialog message. * @return this builder object. * @see #setMessage(int) */ @NonNull public Builder setMessage(@NonNull String message) { if (success && realB != null) { try { setDialogMessage.invoke(realB, message); } catch (ReflectiveOperationException ignored) {} } return this; } /** * Set the resource id of the dialog message to be shown. If no dialog message is provided * via either this method or {@link #setMessage(String)}, the system will use a default * message. *

* The system will use {@link android.content.res.Resources#getString(int, Object...) * getString} to insert the suspended app name into the message, so an example format string * could be {@code "The app %1$s is currently suspended"}. This is optional - if the string * referred to by {@code resId} does not accept an argument, it will be used as is. * * @param resId The resource id of the dialog message. * @return this builder object. * @see #setMessage(String) */ @NonNull public Builder setMessage(@StringRes int resId) { if (success && realB != null) { try { setDialogMessageResId.invoke(realB, resId); } catch (ReflectiveOperationException ignored) {} } return this; } /** * Set the resource id of text to be shown on the neutral button. Tapping this button would * perform the {@link ButtonAction action} specified through * {@link #setNeutralButtonAction(int)}. If this is not provided, the system will use a * default text. * * @param resId The resource id of the button text * @return this builder object. */ @NonNull public Builder setNeutralButtonText(@StringRes int resId) { if (success && realB != null) { try { setNeutralButtonTextResId.invoke(realB, resId); } catch (ReflectiveOperationException ignored) {} } return this; } /** * Set the action expected to happen on neutral button tap. Defaults to * {@link #BUTTON_ACTION_MORE_DETAILS} if this is not provided. * * @param buttonAction Either {@link #BUTTON_ACTION_MORE_DETAILS} or * {@link #BUTTON_ACTION_UNSUSPEND}. * @return this builder object */ @NonNull public Builder setNeutralButtonAction(@ButtonAction int buttonAction) { if (success && realB != null && setNeutralButtonAction != null) { try { setNeutralButtonAction.invoke(realB, buttonAction); } catch (ReflectiveOperationException ignored) {} } return this; } /** * Build the final object based on given inputs. * * @return The {@link SuspendDialogInfo} object built using this builder. */ @NonNull public SuspendDialogInfo build() { return new SuspendDialogInfo(this); } } } public static boolean canSuspend() { return success; } public static boolean canSetNeutralButtonAction() { return success && setNeutralButtonAction != null; } } ================================================ FILE: shared/src/main/java_magisk/org/eu/droid_ng/wellbeing/shim/UserHandlerShim.java ================================================ package org.eu.droid_ng.wellbeing.shim; import android.os.Process; import android.os.UserHandle; @SuppressWarnings("JavaReflectionMemberAccess") public class UserHandlerShim { public static final UserHandle ALL; static { UserHandle UserHandler_ALL; try { UserHandler_ALL = (UserHandle) UserHandle.class .getDeclaredField("ALL").get(null); } catch (Exception ignored) { UserHandler_ALL = UserHandle.getUserHandleForUid(Process.myUid()); } ALL = UserHandler_ALL; } } ================================================ FILE: shared/src/main/java_real/org/eu/droid_ng/wellbeing/shim/PackageManagerDelegate.java ================================================ package org.eu.droid_ng.wellbeing.shim; import android.content.pm.PackageManager; import android.hardware.display.ColorDisplayManager; import android.os.Parcel; import android.os.Parcelable; import android.os.PersistableBundle; import android.app.PendingIntent; import android.app.usage.UsageStatsManager; import android.content.Context; import android.os.PersistableBundle; import android.util.Log; import androidx.annotation.DrawableRes; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlSerializer; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.time.Duration; import java.util.concurrent.TimeUnit; /* This file contains all references to private API. Private API will not be used elsewhere * This class is only used when doing full systems builds, so assume system implement required APIs * */ public class PackageManagerDelegate { public static void registerAppUsageObserver(UsageStatsManager m, int observerId, @NonNull String[] observedEntities, long timeLimit, @NonNull TimeUnit timeUnit, @NonNull PendingIntent callbackIntent) { m.registerAppUsageObserver(observerId, observedEntities, timeLimit, timeUnit, callbackIntent); } public static void registerAppUsageLimitObserver(UsageStatsManager m, int observerId, @NonNull String[] observedEntities, Duration timeLimit, Duration timeUsed, @NonNull PendingIntent callbackIntent) { m.registerAppUsageLimitObserver(observerId, observedEntities, timeLimit, timeUsed, callbackIntent); } public static void unregisterAppUsageObserver(UsageStatsManager m, int observerId) { m.unregisterAppUsageObserver(observerId); } public static void unregisterAppUsageLimitObserver(UsageStatsManager m, int observerId) { m.unregisterAppUsageLimitObserver(observerId); } /* Does not belong here, but for one class im not creating a new delegate */ public interface IColorDisplayManager { /** * Returns whether the device has a wide color gamut display. */ public boolean isDeviceColorManaged(); /** * Set the level of color saturation to apply to the display. * * @param saturationLevel 0-100 (inclusive), where 100 is full saturation * @return whether the saturation level change was applied successfully */ public boolean setSaturationLevel(@IntRange(from = 0, to = 100) int saturationLevel); /** * Set the level of color saturation to apply to a specific app. * * @param packageName the package name of the app whose windows should be desaturated * @param saturationLevel 0-100 (inclusive), where 100 is full saturation * @return whether the saturation level change was applied successfully */ public boolean setAppSaturationLevel(@NonNull String packageName, @IntRange(from = 0, to = 100) int saturationLevel); /** * Returns {@code true} if Night Display is supported by the device. */ public boolean isNightDisplayAvailable(Context context); /** * Returns {@code true} if display white balance is supported by the device. */ public boolean isDisplayWhiteBalanceAvailable(Context context); } public IColorDisplayManager getColorDisplayManager(Context ctx) { ColorDisplayManager cdm = ctx.getSystemService(ColorDisplayManager.class); return new IColorDisplayManager() { @Override public boolean isDeviceColorManaged() { return cdm.isDeviceColorManaged(); } @Override public boolean setSaturationLevel(int saturationLevel) { return cdm.setSaturationLevel(saturationLevel); } @Override public boolean setAppSaturationLevel(String packageName, int saturationLevel) { return cdm.setAppSaturationLevel(packageName, saturationLevel); } @Override public boolean isNightDisplayAvailable(Context context) { return ColorDisplayManager.isNightDisplayAvailable(context); } @Override public boolean isDisplayWhiteBalanceAvailable(Context context) { return ColorDisplayManager.isDisplayWhiteBalanceAvailable(context); } } } private final PackageManager pm; public PackageManagerDelegate(PackageManager pm) { this.pm = pm; } public String[] setPackagesSuspended(@Nullable String[] packageNames, boolean suspend, @Nullable PersistableBundle appExtras, @Nullable PersistableBundle launcherExtras, @Nullable SuspendDialogInfo dialogInfo) { return pm.setPackagesSuspended(packageNames, suspend, appExtras, launcherExtras, dialogInfo == null ? null : dialogInfo.real); } public String[] getUnsuspendablePackages(String[] packageNames) { return pm.getUnsuspendablePackages(packageNames); } public static class SuspendDialogInfo { android.content.pm.SuspendDialogInfo real; /** * Used with {@link Builder#setNeutralButtonAction(int)} to create a neutral button that * starts the Intent#ACTION_SHOW_SUSPENDED_APP_DETAILS activity. * @see Builder#setNeutralButtonAction(int) */ public static final int BUTTON_ACTION_MORE_DETAILS = 0; /** * Used with {@link Builder#setNeutralButtonAction(int)} to create a neutral button that * unsuspends the app that the user was trying to launch and continues with the launch. The * system also sends the broadcast * Intent#ACTION_PACKAGE_UNSUSPENDED_MANUALLY to the suspending app * when this happens. * @see Builder#setNeutralButtonAction(int) * see ACTION_PACKAGE_UNSUSPENDED_MANUALLY */ public static final int BUTTON_ACTION_UNSUSPEND = 1; /** * Button actions to specify what happens when the user taps on the neutral button. * To be used with {@link Builder#setNeutralButtonAction(int)}. * * @hide * @see Builder#setNeutralButtonAction(int) */ @IntDef(flag = true, value = { BUTTON_ACTION_MORE_DETAILS, BUTTON_ACTION_UNSUSPEND }) @Retention(RetentionPolicy.SOURCE) public @interface ButtonAction { } /** * @return the resource id of the icon to be used with the dialog * @hide */ @DrawableRes public int getIconResId() { return real.getIconResId(); } /** * @return the resource id of the title to be used with the dialog * @hide */ @StringRes public int getTitleResId() { return real.getTitleResId(); } /** * @return the resource id of the text to be shown in the dialog's body * @hide */ @StringRes public int getDialogMessageResId() { return real.getDialogMessageResId(); } /** * @return the text to be shown in the dialog's body. Returns {@code null} if {@link * #getDialogMessageResId()} returns a valid resource id * @hide */ @Nullable public String getDialogMessage() { return real.getDialogMessage(); } /** * @return the text to be shown * @hide */ @StringRes public int getNeutralButtonTextResId() { return real.getNeutralButtonTextResId(); } /** * @return The {@link ButtonAction} that happens on tapping this button */ @ButtonAction public int getNeutralButtonAction() { return real.getNeutralButtonAction(); } @Override public int hashCode() { return real.hashCode(); } public SuspendDialogInfo(Builder b) { real = b.realb.build(); } private SuspendDialogInfo(android.content.pm.SuspendDialogInfo r) { real = r; } /** * Builder to build a {@link SuspendDialogInfo} object. */ public static final class Builder { public android.content.pm.SuspendDialogInfo.Builder realb = new android.content.pm.SuspendDialogInfo.Builder(); /** * Set the resource id of the icon to be used. If not provided, no icon will be shown. * * @param resId The resource id of the icon. * @return this builder object. */ @NonNull public Builder setIcon(@DrawableRes int resId) { realb.setIcon(resId); return this; } /** * Set the resource id of the title text to be displayed. If this is not provided, the * system will use a default title. * * @param resId The resource id of the title. * @return this builder object. */ @NonNull public Builder setTitle(@StringRes int resId) { realb.setTitle(resId); return this; } /** * Set the text to show in the body of the dialog. Ignored if a resource id is set via * {@link #setMessage(int)}. *

* The system will use String#format(Locale, String, Object...) to * insert the suspended app name into the message, so an example format string could be * {@code "The app %1$s is currently suspended"}. This is optional - if the string passed in * {@code message} does not accept an argument, it will be used as is. * * @param message The dialog message. * @return this builder object. * @see #setMessage(int) */ @NonNull public Builder setMessage(@NonNull String message) { realb.setMessage(message); return this; } /** * Set the resource id of the dialog message to be shown. If no dialog message is provided * via either this method or {@link #setMessage(String)}, the system will use a default * message. *

* The system will use {@link android.content.res.Resources#getString(int, Object...) * getString} to insert the suspended app name into the message, so an example format string * could be {@code "The app %1$s is currently suspended"}. This is optional - if the string * referred to by {@code resId} does not accept an argument, it will be used as is. * * @param resId The resource id of the dialog message. * @return this builder object. * @see #setMessage(String) */ @NonNull public Builder setMessage(@StringRes int resId) { realb.setMessage(resId); return this; } /** * Set the resource id of text to be shown on the neutral button. Tapping this button would * perform the {@link ButtonAction action} specified through * {@link #setNeutralButtonAction(int)}. If this is not provided, the system will use a * default text. * * @param resId The resource id of the button text * @return this builder object. */ @NonNull public Builder setNeutralButtonText(@StringRes int resId) { realb.setNeutralButtonText(resId); return this; } /** * Set the action expected to happen on neutral button tap. Defaults to * {@link #BUTTON_ACTION_MORE_DETAILS} if this is not provided. * * @param buttonAction Either {@link #BUTTON_ACTION_MORE_DETAILS} or * {@link #BUTTON_ACTION_UNSUSPEND}. * @return this builder object */ @NonNull public Builder setNeutralButtonAction(@ButtonAction int buttonAction) { realb.setNeutralButtonAction(buttonAction); return this; } /** * Build the final object based on given inputs. * * @return The {@link SuspendDialogInfo} object built using this builder. */ @NonNull public SuspendDialogInfo build() { return new SuspendDialogInfo(this); } } } public static boolean canSuspend() { return true; } public static boolean canSetNeutralButtonAction() { return true; } } ================================================ FILE: shared/src/main/java_real/org/eu/droid_ng/wellbeing/shim/UserHandlerShim.java ================================================ package org.eu.droid_ng.wellbeing.shim; import android.os.UserHandle; public class UserHandlerShim { public static final UserHandle ALL = UserHandle.ALL; }