master d37a3d8d5b53 cached
141 files
366.7 KB
101.9k tokens
72 symbols
1 requests
Download .txt
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
Download .txt
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
Download .txt
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.

Copied to clipboard!