Showing preview only (408K chars total). Download the full file or copy to clipboard to get everything.
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.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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.
<signature of Ty Coon>, 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
================================================
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!--suppress ALL -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.eu.droid_ng.wellbeing.overlay">
<overlay android:isStatic="true" android:priority="9999" android:targetPackage="android"/>
<application android:hasCode="false" android:label="Neo Wellbeing overlay"/>
</manifest>
================================================
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
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="config_defaultWellbeingPackage">org.eu.droid_ng.wellbeing</string>
<string name="config_systemWellbeing">org.eu.droid_ng.wellbeing</string>
</resources>
================================================
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
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature
android:name="android.hardware.telephony"
android:required="false" />
<uses-permission
android:name="android.permission.SUSPEND_APPS"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission
android:name="android.permission.SYSTEM_APPLICATION_OVERLAY"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission
android:name="android.permission.PACKAGE_USAGE_STATS"
tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="android.permission.OBSERVE_APP_USAGE"
tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="android.permission.CONTROL_DISPLAY_SATURATION"
tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="android.permission.WRITE_SECURE_SETTINGS"
tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME"
tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="android.permission.ACCESS_INSTANT_APPS"
tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="android.permission.INTERACT_ACROSS_PROFILES"
tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="android.permission.START_ACTIVITIES_FROM_BACKGROUND"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission
android:name="android.permission.MODIFY_PHONE_STATE"
tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="android.permission.MODIFY_QUIET_MODE"
tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="android.permission.READ_DREAM_STATE"
tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="android.permission.WRITE_DREAM_STATE"
tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="android.permission.READ_WALLPAPER_INTERNAL"
tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="android.permission.SET_WALLPAPER_COMPONENT"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.SET_WALLPAPER" />
<uses-permission
android:name="android.permission.SET_WALLPAPER_DIM_AMOUNT"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.GET_PACKAGE_SIZE" />
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-permission
android:name="android.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission
android:name="android.permission.MANAGE_ROLE_HOLDERS"
tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="android.permission.LAUNCH_MULTI_PANE_SETTINGS_DEEP_LINK"
tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="android.permission.REAL_GET_TASKS"
tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="android.permission.START_TASKS_FROM_RECENTS"
tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="android.permission.MODIFY_DAY_NIGHT_MODE"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.ACCESS_AMBIENT_CONTEXT_EVENT" />
<uses-permission
android:name="android.permission.START_CROSS_PROFILE_ACTIVITIES"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<uses-permission android:name="org.eu.droid_ng.wellbeing.framework.permission.BIND" />
<queries>
<package android:name="org.eu.droid_ng.wellbeing.framework" />
<intent>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.HOME" />
</intent>
<intent>
<action android:name="android.intent.action.DIAL" />
<category android:name="android.intent.category.DEFAULT" />
</intent>
</queries>
<application
android:name=".Wellbeing"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:forceQueryable="true"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:testOnly="false"
android:theme="@style/AppTheme"
tools:targetApi="s">
<activity
android:name=".ui.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.MONKEY" />
</intent-filter>
<intent-filter>
<action android:name="com.google.android.apps.wellbeing.action.HOME" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<!-- Use activity-alias for launcher activity to allow easy enable/disable -->
<activity-alias
android:name=".MainActivity"
android:exported="true"
android:targetActivity=".ui.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<!-- Use activity-alias for settings activity to allow easy enable/disable -->
<activity-alias
android:name=".SettingsActivity"
android:exported="true"
android:targetActivity=".ui.MainActivity">
<intent-filter>
<action android:name="com.android.settings.action.IA_SETTINGS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data
android:name="com.android.settings.category"
android:value="com.android.settings.category.ia.homepage" />
<meta-data
android:name="com.android.settings.summary"
android:resource="@string/setting_desc" />
<meta-data
android:name="com.android.settings.title"
android:resource="@string/app_name" />
<meta-data
android:name="com.android.settings.icon"
android:resource="@drawable/ic_settings" />
<meta-data
android:name="com.android.settings.order"
android:value="1" />
<meta-data
android:name="com.android.settings.bg.argb"
android:value="-16725933" />
<meta-data
android:name="com.android.settings.keyhint"
android:value="neo_wellbeing" />
</activity-alias>
<activity
android:name=".ui.DashboardActivity"
android:exported="false"
android:label="@string/dashboard" />
<activity
android:name=".prefs.FocusModeActivity"
android:exported="false"
android:label="@string/focus_mode" />
<activity
android:name=".prefs.BedtimeMode"
android:exported="false"
android:label="@string/bedtime_mode" />
<activity
android:name=".prefs.AppTimers"
android:exported="false"
android:label="@string/app_timers" />
<activity
android:name=".prefs.ManualSuspendActivity"
android:exported="false"
android:label="@string/manually" />
<activity
android:name=".prefs.SettingsActivity"
android:exported="false"
android:label="@string/title_activity_settings" />
<activity
android:name=".prefs.ScheduleActivity"
android:exported="false"
android:label="@string/schedule" />
<activity
android:name=".ui.ShowSuspendedAppDetails"
android:excludeFromRecents="true"
android:exported="true"
android:label="@string/dialog_btn_settings"
android:permission="android.permission.SEND_SHOW_SUSPENDED_APP_DETAILS"
android:taskAffinity="">
<intent-filter>
<action android:name="android.intent.action.SHOW_SUSPENDED_APP_DETAILS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name=".ui.TakeBreakDialogActivity"
android:excludeFromRecents="true"
android:exported="false"
android:label="@string/focus_mode_break"
android:taskAffinity="" />
<receiver
android:name=".widget.ScreenTimeAppWidget"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<intent-filter>
<action android:name="org.eu.droid_ng.wellbeing.APPWIDGET_UPDATE" /> <!-- for manually updating -->
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/appwidget_screen_time" />
</receiver>
<receiver
android:name=".broadcast.ManuallyUnsuspendBroadcastReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.PACKAGE_UNSUSPENDED_MANUALLY" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<receiver
android:name=".broadcast.BootReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver
android:name=".broadcast.NextAlarmChangedReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="android.app.action.NEXT_ALARM_CLOCK_CHANGED" />
</intent-filter>
</receiver>
<receiver
android:name=".broadcast.AppTimersBroadcastReceiver"
android:enabled="true"
android:exported="false" />
<receiver
android:name=".broadcast.NotificationBroadcastReceiver"
android:enabled="true"
android:exported="false" />
<receiver
android:name=".broadcast.AlarmFiresBroadcastReceiver"
android:enabled="true"
android:exported="false" />
<service
android:name=".lib.FocusModeQSTile"
android:exported="true"
android:icon="@drawable/outline_badge_24"
android:label="@string/focus_mode"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
<meta-data
android:name="android.service.quicksettings.ACTIVE_TILE"
android:value="true" />
<meta-data
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
android:value="true" />
</service>
<service
android:name=".lib.BedtimeModeQSTile"
android:exported="true"
android:icon="@drawable/baseline_bedtime_24"
android:label="@string/bedtime_mode"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
<meta-data
android:name="android.service.quicksettings.ACTIVE_TILE"
android:value="true" />
<meta-data
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
android:value="true" />
</service>
<service
android:name=".lib.WellbeingStateHost"
android:enabled="true"
android:exported="false" />
<!-- to-do: On A13, implement com.google.android.apps.wellbeing.action.ACTION_WIND_DOWN_STATE_CHANGED -->
</application>
</manifest>
================================================
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<CharSequence?>): String {
return java.lang.String.join(delimiter, strings)
}
fun String.Companion.join(delimiter: String, strings: Array<String?>): 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<out AppWidgetProvider>) {
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<String, Duration>? = null
private var calculatedScreenTime: Duration? = null
private var mostUsedPackages: Array<String>? = null
const val PACKAGE_MANAGER_MATCH_INSTANT = 0x00800000
val blackListedPackages: HashSet<String> = HashSet()
val restrictedPackages: HashSet<String> = HashSet()
private fun eventsStr(events: Iterable<Event>): 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<String?>): 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<String> {
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<Duration, Pair<Map<String, Duration>, Array<String>>> {
val usageEvents: UsageEvents = usm.queryEvents(startTimeMillis, endTimeMillis)
var currentEvent: Event
val e = HashMap<String, ArrayList<Event>>()
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<String, Duration>()
e.forEach { (pkgName: String, events: ArrayList<Event>) ->
val openActivities = hashMapOf<String, Long>()
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<String>(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<String>
if (mostUsedPackagesTmp[MOST_USED_PKG_CACHE_SIZE - 1] != null) {
@Suppress("UNCHECKED_CAST")
myMostUsedPackages = mostUsedPackagesTmp as Array<String>
} 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<String>
}
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<String>) {
// 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<Consumer<WellbeingService>> = ArrayList()
fun addStateCallback(callback: Consumer<WellbeingService>) {
stateCallbacks.add(callback)
}
fun removeStateCallback(callback: Consumer<WellbeingService>) {
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<Runnable> = 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<ApplicationInfo> {
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<Trigger> = 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<out AppWidgetProvider>) {
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<String /* packageName */, Int /* does NOT contain global flags like FOCUS_MODE_ENABLED or FOCUS_MODE_GLOBAL_BREAK, so always use getAppState() when reading */> = 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<String?>) {
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<String?>(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<String?>, 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<String?>) {
val optionsS: Array<String> = Arrays.stream(breakTimeOptions).mapToObj { i ->
context.resources.getQuantityString(R.plurals.break_mins, i, i)
}.toArray { arrayOfNulls<String>(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<String?>(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<Long, String> {
return frameworkService.bugs as MutableMap<Long, String>
}
private fun doUpdateTile(tile: Class<out TileService>) {
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<String> = 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<String>()
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<String, Long> {
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<String>?) {
loadSettings()
val optionsS: Array<String> = Arrays.stream(breakTimeOptions).mapToObj { i ->
context.resources.getQuantityString(R.plurals.break_mins, i, i)
}.toArray { arrayOfNulls<String>(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<Runnable>()
private fun takeFocusModeBreak(packageNames: Array<String>?, 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<String>?) {
loadSettings()
val packageNames: Array<String> = 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<String>?) {
loadSettings()
val packageNames: Array<String> = 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<String?>) {
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<String?> =
uoid.substring(ll + 2).split(":".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()
return ParsedUoid(action, timeMillis, pkgs)
}
}
}
private fun setUnhintedAppTimerInternal(
oid: Int,
uoid: String,
toObserve: Array<String?>,
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<String?>,
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<String?>,
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<String?>,
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<out Trigger>) {
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<Trigger> {
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<WellbeingService?>? = 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<WellbeingService?>,
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<Notification.Action?>,
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<Notification.Action?>,
notificationIntent: Intent
) {
updateNotification(buildNotification(title, text, icon, actions, notificationIntent))
}
fun updateNotification(
title: Int,
text: Int,
icon: Int,
actions: Array<Notification.Action?>,
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<RecyclerView>(R.id.appTimerPkgs)
Thread {
val a = AppTimersRecyclerViewAdapter(
this,
ati!!.getInstalledApplications(PackageManager.GET_META_DATA)
)
h!!.post {
findViewById<View>(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<ApplicationInfo>) :
RecyclerView.Adapter<AppTimerViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private val mData: List<ApplicationInfo>
private val pm: PackageManager = context.packageManager
val prefs: SharedPreferences = context.getSharedPreferences("appTimers", 0)
val enabledMap: MutableMap<String, Int> = 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<ApplicationInfo> { 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<MaterialCheckBox>(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<String, Drawable>()
private val appNames = hashMapOf<String, String>()
}
================================================
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<MaterialSwitch>(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<MaterialSwitch>(R.id.topsw)
findViewById<View>(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<MaterialCheckBox>(R.id.checkBox2)
checkBox2.isChecked = prefs.getBoolean("greyscale", false)
findViewById<View>(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<MaterialCheckBox>(R.id.checkBox3)
checkBox3.isChecked = prefs.getBoolean("airplane_mode", false)
findViewById<View>(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<View>(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<AppCompatToggleButton>
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<BooleanArray>? = 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<AppCompatToggleButton>(R.id.dayPickerDay1)
val day2 = findViewById<AppCompatToggleButton>(R.id.dayPickerDay2)
val day3 = findViewById<AppCompatToggleButton>(R.id.dayPickerDay3)
val day4 = findViewById<AppCompatToggleButton>(R.id.dayPickerDay4)
val day5 = findViewById<AppCompatToggleButton>(R.id.dayPickerDay5)
val day6 = findViewById<AppCompatToggleButton>(R.id.dayPickerDay6)
val day7 = findViewById<AppCompatToggleButton>(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<BooleanArray>?) {
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<WellbeingService> { 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<View>(R.id.focusModeRoot) as LinearLayoutCompat).layoutTransition
layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
findViewById<View>(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<RecyclerView>(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<MaterialSwitch>(R.id.topsw)
toggle.isChecked = state.isFocusModeEnabled()
findViewById<View>(R.id.topsc).setOnClickListener {
if (state.isFocusModeEnabled()) {
tw.disableFocusMode()
} else {
tw.enableFocusMode()
}
}
val takeBreak = findViewById<View>(R.id.takeBreak)
(findViewById<View>(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<MaterialButton>(R.id.suspendbtn)
val unsuspendbtn = findViewById<MaterialButton>(R.id.desuspendbtn)
val pkgList = findViewById<RecyclerView>(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<String>()
)
}
}
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<ApplicationInfo>,
private val settingsKey: String,
private val callback: Consumer<String?>?
) : RecyclerView.Adapter<PackageNameViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(mContext)
private val mData: List<ApplicationInfo>
private val enabledArr: MutableList<String?>
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<String?, Drawable> = HashMap()
private val appNames: HashMap<String?, String> = 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<Trigger>
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<View>(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<Trigger>())
}
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<String?>? = null
var onDeleteCardCallback: Consumer<String?>? = 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<View>(R.id.chargerLayout).setOnClickListener {
charger.isChecked = !charger.isChecked
if (onValuesChangedCallback != null) {
onValuesChangedCallback!!.accept(iid)
}
}
findViewById<View>(R.id.alarmLayout).setOnClickListener {
alarm.isChecked = !alarm.isChecked
if (onValuesChangedCallback != null) {
onValuesChangedCallback!!.accept(iid)
}
}
findViewById<View>(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<Any?>(findPreference("manual_dialog")) as Preference).isEnabled =
false
(Objects.requireNonNull<Any?>(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<String>()
val bp = findPreference<Preference>("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<LocalTime>? = 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<LocalTime>?) {
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<StatEntry>
@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<StatEntry>
@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<StatEntry>
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..<results.size)
delete(results[i])
}
if (results.isEmpty()) {
insert(type, date, dimension, 1)
} else {
val oldEntry = results[0]
update(StatEntry(oldEntry.date, oldEntry.unit, oldEntry.type,oldEntry.count + 1))
}
}
}
@Database(entities = [StatEntry::class], version = 1)
private abstract class StatDb : RoomDatabase() {
abstract fun statDao(): StatDao
}
class Database(context: Context, private val bgHandler: Handler, private val consolidateDelay: Int) {
private val db = Room.databaseBuilder(
context,
StatDb::class.java, "stats"
).build()
private val dao = db.statDao()
private var lastConsolidate = 0L
fun incrementNow(type: String) {
while (lastConsolidate == -1L) {
Thread.sleep(100)
}
dao.increment(type, LocalDateTime.now(), TimeDimension.HOUR)
maybeConsolidate(type)
}
fun insert(type: String, date: LocalDateTime, dimension: TimeDimension, count: Long) {
while (lastConsolidate == -1L) {
Thread.sleep(100)
}
dao.replace(type, date, dimension, count)
maybeConsolidate(type)
}
fun getCountFor(type: String, dimension: TimeDimension, from: LocalDateTime, to: LocalDateTime): Long {
while (lastConsolidate == -1L) {
Thread.sleep(100)
}
val tfrom = ExactTime.of(from, dimension)
val tto = ExactTime.of(to, dimension)
val results = dao.findStatsOfTypeBetween(type, dimension, tfrom, tto)
if (results.isEmpty()) {
val newdim = TimeDimension.entries[dimension.ordinal + 1]
if (newdim == TimeDimension.ERROR) return 0
return getCountFor(type, newdim, from, to)
}
var count = 0L
results.forEach { count += it.count }
maybeConsolidate(type)
return count
}
fun getTypesForPrefix(prefix: String, dimension: TimeDimension, from: LocalDateTime, to: LocalDateTime): Map<String, Long> {
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<String, Long>()
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<Any?, Any?> {
throw IllegalArgumentException("no longer supported")
}
// since 3
override fun getBugs(): MutableMap<Any?, Any?> {
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<String, Drawable>()
private val appNames = hashMapOf<String, String>()
private lateinit var whatStrings: Array<String?>
private lateinit var whenStrings: Array<String?>
private lateinit var thisStrings: Array<String>
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<String?>?,
title: Int,
currentValue: Int,
newValueConsumer: Consumer<Int>
) {
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<String, Long, String?>?
) {
bgHandler.post {
showData(preProcess,
if (prefix == null) null else Supplier<Map<String, Long>> {
if (remote) /*get().getRemoteEventStatsByPrefix(prefix, dimension, start!!, end!!) TODO*/ hashMapOf()
else get().getEventStatsByPrefix(prefix, dimension, start!!, end!!)
},
if (id == null) null else Supplier<Map<Int, Long>> {
val count: MutableMap<Int, Long> = 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<Map<String, Long>>?,
rawData2Generator: Supplier<Map<Int, Long>>?,
desc: String?,
packageNameGenerator: Function<String, String>?,
subtitleGenerator: BiFunction<String, Long, String?>?
) {
runOnUiThread {
findViewById<View>(R.id.dashboardLoading).visibility = View.VISIBLE
findViewById<View>(R.id.dashboardContainer).visibility = View.GONE
}
preProcess.run()
val bar = findViewById<BarChart>(R.id.chart)
val pie = findViewById<PieChart>(R.id.chart2)
val rawData: Map<String, Long>? = rawDataGenerator?.get()
val rawData2: Map<Int, Long>? = rawData2Generator?.get()
val pieEntries: MutableList<PieEntry> = 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<BarEntry> = 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<RecyclerView>(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<View>(R.id.noData).visibility = View.VISIBLE
} else {
findViewById<View>(R.id.noData).visibility = View.GONE
}
r.adapter = adapter
findViewById<View>(R.id.dashboardLoading).visibility = View.GONE
findViewById<View>(R.id.dashboardContainer).visibility = View.VISIBLE
}
}
inner class DashboardRecyclerViewAdapter(
context: Context?,
data: Map<String, Long>,
packageNameGenerator: Function<String, String>?,
private val subtitleGenerator: BiFunction<String, Long, String?>?
) : RecyclerView.Adapter<DashboardViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private val mData: MutableList<Pair<String, Long>> = 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<MaterialCheckBox>(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<String, Long>) {
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<String, Long, String?>?
) {
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<String, Long, String?>? {
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<MaterialToolbar>(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> { _: WellbeingService ->
// Update summary on new state
updateSummary()
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.main_preferences, rootKey)
findPreference<Preference>("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(stateCallb
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
SYMBOL INDEX (72 symbols across 4 files)
FILE: shared/src/main/java_magisk/org/eu/droid_ng/wellbeing/shim/PackageManagerDelegate.java
class PackageManagerDelegate (line 32) | @SuppressLint("PrivateApi")
method registerAppUsageObserver (line 116) | public static void registerAppUsageObserver(UsageStatsManager m, int o...
method registerAppUsageLimitObserver (line 126) | public static void registerAppUsageLimitObserver(UsageStatsManager m, ...
method unregisterAppUsageObserver (line 136) | public static void unregisterAppUsageObserver(UsageStatsManager m, int...
method unregisterAppUsageLimitObserver (line 145) | public static void unregisterAppUsageLimitObserver(UsageStatsManager m...
type IColorDisplayManager (line 155) | public interface IColorDisplayManager {
method isDeviceColorManaged (line 159) | boolean isDeviceColorManaged();
method setSaturationLevel (line 167) | boolean setSaturationLevel(@IntRange(from = 0, to = 100) int saturat...
method setAppSaturationLevel (line 176) | boolean setAppSaturationLevel(@NonNull String packageName,
method isNightDisplayAvailable (line 182) | boolean isNightDisplayAvailable(Context context);
method isDisplayWhiteBalanceAvailable (line 187) | boolean isDisplayWhiteBalanceAvailable(Context context);
method getColorDisplayManager (line 190) | @NonNull
method PackageManagerDelegate (line 272) | public PackageManagerDelegate(PackageManager pm) {
method setPackagesSuspended (line 276) | public String[] setPackagesSuspended(@Nullable String[] packageNames, ...
method getUnsuspendablePackages (line 288) | public String[] getUnsuspendablePackages(String[] packageNames) {
class SuspendDialogInfo (line 299) | public static class SuspendDialogInfo {
method getIconResId (line 336) | @DrawableRes
method getTitleResId (line 349) | @StringRes
method getDialogMessageResId (line 362) | @StringRes
method getDialogMessage (line 376) | @Nullable
method getNeutralButtonTextResId (line 389) | @StringRes
method getNeutralButtonAction (line 402) | @ButtonAction
method hashCode (line 412) | @Override
method SuspendDialogInfo (line 417) | SuspendDialogInfo(Builder b) {
class Builder (line 432) | public static final class Builder {
method Builder (line 435) | public Builder() {
method setIcon (line 448) | @NonNull
method setTitle (line 465) | @NonNull
method setMessage (line 488) | @NonNull
method setMessage (line 512) | @NonNull
method setNeutralButtonText (line 531) | @NonNull
method setNeutralButtonAction (line 549) | @NonNull
method build (line 564) | @NonNull
method canSuspend (line 571) | public static boolean canSuspend() {
method canSetNeutralButtonAction (line 575) | public static boolean canSetNeutralButtonAction() {
FILE: shared/src/main/java_magisk/org/eu/droid_ng/wellbeing/shim/UserHandlerShim.java
class UserHandlerShim (line 6) | @SuppressWarnings("JavaReflectionMemberAccess")
FILE: shared/src/main/java_real/org/eu/droid_ng/wellbeing/shim/PackageManagerDelegate.java
class PackageManagerDelegate (line 32) | public class PackageManagerDelegate {
method registerAppUsageObserver (line 34) | public static void registerAppUsageObserver(UsageStatsManager m, int o...
method registerAppUsageLimitObserver (line 39) | public static void registerAppUsageLimitObserver(UsageStatsManager m, ...
method unregisterAppUsageObserver (line 44) | public static void unregisterAppUsageObserver(UsageStatsManager m, int...
method unregisterAppUsageLimitObserver (line 48) | public static void unregisterAppUsageLimitObserver(UsageStatsManager m...
type IColorDisplayManager (line 53) | public interface IColorDisplayManager {
method isDeviceColorManaged (line 57) | public boolean isDeviceColorManaged();
method setSaturationLevel (line 65) | public boolean setSaturationLevel(@IntRange(from = 0, to = 100) int ...
method setAppSaturationLevel (line 74) | public boolean setAppSaturationLevel(@NonNull String packageName,
method isNightDisplayAvailable (line 80) | public boolean isNightDisplayAvailable(Context context);
method isDisplayWhiteBalanceAvailable (line 85) | public boolean isDisplayWhiteBalanceAvailable(Context context);
method getColorDisplayManager (line 88) | public IColorDisplayManager getColorDisplayManager(Context ctx) {
method PackageManagerDelegate (line 120) | public PackageManagerDelegate(PackageManager pm) {
method setPackagesSuspended (line 124) | public String[] setPackagesSuspended(@Nullable String[] packageNames, ...
method getUnsuspendablePackages (line 128) | public String[] getUnsuspendablePackages(String[] packageNames) {
class SuspendDialogInfo (line 132) | public static class SuspendDialogInfo {
method getIconResId (line 172) | @DrawableRes
method getTitleResId (line 181) | @StringRes
method getDialogMessageResId (line 190) | @StringRes
method getDialogMessage (line 200) | @Nullable
method getNeutralButtonTextResId (line 209) | @StringRes
method getNeutralButtonAction (line 217) | @ButtonAction
method hashCode (line 222) | @Override
method SuspendDialogInfo (line 227) | public SuspendDialogInfo(Builder b) {
method SuspendDialogInfo (line 231) | private SuspendDialogInfo(android.content.pm.SuspendDialogInfo r) {
class Builder (line 238) | public static final class Builder {
method setIcon (line 246) | @NonNull
method setTitle (line 259) | @NonNull
method setMessage (line 278) | @NonNull
method setMessage (line 298) | @NonNull
method setNeutralButtonText (line 313) | @NonNull
method setNeutralButtonAction (line 327) | @NonNull
method build (line 338) | @NonNull
method canSuspend (line 345) | public static boolean canSuspend() {
method canSetNeutralButtonAction (line 349) | public static boolean canSetNeutralButtonAction() {
FILE: shared/src/main/java_real/org/eu/droid_ng/wellbeing/shim/UserHandlerShim.java
class UserHandlerShim (line 5) | public class UserHandlerShim {
Condensed preview — 141 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (420K chars).
[
{
"path": ".gitignore",
"chars": 116,
"preview": "*.iml\n.gradle\n.kotlin\n/local.properties\n.idea\n.DS_Store\n/build\n/captures\n.externalNativeBuild\n.cxx\nlocal.properties\n"
},
{
"path": "LICENSE",
"chars": 18092,
"preview": " GNU GENERAL PUBLIC LICENSE\n Version 2, June 1991\n\n Copyright (C) 1989, 1991 Fr"
},
{
"path": "NeoWellbeingOverlay/.gitignore",
"chars": 30,
"preview": "overlay.apk.us\noverlay.apk.uz\n"
},
{
"path": "NeoWellbeingOverlay/AndroidManifest.xml",
"chars": 378,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?>\n<!--suppress ALL -->\n<manifest xmlns:android=\"http://schemas.andr"
},
{
"path": "NeoWellbeingOverlay/Makefile",
"chars": 463,
"preview": "KEY := android\nKEYSTORE := ~/.local/publish.keystore\nDEFAULT: overlay.apk\nclean:\n\trm -f overlay.apk overlay.apk.uz overl"
},
{
"path": "NeoWellbeingOverlay/README",
"chars": 74,
"preview": "Creation process of overlay.apk:\n\n make KEY=android KEYSTORE=my.keystore\n"
},
{
"path": "NeoWellbeingOverlay/res/values/strings.xml",
"chars": 226,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <string name=\"config_defaultWellbeingPackage\">org.eu.droid_ng.wel"
},
{
"path": "README.md",
"chars": 37,
"preview": "Work in progress. Will eat your cat.\n"
},
{
"path": "app/.gitignore",
"chars": 6,
"preview": "/build"
},
{
"path": "app/build.gradle.kts",
"chars": 5940,
"preview": "import java.nio.file.Files\n\nplugins {\n\tid(\"com.android.application\")\n\tid(\"org.jetbrains.kotlin.android\")\n\tid(\"com.google"
},
{
"path": "app/customize.sh",
"chars": 96,
"preview": "#!/bin/sh\n\nif [ \"$API\" -lt 29 ]; then\n abort \"! Neo Wellbeing requires Android 10 or later\"\nfi\n"
},
{
"path": "app/proguard-rules.pro",
"chars": 765,
"preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
},
{
"path": "app/schemas/org.eu.droid_ng.wellbeing.shared.StatDb/1.json",
"chars": 1513,
"preview": "{\n \"formatVersion\": 1,\n \"database\": {\n \"version\": 1,\n \"identityHash\": \"cbf447b404076250e80614e02b0d462e\",\n \"e"
},
{
"path": "app/src/main/Android.bp",
"chars": 968,
"preview": "android_app {\n name: \"NeoWellbeing\",\n defaults: [\"platform_app_defaults\"],\n static_libs: [\n \"NeoWellbein"
},
{
"path": "app/src/main/AndroidManifest.xml",
"chars": 13800,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:to"
},
{
"path": "app/src/main/java/org/eu/droid_ng/wellbeing/Wellbeing.kt",
"chars": 815,
"preview": "package org.eu.droid_ng.wellbeing\n\nimport android.app.Application\nimport org.eu.droid_ng.wellbeing.shared.BugUtils\nimpor"
},
{
"path": "app/src/main/java/org/eu/droid_ng/wellbeing/broadcast/AlarmFiresBroadcastReceiver.kt",
"chars": 396,
"preview": "package org.eu.droid_ng.wellbeing.broadcast\n\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimp"
},
{
"path": "app/src/main/java/org/eu/droid_ng/wellbeing/broadcast/AppTimersBroadcastReceiver.kt",
"chars": 547,
"preview": "package org.eu.droid_ng.wellbeing.broadcast\n\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimp"
},
{
"path": "app/src/main/java/org/eu/droid_ng/wellbeing/broadcast/BootReceiver.kt",
"chars": 478,
"preview": "package org.eu.droid_ng.wellbeing.broadcast\n\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimp"
},
{
"path": "app/src/main/java/org/eu/droid_ng/wellbeing/broadcast/ManuallyUnsuspendBroadcastReceiver.kt",
"chars": 971,
"preview": "package org.eu.droid_ng.wellbeing.broadcast\n\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimp"
},
{
"path": "app/src/main/java/org/eu/droid_ng/wellbeing/broadcast/NextAlarmChangedReceiver.kt",
"chars": 498,
"preview": "package org.eu.droid_ng.wellbeing.broadcast\n\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimp"
},
{
"path": "app/src/main/java/org/eu/droid_ng/wellbeing/broadcast/NotificationBroadcastReceiver.kt",
"chars": 474,
"preview": "package org.eu.droid_ng.wellbeing.broadcast\n\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimp"
},
{
"path": "app/src/main/java/org/eu/droid_ng/wellbeing/ext.kt",
"chars": 308,
"preview": "package org.eu.droid_ng.wellbeing\n\nfun String.Companion.join(delimiter: String, strings: Iterable<CharSequence?>): Strin"
},
{
"path": "app/src/main/java/org/eu/droid_ng/wellbeing/lib/AlarmCoordinator.kt",
"chars": 690,
"preview": "package org.eu.droid_ng.wellbeing.lib\n\nimport android.app.AlarmManager\nimport android.content.Context\nimport java.time.I"
},
{
"path": "app/src/main/java/org/eu/droid_ng/wellbeing/lib/QSTiles.kt",
"chars": 1367,
"preview": "package org.eu.droid_ng.wellbeing.lib\n\nimport android.service.quicksettings.Tile.STATE_ACTIVE\nimport android.service.qui"
},
{
"path": "app/src/main/java/org/eu/droid_ng/wellbeing/lib/ScheduleUtils.kt",
"chars": 6707,
"preview": "package org.eu.droid_ng.wellbeing.lib\n\nimport android.app.AlarmManager\nimport android.app.PendingIntent\nimport android.a"
},
{
"path": "app/src/main/java/org/eu/droid_ng/wellbeing/lib/State.kt",
"chars": 1681,
"preview": "package org.eu.droid_ng.wellbeing.lib\n\nclass State(private val value: Int) {\n\tcompanion object {\n\t\t/* Update of State (p"
},
{
"path": "app/src/main/java/org/eu/droid_ng/wellbeing/lib/Utils.kt",
"chars": 10131,
"preview": "package org.eu.droid_ng.wellbeing.lib\n\nimport android.annotation.SuppressLint\nimport android.app.usage.UsageEvents\nimpor"
},
{
"path": "app/src/main/java/org/eu/droid_ng/wellbeing/lib/WellbeingAirplaneState.kt",
"chars": 2850,
"preview": "package org.eu.droid_ng.wellbeing.lib\n\nimport android.content.Context\nimport android.provider.Settings\n\nenum class Wellb"
},
{
"path": "app/src/main/java/org/eu/droid_ng/wellbeing/lib/WellbeingService.kt",
"chars": 42447,
"preview": "package org.eu.droid_ng.wellbeing.lib\n\nimport android.app.*\nimport android.app.usage.UsageStatsManager\nimport android.ap"
},
{
"path": "app/src/main/java/org/eu/droid_ng/wellbeing/lib/WellbeingStateUtil.kt",
"chars": 10211,
"preview": "package org.eu.droid_ng.wellbeing.lib\n\nimport android.app.*\nimport android.content.ComponentName\nimport android.content."
},
{
"path": "app/src/main/java/org/eu/droid_ng/wellbeing/prefs/AppTimers.kt",
"chars": 8831,
"preview": "package org.eu.droid_ng.wellbeing.prefs\n\nimport android.content.Context\nimport android.content.Intent\nimport android.con"
},
{
"path": "app/src/main/java/org/eu/droid_ng/wellbeing/prefs/BedtimeMode.kt",
"chars": 2800,
"preview": "package org.eu.droid_ng.wellbeing.prefs\n\nimport android.content.Intent\nimport android.os.Bundle\nimport android.view.View"
},
{
"path": "app/src/main/java/org/eu/droid_ng/wellbeing/prefs/DayPicker.kt",
"chars": 2649,
"preview": "package org.eu.droid_ng.wellbeing.prefs\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport android."
},
{
"path": "app/src/main/java/org/eu/droid_ng/wellbeing/prefs/FocusModeActivity.kt",
"chars": 2804,
"preview": "package org.eu.droid_ng.wellbeing.prefs\n\nimport android.animation.LayoutTransition\nimport android.content.Intent\nimport "
},
{
"path": "app/src/main/java/org/eu/droid_ng/wellbeing/prefs/ManualSuspendActivity.kt",
"chars": 1457,
"preview": "package org.eu.droid_ng.wellbeing.prefs\n\nimport android.content.pm.PackageManager\nimport android.os.Bundle\nimport androi"
},
{
"path": "app/src/main/java/org/eu/droid_ng/wellbeing/prefs/PackageRecyclerViewAdapter.kt",
"chars": 5312,
"preview": "package org.eu.droid_ng.wellbeing.prefs\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport an"
},
{
"path": "app/src/main/java/org/eu/droid_ng/wellbeing/prefs/ScheduleActivity.kt",
"chars": 3191,
"preview": "package org.eu.droid_ng.wellbeing.prefs\n\nimport android.os.Bundle\nimport android.util.Log\nimport android.util.TypedValue"
},
{
"path": "app/src/main/java/org/eu/droid_ng/wellbeing/prefs/ScheduleCardView.kt",
"chars": 3256,
"preview": "package org.eu.droid_ng.wellbeing.prefs\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport android."
},
{
"path": "app/src/main/java/org/eu/droid_ng/wellbeing/prefs/SettingsActivity.kt",
"chars": 3758,
"preview": "package org.eu.droid_ng.wellbeing.prefs\n\nimport android.content.ClipData\nimport android.content.ClipboardManager\nimport "
},
{
"path": "app/src/main/java/org/eu/droid_ng/wellbeing/prefs/TimeSettingView.kt",
"chars": 2397,
"preview": "package org.eu.droid_ng.wellbeing.prefs\n\nimport android.app.TimePickerDialog\nimport android.content.Context\nimport andro"
},
{
"path": "app/src/main/java/org/eu/droid_ng/wellbeing/shared/Database.kt",
"chars": 8511,
"preview": "package org.eu.droid_ng.wellbeing.shared\n\nimport android.content.Context\nimport android.os.Handler\nimport android.util.L"
},
{
"path": "app/src/main/java/org/eu/droid_ng/wellbeing/shared/WellbeingFrameworkClient.kt",
"chars": 4570,
"preview": "package org.eu.droid_ng.wellbeing.shared\n\nimport android.content.ComponentName\nimport android.content.Context\nimport and"
},
{
"path": "app/src/main/java/org/eu/droid_ng/wellbeing/ui/DashboardActivity.kt",
"chars": 17777,
"preview": "package org.eu.droid_ng.wellbeing.ui\n\nimport android.content.Context\nimport android.content.pm.PackageManager\nimport and"
},
{
"path": "app/src/main/java/org/eu/droid_ng/wellbeing/ui/MainActivity.kt",
"chars": 915,
"preview": "package org.eu.droid_ng.wellbeing.ui\n\nimport android.os.Bundle\nimport androidx.appcompat.app.AppCompatActivity\nimport co"
},
{
"path": "app/src/main/java/org/eu/droid_ng/wellbeing/ui/MainPreferenceFragment.kt",
"chars": 2041,
"preview": "package org.eu.droid_ng.wellbeing.ui\n\nimport android.os.Bundle\nimport androidx.preference.Preference\nimport androidx.pre"
},
{
"path": "app/src/main/java/org/eu/droid_ng/wellbeing/ui/ShowSuspendedAppDetails.kt",
"chars": 4218,
"preview": "package org.eu.droid_ng.wellbeing.ui\n\nimport android.annotation.SuppressLint\nimport android.content.Intent\nimport androi"
},
{
"path": "app/src/main/java/org/eu/droid_ng/wellbeing/ui/TakeBreakDialogActivity.kt",
"chars": 1475,
"preview": "package org.eu.droid_ng.wellbeing.ui\n\nimport android.os.Bundle\nimport android.view.View\nimport android.view.ViewGroup\nim"
},
{
"path": "app/src/main/java/org/eu/droid_ng/wellbeing/widget/ScreenTimeAppWidget.kt",
"chars": 4043,
"preview": "package org.eu.droid_ng.wellbeing.widget\n\nimport android.app.PendingIntent\nimport android.appwidget.AppWidgetManager\nimp"
},
{
"path": "app/src/main/privapp-permissions-wellbeing.xml",
"chars": 3052,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n ~ Copyright (C) 2017 The Android Open Source Project\n ~\n ~ Licensed unde"
},
{
"path": "app/src/main/res/drawable/appwidget_background.xml",
"chars": 448,
"preview": "<shape xmlns:android=\"http://schemas.android.com/apk/res/android\" android:shape=\"rectangle\">\n <solid android:color=\""
},
{
"path": "app/src/main/res/drawable/appwidget_screen_time_bg.xml",
"chars": 241,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n\tandroid:shape="
},
{
"path": "app/src/main/res/drawable/baseline_airplanemode_active_24.xml",
"chars": 437,
"preview": "<vector android:height=\"28dp\" android:tint=\"?android:attr/colorControlNormal\"\n android:viewportHeight=\"24\" android:vi"
},
{
"path": "app/src/main/res/drawable/baseline_alarm_24.xml",
"chars": 593,
"preview": "<vector android:height=\"28dp\" android:tint=\"?android:attr/colorControlNormal\"\n android:viewportHeight=\"24\" android:vi"
},
{
"path": "app/src/main/res/drawable/baseline_arrow_drop_down_24.xml",
"chars": 320,
"preview": "<vector android:height=\"24dp\" android:tint=\"?android:attr/colorControlNormal\"\n android:viewportHeight=\"24\" android:vi"
},
{
"path": "app/src/main/res/drawable/baseline_battery_charging_full_24.xml",
"chars": 467,
"preview": "<vector android:height=\"28dp\" android:tint=\"?android:attr/colorControlNormal\"\n android:viewportHeight=\"24\" android:vi"
},
{
"path": "app/src/main/res/drawable/baseline_bedtime_24.xml",
"chars": 459,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector android:height=\"32dip\" android:width=\"32dip\" android:viewportWidth=\"24.0\""
},
{
"path": "app/src/main/res/drawable/baseline_cancel_24.xml",
"chars": 483,
"preview": "<vector android:height=\"24dp\" android:tint=\"?android:attr/colorControlNormal\"\n android:viewportHeight=\"24\" android:vi"
},
{
"path": "app/src/main/res/drawable/baseline_delete_24.xml",
"chars": 388,
"preview": "<vector android:height=\"28dp\" android:tint=\"?android:attr/colorControlNormal\"\n android:viewportHeight=\"24\" android:vi"
},
{
"path": "app/src/main/res/drawable/baseline_exit_to_app_24.xml",
"chars": 515,
"preview": "<vector android:autoMirrored=\"true\" android:height=\"24dp\"\n android:tint=\"?android:attr/colorControlNormal\" android:vi"
},
{
"path": "app/src/main/res/drawable/baseline_gradient_24.xml",
"chars": 601,
"preview": "<vector android:height=\"28dp\" android:tint=\"?android:attr/colorControlNormal\"\n android:viewportHeight=\"24\" android:vi"
},
{
"path": "app/src/main/res/drawable/baseline_schedule_24.xml",
"chars": 574,
"preview": "<vector android:height=\"28dp\" android:tint=\"?android:attr/colorControlNormal\"\n android:viewportHeight=\"24\" android:vi"
},
{
"path": "app/src/main/res/drawable/dpicker_background.xml",
"chars": 301,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\t<item andr"
},
{
"path": "app/src/main/res/drawable/dpicker_outline_oval.xml",
"chars": 262,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n\tandroid:shape="
},
{
"path": "app/src/main/res/drawable/dpicker_shape_oval.xml",
"chars": 247,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n\tandroid:shape="
},
{
"path": "app/src/main/res/drawable/dpicker_text_color.xml",
"chars": 282,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\t<item andr"
},
{
"path": "app/src/main/res/drawable/ic_baseline_access_time_24dp.xml",
"chars": 620,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/ic_baseline_app_blocking_24.xml",
"chars": 707,
"preview": "<vector android:height=\"24dp\" android:tint=\"?android:attr/colorControlNormal\"\n android:viewportHeight=\"24\" android:vi"
},
{
"path": "app/src/main/res/drawable/ic_baseline_bug_report_24.xml",
"chars": 751,
"preview": "<vector android:height=\"32dp\" android:tint=\"?android:attr/colorControlNormal\"\n android:viewportHeight=\"24\" android:vi"
},
{
"path": "app/src/main/res/drawable/ic_baseline_dashboard_24dp.xml",
"chars": 411,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/ic_baseline_king_bed_24dp.xml",
"chars": 508,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/ic_baseline_person_24.xml",
"chars": 430,
"preview": "<vector android:height=\"32dp\" android:tint=\"?android:attr/colorControlNormal\"\n android:viewportHeight=\"24\" android:vi"
},
{
"path": "app/src/main/res/drawable/ic_baseline_person_24dp.xml",
"chars": 459,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/ic_baseline_settings_24dp.xml",
"chars": 1314,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n\txmlns:tools=\"http://schemas.android.com/tools\"\n\tandr"
},
{
"path": "app/src/main/res/drawable/ic_launcher_foreground.xml",
"chars": 2169,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"108dp\"\n android:height=\"108dp\"\n"
},
{
"path": "app/src/main/res/drawable/ic_plus_24.xml",
"chars": 362,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector android:height=\"24dp\" android:tint=\"?android:attr/colorControlNormal\"\n\tan"
},
{
"path": "app/src/main/res/drawable/ic_settings.xml",
"chars": 450,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n\tandroid:tint=\"?android:attr/colorBackground\"\n\tandroi"
},
{
"path": "app/src/main/res/drawable/outline_badge_24.xml",
"chars": 1021,
"preview": "<vector android:height=\"32dp\" android:tint=\"?android:attr/colorControlNormal\"\n android:viewportHeight=\"24\" android:vi"
},
{
"path": "app/src/main/res/drawable-anydpi/ic_focus_mode.xml",
"chars": 513,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n\tandroid:width=\"32dp\"\n\tandroid:height=\"32dp\"\n\tandroid"
},
{
"path": "app/src/main/res/drawable-anydpi/ic_stat_name.xml",
"chars": 554,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable-anydpi/ic_take_break.xml",
"chars": 855,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"28dp\"\n android:height=\"28dp\"\n "
},
{
"path": "app/src/main/res/layout/activity_app_timers.xml",
"chars": 2211,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schema"
},
{
"path": "app/src/main/res/layout/activity_bedtime_mode.xml",
"chars": 9298,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schema"
},
{
"path": "app/src/main/res/layout/activity_dashboard.xml",
"chars": 5149,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schema"
},
{
"path": "app/src/main/res/layout/activity_focusmode.xml",
"chars": 7516,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schema"
},
{
"path": "app/src/main/res/layout/activity_manual_suspend.xml",
"chars": 2793,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schema"
},
{
"path": "app/src/main/res/layout/activity_schedule.xml",
"chars": 2177,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout\n\txmlns:android=\"http://schem"
},
{
"path": "app/src/main/res/layout/activity_show_suspended_app_details.xml",
"chars": 12214,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schema"
},
{
"path": "app/src/main/res/layout/appitem.xml",
"chars": 2505,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas."
},
{
"path": "app/src/main/res/layout/appwidget_screen_time.xml",
"chars": 4924,
"preview": "<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:orientation=\"vertical\"\n android:"
},
{
"path": "app/src/main/res/layout/dpicker.xml",
"chars": 4227,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.appcompat.widget.LinearLayoutCompat xmlns:android=\"http://schemas.andro"
},
{
"path": "app/src/main/res/layout/preference_material_switch.xml",
"chars": 489,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Derived from https://github.com/androidx/androidx/blob/8cb282cc/preference/p"
},
{
"path": "app/src/main/res/layout/schedule_card.xml",
"chars": 4872,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.google.android.material.card.MaterialCardView xmlns:android=\"http://schemas."
},
{
"path": "app/src/main/res/layout/settings_activity.xml",
"chars": 1718,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schema"
},
{
"path": "app/src/main/res/layout/take_a_break_activity.xml",
"chars": 1511,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schema"
},
{
"path": "app/src/main/res/mipmap-anydpi/ic_launcher.xml",
"chars": 337,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n <b"
},
{
"path": "app/src/main/res/mipmap-anydpi/ic_launcher_round.xml",
"chars": 337,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n <b"
},
{
"path": "app/src/main/res/values/arrays.xml",
"chars": 1194,
"preview": "<resources>\n\t<string-array name=\"break_entries\">\n\t\t<item>@string/always_ask</item>\n\t\t<item>@string/min_1</item>\n\t\t<item>"
},
{
"path": "app/src/main/res/values/dimens.xml",
"chars": 123,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <dimen name=\"app_widget_background_radius\">16dp</dimen>\n</resourc"
},
{
"path": "app/src/main/res/values/ic_launcher_background.xml",
"chars": 120,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <color name=\"ic_launcher_background\">#FEFCE9</color>\n</resources>"
},
{
"path": "app/src/main/res/values/strings.xml",
"chars": 8419,
"preview": "<resources xmlns:tools=\"http://schemas.android.com/tools\" tools:ignore=\"MissingTranslation\">\n <!-- App Name -->\n <"
},
{
"path": "app/src/main/res/values/styles.xml",
"chars": 1693,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n\n <style name=\"AppT"
},
{
"path": "app/src/main/res/values-fil/strings.xml",
"chars": 6536,
"preview": "<resources>\n\t<string name=\"loading\">Ikinakarga…</string>\n\t<string name=\"setting_desc\">Mode sa pag-focus, mode sa pagtulo"
},
{
"path": "app/src/main/res/values-sw360dp/values-preference.xml",
"chars": 252,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n <bool name=\"config"
},
{
"path": "app/src/main/res/values-v31/dimens.xml",
"chars": 169,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <dimen name=\"app_widget_background_radius\">@android:dimen/system_"
},
{
"path": "app/src/main/res/xml/appwidget_screen_time.xml",
"chars": 826,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<appwidget-provider xmlns:android=\"http://schemas.android.com/apk/res/android\"\n "
},
{
"path": "app/src/main/res/xml/backup_rules.xml",
"chars": 414,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n Sample backup rules file; uncomment and customize as necessary.\n See htt"
},
{
"path": "app/src/main/res/xml/data_extraction_rules.xml",
"chars": 436,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n Sample data extraction rules file; uncomment and customize as necessary.\n "
},
{
"path": "app/src/main/res/xml/main_preferences.xml",
"chars": 2614,
"preview": "<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:app=\"http://schemas.android.com/a"
},
{
"path": "app/src/main/res/xml/root_preferences.xml",
"chars": 2427,
"preview": "<PreferenceScreen xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n\t<PreferenceCategory app:title=\"@string/focus_mo"
},
{
"path": "app/update-binary",
"chars": 632,
"preview": "#!/sbin/sh\n\n#################\n# Initialization\n#################\n\numask 022\n\n# echo before loading util_functions\nui_pri"
},
{
"path": "build.gradle.kts",
"chars": 644,
"preview": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\nplugins {\n\tval agpVe"
},
{
"path": "framework/.gitignore",
"chars": 6,
"preview": "/build"
},
{
"path": "framework/build.gradle.kts",
"chars": 1563,
"preview": "plugins {\n id(\"com.android.application\")\n id(\"org.jetbrains.kotlin.android\")\n}\n\nandroid {\n namespace = \"org.eu."
},
{
"path": "framework/proguard-rules.pro",
"chars": 765,
"preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
},
{
"path": "framework/src/main/Android.bp",
"chars": 416,
"preview": "android_app {\n name: \"NeoWellbeingFramework\",\n defaults: [\"platform_app_defaults\"],\n static_libs: [\n \"an"
},
{
"path": "framework/src/main/AndroidManifest.xml",
"chars": 1884,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:to"
},
{
"path": "framework/src/main/java/org/eu/droid_ng/wellbeing/framework/Framework.kt",
"chars": 1789,
"preview": "package org.eu.droid_ng.wellbeing.framework\n\nimport android.app.Application\nimport android.util.Log\nimport org.eu.droid_"
},
{
"path": "framework/src/main/java/org/eu/droid_ng/wellbeing/framework/WellbeingBootReceiver.kt",
"chars": 893,
"preview": "package org.eu.droid_ng.wellbeing.framework\n\nimport android.app.admin.DevicePolicyManager\nimport android.content.Broadca"
},
{
"path": "framework/src/main/java/org/eu/droid_ng/wellbeing/framework/WellbeingFrameworkService.kt",
"chars": 1363,
"preview": "package org.eu.droid_ng.wellbeing.framework\n\nimport android.app.Service\nimport android.content.Context\nimport android.co"
},
{
"path": "framework/src/main/java/org/eu/droid_ng/wellbeing/framework/WellbeingFrameworkServiceImpl.kt",
"chars": 1733,
"preview": "package org.eu.droid_ng.wellbeing.framework\n\nimport android.content.Context\nimport android.content.Intent\nimport android"
},
{
"path": "framework/src/main/res/values/strings.xml",
"chars": 106,
"preview": "<resources>\n <string name=\"app_name\" translatable=\"false\">Neo Wellbeing Framework</string>\n</resources>"
},
{
"path": "gradle/wrapper/gradle-wrapper.properties",
"chars": 337,
"preview": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionSha256Sum=a4b4158601f8636cdeeab09bd76afb640"
},
{
"path": "gradle.properties",
"chars": 1914,
"preview": "# Project-wide Gradle settings.\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will ov"
},
{
"path": "gradlew",
"chars": 8669,
"preview": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Lice"
},
{
"path": "gradlew.bat",
"chars": 2918,
"preview": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (th"
},
{
"path": "settings.gradle.kts",
"chars": 377,
"preview": "@file:Suppress(\"UnstableApiUsage\")\n\npluginManagement {\n\trepositories {\n\t\tgradlePluginPortal()\n\t\tgoogle()\n\t\tmavenCentral("
},
{
"path": "shared/.gitignore",
"chars": 6,
"preview": "/build"
},
{
"path": "shared/build.gradle.kts",
"chars": 741,
"preview": "plugins {\n\tid(\"com.android.library\")\n\tid(\"org.jetbrains.kotlin.android\")\n}\n\nandroid {\n\tnamespace = \"org.eu.droid_ng.well"
},
{
"path": "shared/consumer-rules.pro",
"chars": 0,
"preview": ""
},
{
"path": "shared/proguard-rules.pro",
"chars": 765,
"preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
},
{
"path": "shared/src/main/Android.bp",
"chars": 394,
"preview": "android_library {\n name: \"NeoWellbeing-shared\",\n resource_dirs: [\"res\"],\n\n static_libs: [\n \"androidx.ann"
},
{
"path": "shared/src/main/AndroidManifest.xml",
"chars": 61,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest>\n</manifest>"
},
{
"path": "shared/src/main/aidl/org/eu/droid_ng/wellbeing/framework/IWellbeingFrameworkService.aidl",
"chars": 552,
"preview": "// IWellbeingFrameworkService.aidl\npackage org.eu.droid_ng.wellbeing.framework;\n\n// Declare any non-default types here w"
},
{
"path": "shared/src/main/java/org/eu/droid_ng/wellbeing/shared/BugUtils.kt",
"chars": 2038,
"preview": "package org.eu.droid_ng.wellbeing.shared\n\nimport android.content.Context\nimport android.util.ArrayMap\nimport android.uti"
},
{
"path": "shared/src/main/java_magisk/org/eu/droid_ng/wellbeing/shim/PackageManagerDelegate.java",
"chars": 21560,
"preview": "package org.eu.droid_ng.wellbeing.shim;\n\nimport android.annotation.SuppressLint;\nimport android.app.PendingIntent;\nimpor"
},
{
"path": "shared/src/main/java_magisk/org/eu/droid_ng/wellbeing/shim/UserHandlerShim.java",
"chars": 564,
"preview": "package org.eu.droid_ng.wellbeing.shim;\n\nimport android.os.Process;\nimport android.os.UserHandle;\n\n@SuppressWarnings(\"Ja"
},
{
"path": "shared/src/main/java_real/org/eu/droid_ng/wellbeing/shim/PackageManagerDelegate.java",
"chars": 11285,
"preview": "package org.eu.droid_ng.wellbeing.shim;\n\nimport android.content.pm.PackageManager;\nimport android.hardware.display.Color"
},
{
"path": "shared/src/main/java_real/org/eu/droid_ng/wellbeing/shim/UserHandlerShim.java",
"chars": 162,
"preview": "package org.eu.droid_ng.wellbeing.shim;\n\nimport android.os.UserHandle;\n\npublic class UserHandlerShim {\n public static"
}
]
// ... and 2 more files (download for full content)
About this extraction
This page contains the full source code of the NeoApplications/Neo-Wellbeing GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 141 files (366.7 KB), approximately 101.9k tokens, and a symbol index with 72 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.