mirror of
https://codeberg.org/timelimit/opentimelimit-android.git
synced 2025-10-03 09:49:24 +02:00
Initial commit
This commit is contained in:
commit
afb86e9d0e
420 changed files with 26435 additions and 0 deletions
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
/.idea/caches/build_file_checksums.ser
|
||||||
|
/.idea/libraries
|
||||||
|
/.idea/modules.xml
|
||||||
|
/.idea/workspace.xml
|
||||||
|
.DS_Store
|
||||||
|
/build
|
||||||
|
/captures
|
||||||
|
.externalNativeBuild
|
||||||
|
.idea
|
675
LICENSE
Normal file
675
LICENSE
Normal file
|
@ -0,0 +1,675 @@
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
|
GNU General Public License for most of our software; it applies also to
|
||||||
|
any other work released this way by its authors. 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
|
||||||
|
them 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 prevent others from denying you
|
||||||
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
|
freedoms that you received. 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.
|
||||||
|
|
||||||
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
|
that there is no warranty for this free software. For both users' and
|
||||||
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
|
changed, so that their problems will not be attributed erroneously to
|
||||||
|
authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run
|
||||||
|
modified versions of the software inside them, although the manufacturer
|
||||||
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
|
protecting users' freedom to change the software. The systematic
|
||||||
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
|
products. If such problems arise substantially in other domains, we
|
||||||
|
stand ready to extend this provision to those domains in future versions
|
||||||
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents.
|
||||||
|
States should not allow patents to restrict development and use of
|
||||||
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
|
avoid the special danger that patents applied to a free program could
|
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey 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;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If 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 convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the special requirements of the GNU Affero General Public License,
|
||||||
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU 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 that a certain numbered version of the GNU General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
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.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
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
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
opentimelimit-android
|
||||||
|
Copyright (C) 2019 timelimit.io
|
||||||
|
|
||||||
|
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 3 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, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program does terminal interaction, make it output a short
|
||||||
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
opentimelimit-android Copyright (C) 2019 timelimit.io
|
||||||
|
This program 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, your program's commands
|
||||||
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
|
<http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
The GNU 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. But first, please read
|
||||||
|
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||||
|
|
10
Readme.md
Normal file
10
Readme.md
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# Open TimeLimit
|
||||||
|
|
||||||
|
This App allows setting time limits for the usage of Android phones/ devices.
|
||||||
|
|
||||||
|
It is a fork of the proprietary App [TimeLimit](https://timelimit.io)
|
||||||
|
with all networking related features removed.
|
||||||
|
|
||||||
|
### Building
|
||||||
|
|
||||||
|
Open it with Android Studio and press the Run button.
|
3
app/.gitignore
vendored
Normal file
3
app/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
/build
|
||||||
|
play-key.json
|
||||||
|
keystore.properties
|
4
app/asset sources
Normal file
4
app/asset sources
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
ic_launcher: https://romannurik.github.io/AndroidAssetStudio/icons-launcher.html#foreground.type=clipart&foreground.clipart=timelapse&foreground.space.trim=1&foreground.space.pad=0.25&foreColor=rgb(255%2C%20255%2C%20255)&backColor=rgb(103%2C%2058%2C%20183)&crop=0&backgroundShape=square&effects=score&name=ic_launcher
|
||||||
|
ic_stat_timelapse: https://romannurik.github.io/AndroidAssetStudio/icons-notification.html#source.type=clipart&source.clipart=timelapse&source.space.trim=1&source.space.pad=0&name=ic_stat_timelapse
|
||||||
|
ic_stat_block: https://romannurik.github.io/AndroidAssetStudio/icons-notification.html#source.type=clipart&source.clipart=block&source.space.trim=1&source.space.pad=0&name=ic_stat_block
|
||||||
|
ic_stat_check: https://romannurik.github.io/AndroidAssetStudio/icons-notification.html#source.type=clipart&source.clipart=check&source.space.trim=1&source.space.pad=0&name=ic_stat_check
|
120
app/build.gradle
Normal file
120
app/build.gradle
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
apply plugin: 'com.android.application'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
apply plugin: 'kotlin-android-extensions'
|
||||||
|
apply plugin: "androidx.navigation.safeargs"
|
||||||
|
apply plugin: 'kotlin-kapt'
|
||||||
|
|
||||||
|
androidExtensions {
|
||||||
|
experimental = true
|
||||||
|
}
|
||||||
|
|
||||||
|
def keystoreProperties = new Properties()
|
||||||
|
keystoreProperties.load(new FileInputStream(project.file("keystore.properties")))
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion 28
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "io.timelimit.android.open"
|
||||||
|
minSdkVersion 19
|
||||||
|
targetSdkVersion 28
|
||||||
|
versionCode 1
|
||||||
|
versionName "0.1.0"
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
kapt {
|
||||||
|
arguments {
|
||||||
|
arg("room.schemaLocation", "$projectDir/schemas".toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
release {
|
||||||
|
keyAlias keystoreProperties['keyAlias']
|
||||||
|
keyPassword keystoreProperties['keyPassword']
|
||||||
|
storeFile file(keystoreProperties['storeFile'])
|
||||||
|
storePassword keystoreProperties['storePassword']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lintOptions {
|
||||||
|
disable 'GoogleAppIndexingWarning'
|
||||||
|
baseline file("lint-baseline.xml")
|
||||||
|
}
|
||||||
|
|
||||||
|
dataBinding {
|
||||||
|
enabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
signingConfig signingConfigs.release
|
||||||
|
minifyEnabled true
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_7
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
def nav_version = "1.0.0-alpha09"
|
||||||
|
def room_version = "2.0.0"
|
||||||
|
def paging_version = "2.1.0-rc01"
|
||||||
|
|
||||||
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
|
implementation 'androidx.appcompat:appcompat:1.0.2'
|
||||||
|
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||||
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
|
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
|
||||||
|
|
||||||
|
implementation "android.arch.navigation:navigation-fragment:$nav_version"
|
||||||
|
implementation "android.arch.navigation:navigation-ui:$nav_version"
|
||||||
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha3'
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
|
||||||
|
|
||||||
|
implementation "androidx.room:room-runtime:$room_version"
|
||||||
|
kapt "androidx.room:room-compiler:$room_version"
|
||||||
|
testImplementation "androidx.room:room-testing:$room_version"
|
||||||
|
|
||||||
|
implementation "androidx.paging:paging-runtime:$paging_version"
|
||||||
|
|
||||||
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.0'
|
||||||
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.0'
|
||||||
|
|
||||||
|
testImplementation 'junit:junit:4.12'
|
||||||
|
androidTestImplementation 'androidx.test:runner:1.1.1'
|
||||||
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
|
||||||
|
|
||||||
|
implementation 'com.jakewharton.threetenabp:threetenabp:1.1.0'
|
||||||
|
|
||||||
|
implementation 'org.mindrot:jbcrypt:0.4'
|
||||||
|
|
||||||
|
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.11.0'
|
||||||
|
|
||||||
|
implementation 'com.google.android:flexbox:1.0.0'
|
||||||
|
|
||||||
|
implementation 'com.jaredrummler:android-device-names:1.1.7'
|
||||||
|
|
||||||
|
implementation 'com.squareup.okio:okio:2.1.0'
|
||||||
|
}
|
47
app/proguard-rules.pro
vendored
Normal file
47
app/proguard-rules.pro
vendored
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
# Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
#
|
||||||
|
# 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 version 3 of the License.
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
-dontwarn okio.**
|
||||||
|
|
||||||
|
# ServiceLoader support
|
||||||
|
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
|
||||||
|
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
|
||||||
|
|
||||||
|
# Most of volatile fields are updated with AFU and should not be mangled
|
||||||
|
-keepclassmembernames class kotlinx.** {
|
||||||
|
volatile <fields>;
|
||||||
|
}
|
431
app/schemas/io.timelimit.android.data.RoomDatabase/1.json
Normal file
431
app/schemas/io.timelimit.android.data.RoomDatabase/1.json
Normal file
|
@ -0,0 +1,431 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 1,
|
||||||
|
"identityHash": "cb315927a05cf9a1e8a76f795a54744e",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "user",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `password` TEXT NOT NULL, `type` TEXT NOT NULL, `timezone` TEXT NOT NULL, `disable_limits_until` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "password",
|
||||||
|
"columnName": "password",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "type",
|
||||||
|
"columnName": "type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timeZone",
|
||||||
|
"columnName": "timezone",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "disableLimitsUntil",
|
||||||
|
"columnName": "disable_limits_until",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "device",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `model` TEXT NOT NULL, `added_at` INTEGER NOT NULL, `current_user_id` TEXT NOT NULL, `current_protection_level` TEXT NOT NULL, `highest_permission_level` TEXT NOT NULL, `current_usage_stats_permission` TEXT NOT NULL, `highest_usage_stats_permission` TEXT NOT NULL, `current_notification_access_permission` TEXT NOT NULL, `highest_notification_access_permission` TEXT NOT NULL, `current_app_version` INTEGER NOT NULL, `highest_app_version` INTEGER NOT NULL, `tried_disabling_device_admin` INTEGER NOT NULL, `had_manipulation` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "model",
|
||||||
|
"columnName": "model",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "addedAt",
|
||||||
|
"columnName": "added_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "currentUserId",
|
||||||
|
"columnName": "current_user_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "currentProtectionLevel",
|
||||||
|
"columnName": "current_protection_level",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "highestProtectionLevel",
|
||||||
|
"columnName": "highest_permission_level",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "currentUsageStatsPermission",
|
||||||
|
"columnName": "current_usage_stats_permission",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "highestUsageStatsPermission",
|
||||||
|
"columnName": "highest_usage_stats_permission",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "currentNotificationAccessPermission",
|
||||||
|
"columnName": "current_notification_access_permission",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "highestNotificationAccessPermission",
|
||||||
|
"columnName": "highest_notification_access_permission",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "currentAppVersion",
|
||||||
|
"columnName": "current_app_version",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "highestAppVersion",
|
||||||
|
"columnName": "highest_app_version",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "manipulationTriedDisablingDeviceAdmin",
|
||||||
|
"columnName": "tried_disabling_device_admin",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "hadManipulation",
|
||||||
|
"columnName": "had_manipulation",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "app",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `title` TEXT NOT NULL, `launchable` INTEGER NOT NULL, `recommendation` TEXT NOT NULL, PRIMARY KEY(`package_name`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "packageName",
|
||||||
|
"columnName": "package_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isLaunchable",
|
||||||
|
"columnName": "launchable",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "recommendation",
|
||||||
|
"columnName": "recommendation",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"package_name"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_app_package_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"package_name"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX `index_app_package_name` ON `${TABLE_NAME}` (`package_name`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "category_app",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`category_id` TEXT NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`category_id`, `package_name`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "categoryId",
|
||||||
|
"columnName": "category_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "packageName",
|
||||||
|
"columnName": "package_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"category_id",
|
||||||
|
"package_name"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_category_app_category_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"category_id"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX `index_category_app_category_id` ON `${TABLE_NAME}` (`category_id`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_category_app_package_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"package_name"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX `index_category_app_package_name` ON `${TABLE_NAME}` (`package_name`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "category",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `child_id` TEXT NOT NULL, `title` TEXT NOT NULL, `blocked_times` TEXT NOT NULL, `extra_time` INTEGER NOT NULL, `temporarily_blocked` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "childId",
|
||||||
|
"columnName": "child_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "blockedMinutesInWeek",
|
||||||
|
"columnName": "blocked_times",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "extraTimeInMillis",
|
||||||
|
"columnName": "extra_time",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "temporarilyBlocked",
|
||||||
|
"columnName": "temporarily_blocked",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "used_time",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`day_of_epoch` INTEGER NOT NULL, `used_time` INTEGER NOT NULL, `category_id` TEXT NOT NULL, PRIMARY KEY(`category_id`, `day_of_epoch`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "dayOfEpoch",
|
||||||
|
"columnName": "day_of_epoch",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "usedMillis",
|
||||||
|
"columnName": "used_time",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "categoryId",
|
||||||
|
"columnName": "category_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"category_id",
|
||||||
|
"day_of_epoch"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "time_limit_rule",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `category_id` TEXT NOT NULL, `apply_to_extra_time_usage` INTEGER NOT NULL, `day_mask` INTEGER NOT NULL, `max_time` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "categoryId",
|
||||||
|
"columnName": "category_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "applyToExtraTimeUsage",
|
||||||
|
"columnName": "apply_to_extra_time_usage",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "dayMask",
|
||||||
|
"columnName": "day_mask",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maximumTimeInMillis",
|
||||||
|
"columnName": "max_time",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "config",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "key",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "value",
|
||||||
|
"columnName": "value",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "temporarily_allowed_app",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, PRIMARY KEY(`package_name`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "packageName",
|
||||||
|
"columnName": "package_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"package_name"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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, \"cb315927a05cf9a1e8a76f795a54744e\")"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
116
app/src/main/AndroidManifest.xml
Normal file
116
app/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
|
||||||
|
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 version 3 of the License.
|
||||||
|
|
||||||
|
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, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
package="io.timelimit.android">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
<!-- suppress DeprecatedClassUsageInspection -->
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.GET_TASKS"
|
||||||
|
android:maxSdkVersion="20" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.PACKAGE_USAGE_STATS"
|
||||||
|
tools:ignore="ProtectedPermissions" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:name=".Application"
|
||||||
|
android:allowBackup="false"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/AppTheme">
|
||||||
|
|
||||||
|
<!-- UI -->
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:launchMode="singleTop"
|
||||||
|
android:taskAffinity=":main"
|
||||||
|
android:name=".ui.MainActivity">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ui.lock.LockActivity"
|
||||||
|
android:autoRemoveFromRecents="true"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:exported="false"
|
||||||
|
android:noHistory="true"
|
||||||
|
android:resizeableActivity="false"
|
||||||
|
android:supportsPictureInPicture="false"
|
||||||
|
android:taskAffinity=":lock"
|
||||||
|
tools:ignore="UnusedAttribute" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
tools:ignore="UnusedAttribute"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:autoRemoveFromRecents="true"
|
||||||
|
android:taskAffinity=":manipulationwarning"
|
||||||
|
android:showOnLockScreen="true"
|
||||||
|
android:exported="false"
|
||||||
|
android:name=".ui.manipulation.UnlockAfterManipulationActivity" />
|
||||||
|
|
||||||
|
<!-- system integration -->
|
||||||
|
|
||||||
|
<receiver android:name=".integration.platform.android.receiver.BootReceiver">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<receiver android:name=".integration.platform.android.receiver.UpdateReceiver">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".integration.platform.android.BackgroundService"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".integration.platform.android.AdminReceiver"
|
||||||
|
android:description="@string/admin_description"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:permission="android.permission.BIND_DEVICE_ADMIN">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.app.device_admin"
|
||||||
|
android:resource="@xml/admin" />
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".integration.platform.android.NotificationListener"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.service.notification.NotificationListenerService" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
27
app/src/main/java/io/timelimit/android/Application.kt
Normal file
27
app/src/main/java/io/timelimit/android/Application.kt
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import com.jakewharton.threetenabp.AndroidThreeTen
|
||||||
|
|
||||||
|
class Application : Application() {
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
AndroidThreeTen.init(this)
|
||||||
|
}
|
||||||
|
}
|
26
app/src/main/java/io/timelimit/android/async/Threads.kt
Normal file
26
app/src/main/java/io/timelimit/android/async/Threads.kt
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.async
|
||||||
|
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
|
object Threads {
|
||||||
|
val database = Executors.newSingleThreadExecutor()!!
|
||||||
|
val mainThreadHandler = Handler(Looper.getMainLooper())
|
||||||
|
val crypto = Executors.newSingleThreadExecutor()!!
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.coroutines
|
||||||
|
|
||||||
|
import java.util.concurrent.Executor
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
|
suspend fun Executor.executeAndWait(runnable: Runnable) {
|
||||||
|
suspendCoroutine<Void?> {
|
||||||
|
this.execute {
|
||||||
|
try {
|
||||||
|
runnable.run()
|
||||||
|
|
||||||
|
it.resume(null)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
it.resumeWithException(ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <R> Executor.executeAndWait(function: () -> R) = suspendCoroutine<R> {
|
||||||
|
this.execute {
|
||||||
|
try {
|
||||||
|
it.resume(function())
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
it.resumeWithException(ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.coroutines
|
||||||
|
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
open class CoroutineFragment: Fragment(), CoroutineScope {
|
||||||
|
private val job = Job()
|
||||||
|
|
||||||
|
override val coroutineContext: CoroutineContext
|
||||||
|
get() = job + Dispatchers.Main
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.coroutines
|
||||||
|
|
||||||
|
import io.timelimit.android.async.Threads
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
|
fun <T> runAsync(block: suspend CoroutineScope.() -> T) {
|
||||||
|
GlobalScope.launch (Dispatchers.Main) {
|
||||||
|
block()
|
||||||
|
}.invokeOnCompletion {
|
||||||
|
if (it != null && (!(it is CancellationException))) {
|
||||||
|
Threads.mainThreadHandler.post {
|
||||||
|
throw it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> runAsyncExpectForever(block: suspend CoroutineScope.() -> T) {
|
||||||
|
runAsync {
|
||||||
|
block()
|
||||||
|
|
||||||
|
throw IllegalStateException()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.crypto
|
||||||
|
|
||||||
|
import io.timelimit.android.async.Threads
|
||||||
|
import io.timelimit.android.coroutines.executeAndWait
|
||||||
|
import org.mindrot.jbcrypt.BCrypt
|
||||||
|
|
||||||
|
object PasswordHashing {
|
||||||
|
suspend fun hashCoroutine(password: String) = Threads.crypto.executeAndWait {
|
||||||
|
hashSync(password)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hashSync(password: String) = hashSyncWithSalt(password, generateSalt())
|
||||||
|
|
||||||
|
fun hashSyncWithSalt(password: String, salt: String): String = BCrypt.hashpw(password, salt)
|
||||||
|
fun generateSalt(): String = BCrypt.gensalt()
|
||||||
|
|
||||||
|
fun validateSync(password: String, hashed: String): Boolean {
|
||||||
|
try {
|
||||||
|
return BCrypt.checkpw(password, hashed)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
58
app/src/main/java/io/timelimit/android/data/Database.kt
Normal file
58
app/src/main/java/io/timelimit/android/data/Database.kt
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.data
|
||||||
|
|
||||||
|
import io.timelimit.android.data.dao.*
|
||||||
|
import java.io.Closeable
|
||||||
|
|
||||||
|
interface Database {
|
||||||
|
fun app(): AppDao
|
||||||
|
fun categoryApp(): CategoryAppDao
|
||||||
|
fun category(): CategoryDao
|
||||||
|
fun config(): ConfigDao
|
||||||
|
fun device(): DeviceDao
|
||||||
|
fun timeLimitRules(): TimeLimitRuleDao
|
||||||
|
fun usedTimes(): UsedTimeDao
|
||||||
|
fun user(): UserDao
|
||||||
|
fun temporarilyAllowedApp(): TemporarilyAllowedAppDao
|
||||||
|
|
||||||
|
fun beginTransaction()
|
||||||
|
fun setTransactionSuccessful()
|
||||||
|
fun endTransaction()
|
||||||
|
|
||||||
|
fun deleteAllData()
|
||||||
|
fun close()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Database.transaction(): Transaction {
|
||||||
|
val db = this
|
||||||
|
|
||||||
|
db.beginTransaction()
|
||||||
|
|
||||||
|
return object: Transaction {
|
||||||
|
override fun setSuccess() {
|
||||||
|
db.setTransactionSuccessful()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
db.endTransaction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Transaction: Closeable {
|
||||||
|
fun setSuccess()
|
||||||
|
}
|
54
app/src/main/java/io/timelimit/android/data/IdGenerator.kt
Normal file
54
app/src/main/java/io/timelimit/android/data/IdGenerator.kt
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.data
|
||||||
|
|
||||||
|
import java.security.SecureRandom
|
||||||
|
|
||||||
|
object IdGenerator {
|
||||||
|
private const val LENGTH = 6
|
||||||
|
private const val CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
private val random: SecureRandom by lazy { SecureRandom() }
|
||||||
|
|
||||||
|
fun generateId(): String {
|
||||||
|
val output = StringBuilder(LENGTH)
|
||||||
|
|
||||||
|
for (i in 1..LENGTH) {
|
||||||
|
output.append(CHARS[random.nextInt(CHARS.length)])
|
||||||
|
}
|
||||||
|
|
||||||
|
return output.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isIdValid(id: String): Boolean {
|
||||||
|
if (id.length != LENGTH) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for (char in id) {
|
||||||
|
if (!CHARS.contains(char)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assertIdValid(id: String) {
|
||||||
|
if (!isIdValid(id)) {
|
||||||
|
throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.data
|
||||||
|
|
||||||
|
import android.util.JsonWriter
|
||||||
|
|
||||||
|
interface JsonSerializable {
|
||||||
|
fun serialize(writer: JsonWriter)
|
||||||
|
}
|
93
app/src/main/java/io/timelimit/android/data/RoomDatabase.kt
Normal file
93
app/src/main/java/io/timelimit/android/data/RoomDatabase.kt
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.data
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import io.timelimit.android.data.model.*
|
||||||
|
|
||||||
|
@Database(entities = [
|
||||||
|
User::class,
|
||||||
|
Device::class,
|
||||||
|
App::class,
|
||||||
|
CategoryApp::class,
|
||||||
|
Category::class,
|
||||||
|
UsedTimeItem::class,
|
||||||
|
TimeLimitRule::class,
|
||||||
|
ConfigurationItem::class,
|
||||||
|
TemporarilyAllowedApp::class
|
||||||
|
], version = 1)
|
||||||
|
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
|
||||||
|
companion object {
|
||||||
|
private val lock = Object()
|
||||||
|
private var instance: io.timelimit.android.data.Database? = null
|
||||||
|
const val DEFAULT_DB_NAME = "db"
|
||||||
|
const val BACKUP_DB_NAME = "db2"
|
||||||
|
|
||||||
|
fun with(context: Context): io.timelimit.android.data.Database {
|
||||||
|
if (instance == null) {
|
||||||
|
synchronized(lock) {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = createOrOpenLocalStorageInstance(context, DEFAULT_DB_NAME)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance!!
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createInMemoryInstance(context: Context): io.timelimit.android.data.Database {
|
||||||
|
return Room.inMemoryDatabaseBuilder(
|
||||||
|
context,
|
||||||
|
io.timelimit.android.data.RoomDatabase::class.java
|
||||||
|
).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createOrOpenLocalStorageInstance(context: Context, name: String): io.timelimit.android.data.Database {
|
||||||
|
return Room.databaseBuilder(
|
||||||
|
context,
|
||||||
|
io.timelimit.android.data.RoomDatabase::class.java,
|
||||||
|
name
|
||||||
|
)
|
||||||
|
.setJournalMode(JournalMode.TRUNCATE)
|
||||||
|
.fallbackToDestructiveMigration()
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the room compiler needs this
|
||||||
|
override fun setTransactionSuccessful() {
|
||||||
|
super.setTransactionSuccessful()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun beginTransaction() {
|
||||||
|
super.beginTransaction()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun endTransaction() {
|
||||||
|
super.endTransaction()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deleteAllData() {
|
||||||
|
clearAllTables()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
super.close()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,148 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.data.backup
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.util.AtomicFile
|
||||||
|
import io.timelimit.android.BuildConfig
|
||||||
|
import io.timelimit.android.coroutines.executeAndWait
|
||||||
|
import io.timelimit.android.data.RoomDatabase
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import okio.buffer
|
||||||
|
import okio.sink
|
||||||
|
import okio.source
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
|
class DatabaseBackup(private val context: Context) {
|
||||||
|
companion object {
|
||||||
|
private const val CONFIG_FILE = "config.json"
|
||||||
|
private const val LOG_TAG = "DatabaseBackup"
|
||||||
|
|
||||||
|
private var instance: DatabaseBackup? = null
|
||||||
|
private val lock = Object()
|
||||||
|
|
||||||
|
fun with(context: Context): DatabaseBackup {
|
||||||
|
if (instance == null) {
|
||||||
|
synchronized(lock) {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = DatabaseBackup(context.applicationContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val executor = Executors.newSingleThreadExecutor()
|
||||||
|
private val jsonFile = AtomicFile(context.getDatabasePath(CONFIG_FILE))
|
||||||
|
private val databaseFile = context.getDatabasePath(RoomDatabase.DEFAULT_DB_NAME)
|
||||||
|
private val databaseBackupFile = context.getDatabasePath(RoomDatabase.BACKUP_DB_NAME)
|
||||||
|
private val lock = Mutex()
|
||||||
|
|
||||||
|
suspend fun tryRestoreDatabaseBackupAsyncAndWait() {
|
||||||
|
executor.executeAndWait { tryRestoreDatabaseBackupSync() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tryRestoreDatabaseBackupSync() {
|
||||||
|
runBlocking {
|
||||||
|
lock.withLock {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "try restoring backup")
|
||||||
|
}
|
||||||
|
|
||||||
|
val database = RoomDatabase.with(context)
|
||||||
|
|
||||||
|
if (database.config().getOwnDeviceIdSync().orEmpty().isNotEmpty()) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "database is not empty -> don't restore backup")
|
||||||
|
}
|
||||||
|
|
||||||
|
return@runBlocking
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
jsonFile.openRead().use { inputStream ->
|
||||||
|
|
||||||
|
DatabaseBackupLowlevel.restoreFromBackupJson(database, inputStream)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "database was restored from backup")
|
||||||
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.w(LOG_TAG, "error during restoring database backup", ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun tryCreateDatabaseBackupAsync() {
|
||||||
|
executor.submit { tryCreateDatabaseBackupSync() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tryCreateDatabaseBackupSync() {
|
||||||
|
runBlocking {
|
||||||
|
lock.withLock {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "create backup")
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// create a temp copy of the database
|
||||||
|
databaseBackupFile.delete()
|
||||||
|
databaseFile.source().buffer().readAll(databaseBackupFile.sink(append = false))
|
||||||
|
|
||||||
|
// open the temp copy
|
||||||
|
val database = RoomDatabase.createOrOpenLocalStorageInstance(context, RoomDatabase.BACKUP_DB_NAME)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// open the output file
|
||||||
|
val output = jsonFile.startWrite()
|
||||||
|
|
||||||
|
try {
|
||||||
|
DatabaseBackupLowlevel.outputAsBackupJson(database, output)
|
||||||
|
|
||||||
|
jsonFile.finishWrite(output)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
jsonFile.failWrite(output)
|
||||||
|
|
||||||
|
throw ex
|
||||||
|
}
|
||||||
|
|
||||||
|
null
|
||||||
|
} finally {
|
||||||
|
database.close()
|
||||||
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.w(LOG_TAG, "failed to create backup", ex)
|
||||||
|
}
|
||||||
|
|
||||||
|
null
|
||||||
|
} finally {
|
||||||
|
// delete the temp copy
|
||||||
|
databaseBackupFile.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,179 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.data.backup
|
||||||
|
|
||||||
|
import android.util.JsonReader
|
||||||
|
import android.util.JsonWriter
|
||||||
|
import io.timelimit.android.data.Database
|
||||||
|
import io.timelimit.android.data.JsonSerializable
|
||||||
|
import io.timelimit.android.data.model.*
|
||||||
|
import io.timelimit.android.data.transaction
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.io.OutputStreamWriter
|
||||||
|
|
||||||
|
object DatabaseBackupLowlevel {
|
||||||
|
private const val PAGE_SIZE = 50
|
||||||
|
|
||||||
|
private const val APP = "app"
|
||||||
|
private const val CATEGORY = "category"
|
||||||
|
private const val CATEGORY_APP = "categoryApp"
|
||||||
|
private const val CONFIG = "config"
|
||||||
|
private const val DEVICE = "device"
|
||||||
|
private const val TIME_LIMIT_RULE = "timelimitRule"
|
||||||
|
private const val USED_TIME_ITEM = "usedTime"
|
||||||
|
private const val USER = "user"
|
||||||
|
|
||||||
|
fun outputAsBackupJson(database: Database, outputStream: OutputStream) {
|
||||||
|
val writer = JsonWriter(OutputStreamWriter(outputStream, Charsets.UTF_8))
|
||||||
|
|
||||||
|
writer.beginObject()
|
||||||
|
|
||||||
|
fun <T: JsonSerializable> handleCollection(
|
||||||
|
name: String,
|
||||||
|
readPage: (offset: Int, pageSize: Int) -> List<T>
|
||||||
|
) {
|
||||||
|
writer.name(name).beginArray()
|
||||||
|
|
||||||
|
var offset = 0
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
val page = readPage(offset, PAGE_SIZE)
|
||||||
|
offset += page.size
|
||||||
|
|
||||||
|
if (page.isEmpty()) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
page.forEach { it.serialize(writer) }
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.endArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCollection(APP) {offset, pageSize -> database.app().getAppPageSync(offset, pageSize) }
|
||||||
|
handleCollection(CATEGORY) {offset: Int, pageSize: Int -> database.category().getCategoryPageSync(offset, pageSize) }
|
||||||
|
handleCollection(CATEGORY_APP) { offset, pageSize -> database.categoryApp().getCategoryAppPageSync(offset, pageSize) }
|
||||||
|
|
||||||
|
writer.name(CONFIG).beginArray()
|
||||||
|
database.config().getConfigItemsSync().forEach { it.serialize(writer) }
|
||||||
|
writer.endArray()
|
||||||
|
|
||||||
|
handleCollection(DEVICE) { offset, pageSize -> database.device().getDevicePageSync(offset, pageSize) }
|
||||||
|
handleCollection(TIME_LIMIT_RULE) { offset, pageSize -> database.timeLimitRules().getRulePageSync(offset, pageSize) }
|
||||||
|
handleCollection(USED_TIME_ITEM) { offset, pageSize -> database.usedTimes().getUsedTimePageSync(offset, pageSize) }
|
||||||
|
handleCollection(USER) { offset, pageSize -> database.user().getUserPageSync(offset, pageSize) }
|
||||||
|
|
||||||
|
writer.endObject().flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restoreFromBackupJson(database: Database, inputStream: InputStream) {
|
||||||
|
val reader = JsonReader(InputStreamReader(inputStream, Charsets.UTF_8))
|
||||||
|
|
||||||
|
database.transaction().use {
|
||||||
|
transaction ->
|
||||||
|
|
||||||
|
database.deleteAllData()
|
||||||
|
|
||||||
|
reader.beginObject()
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
when (reader.nextName()) {
|
||||||
|
APP -> {
|
||||||
|
reader.beginArray()
|
||||||
|
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
database.app().addAppSync(App.parse(reader))
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.endArray()
|
||||||
|
}
|
||||||
|
CATEGORY -> {
|
||||||
|
reader.beginArray()
|
||||||
|
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
database.category().addCategory(Category.parse(reader))
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.endArray()
|
||||||
|
}
|
||||||
|
CATEGORY_APP -> {
|
||||||
|
reader.beginArray()
|
||||||
|
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
database.categoryApp().addCategoryAppSync(CategoryApp.parse(reader))
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.endArray()
|
||||||
|
}
|
||||||
|
CONFIG -> {
|
||||||
|
reader.beginArray()
|
||||||
|
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
val item = ConfigurationItem.parse(reader)
|
||||||
|
|
||||||
|
if (item != null) {
|
||||||
|
database.config().updateValueOfKeySync(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.endArray()
|
||||||
|
}
|
||||||
|
DEVICE -> {
|
||||||
|
reader.beginArray()
|
||||||
|
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
database.device().addDeviceSync(Device.parse(reader))
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.endArray()
|
||||||
|
}
|
||||||
|
TIME_LIMIT_RULE -> {
|
||||||
|
reader.beginArray()
|
||||||
|
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
database.timeLimitRules().addTimeLimitRule(TimeLimitRule.parse(reader))
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.endArray()
|
||||||
|
}
|
||||||
|
USED_TIME_ITEM -> {
|
||||||
|
reader.beginArray()
|
||||||
|
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
database.usedTimes().insertUsedTime(UsedTimeItem.parse(reader))
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.endArray()
|
||||||
|
}
|
||||||
|
USER -> {
|
||||||
|
reader.beginArray()
|
||||||
|
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
database.user().addUserSync(User.parse(reader))
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.endArray()
|
||||||
|
}
|
||||||
|
else -> reader.skipValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.endObject()
|
||||||
|
|
||||||
|
transaction.setSuccess()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,115 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.data.customtypes
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.text.TextUtils
|
||||||
|
import androidx.room.TypeConverter
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
|
sealed class Bitmask(private val data: BitSet) {
|
||||||
|
fun read(index: Int): Boolean {
|
||||||
|
return data[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class MutableBitmask (val data: BitSet): Bitmask(data) {
|
||||||
|
fun write(index: Int, value: Boolean) {
|
||||||
|
data[index] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toImmutable(): ImmutableBitmask {
|
||||||
|
return ImmutableBitmask(data.clone() as BitSet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ImmutableBitmask(val dataNotToModify: BitSet): Bitmask(dataNotToModify), Parcelable {
|
||||||
|
fun toMutable(): MutableBitmask {
|
||||||
|
return MutableBitmask(dataNotToModify)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// format: index of start of set bits, index of stop of set bits, ...
|
||||||
|
class ImmutableBitmaskAdapter {
|
||||||
|
@TypeConverter
|
||||||
|
fun toString(mask: ImmutableBitmask) = ImmutableBitmaskJson.serialize(mask)
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun toImmutableBitmask(data: String) = ImmutableBitmaskJson.parse(data, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
object ImmutableBitmaskJson {
|
||||||
|
fun serialize(mask: ImmutableBitmask): String {
|
||||||
|
val output = ArrayList<Int>()
|
||||||
|
|
||||||
|
if (mask.read(0)) {
|
||||||
|
// if first bit is set
|
||||||
|
|
||||||
|
output.add(0)
|
||||||
|
} else {
|
||||||
|
// if first bit is not set
|
||||||
|
|
||||||
|
val start = mask.dataNotToModify.nextSetBit(0)
|
||||||
|
|
||||||
|
if (start == -1) {
|
||||||
|
// nothing is set
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
output.add(start)
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
val startIndex = output.last()
|
||||||
|
val stopIndex = mask.dataNotToModify.nextClearBit(startIndex)
|
||||||
|
|
||||||
|
output.add(stopIndex)
|
||||||
|
|
||||||
|
val nextStartIndex = mask.dataNotToModify.nextSetBit(stopIndex)
|
||||||
|
if (nextStartIndex == -1) {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
output.add(nextStartIndex)
|
||||||
|
}
|
||||||
|
} while (true)
|
||||||
|
|
||||||
|
return TextUtils.join(",", output.map { it.toString() })
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parse(data: String, maxSize: Int?): ImmutableBitmask {
|
||||||
|
val indexes = data.split(",").filter{ it.isNotBlank() }.map { it.toInt() }
|
||||||
|
val iterator = indexes.iterator()
|
||||||
|
val output = BitSet()
|
||||||
|
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
val start = iterator.next()
|
||||||
|
val end = iterator.next()
|
||||||
|
|
||||||
|
if (maxSize != null) {
|
||||||
|
if (start > maxSize || end > maxSize) {
|
||||||
|
throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output.set(start, end)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ImmutableBitmask(output)
|
||||||
|
}
|
||||||
|
}
|
49
app/src/main/java/io/timelimit/android/data/dao/AppDao.kt
Normal file
49
app/src/main/java/io/timelimit/android/data/dao/AppDao.kt
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.data.dao
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.room.*
|
||||||
|
import io.timelimit.android.data.model.App
|
||||||
|
import io.timelimit.android.data.model.AppRecommendation
|
||||||
|
import io.timelimit.android.data.model.AppRecommendationConverter
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
@TypeConverters(
|
||||||
|
AppRecommendationConverter::class
|
||||||
|
)
|
||||||
|
interface AppDao {
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun addAppsSync(apps: Collection<App>)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun addAppSync(app: App)
|
||||||
|
|
||||||
|
@Query("DELETE FROM app WHERE package_name IN (:packageNames)")
|
||||||
|
fun removeAppsByPackageNamesSync(packageNames: List<String>)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM app")
|
||||||
|
fun getApps(): LiveData<List<App>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM app WHERE package_name = :packageName")
|
||||||
|
fun getAppsByPackageName(packageName: String): LiveData<List<App>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM app LIMIT :pageSize OFFSET :offset")
|
||||||
|
fun getAppPageSync(offset: Int, pageSize: Int): List<App>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM app WHERE recommendation = :recommendation")
|
||||||
|
fun getAppsByRecommendationLive(recommendation: AppRecommendation): LiveData<List<App>>
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.data.dao
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.Query
|
||||||
|
import io.timelimit.android.data.model.CategoryApp
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
abstract class CategoryAppDao {
|
||||||
|
@Query("SELECT * FROM category_app WHERE category_id IN (:categoryIds) AND package_name = :packageName")
|
||||||
|
abstract fun getCategoryApp(categoryIds: List<String>, packageName: String): LiveData<CategoryApp?>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM category_app WHERE category_id = :categoryId")
|
||||||
|
abstract fun getCategoryApps(categoryId: String): LiveData<List<CategoryApp>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM category_app WHERE category_id IN (:categoryIds)")
|
||||||
|
abstract fun getCategoryApps(categoryIds: List<String>): LiveData<List<CategoryApp>>
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
abstract fun addCategoryAppsSync(items: Collection<CategoryApp>)
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
abstract fun addCategoryAppSync(item: CategoryApp)
|
||||||
|
|
||||||
|
@Query("DELETE FROM category_app WHERE category_id IN (:categoryIds) AND package_name IN (:packageNames)")
|
||||||
|
abstract fun removeCategoryAppsSyncByCategoryIds(packageNames: List<String>, categoryIds: List<String>)
|
||||||
|
|
||||||
|
@Query("DELETE FROM category_app WHERE category_id = :categoryId")
|
||||||
|
abstract fun deleteCategoryAppsByCategoryId(categoryId: String)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM category_app LIMIT :pageSize OFFSET :offset")
|
||||||
|
abstract fun getCategoryAppPageSync(offset: Int, pageSize: Int): List<CategoryApp>
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.data.dao
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.room.*
|
||||||
|
import io.timelimit.android.data.customtypes.ImmutableBitmask
|
||||||
|
import io.timelimit.android.data.customtypes.ImmutableBitmaskAdapter
|
||||||
|
import io.timelimit.android.data.model.Category
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
abstract class CategoryDao {
|
||||||
|
@Query("SELECT * FROM category WHERE child_id = :childId")
|
||||||
|
abstract fun getCategoriesByChildId(childId: String): LiveData<List<Category>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM category WHERE child_id = :childId AND id = :categoryId")
|
||||||
|
abstract fun getCategoryByChildIdAndId(childId: String, categoryId: String): LiveData<Category?>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM category WHERE id = :categoryId")
|
||||||
|
abstract fun getCategoryByIdSync(categoryId: String): Category?
|
||||||
|
|
||||||
|
@Query("SELECT * FROM category WHERE child_id = :childId")
|
||||||
|
abstract fun getCategoriesByChildIdSync(childId: String): List<Category>
|
||||||
|
|
||||||
|
@Query("DELETE FROM category WHERE id = :categoryId")
|
||||||
|
abstract fun deleteCategory(categoryId: String)
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
abstract fun addCategory(category: Category)
|
||||||
|
|
||||||
|
@Query("UPDATE category SET title = :newTitle WHERE id = :categoryId")
|
||||||
|
abstract fun updateCategoryTitle(categoryId: String, newTitle: String)
|
||||||
|
|
||||||
|
@Query("UPDATE category SET extra_time = :newExtraTime WHERE id = :categoryId")
|
||||||
|
abstract fun updateCategoryExtraTime(categoryId: String, newExtraTime: Long)
|
||||||
|
|
||||||
|
@Query("UPDATE category SET extra_time = extra_time + :addedExtraTime WHERE id = :categoryId")
|
||||||
|
abstract fun incrementCategoryExtraTime(categoryId: String, addedExtraTime: Long)
|
||||||
|
|
||||||
|
@Query("UPDATE category SET extra_time = MAX(0, extra_time - :removedExtraTime) WHERE id = :categoryId")
|
||||||
|
abstract fun subtractCategoryExtraTime(categoryId: String, removedExtraTime: Int)
|
||||||
|
|
||||||
|
@TypeConverters(ImmutableBitmaskAdapter::class)
|
||||||
|
@Query("UPDATE category SET blocked_times = :blockedMinutesInWeek WHERE id = :categoryId")
|
||||||
|
abstract fun updateCategoryBlockedTimes(categoryId: String, blockedMinutesInWeek: ImmutableBitmask)
|
||||||
|
|
||||||
|
@Query("UPDATE category SET temporarily_blocked = :blocked WHERE id = :categoryId")
|
||||||
|
abstract fun updateCategoryTemporarilyBlocked(categoryId: String, blocked: Boolean)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM category LIMIT :pageSize OFFSET :offset")
|
||||||
|
abstract fun getCategoryPageSync(offset: Int, pageSize: Int): List<Category>
|
||||||
|
|
||||||
|
@Query("SELECT id, child_id, temporarily_blocked FROM category")
|
||||||
|
abstract fun getAllCategoriesShortInfo(): LiveData<List<CategoryShortInfo>>
|
||||||
|
}
|
||||||
|
|
||||||
|
data class CategoryShortInfo(
|
||||||
|
@ColumnInfo(name = "child_id")
|
||||||
|
val childId: String,
|
||||||
|
@ColumnInfo(name = "id")
|
||||||
|
val categoryId: String,
|
||||||
|
@ColumnInfo(name = "temporarily_blocked")
|
||||||
|
val temporarilyBlocked: Boolean
|
||||||
|
)
|
115
app/src/main/java/io/timelimit/android/data/dao/ConfigDao.kt
Normal file
115
app/src/main/java/io/timelimit/android/data/dao/ConfigDao.kt
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.data.dao
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.Transformations
|
||||||
|
import androidx.room.*
|
||||||
|
import io.timelimit.android.data.model.ConfigurationItem
|
||||||
|
import io.timelimit.android.data.model.ConfigurationItemType
|
||||||
|
import io.timelimit.android.data.model.ConfigurationItemTypeConverter
|
||||||
|
import io.timelimit.android.data.model.ConfigurationItemTypeUtil
|
||||||
|
import io.timelimit.android.livedata.ignoreUnchanged
|
||||||
|
import io.timelimit.android.livedata.map
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
@TypeConverters(ConfigurationItemTypeConverter::class)
|
||||||
|
abstract class ConfigDao {
|
||||||
|
@Query("SELECT * FROM config WHERE id IN (:keys)")
|
||||||
|
protected abstract fun getConfigItemsSync(keys: List<Int>): List<ConfigurationItem>
|
||||||
|
|
||||||
|
fun getConfigItemsSync() = getConfigItemsSync(ConfigurationItemTypeUtil.TYPES.map { ConfigurationItemTypeUtil.serialize(it) })
|
||||||
|
|
||||||
|
@Query("SELECT * FROM config WHERE id = :key")
|
||||||
|
protected abstract fun getRowByKeyAsync(key: ConfigurationItemType): LiveData<ConfigurationItem?>
|
||||||
|
|
||||||
|
private fun getValueOfKeyAsync(key: ConfigurationItemType): LiveData<String?> {
|
||||||
|
return Transformations.map(getRowByKeyAsync(key)) { it?.value }.ignoreUnchanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query("SELECT * FROM config WHERE id = :key")
|
||||||
|
protected abstract fun getRowByKeySync(key: ConfigurationItemType): ConfigurationItem?
|
||||||
|
|
||||||
|
private fun getValueOfKeySync(key: ConfigurationItemType): String? {
|
||||||
|
return getRowByKeySync(key)?.value
|
||||||
|
}
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
abstract fun updateValueOfKeySync(item: ConfigurationItem)
|
||||||
|
|
||||||
|
@Query("DELETE FROM config WHERE id = :key")
|
||||||
|
protected abstract fun removeConfigItemSync(key: ConfigurationItemType)
|
||||||
|
|
||||||
|
private fun updateValueSync(key: ConfigurationItemType, value: String?) {
|
||||||
|
if (value != null) {
|
||||||
|
updateValueOfKeySync(ConfigurationItem(key, value))
|
||||||
|
} else {
|
||||||
|
removeConfigItemSync(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getOwnDeviceId(): LiveData<String?> {
|
||||||
|
return getValueOfKeyAsync(ConfigurationItemType.OwnDeviceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getOwnDeviceIdSync(): String? {
|
||||||
|
return getValueOfKeySync(ConfigurationItemType.OwnDeviceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setOwnDeviceIdSync(deviceId: String) {
|
||||||
|
updateValueSync(ConfigurationItemType.OwnDeviceId, deviceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getShownHintsLive(): LiveData<Long> {
|
||||||
|
return getValueOfKeyAsync(ConfigurationItemType.ShownHints).map {
|
||||||
|
if (it == null) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
it.toLong(16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getShownHintsSync(): Long {
|
||||||
|
val v = getValueOfKeySync(ConfigurationItemType.ShownHints)
|
||||||
|
|
||||||
|
if (v == null) {
|
||||||
|
return 0
|
||||||
|
} else {
|
||||||
|
return v.toLong(16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun wereHintsShown(flags: Long) = getShownHintsLive().map {
|
||||||
|
(it and flags) == flags
|
||||||
|
}.ignoreUnchanged()
|
||||||
|
|
||||||
|
fun wereAnyHintsShown() = getShownHintsLive().map { it != 0L }.ignoreUnchanged()
|
||||||
|
|
||||||
|
fun setHintsShownSync(flags: Long) {
|
||||||
|
updateValueSync(
|
||||||
|
ConfigurationItemType.ShownHints,
|
||||||
|
(getShownHintsSync() or flags).toString(16)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetShownHintsSync() {
|
||||||
|
updateValueSync(ConfigurationItemType.ShownHints, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun wasDeviceLockedSync() = getValueOfKeySync(ConfigurationItemType.WasDeviceLocked) == "true"
|
||||||
|
fun setWasDeviceLockedSync(value: Boolean) = updateValueSync(ConfigurationItemType.WasDeviceLocked, if (value) "true" else "false")
|
||||||
|
}
|
64
app/src/main/java/io/timelimit/android/data/dao/DeviceDao.kt
Normal file
64
app/src/main/java/io/timelimit/android/data/dao/DeviceDao.kt
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.data.dao
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.room.*
|
||||||
|
import io.timelimit.android.data.model.Device
|
||||||
|
import io.timelimit.android.integration.platform.NewPermissionStatusConverter
|
||||||
|
import io.timelimit.android.integration.platform.ProtectionLevelConverter
|
||||||
|
import io.timelimit.android.integration.platform.RuntimePermissionStatusConverter
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
@TypeConverters(
|
||||||
|
ProtectionLevelConverter::class,
|
||||||
|
RuntimePermissionStatusConverter::class,
|
||||||
|
NewPermissionStatusConverter::class
|
||||||
|
)
|
||||||
|
abstract class DeviceDao {
|
||||||
|
@Query("SELECT * FROM device WHERE id = :deviceId")
|
||||||
|
abstract fun getDeviceById(deviceId: String): LiveData<Device?>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM device WHERE id = :deviceId")
|
||||||
|
abstract fun getDeviceByIdSync(deviceId: String): Device?
|
||||||
|
|
||||||
|
@Query("SELECT * FROM device")
|
||||||
|
abstract fun getAllDevicesLive(): LiveData<List<Device>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM device")
|
||||||
|
abstract fun getAllDevicesSync(): List<Device>
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
abstract fun addDeviceSync(device: Device)
|
||||||
|
|
||||||
|
@Query("UPDATE device SET current_user_id = :userId WHERE id = :deviceId")
|
||||||
|
abstract fun updateDeviceUser(deviceId: String, userId: String)
|
||||||
|
|
||||||
|
@Update
|
||||||
|
abstract fun updateDeviceEntry(device: Device)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM device WHERE current_user_id = :userId")
|
||||||
|
abstract fun getDevicesByUserId(userId: String): LiveData<List<Device>>
|
||||||
|
|
||||||
|
@Query("UPDATE device SET name = :name WHERE id = :deviceId")
|
||||||
|
abstract fun updateDeviceName(deviceId: String, name: String): Int
|
||||||
|
|
||||||
|
@Query("SELECT * FROM device LIMIT :pageSize OFFSET :offset")
|
||||||
|
abstract fun getDevicePageSync(offset: Int, pageSize: Int): List<Device>
|
||||||
|
|
||||||
|
@Query("UPDATE device SET current_user_id = \"\" WHERE current_user_id = :userId")
|
||||||
|
abstract fun unassignCurrentUserFromAllDevices(userId: String)
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.data.dao
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.Transformations
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.Query
|
||||||
|
import io.timelimit.android.data.model.TemporarilyAllowedApp
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
abstract class TemporarilyAllowedAppDao {
|
||||||
|
@Query("SELECT * FROM temporarily_allowed_app")
|
||||||
|
abstract fun getTemporarilyAllowedAppsInternal(): LiveData<List<TemporarilyAllowedApp>>
|
||||||
|
|
||||||
|
fun getTemporarilyAllowedApps(): LiveData<List<String>> {
|
||||||
|
return Transformations.map(getTemporarilyAllowedAppsInternal()) { it.map { it.packageName } }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
abstract fun addTemporarilyAllowedAppSync(app: TemporarilyAllowedApp)
|
||||||
|
|
||||||
|
@Query("DELETE FROM temporarily_allowed_app")
|
||||||
|
abstract fun removeAllTemporarilyAllowedAppsSync()
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.data.dao
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Update
|
||||||
|
import io.timelimit.android.data.model.TimeLimitRule
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
abstract class TimeLimitRuleDao {
|
||||||
|
@Query("SELECT * FROM time_limit_rule WHERE category_id = :categoryId")
|
||||||
|
abstract fun getTimeLimitRulesByCategory(categoryId: String): LiveData<List<TimeLimitRule>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM time_limit_rule WHERE category_id IN (:categoryIds)")
|
||||||
|
abstract fun getTimeLimitRulesByCategories(categoryIds: List<String>): LiveData<List<TimeLimitRule>>
|
||||||
|
|
||||||
|
@Query("DELETE FROM time_limit_rule WHERE category_id = :categoryId")
|
||||||
|
abstract fun deleteTimeLimitRulesByCategory(categoryId: String)
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
abstract fun addTimeLimitRule(rule: TimeLimitRule)
|
||||||
|
|
||||||
|
@Update
|
||||||
|
abstract fun updateTimeLimitRule(rule: TimeLimitRule)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM time_limit_rule WHERE id = :timeLimitRuleId")
|
||||||
|
abstract fun getTimeLimitRuleByIdSync(timeLimitRuleId: String): TimeLimitRule?
|
||||||
|
|
||||||
|
@Query("SELECT * FROM time_limit_rule WHERE id = :timeLimitRuleId")
|
||||||
|
abstract fun getTimeLimitRuleByIdLive(timeLimitRuleId: String): LiveData<TimeLimitRule?>
|
||||||
|
|
||||||
|
@Query("DELETE FROM time_limit_rule WHERE id = :timeLimitRuleId")
|
||||||
|
abstract fun deleteTimeLimitRuleByIdSync(timeLimitRuleId: String)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM time_limit_rule LIMIT :pageSize OFFSET :offset")
|
||||||
|
abstract fun getRulePageSync(offset: Int, pageSize: Int): List<TimeLimitRule>
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.data.dao
|
||||||
|
|
||||||
|
import android.util.SparseArray
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.Transformations
|
||||||
|
import androidx.paging.DataSource
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.Query
|
||||||
|
import io.timelimit.android.data.model.UsedTimeItem
|
||||||
|
import io.timelimit.android.livedata.ignoreUnchanged
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
abstract class UsedTimeDao {
|
||||||
|
@Query("SELECT * FROM used_time WHERE category_id = :categoryId AND day_of_epoch >= :startingDayOfEpoch AND day_of_epoch <= :endDayOfEpoch")
|
||||||
|
protected abstract fun getUsedTimesOfWeekInternal(categoryId: String, startingDayOfEpoch: Int, endDayOfEpoch: Int): LiveData<List<UsedTimeItem>>
|
||||||
|
|
||||||
|
fun getUsedTimesOfWeek(categoryId: String, firstDayOfWeekAsEpochDay: Int): LiveData<SparseArray<UsedTimeItem>> {
|
||||||
|
return Transformations.map(getUsedTimesOfWeekInternal(categoryId, firstDayOfWeekAsEpochDay, firstDayOfWeekAsEpochDay + 6).ignoreUnchanged()) {
|
||||||
|
val result = SparseArray<UsedTimeItem>()
|
||||||
|
|
||||||
|
it.forEach {
|
||||||
|
result.put(it.dayOfEpoch - firstDayOfWeekAsEpochDay, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
abstract fun insertUsedTime(item: UsedTimeItem)
|
||||||
|
|
||||||
|
@Query("UPDATE used_time SET used_time = used_time + :timeToAdd WHERE category_id = :categoryId AND day_of_epoch = :dayOfEpoch")
|
||||||
|
abstract fun addUsedTime(categoryId: String, dayOfEpoch: Int, timeToAdd: Int): Int
|
||||||
|
|
||||||
|
@Query("SELECT * FROM used_time WHERE category_id = :categoryId AND day_of_epoch = :dayOfEpoch")
|
||||||
|
abstract fun getUsedTimeItem(categoryId: String, dayOfEpoch: Int): LiveData<UsedTimeItem?>
|
||||||
|
|
||||||
|
@Query("DELETE FROM used_time WHERE category_id = :categoryId")
|
||||||
|
abstract fun deleteUsedTimeItems(categoryId: String)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM used_time LIMIT :pageSize OFFSET :offset")
|
||||||
|
abstract fun getUsedTimePageSync(offset: Int, pageSize: Int): List<UsedTimeItem>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM used_time WHERE category_id = :categoryId ORDER BY day_of_epoch DESC")
|
||||||
|
abstract fun getUsedTimesByCategoryId(categoryId: String): DataSource.Factory<Int, UsedTimeItem>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM used_time WHERE category_id IN (:categoryIds) AND day_of_epoch >= :startingDayOfEpoch AND day_of_epoch <= :endDayOfEpoch")
|
||||||
|
abstract fun getUsedTimesByDayAndCategoryIds(categoryIds: List<String>, startingDayOfEpoch: Int, endDayOfEpoch: Int): LiveData<List<UsedTimeItem>>
|
||||||
|
}
|
62
app/src/main/java/io/timelimit/android/data/dao/UserDao.kt
Normal file
62
app/src/main/java/io/timelimit/android/data/dao/UserDao.kt
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.data.dao
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Update
|
||||||
|
import io.timelimit.android.data.model.User
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
abstract class UserDao {
|
||||||
|
@Query("SELECT * from user WHERE id = :userId")
|
||||||
|
abstract fun getUserByIdLive(userId: String): LiveData<User?>
|
||||||
|
|
||||||
|
@Query("SELECT * from user WHERE id = :userId AND type = \"child\"")
|
||||||
|
abstract fun getChildUserByIdLive(userId: String): LiveData<User?>
|
||||||
|
|
||||||
|
@Query("SELECT * from user WHERE id = :userId AND type = \"parent\"")
|
||||||
|
abstract fun getParentUserByIdLive(userId: String): LiveData<User?>
|
||||||
|
|
||||||
|
@Query("SELECT * from user WHERE id = :userId")
|
||||||
|
abstract fun getUserByIdSync(userId: String): User?
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
abstract fun addUserSync(user: User)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM user")
|
||||||
|
abstract fun getAllUsersLive(): LiveData<List<User>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM user WHERE type = \"parent\"")
|
||||||
|
abstract fun getParentUsersLive(): LiveData<List<User>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM user WHERE type = \"parent\"")
|
||||||
|
abstract fun getParentUsersSync(): List<User>
|
||||||
|
|
||||||
|
@Query("DELETE FROM user WHERE id IN (:userIds)")
|
||||||
|
abstract fun deleteUsersByIds(userIds: List<String>)
|
||||||
|
|
||||||
|
@Update
|
||||||
|
abstract fun updateUserSync(user: User)
|
||||||
|
|
||||||
|
@Query("UPDATE user SET disable_limits_until = :timestamp WHERE id = :childId AND type = \"child\"")
|
||||||
|
abstract fun updateDisableChildUserLimitsUntil(childId: String, timestamp: Long): Int
|
||||||
|
|
||||||
|
@Query("SELECT * FROM user LIMIT :pageSize OFFSET :offset")
|
||||||
|
abstract fun getUserPageSync(offset: Int, pageSize: Int): List<User>
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.data.extensions
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import io.timelimit.android.data.model.User
|
||||||
|
import io.timelimit.android.date.DateInTimezone
|
||||||
|
import io.timelimit.android.integration.time.TimeApi
|
||||||
|
import io.timelimit.android.livedata.ignoreUnchanged
|
||||||
|
import io.timelimit.android.livedata.liveDataFromFunction
|
||||||
|
import io.timelimit.android.livedata.switchMap
|
||||||
|
|
||||||
|
fun LiveData<User?>.getDateLive(timeApi: TimeApi) = this.mapToTimezone().switchMap {
|
||||||
|
timeZone ->
|
||||||
|
|
||||||
|
liveDataFromFunction {
|
||||||
|
DateInTimezone.newInstance(timeApi.getCurrentTimeInMillis(), timeZone)
|
||||||
|
}
|
||||||
|
}.ignoreUnchanged()
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.data.extensions
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import io.timelimit.android.data.model.User
|
||||||
|
import io.timelimit.android.livedata.ignoreUnchanged
|
||||||
|
import io.timelimit.android.livedata.map
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
fun User?.getTimezone(): TimeZone {
|
||||||
|
return if (this != null) {
|
||||||
|
TimeZone.getTimeZone(this.timeZone)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
} ?: TimeZone.getDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun LiveData<User?>.mapToTimezone() = this.map { it.getTimezone() }.ignoreUnchanged()
|
108
app/src/main/java/io/timelimit/android/data/model/App.kt
Normal file
108
app/src/main/java/io/timelimit/android/data/model/App.kt
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.data.model
|
||||||
|
|
||||||
|
import android.util.JsonReader
|
||||||
|
import android.util.JsonWriter
|
||||||
|
import androidx.room.*
|
||||||
|
import io.timelimit.android.data.JsonSerializable
|
||||||
|
|
||||||
|
@Entity(tableName = "app")
|
||||||
|
@TypeConverters(AppRecommendationConverter::class)
|
||||||
|
data class App (
|
||||||
|
@PrimaryKey
|
||||||
|
@ColumnInfo(index = true, name = "package_name")
|
||||||
|
val packageName: String,
|
||||||
|
@ColumnInfo(name = "title")
|
||||||
|
val title: String,
|
||||||
|
@ColumnInfo(name = "launchable")
|
||||||
|
val isLaunchable: Boolean,
|
||||||
|
@ColumnInfo(name = "recommendation")
|
||||||
|
val recommendation: AppRecommendation
|
||||||
|
): JsonSerializable {
|
||||||
|
companion object {
|
||||||
|
private const val PACKAGE_NAME = "packageName"
|
||||||
|
private const val TITLE = "title"
|
||||||
|
private const val IS_LAUNCHABLE = "isLaunchable"
|
||||||
|
private const val RECOMMENDATION = "recommendation"
|
||||||
|
|
||||||
|
fun parse(reader: JsonReader): App {
|
||||||
|
var packageName: String? = null
|
||||||
|
var title: String? = null
|
||||||
|
var isLaunchable: Boolean? = null
|
||||||
|
var recommendation: AppRecommendation? = null
|
||||||
|
|
||||||
|
reader.beginObject()
|
||||||
|
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
when (reader.nextName()) {
|
||||||
|
PACKAGE_NAME -> packageName = reader.nextString()
|
||||||
|
TITLE -> title = reader.nextString()
|
||||||
|
IS_LAUNCHABLE -> isLaunchable = reader.nextBoolean()
|
||||||
|
RECOMMENDATION -> recommendation = AppRecommendationJson.parse(reader.nextString())
|
||||||
|
else -> reader.skipValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.endObject()
|
||||||
|
|
||||||
|
return App(
|
||||||
|
packageName = packageName!!,
|
||||||
|
title = title!!,
|
||||||
|
isLaunchable = isLaunchable!!,
|
||||||
|
recommendation = recommendation!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serialize(writer: JsonWriter) {
|
||||||
|
writer.beginObject()
|
||||||
|
|
||||||
|
writer.name(PACKAGE_NAME).value(packageName)
|
||||||
|
writer.name(TITLE).value(title)
|
||||||
|
writer.name(IS_LAUNCHABLE).value(isLaunchable)
|
||||||
|
writer.name(RECOMMENDATION).value(AppRecommendationJson.serialize(recommendation))
|
||||||
|
|
||||||
|
writer.endObject()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class AppRecommendation {
|
||||||
|
None, Whitelist, Blacklist
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppRecommendationConverter {
|
||||||
|
@TypeConverter
|
||||||
|
fun toAppRecommendation(value: String) = AppRecommendationJson.parse(value)
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun toString(value: AppRecommendation) = AppRecommendationJson.serialize(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
object AppRecommendationJson {
|
||||||
|
fun parse(value: String) = when(value) {
|
||||||
|
"whitelist" -> AppRecommendation.Whitelist
|
||||||
|
"blacklist" -> AppRecommendation.Blacklist
|
||||||
|
"none" -> AppRecommendation.None
|
||||||
|
else -> throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun serialize(value: AppRecommendation) = when(value) {
|
||||||
|
AppRecommendation.None -> "none"
|
||||||
|
AppRecommendation.Blacklist -> "blacklist"
|
||||||
|
AppRecommendation.Whitelist -> "whitelist"
|
||||||
|
}
|
||||||
|
}
|
118
app/src/main/java/io/timelimit/android/data/model/Category.kt
Normal file
118
app/src/main/java/io/timelimit/android/data/model/Category.kt
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.data.model
|
||||||
|
|
||||||
|
import android.util.JsonReader
|
||||||
|
import android.util.JsonWriter
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import androidx.room.TypeConverters
|
||||||
|
import io.timelimit.android.data.IdGenerator
|
||||||
|
import io.timelimit.android.data.JsonSerializable
|
||||||
|
import io.timelimit.android.data.customtypes.ImmutableBitmask
|
||||||
|
import io.timelimit.android.data.customtypes.ImmutableBitmaskAdapter
|
||||||
|
import io.timelimit.android.data.customtypes.ImmutableBitmaskJson
|
||||||
|
|
||||||
|
@Entity(tableName = "category")
|
||||||
|
@TypeConverters(ImmutableBitmaskAdapter::class)
|
||||||
|
data class Category(
|
||||||
|
@PrimaryKey
|
||||||
|
@ColumnInfo(name = "id")
|
||||||
|
val id: String,
|
||||||
|
@ColumnInfo(name = "child_id")
|
||||||
|
val childId: String,
|
||||||
|
@ColumnInfo(name = "title")
|
||||||
|
val title: String,
|
||||||
|
@ColumnInfo(name = "blocked_times")
|
||||||
|
val blockedMinutesInWeek: ImmutableBitmask, // 10080 bit -> ~10 KB
|
||||||
|
@ColumnInfo(name = "extra_time")
|
||||||
|
val extraTimeInMillis: Long,
|
||||||
|
@ColumnInfo(name = "temporarily_blocked")
|
||||||
|
val temporarilyBlocked: Boolean
|
||||||
|
): JsonSerializable {
|
||||||
|
companion object {
|
||||||
|
const val MINUTES_PER_DAY = 60 * 24
|
||||||
|
const val BLOCKED_MINUTES_IN_WEEK_LENGTH = MINUTES_PER_DAY * 7
|
||||||
|
|
||||||
|
private const val ID = "id"
|
||||||
|
private const val CHILD_ID = "childId"
|
||||||
|
private const val TITLE = "title"
|
||||||
|
private const val BLOCKED_MINUTES_IN_WEEK = "blockedMinutesInWeek"
|
||||||
|
private const val EXTRA_TIME_IN_MILLIS = "extraTimeInMillis"
|
||||||
|
private const val TEMPORARILY_BLOCKED = "temporarilyBlocked"
|
||||||
|
|
||||||
|
fun parse(reader: JsonReader): Category {
|
||||||
|
var id: String? = null
|
||||||
|
var childId: String? = null
|
||||||
|
var title: String? = null
|
||||||
|
var blockedMinutesInWeek: ImmutableBitmask? = null
|
||||||
|
var extraTimeInMillis: Long? = null
|
||||||
|
var temporarilyBlocked: Boolean? = null
|
||||||
|
|
||||||
|
reader.beginObject()
|
||||||
|
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
when (reader.nextName()) {
|
||||||
|
ID -> id = reader.nextString()
|
||||||
|
CHILD_ID -> childId = reader.nextString()
|
||||||
|
TITLE -> title = reader.nextString()
|
||||||
|
BLOCKED_MINUTES_IN_WEEK -> blockedMinutesInWeek = ImmutableBitmaskJson.parse(reader.nextString(), BLOCKED_MINUTES_IN_WEEK_LENGTH)
|
||||||
|
EXTRA_TIME_IN_MILLIS -> extraTimeInMillis = reader.nextLong()
|
||||||
|
TEMPORARILY_BLOCKED -> temporarilyBlocked = reader.nextBoolean()
|
||||||
|
else -> reader.skipValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.endObject()
|
||||||
|
|
||||||
|
return Category(
|
||||||
|
id = id!!,
|
||||||
|
childId = childId!!,
|
||||||
|
title = title!!,
|
||||||
|
blockedMinutesInWeek = blockedMinutesInWeek!!,
|
||||||
|
extraTimeInMillis = extraTimeInMillis!!,
|
||||||
|
temporarilyBlocked = temporarilyBlocked!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(id)
|
||||||
|
IdGenerator.assertIdValid(childId)
|
||||||
|
|
||||||
|
if (extraTimeInMillis < 0) {
|
||||||
|
throw IllegalStateException()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (title.isEmpty()) {
|
||||||
|
throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serialize(writer: JsonWriter) {
|
||||||
|
writer.beginObject()
|
||||||
|
|
||||||
|
writer.name(ID).value(id)
|
||||||
|
writer.name(CHILD_ID).value(childId)
|
||||||
|
writer.name(TITLE).value(title)
|
||||||
|
writer.name(BLOCKED_MINUTES_IN_WEEK).value(ImmutableBitmaskJson.serialize(blockedMinutesInWeek))
|
||||||
|
writer.name(EXTRA_TIME_IN_MILLIS).value(extraTimeInMillis)
|
||||||
|
writer.name(TEMPORARILY_BLOCKED).value(temporarilyBlocked)
|
||||||
|
|
||||||
|
writer.endObject()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.data.model
|
||||||
|
|
||||||
|
import android.util.JsonReader
|
||||||
|
import android.util.JsonWriter
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import io.timelimit.android.data.IdGenerator
|
||||||
|
import io.timelimit.android.data.JsonSerializable
|
||||||
|
|
||||||
|
@Entity(primaryKeys = ["category_id", "package_name"], tableName = "category_app")
|
||||||
|
data class CategoryApp(
|
||||||
|
@ColumnInfo(index = true, name = "category_id")
|
||||||
|
val categoryId: String,
|
||||||
|
@ColumnInfo(index = true, name = "package_name")
|
||||||
|
val packageName: String
|
||||||
|
): JsonSerializable {
|
||||||
|
companion object {
|
||||||
|
private const val CATEGORY_ID = "categoryId"
|
||||||
|
private const val PACKAGE_NAME = "packageName"
|
||||||
|
|
||||||
|
fun parse(reader: JsonReader): CategoryApp {
|
||||||
|
var categoryId: String? = null
|
||||||
|
var packageName: String? = null
|
||||||
|
|
||||||
|
reader.beginObject()
|
||||||
|
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
when (reader.nextName()) {
|
||||||
|
CATEGORY_ID -> categoryId = reader.nextString()
|
||||||
|
PACKAGE_NAME -> packageName = reader.nextString()
|
||||||
|
else -> reader.skipValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.endObject()
|
||||||
|
|
||||||
|
return CategoryApp(
|
||||||
|
categoryId = categoryId!!,
|
||||||
|
packageName = packageName!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(categoryId)
|
||||||
|
|
||||||
|
if (packageName.isEmpty()) {
|
||||||
|
throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serialize(writer: JsonWriter) {
|
||||||
|
writer.beginObject()
|
||||||
|
|
||||||
|
writer.name(CATEGORY_ID).value(categoryId)
|
||||||
|
writer.name(PACKAGE_NAME).value(packageName)
|
||||||
|
|
||||||
|
writer.endObject()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,120 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.data.model
|
||||||
|
|
||||||
|
import android.util.JsonReader
|
||||||
|
import android.util.JsonWriter
|
||||||
|
import androidx.room.*
|
||||||
|
import io.timelimit.android.data.JsonSerializable
|
||||||
|
|
||||||
|
@Entity(tableName = "config")
|
||||||
|
@TypeConverters(ConfigurationItemTypeConverter::class)
|
||||||
|
data class ConfigurationItem(
|
||||||
|
@PrimaryKey
|
||||||
|
@ColumnInfo(name = "id")
|
||||||
|
val key: ConfigurationItemType,
|
||||||
|
@ColumnInfo(name = "value")
|
||||||
|
val value: String
|
||||||
|
): JsonSerializable {
|
||||||
|
companion object {
|
||||||
|
private const val KEY = "key"
|
||||||
|
private const val VALUE = "value"
|
||||||
|
|
||||||
|
// returns null if parsing failed
|
||||||
|
fun parse(reader: JsonReader): ConfigurationItem? {
|
||||||
|
var key: Int? = null
|
||||||
|
var value: String? = null
|
||||||
|
|
||||||
|
reader.beginObject()
|
||||||
|
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
when (reader.nextName()) {
|
||||||
|
KEY -> key = reader.nextInt()
|
||||||
|
VALUE -> value = reader.nextString()
|
||||||
|
else -> reader.skipValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.endObject()
|
||||||
|
|
||||||
|
key!!
|
||||||
|
value!!
|
||||||
|
|
||||||
|
try {
|
||||||
|
return ConfigurationItem(
|
||||||
|
key = ConfigurationItemTypeUtil.parse(key),
|
||||||
|
value = value
|
||||||
|
)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serialize(writer: JsonWriter) {
|
||||||
|
writer.beginObject()
|
||||||
|
|
||||||
|
writer.name(KEY).value(ConfigurationItemTypeUtil.serialize(key))
|
||||||
|
writer.name(VALUE).value(value)
|
||||||
|
|
||||||
|
writer.endObject()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ConfigurationItemType {
|
||||||
|
OwnDeviceId,
|
||||||
|
ShownHints,
|
||||||
|
WasDeviceLocked
|
||||||
|
}
|
||||||
|
|
||||||
|
object ConfigurationItemTypeUtil {
|
||||||
|
private const val OWN_DEVICE_ID = 1
|
||||||
|
private const val SHOWN_HINTS = 2
|
||||||
|
private const val WAS_DEVICE_LOCKED = 3
|
||||||
|
|
||||||
|
val TYPES = listOf(
|
||||||
|
ConfigurationItemType.OwnDeviceId,
|
||||||
|
ConfigurationItemType.ShownHints,
|
||||||
|
ConfigurationItemType.WasDeviceLocked
|
||||||
|
)
|
||||||
|
|
||||||
|
fun serialize(value: ConfigurationItemType) = when(value) {
|
||||||
|
ConfigurationItemType.OwnDeviceId -> OWN_DEVICE_ID
|
||||||
|
ConfigurationItemType.ShownHints -> SHOWN_HINTS
|
||||||
|
ConfigurationItemType.WasDeviceLocked -> WAS_DEVICE_LOCKED
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parse(value: Int) = when(value) {
|
||||||
|
OWN_DEVICE_ID -> ConfigurationItemType.OwnDeviceId
|
||||||
|
SHOWN_HINTS -> ConfigurationItemType.ShownHints
|
||||||
|
WAS_DEVICE_LOCKED -> ConfigurationItemType.WasDeviceLocked
|
||||||
|
else -> throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConfigurationItemTypeConverter {
|
||||||
|
@TypeConverter
|
||||||
|
fun toInt(value: ConfigurationItemType) = ConfigurationItemTypeUtil.serialize(value)
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun toConfigurationItemType(value: Int) = ConfigurationItemTypeUtil.parse(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
object HintsToShow {
|
||||||
|
const val OVERVIEW_INTRODUCTION = 1L
|
||||||
|
const val DEVICE_SCREEN_INTRODUCTION = 2L
|
||||||
|
const val CATEGORIES_INTRODUCTION = 4L
|
||||||
|
}
|
210
app/src/main/java/io/timelimit/android/data/model/Device.kt
Normal file
210
app/src/main/java/io/timelimit/android/data/model/Device.kt
Normal file
|
@ -0,0 +1,210 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.data.model
|
||||||
|
|
||||||
|
import android.util.JsonReader
|
||||||
|
import android.util.JsonWriter
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import androidx.room.TypeConverters
|
||||||
|
import io.timelimit.android.data.IdGenerator
|
||||||
|
import io.timelimit.android.data.JsonSerializable
|
||||||
|
import io.timelimit.android.integration.platform.*
|
||||||
|
|
||||||
|
@Entity(tableName = "device")
|
||||||
|
@TypeConverters(
|
||||||
|
ProtectionLevelConverter::class,
|
||||||
|
RuntimePermissionStatusConverter::class,
|
||||||
|
NewPermissionStatusConverter::class
|
||||||
|
)
|
||||||
|
data class Device(
|
||||||
|
@PrimaryKey
|
||||||
|
@ColumnInfo(name = "id")
|
||||||
|
val id: String,
|
||||||
|
@ColumnInfo(name = "name")
|
||||||
|
val name: String,
|
||||||
|
@ColumnInfo(name = "model")
|
||||||
|
val model: String,
|
||||||
|
@ColumnInfo(name = "added_at")
|
||||||
|
val addedAt: Long,
|
||||||
|
@ColumnInfo(name = "current_user_id")
|
||||||
|
val currentUserId: String, // empty if not set
|
||||||
|
@ColumnInfo(name = "current_protection_level")
|
||||||
|
val currentProtectionLevel: ProtectionLevel,
|
||||||
|
@ColumnInfo(name = "highest_permission_level")
|
||||||
|
val highestProtectionLevel: ProtectionLevel,
|
||||||
|
@ColumnInfo(name = "current_usage_stats_permission")
|
||||||
|
val currentUsageStatsPermission: RuntimePermissionStatus,
|
||||||
|
@ColumnInfo(name = "highest_usage_stats_permission")
|
||||||
|
val highestUsageStatsPermission: RuntimePermissionStatus,
|
||||||
|
@ColumnInfo(name = "current_notification_access_permission")
|
||||||
|
val currentNotificationAccessPermission: NewPermissionStatus,
|
||||||
|
@ColumnInfo(name = "highest_notification_access_permission")
|
||||||
|
val highestNotificationAccessPermission: NewPermissionStatus,
|
||||||
|
@ColumnInfo(name = "current_app_version")
|
||||||
|
val currentAppVersion: Int,
|
||||||
|
@ColumnInfo(name = "highest_app_version")
|
||||||
|
val highestAppVersion: Int,
|
||||||
|
@ColumnInfo(name = "tried_disabling_device_admin")
|
||||||
|
val manipulationTriedDisablingDeviceAdmin: Boolean,
|
||||||
|
@ColumnInfo(name = "had_manipulation")
|
||||||
|
val hadManipulation: Boolean
|
||||||
|
): JsonSerializable {
|
||||||
|
companion object {
|
||||||
|
private const val ID = "id"
|
||||||
|
private const val NAME = "n"
|
||||||
|
private const val MODEL = "m"
|
||||||
|
private const val ADDED_AT = "aa"
|
||||||
|
private const val CURRENT_USER_ID = "u"
|
||||||
|
private const val CURRENT_PROTECTION_LEVEL = "pc"
|
||||||
|
private const val HIGHEST_PROTECTION_LEVEL = "pm"
|
||||||
|
private const val CURRENT_USAGE_STATS_PERMISSION = "uc"
|
||||||
|
private const val HIGHEST_USAGE_STATS_PERMISSION = "um"
|
||||||
|
private const val CURRENT_NOTIFICATION_ACCESS_PERMISSION = "nc"
|
||||||
|
private const val HIGHEST_NOTIFICATION_ACCESS_PERMISSION = "nm"
|
||||||
|
private const val CURRENT_APP_VERSION = "ac"
|
||||||
|
private const val HIGHEST_APP_VERSION = "am"
|
||||||
|
private const val TRIED_DISABLING_DEVICE_ADMIN = "tdda"
|
||||||
|
private const val HAD_MANIPULATION = "hm"
|
||||||
|
|
||||||
|
fun parse(reader: JsonReader): Device {
|
||||||
|
var id: String? = null
|
||||||
|
var name: String? = null
|
||||||
|
var model: String? = null
|
||||||
|
var addedAt: Long? = null
|
||||||
|
var currentUserId: String? = null
|
||||||
|
var currentProtectionLevel: ProtectionLevel? = null
|
||||||
|
var highestProtectionLevel: ProtectionLevel? = null
|
||||||
|
var currentUsageStatsPermission: RuntimePermissionStatus? = null
|
||||||
|
var highestUsageStatsPermission: RuntimePermissionStatus? = null
|
||||||
|
var currentNotificationAccessPermission: NewPermissionStatus? = null
|
||||||
|
var highestNotificationAccessPermission: NewPermissionStatus? = null
|
||||||
|
var currentAppVersion: Int? = null
|
||||||
|
var highestAppVersion: Int? = null
|
||||||
|
var manipulationTriedDisablingDeviceAdmin: Boolean? = null
|
||||||
|
var hadManipulation: Boolean? = null
|
||||||
|
|
||||||
|
reader.beginObject()
|
||||||
|
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
when (reader.nextName()) {
|
||||||
|
ID -> id = reader.nextString()
|
||||||
|
NAME -> name = reader.nextString()
|
||||||
|
MODEL -> model = reader.nextString()
|
||||||
|
ADDED_AT -> addedAt = reader.nextLong()
|
||||||
|
CURRENT_USER_ID -> currentUserId = reader.nextString()
|
||||||
|
CURRENT_PROTECTION_LEVEL -> currentProtectionLevel = ProtectionLevelUtil.parse(reader.nextString())
|
||||||
|
HIGHEST_PROTECTION_LEVEL -> highestProtectionLevel = ProtectionLevelUtil.parse(reader.nextString())
|
||||||
|
CURRENT_USAGE_STATS_PERMISSION -> currentUsageStatsPermission = RuntimePermissionStatusUtil.parse(reader.nextString())
|
||||||
|
HIGHEST_USAGE_STATS_PERMISSION -> highestUsageStatsPermission = RuntimePermissionStatusUtil.parse(reader.nextString())
|
||||||
|
CURRENT_NOTIFICATION_ACCESS_PERMISSION -> currentNotificationAccessPermission = NewPermissionStatusUtil.parse(reader.nextString())
|
||||||
|
HIGHEST_NOTIFICATION_ACCESS_PERMISSION -> highestNotificationAccessPermission = NewPermissionStatusUtil.parse(reader.nextString())
|
||||||
|
CURRENT_APP_VERSION -> currentAppVersion = reader.nextInt()
|
||||||
|
HIGHEST_APP_VERSION -> highestAppVersion = reader.nextInt()
|
||||||
|
TRIED_DISABLING_DEVICE_ADMIN -> manipulationTriedDisablingDeviceAdmin = reader.nextBoolean()
|
||||||
|
HAD_MANIPULATION -> hadManipulation = reader.nextBoolean()
|
||||||
|
else -> reader.skipValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.endObject()
|
||||||
|
|
||||||
|
return Device(
|
||||||
|
id = id!!,
|
||||||
|
name = name!!,
|
||||||
|
model = model!!,
|
||||||
|
addedAt = addedAt!!,
|
||||||
|
currentUserId = currentUserId!!,
|
||||||
|
currentProtectionLevel = currentProtectionLevel!!,
|
||||||
|
highestProtectionLevel = highestProtectionLevel!!,
|
||||||
|
currentUsageStatsPermission = currentUsageStatsPermission!!,
|
||||||
|
highestUsageStatsPermission = highestUsageStatsPermission!!,
|
||||||
|
currentNotificationAccessPermission = currentNotificationAccessPermission!!,
|
||||||
|
highestNotificationAccessPermission = highestNotificationAccessPermission!!,
|
||||||
|
currentAppVersion = currentAppVersion!!,
|
||||||
|
highestAppVersion = highestAppVersion!!,
|
||||||
|
manipulationTriedDisablingDeviceAdmin = manipulationTriedDisablingDeviceAdmin!!,
|
||||||
|
hadManipulation = hadManipulation!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(id)
|
||||||
|
|
||||||
|
if (currentUserId.isNotEmpty()) {
|
||||||
|
IdGenerator.assertIdValid(currentUserId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.isEmpty()) {
|
||||||
|
throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.isEmpty()) {
|
||||||
|
throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addedAt < 0) {
|
||||||
|
throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentAppVersion < 0 || highestAppVersion < 0) {
|
||||||
|
throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serialize(writer: JsonWriter) {
|
||||||
|
writer.beginObject()
|
||||||
|
|
||||||
|
writer.name(ID).value(id)
|
||||||
|
writer.name(NAME).value(name)
|
||||||
|
writer.name(MODEL).value(model)
|
||||||
|
writer.name(ADDED_AT).value(addedAt)
|
||||||
|
writer.name(CURRENT_USER_ID).value(currentUserId)
|
||||||
|
writer.name(CURRENT_PROTECTION_LEVEL).value(ProtectionLevelUtil.serialize(currentProtectionLevel))
|
||||||
|
writer.name(HIGHEST_PROTECTION_LEVEL).value(ProtectionLevelUtil.serialize(highestProtectionLevel))
|
||||||
|
writer.name(CURRENT_USAGE_STATS_PERMISSION).value(RuntimePermissionStatusUtil.serialize(currentUsageStatsPermission))
|
||||||
|
writer.name(HIGHEST_USAGE_STATS_PERMISSION).value(RuntimePermissionStatusUtil.serialize(highestUsageStatsPermission))
|
||||||
|
writer.name(CURRENT_NOTIFICATION_ACCESS_PERMISSION).value(NewPermissionStatusUtil.serialize(currentNotificationAccessPermission))
|
||||||
|
writer.name(HIGHEST_NOTIFICATION_ACCESS_PERMISSION).value(NewPermissionStatusUtil.serialize(highestNotificationAccessPermission))
|
||||||
|
writer.name(CURRENT_APP_VERSION).value(currentAppVersion)
|
||||||
|
writer.name(HIGHEST_APP_VERSION).value(highestAppVersion)
|
||||||
|
writer.name(TRIED_DISABLING_DEVICE_ADMIN).value(manipulationTriedDisablingDeviceAdmin)
|
||||||
|
writer.name(HAD_MANIPULATION).value(hadManipulation)
|
||||||
|
|
||||||
|
writer.endObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
val manipulationOfProtectionLevel = currentProtectionLevel != highestProtectionLevel
|
||||||
|
@Transient
|
||||||
|
val manipulationOfUsageStats = currentUsageStatsPermission != highestUsageStatsPermission
|
||||||
|
@Transient
|
||||||
|
val manipulationOfNotificationAccess = currentNotificationAccessPermission != highestNotificationAccessPermission
|
||||||
|
@Transient
|
||||||
|
val manipulationOfAppVersion = currentAppVersion != highestAppVersion
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
val hasActiveManipulationWarning = manipulationOfProtectionLevel ||
|
||||||
|
manipulationOfUsageStats ||
|
||||||
|
manipulationOfNotificationAccess ||
|
||||||
|
manipulationOfAppVersion ||
|
||||||
|
manipulationTriedDisablingDeviceAdmin
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
val hasAnyManipulation = hasActiveManipulationWarning || hadManipulation
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.data.model
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
@Entity(tableName = "temporarily_allowed_app")
|
||||||
|
data class TemporarilyAllowedApp(
|
||||||
|
@PrimaryKey
|
||||||
|
@ColumnInfo(name = "package_name")
|
||||||
|
val packageName: String
|
||||||
|
)
|
|
@ -0,0 +1,109 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.data.model
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.util.JsonReader
|
||||||
|
import android.util.JsonWriter
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import androidx.room.TypeConverters
|
||||||
|
import io.timelimit.android.data.IdGenerator
|
||||||
|
import io.timelimit.android.data.JsonSerializable
|
||||||
|
import io.timelimit.android.data.customtypes.ImmutableBitmaskAdapter
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
|
||||||
|
@Entity(tableName = "time_limit_rule")
|
||||||
|
@TypeConverters(ImmutableBitmaskAdapter::class)
|
||||||
|
@Parcelize
|
||||||
|
data class TimeLimitRule(
|
||||||
|
@PrimaryKey
|
||||||
|
@ColumnInfo(name = "id")
|
||||||
|
val id: String,
|
||||||
|
@ColumnInfo(name = "category_id")
|
||||||
|
val categoryId: String,
|
||||||
|
@ColumnInfo(name = "apply_to_extra_time_usage")
|
||||||
|
val applyToExtraTimeUsage: Boolean,
|
||||||
|
@ColumnInfo(name = "day_mask")
|
||||||
|
val dayMask: Byte,
|
||||||
|
@ColumnInfo(name = "max_time")
|
||||||
|
val maximumTimeInMillis: Int
|
||||||
|
): Parcelable, JsonSerializable {
|
||||||
|
companion object {
|
||||||
|
private const val RULE_ID = "ruleId"
|
||||||
|
private const val CATEGORY_ID = "categoryId"
|
||||||
|
private const val MAX_TIME_IN_MILLIS = "time"
|
||||||
|
private const val DAY_MASK = "days"
|
||||||
|
private const val APPLY_TO_EXTRA_TIME_USAGE = "extraTime"
|
||||||
|
|
||||||
|
fun parse(reader: JsonReader): TimeLimitRule {
|
||||||
|
var id: String? = null
|
||||||
|
var categoryId: String? = null
|
||||||
|
var applyToExtraTimeUsage: Boolean? = null
|
||||||
|
var dayMask: Byte? = null
|
||||||
|
var maximumTimeInMillis: Int? = null
|
||||||
|
|
||||||
|
reader.beginObject()
|
||||||
|
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
when (reader.nextName()) {
|
||||||
|
RULE_ID -> id = reader.nextString()
|
||||||
|
CATEGORY_ID -> categoryId = reader.nextString()
|
||||||
|
MAX_TIME_IN_MILLIS -> maximumTimeInMillis = reader.nextInt()
|
||||||
|
DAY_MASK -> dayMask = reader.nextInt().toByte()
|
||||||
|
APPLY_TO_EXTRA_TIME_USAGE -> applyToExtraTimeUsage = reader.nextBoolean()
|
||||||
|
else -> reader.skipValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.endObject()
|
||||||
|
|
||||||
|
return TimeLimitRule(
|
||||||
|
id = id!!,
|
||||||
|
categoryId = categoryId!!,
|
||||||
|
applyToExtraTimeUsage = applyToExtraTimeUsage!!,
|
||||||
|
dayMask = dayMask!!,
|
||||||
|
maximumTimeInMillis = maximumTimeInMillis!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(id)
|
||||||
|
IdGenerator.assertIdValid(categoryId)
|
||||||
|
|
||||||
|
if (maximumTimeInMillis < 0) {
|
||||||
|
throw IllegalArgumentException("maximumTimeInMillis $maximumTimeInMillis < 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dayMask < 0 || dayMask > (1 or 2 or 4 or 8 or 16 or 32 or 64)) {
|
||||||
|
throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serialize(writer: JsonWriter) {
|
||||||
|
writer.beginObject()
|
||||||
|
|
||||||
|
writer.name(RULE_ID).value(id)
|
||||||
|
writer.name(CATEGORY_ID).value(categoryId)
|
||||||
|
writer.name(MAX_TIME_IN_MILLIS).value(maximumTimeInMillis)
|
||||||
|
writer.name(DAY_MASK).value(dayMask)
|
||||||
|
writer.name(APPLY_TO_EXTRA_TIME_USAGE).value(applyToExtraTimeUsage)
|
||||||
|
|
||||||
|
writer.endObject()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.data.model
|
||||||
|
|
||||||
|
import android.util.JsonReader
|
||||||
|
import android.util.JsonWriter
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import io.timelimit.android.data.IdGenerator
|
||||||
|
import io.timelimit.android.data.JsonSerializable
|
||||||
|
|
||||||
|
@Entity(primaryKeys = ["category_id", "day_of_epoch"], tableName = "used_time")
|
||||||
|
data class UsedTimeItem(
|
||||||
|
@ColumnInfo(name = "day_of_epoch")
|
||||||
|
val dayOfEpoch: Int,
|
||||||
|
@ColumnInfo(name = "used_time")
|
||||||
|
val usedMillis: Long,
|
||||||
|
@ColumnInfo(name = "category_id")
|
||||||
|
val categoryId: String
|
||||||
|
): JsonSerializable {
|
||||||
|
companion object {
|
||||||
|
private const val DAY_OF_EPOCH = "day"
|
||||||
|
private const val USED_TIME_MILLIS = "time"
|
||||||
|
private const val CATEGORY_ID = "category"
|
||||||
|
|
||||||
|
fun parse(reader: JsonReader): UsedTimeItem {
|
||||||
|
reader.beginObject()
|
||||||
|
|
||||||
|
var dayOfEpoch: Int? = null
|
||||||
|
var usedMillis: Long? = null
|
||||||
|
var categoryId: String? = null
|
||||||
|
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
when (reader.nextName()) {
|
||||||
|
DAY_OF_EPOCH -> dayOfEpoch = reader.nextInt()
|
||||||
|
USED_TIME_MILLIS -> usedMillis = reader.nextLong()
|
||||||
|
CATEGORY_ID -> categoryId = reader.nextString()
|
||||||
|
else -> reader.skipValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.endObject()
|
||||||
|
|
||||||
|
return UsedTimeItem(
|
||||||
|
dayOfEpoch = dayOfEpoch!!,
|
||||||
|
usedMillis = usedMillis!!,
|
||||||
|
categoryId = categoryId!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(categoryId)
|
||||||
|
|
||||||
|
if (dayOfEpoch < 0) {
|
||||||
|
throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usedMillis < 0) {
|
||||||
|
throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serialize(writer: JsonWriter) {
|
||||||
|
writer.beginObject()
|
||||||
|
|
||||||
|
writer.name(DAY_OF_EPOCH).value(dayOfEpoch)
|
||||||
|
writer.name(USED_TIME_MILLIS).value(usedMillis)
|
||||||
|
writer.name(CATEGORY_ID).value(categoryId)
|
||||||
|
|
||||||
|
writer.endObject()
|
||||||
|
}
|
||||||
|
}
|
139
app/src/main/java/io/timelimit/android/data/model/User.kt
Normal file
139
app/src/main/java/io/timelimit/android/data/model/User.kt
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.data.model
|
||||||
|
|
||||||
|
import android.util.JsonReader
|
||||||
|
import android.util.JsonWriter
|
||||||
|
import androidx.room.*
|
||||||
|
import io.timelimit.android.data.IdGenerator
|
||||||
|
import io.timelimit.android.data.JsonSerializable
|
||||||
|
|
||||||
|
@Entity(tableName = "user")
|
||||||
|
@TypeConverters(UserTypeConverter::class)
|
||||||
|
data class User(
|
||||||
|
@PrimaryKey
|
||||||
|
@ColumnInfo(name = "id")
|
||||||
|
val id: String,
|
||||||
|
@ColumnInfo(name = "name")
|
||||||
|
val name: String,
|
||||||
|
@ColumnInfo(name = "password")
|
||||||
|
val password: String, // protected using bcrypt, can be empty if not configured
|
||||||
|
@ColumnInfo(name = "type")
|
||||||
|
val type: UserType,
|
||||||
|
@ColumnInfo(name = "timezone")
|
||||||
|
val timeZone: String,
|
||||||
|
// 0 = time limits enabled
|
||||||
|
@ColumnInfo(name = "disable_limits_until")
|
||||||
|
val disableLimitsUntil: Long
|
||||||
|
): JsonSerializable {
|
||||||
|
companion object {
|
||||||
|
private const val ID = "id"
|
||||||
|
private const val NAME = "name"
|
||||||
|
private const val PASSWORD = "password"
|
||||||
|
private const val TYPE = "type"
|
||||||
|
private const val TIMEZONE = "timeZone"
|
||||||
|
private const val DISABLE_LIMITS_UNTIL = "disableLimitsUntil"
|
||||||
|
|
||||||
|
fun parse(reader: JsonReader): User {
|
||||||
|
var id: String? = null
|
||||||
|
var name: String? = null
|
||||||
|
var password: String? = null
|
||||||
|
var type: UserType? = null
|
||||||
|
var timeZone: String? = null
|
||||||
|
var disableLimitsUntil: Long? = null
|
||||||
|
|
||||||
|
reader.beginObject()
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
when(reader.nextName()) {
|
||||||
|
ID -> id = reader.nextString()
|
||||||
|
NAME -> name = reader.nextString()
|
||||||
|
PASSWORD -> password = reader.nextString()
|
||||||
|
TYPE -> type = UserTypeJson.parse(reader.nextString())
|
||||||
|
TIMEZONE -> timeZone = reader.nextString()
|
||||||
|
DISABLE_LIMITS_UNTIL -> disableLimitsUntil = reader.nextLong()
|
||||||
|
else -> reader.skipValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.endObject()
|
||||||
|
|
||||||
|
return User(
|
||||||
|
id = id!!,
|
||||||
|
name = name!!,
|
||||||
|
password = password!!,
|
||||||
|
type = type!!,
|
||||||
|
timeZone = timeZone!!,
|
||||||
|
disableLimitsUntil = disableLimitsUntil!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(id)
|
||||||
|
|
||||||
|
if (disableLimitsUntil < 0) {
|
||||||
|
throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.isEmpty()) {
|
||||||
|
throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeZone.isEmpty()) {
|
||||||
|
throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serialize(writer: JsonWriter) {
|
||||||
|
writer.beginObject()
|
||||||
|
|
||||||
|
writer.name(ID).value(id)
|
||||||
|
writer.name(NAME).value(name)
|
||||||
|
writer.name(PASSWORD).value(password)
|
||||||
|
writer.name(TYPE).value(UserTypeJson.serialize(type))
|
||||||
|
writer.name(TIMEZONE).value(timeZone)
|
||||||
|
writer.name(DISABLE_LIMITS_UNTIL).value(disableLimitsUntil)
|
||||||
|
|
||||||
|
writer.endObject()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class UserType {
|
||||||
|
Parent, Child
|
||||||
|
}
|
||||||
|
|
||||||
|
object UserTypeJson {
|
||||||
|
private const val PARENT = "parent"
|
||||||
|
private const val CHILD = "child"
|
||||||
|
|
||||||
|
fun parse(value: String) = when(value) {
|
||||||
|
PARENT -> UserType.Parent
|
||||||
|
CHILD -> UserType.Child
|
||||||
|
else -> throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun serialize(value: UserType) = when(value) {
|
||||||
|
UserType.Parent -> PARENT
|
||||||
|
UserType.Child -> CHILD
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserTypeConverter {
|
||||||
|
@TypeConverter
|
||||||
|
fun toUserType(value: String) = UserTypeJson.parse(value)
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun toString(value: UserType) = UserTypeJson.serialize(value)
|
||||||
|
}
|
38
app/src/main/java/io/timelimit/android/date/CalendarCache.kt
Normal file
38
app/src/main/java/io/timelimit/android/date/CalendarCache.kt
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.date
|
||||||
|
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
object CalendarCache {
|
||||||
|
private val cache = Collections.synchronizedMap(HashMap<Long, Calendar>())
|
||||||
|
|
||||||
|
fun getCalendar(): Calendar {
|
||||||
|
val threadId = Thread.currentThread().id
|
||||||
|
|
||||||
|
val item = cache[threadId]
|
||||||
|
|
||||||
|
if (item != null) {
|
||||||
|
return item
|
||||||
|
} else {
|
||||||
|
val newItem = GregorianCalendar()
|
||||||
|
|
||||||
|
cache[threadId] = newItem
|
||||||
|
|
||||||
|
return newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.date
|
||||||
|
|
||||||
|
import org.threeten.bp.LocalDate
|
||||||
|
import org.threeten.bp.temporal.ChronoUnit
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
data class DateInTimezone(val dayOfWeek: Int, val dayOfEpoch: Int) {
|
||||||
|
companion object {
|
||||||
|
fun convertDayOfWeek(dayOfWeek: Int) = when(dayOfWeek) {
|
||||||
|
Calendar.MONDAY -> 0
|
||||||
|
Calendar.TUESDAY -> 1
|
||||||
|
Calendar.WEDNESDAY -> 2
|
||||||
|
Calendar.THURSDAY -> 3
|
||||||
|
Calendar.FRIDAY -> 4
|
||||||
|
Calendar.SATURDAY -> 5
|
||||||
|
Calendar.SUNDAY -> 6
|
||||||
|
else -> throw IllegalStateException()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun newInstance(timeInMillis: Long, timeZone: TimeZone): DateInTimezone {
|
||||||
|
val calendar = CalendarCache.getCalendar()
|
||||||
|
|
||||||
|
calendar.firstDayOfWeek = Calendar.MONDAY
|
||||||
|
|
||||||
|
calendar.timeZone = timeZone
|
||||||
|
calendar.timeInMillis = timeInMillis
|
||||||
|
|
||||||
|
val dayOfWeek = convertDayOfWeek(calendar.get(Calendar.DAY_OF_WEEK))
|
||||||
|
|
||||||
|
val localDate = LocalDate.of(
|
||||||
|
calendar.get(Calendar.YEAR),
|
||||||
|
calendar.get(Calendar.MONTH) + 1,
|
||||||
|
calendar.get(Calendar.DAY_OF_MONTH)
|
||||||
|
)
|
||||||
|
|
||||||
|
val dayOfEpoch = ChronoUnit.DAYS.between(LocalDate.ofEpochDay(0), localDate).toInt()
|
||||||
|
|
||||||
|
return DateInTimezone(dayOfWeek, dayOfEpoch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
app/src/main/java/io/timelimit/android/date/DayOfWeek.kt
Normal file
22
app/src/main/java/io/timelimit/android/date/DayOfWeek.kt
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.date
|
||||||
|
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
fun getDayOfWeek(calendar: Calendar): Int {
|
||||||
|
return DateInTimezone.convertDayOfWeek(calendar.get(Calendar.DAY_OF_WEEK))
|
||||||
|
}
|
33
app/src/main/java/io/timelimit/android/date/MinuteOfWeek.kt
Normal file
33
app/src/main/java/io/timelimit/android/date/MinuteOfWeek.kt
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.date
|
||||||
|
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
fun getMinuteOfWeek(timeInMillis: Long, timeZone: TimeZone): Int {
|
||||||
|
val calendar = CalendarCache.getCalendar()
|
||||||
|
|
||||||
|
calendar.firstDayOfWeek = Calendar.MONDAY
|
||||||
|
|
||||||
|
calendar.timeZone = timeZone
|
||||||
|
calendar.timeInMillis = timeInMillis
|
||||||
|
|
||||||
|
val dayOfWeek = getDayOfWeek(calendar)
|
||||||
|
val hourOfDay = calendar.get(Calendar.HOUR_OF_DAY)
|
||||||
|
val minuteOfHour = calendar.get(Calendar.MINUTE)
|
||||||
|
|
||||||
|
return minuteOfHour + 60 * (hourOfDay + 24 * dayOfWeek)
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.extensions
|
||||||
|
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
|
||||||
|
fun DialogFragment.showSafe(fragmentManager: FragmentManager, tag: String) {
|
||||||
|
if (!fragmentManager.isStateSaved) {
|
||||||
|
show(fragmentManager, tag)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.extensions
|
||||||
|
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
|
import android.widget.EditText
|
||||||
|
|
||||||
|
fun EditText.setOnEnterListenr(listener: () -> Unit) {
|
||||||
|
this.setOnEditorActionListener { _, actionId, _ ->
|
||||||
|
if (actionId == EditorInfo.IME_ACTION_GO) {
|
||||||
|
listener()
|
||||||
|
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setOnKeyListener { _, keyCode, keyEvent ->
|
||||||
|
if (keyEvent.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) {
|
||||||
|
listener()
|
||||||
|
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.extensions
|
||||||
|
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavDirections
|
||||||
|
|
||||||
|
fun NavController.safeNavigate(directions: NavDirections, currentScreen: Int) {
|
||||||
|
if (this.currentDestination?.id == currentScreen) {
|
||||||
|
navigate(directions)
|
||||||
|
}
|
||||||
|
}
|
24
app/src/main/java/io/timelimit/android/extensions/Set.kt
Normal file
24
app/src/main/java/io/timelimit/android/extensions/Set.kt
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.extensions
|
||||||
|
|
||||||
|
fun <T> MutableSet<T>.toggle(item: T) {
|
||||||
|
synchronized(this) {
|
||||||
|
if (!this.remove(item)) {
|
||||||
|
this.add(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,173 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.integration.platform
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.room.TypeConverter
|
||||||
|
import io.timelimit.android.data.model.App
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
|
||||||
|
abstract class PlatformIntegration(
|
||||||
|
val maximumProtectionLevel: ProtectionLevel
|
||||||
|
) {
|
||||||
|
abstract fun getLocalApps(): Collection<App>
|
||||||
|
abstract fun getLocalAppTitle(packageName: String): String?
|
||||||
|
abstract fun getAppIcon(packageName: String): Drawable?
|
||||||
|
abstract fun getCurrentProtectionLevel(): ProtectionLevel
|
||||||
|
abstract fun getForegroundAppPermissionStatus(): RuntimePermissionStatus
|
||||||
|
abstract fun getDrawOverOtherAppsPermissionStatus(): RuntimePermissionStatus
|
||||||
|
abstract fun getNotificationAccessPermissionStatus(): NewPermissionStatus
|
||||||
|
abstract fun disableDeviceAdmin()
|
||||||
|
abstract fun trySetLockScreenPassword(password: String): Boolean
|
||||||
|
// this must have a fallback if the permission is not granted
|
||||||
|
abstract fun showOverlayMessage(text: String)
|
||||||
|
|
||||||
|
abstract fun showAppLockScreen(currentPackageName: String)
|
||||||
|
// this should throw an SecurityException if the permission is missing
|
||||||
|
abstract fun getForegroundAppPackageName(): String?
|
||||||
|
abstract fun setAppStatusMessage(message: AppStatusMessage?)
|
||||||
|
abstract fun isScreenOn(): Boolean
|
||||||
|
abstract fun setShowNotificationToRevokeTemporarilyAllowedApps(show: Boolean)
|
||||||
|
// returns package names for which it was set
|
||||||
|
abstract fun setSuspendedApps(packageNames: List<String>, suspend: Boolean): List<String>
|
||||||
|
abstract fun stopSuspendingForAllApps()
|
||||||
|
|
||||||
|
// returns true on success
|
||||||
|
abstract fun setEnableSystemLockdown(enableLockdown: Boolean): Boolean
|
||||||
|
// returns true on success
|
||||||
|
abstract fun setLockTaskPackages(packageNames: List<String>): Boolean
|
||||||
|
|
||||||
|
var installedAppsChangeListener: Runnable? = null
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ProtectionLevel {
|
||||||
|
None, SimpleDeviceAdmin, PasswordDeviceAdmin, DeviceOwner
|
||||||
|
}
|
||||||
|
|
||||||
|
object ProtectionLevelUtil {
|
||||||
|
private const val NONE = "none"
|
||||||
|
private const val SIMPLE_DEVICE_ADMIN = "simple device admin"
|
||||||
|
private const val PASSWORD_DEVICE_ADMIN = "password device admin"
|
||||||
|
private const val DEVICE_OWNER = "device owner"
|
||||||
|
|
||||||
|
fun serialize(level: ProtectionLevel) = when(level) {
|
||||||
|
ProtectionLevel.None -> NONE
|
||||||
|
ProtectionLevel.SimpleDeviceAdmin -> SIMPLE_DEVICE_ADMIN
|
||||||
|
ProtectionLevel.PasswordDeviceAdmin -> PASSWORD_DEVICE_ADMIN
|
||||||
|
ProtectionLevel.DeviceOwner -> DEVICE_OWNER
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parse(level: String) = when(level) {
|
||||||
|
NONE -> ProtectionLevel.None
|
||||||
|
SIMPLE_DEVICE_ADMIN -> ProtectionLevel.SimpleDeviceAdmin
|
||||||
|
PASSWORD_DEVICE_ADMIN -> ProtectionLevel.PasswordDeviceAdmin
|
||||||
|
DEVICE_OWNER -> ProtectionLevel.DeviceOwner
|
||||||
|
else -> throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toInt(level: ProtectionLevel) = when(level) {
|
||||||
|
ProtectionLevel.None -> 0
|
||||||
|
ProtectionLevel.SimpleDeviceAdmin -> 1
|
||||||
|
ProtectionLevel.PasswordDeviceAdmin -> 2
|
||||||
|
ProtectionLevel.DeviceOwner -> 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProtectionLevelConverter {
|
||||||
|
@TypeConverter
|
||||||
|
fun fromString(value: String) = ProtectionLevelUtil.parse(value)
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun toString(value: ProtectionLevel) = ProtectionLevelUtil.serialize(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class RuntimePermissionStatus {
|
||||||
|
NotRequired, Granted, NotGranted
|
||||||
|
}
|
||||||
|
|
||||||
|
object RuntimePermissionStatusUtil {
|
||||||
|
private const val NOT_REQUIRED = "not required"
|
||||||
|
private const val GRANTED = "granted"
|
||||||
|
private const val NOT_GRANTED = "not granted"
|
||||||
|
|
||||||
|
fun serialize(value: RuntimePermissionStatus) = when(value) {
|
||||||
|
RuntimePermissionStatus.NotRequired -> NOT_REQUIRED
|
||||||
|
RuntimePermissionStatus.Granted -> GRANTED
|
||||||
|
RuntimePermissionStatus.NotGranted -> NOT_GRANTED
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parse(value: String) = when(value) {
|
||||||
|
NOT_REQUIRED -> RuntimePermissionStatus.NotRequired
|
||||||
|
GRANTED -> RuntimePermissionStatus.Granted
|
||||||
|
NOT_GRANTED -> RuntimePermissionStatus.NotGranted
|
||||||
|
else -> throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toInt(value: RuntimePermissionStatus) = when(value) {
|
||||||
|
RuntimePermissionStatus.NotGranted -> 0
|
||||||
|
RuntimePermissionStatus.NotRequired -> 1
|
||||||
|
RuntimePermissionStatus.Granted -> 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RuntimePermissionStatusConverter {
|
||||||
|
@TypeConverter
|
||||||
|
fun fromString(value: String) = RuntimePermissionStatusUtil.parse(value)
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun toString(value: RuntimePermissionStatus) = RuntimePermissionStatusUtil.serialize(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class NewPermissionStatus {
|
||||||
|
NotSupported, Granted, NotGranted
|
||||||
|
}
|
||||||
|
|
||||||
|
object NewPermissionStatusUtil {
|
||||||
|
private const val NOT_SUPPORTED = "not supported"
|
||||||
|
private const val GRANTED = "granted"
|
||||||
|
private const val NOT_GRANTED = "not granted"
|
||||||
|
|
||||||
|
fun serialize(value: NewPermissionStatus) = when(value) {
|
||||||
|
NewPermissionStatus.NotSupported -> NOT_SUPPORTED
|
||||||
|
NewPermissionStatus.Granted -> GRANTED
|
||||||
|
NewPermissionStatus.NotGranted -> NOT_GRANTED
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parse(value: String) = when(value) {
|
||||||
|
NOT_SUPPORTED -> NewPermissionStatus.NotSupported
|
||||||
|
GRANTED -> NewPermissionStatus.Granted
|
||||||
|
NOT_GRANTED -> NewPermissionStatus.NotGranted
|
||||||
|
else -> throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toInt(value: NewPermissionStatus) = when(value) {
|
||||||
|
NewPermissionStatus.NotGranted -> 0
|
||||||
|
NewPermissionStatus.NotSupported -> 1
|
||||||
|
NewPermissionStatus.Granted -> 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NewPermissionStatusConverter {
|
||||||
|
@TypeConverter
|
||||||
|
fun fromString(value: String) = NewPermissionStatusUtil.parse(value)
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun toString(value: NewPermissionStatus) = NewPermissionStatusUtil.serialize(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class AppStatusMessage(val title: String, val text: String): Parcelable
|
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.integration.platform.android
|
||||||
|
|
||||||
|
import android.app.admin.DeviceAdminReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.UserHandle
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.coroutines.runAsync
|
||||||
|
import io.timelimit.android.livedata.waitForNullableValue
|
||||||
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
|
import io.timelimit.android.sync.actions.TriedDisablingDeviceAdminAction
|
||||||
|
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||||
|
|
||||||
|
class AdminReceiver: DeviceAdminReceiver() {
|
||||||
|
override fun onEnabled(context: Context, intent: Intent?) {
|
||||||
|
super.onEnabled(context, intent)
|
||||||
|
|
||||||
|
DefaultAppLogic.with(context).backgroundTaskLogic.syncDeviceStatusAsync()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDisabled(context: Context, intent: Intent?) {
|
||||||
|
super.onDisabled(context, intent)
|
||||||
|
|
||||||
|
DefaultAppLogic.with(context).backgroundTaskLogic.syncDeviceStatusAsync()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDisableRequested(context: Context, intent: Intent?): CharSequence {
|
||||||
|
runAsync {
|
||||||
|
val logic = DefaultAppLogic.with(context)
|
||||||
|
|
||||||
|
if (logic.database.config().getOwnDeviceId().waitForNullableValue() != null) {
|
||||||
|
ApplyActionUtil.applyAppLogicAction(
|
||||||
|
TriedDisablingDeviceAdminAction,
|
||||||
|
logic
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.getString(R.string.admin_disable_warning)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPasswordSucceeded(context: Context, intent: Intent?) {
|
||||||
|
super.onPasswordSucceeded(context, intent)
|
||||||
|
|
||||||
|
DefaultAppLogic.with(context).manipulationLogic.reportManualUnlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPasswordSucceeded(context: Context, intent: Intent?, user: UserHandle?) {
|
||||||
|
super.onPasswordSucceeded(context, intent, user)
|
||||||
|
|
||||||
|
DefaultAppLogic.with(context).manipulationLogic.reportManualUnlock()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.integration.platform.android
|
||||||
|
|
||||||
|
import android.app.admin.DevicePolicyManager
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import io.timelimit.android.integration.platform.ProtectionLevel
|
||||||
|
|
||||||
|
object AdminStatus {
|
||||||
|
fun getAdminStatus(context: Context, policyManager: DevicePolicyManager): ProtectionLevel {
|
||||||
|
val component = ComponentName(context, AdminReceiver::class.java)
|
||||||
|
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
if (policyManager.isDeviceOwnerApp(context.packageName)) {
|
||||||
|
ProtectionLevel.DeviceOwner
|
||||||
|
} else if (policyManager.isAdminActive(component)) {
|
||||||
|
ProtectionLevel.SimpleDeviceAdmin
|
||||||
|
} else {
|
||||||
|
ProtectionLevel.None
|
||||||
|
}
|
||||||
|
} else /* if below Lollipop */ {
|
||||||
|
if (policyManager.isAdminActive(component)) {
|
||||||
|
ProtectionLevel.PasswordDeviceAdmin
|
||||||
|
} else {
|
||||||
|
ProtectionLevel.None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,309 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.integration.platform.android
|
||||||
|
|
||||||
|
import android.annotation.TargetApi
|
||||||
|
import android.app.ActivityManager
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.app.admin.DevicePolicyManager
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.ApplicationInfo
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.PowerManager
|
||||||
|
import android.os.UserManager
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import io.timelimit.android.BuildConfig
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.data.model.App
|
||||||
|
import io.timelimit.android.integration.platform.*
|
||||||
|
import io.timelimit.android.integration.platform.android.foregroundapp.ForegroundAppHelper
|
||||||
|
import io.timelimit.android.ui.lock.LockActivity
|
||||||
|
|
||||||
|
|
||||||
|
class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectionLevel) {
|
||||||
|
companion object {
|
||||||
|
private const val LOG_TAG = "AndroidIntegration"
|
||||||
|
|
||||||
|
val maximumProtectionLevel: ProtectionLevel
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
maximumProtectionLevel = ProtectionLevel.DeviceOwner
|
||||||
|
} else {
|
||||||
|
maximumProtectionLevel = ProtectionLevel.PasswordDeviceAdmin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val context = context.applicationContext
|
||||||
|
private val policyManager = this.context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
|
||||||
|
private val foregroundAppHelper = ForegroundAppHelper.with(this.context)
|
||||||
|
private val powerManager = this.context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||||
|
private val activityManager = this.context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
||||||
|
private val notificationManager = this.context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
private val deviceAdmin = ComponentName(context.applicationContext, AdminReceiver::class.java)
|
||||||
|
|
||||||
|
init {
|
||||||
|
AppsChangeListener.registerBroadcastReceiver(this.context, object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
installedAppsChangeListener?.run()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLocalApps(): Collection<App> {
|
||||||
|
return AndroidIntegrationApps.getLocalApps(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLocalAppTitle(packageName: String): String? {
|
||||||
|
return AndroidIntegrationApps.getAppTitle(packageName, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAppIcon(packageName: String): Drawable? {
|
||||||
|
return AndroidIntegrationApps.getAppIcon(packageName, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCurrentProtectionLevel(): ProtectionLevel {
|
||||||
|
return AdminStatus.getAdminStatus(context, policyManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getForegroundAppPackageName(): String? {
|
||||||
|
return foregroundAppHelper.getForegroundAppPackage()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getForegroundAppPermissionStatus(): RuntimePermissionStatus {
|
||||||
|
return foregroundAppHelper.getPermissionStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showOverlayMessage(text: String) {
|
||||||
|
Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDrawOverOtherAppsPermissionStatus(): RuntimePermissionStatus {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
if (Settings.canDrawOverlays(context)) {
|
||||||
|
return RuntimePermissionStatus.Granted
|
||||||
|
} else {
|
||||||
|
return RuntimePermissionStatus.NotGranted
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return RuntimePermissionStatus.NotRequired
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getNotificationAccessPermissionStatus(): NewPermissionStatus {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
if (activityManager.isLowRamDevice) {
|
||||||
|
return NewPermissionStatus.NotSupported
|
||||||
|
} else if (NotificationManagerCompat.getEnabledListenerPackages(context).contains(context.packageName)) {
|
||||||
|
return NewPermissionStatus.Granted
|
||||||
|
} else {
|
||||||
|
return NewPermissionStatus.NotGranted
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return NewPermissionStatus.NotSupported
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun trySetLockScreenPassword(password: String): Boolean {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "set password")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
try {
|
||||||
|
if (password.isBlank()) {
|
||||||
|
return policyManager.resetPassword("", 0)
|
||||||
|
} else if (policyManager.resetPassword(password, DevicePolicyManager.RESET_PASSWORD_REQUIRE_ENTRY)) {
|
||||||
|
policyManager.lockNow()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} catch (ex: SecurityException) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.w(LOG_TAG, "error setting password", ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private var lastAppStatusMessage: AppStatusMessage? = null
|
||||||
|
|
||||||
|
override fun setAppStatusMessage(message: AppStatusMessage?) {
|
||||||
|
if (lastAppStatusMessage != message) {
|
||||||
|
lastAppStatusMessage = message
|
||||||
|
|
||||||
|
BackgroundService.setStatusMessage(message, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showAppLockScreen(currentPackageName: String) {
|
||||||
|
LockActivity.start(context, currentPackageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isScreenOn(): Boolean {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
|
||||||
|
return powerManager.isInteractive
|
||||||
|
} else {
|
||||||
|
return powerManager.isScreenOn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setShowNotificationToRevokeTemporarilyAllowedApps(show: Boolean) {
|
||||||
|
if (show) {
|
||||||
|
NotificationChannels.createAppStatusChannel(notificationManager, context)
|
||||||
|
|
||||||
|
val actionIntent = PendingIntent.getService(
|
||||||
|
context,
|
||||||
|
PendingIntentIds.REVOKE_TEMPORARILY_ALLOWED,
|
||||||
|
BackgroundService.prepareRevokeTemporarilyAllowed(context),
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
)
|
||||||
|
|
||||||
|
val notification = NotificationCompat.Builder(context, NotificationChannels.APP_STATUS)
|
||||||
|
.setSmallIcon(R.drawable.ic_stat_check)
|
||||||
|
.setContentTitle(context.getString(R.string.background_logic_temporarily_allowed_title))
|
||||||
|
.setContentText(context.getString(R.string.background_logic_temporarily_allowed_text))
|
||||||
|
.setContentIntent(actionIntent)
|
||||||
|
.setWhen(0)
|
||||||
|
.setShowWhen(false)
|
||||||
|
.setSound(null)
|
||||||
|
.setOnlyAlertOnce(true)
|
||||||
|
.setLocalOnly(true)
|
||||||
|
.setAutoCancel(false)
|
||||||
|
.setOngoing(true)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
notificationManager.notify(NotificationIds.REVOKE_TEMPORARILY_ALLOWED_APPS, notification)
|
||||||
|
} else {
|
||||||
|
notificationManager.cancel(NotificationIds.REVOKE_TEMPORARILY_ALLOWED_APPS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun disableDeviceAdmin() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
if (policyManager.isDeviceOwnerApp(context.packageName)) {
|
||||||
|
setEnableSystemLockdown(false)
|
||||||
|
policyManager.clearDeviceOwnerApp(context.packageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
policyManager.removeActiveAdmin(deviceAdmin)
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.N)
|
||||||
|
override fun setSuspendedApps(packageNames: List<String>, suspend: Boolean): List<String> {
|
||||||
|
if (
|
||||||
|
(getCurrentProtectionLevel() == ProtectionLevel.DeviceOwner) &&
|
||||||
|
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
|
||||||
|
) {
|
||||||
|
val failedApps = policyManager.setPackagesSuspended(
|
||||||
|
deviceAdmin,
|
||||||
|
packageNames.toTypedArray(),
|
||||||
|
suspend
|
||||||
|
)
|
||||||
|
|
||||||
|
return packageNames.filterNot { failedApps.contains(it) }
|
||||||
|
} else {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setEnableSystemLockdown(enableLockdown: Boolean): Boolean {
|
||||||
|
return if (
|
||||||
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
|
||||||
|
policyManager.isDeviceOwnerApp(context.packageName)
|
||||||
|
) {
|
||||||
|
if (enableLockdown) {
|
||||||
|
// disable problematic features
|
||||||
|
policyManager.addUserRestriction(deviceAdmin, UserManager.DISALLOW_ADD_USER)
|
||||||
|
policyManager.addUserRestriction(deviceAdmin, UserManager.DISALLOW_FACTORY_RESET)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
policyManager.addUserRestriction(deviceAdmin, UserManager.DISALLOW_SAFE_BOOT)
|
||||||
|
}
|
||||||
|
} else /* disable lockdown */ {
|
||||||
|
// enable problematic features
|
||||||
|
policyManager.clearUserRestriction(deviceAdmin, UserManager.DISALLOW_ADD_USER)
|
||||||
|
policyManager.clearUserRestriction(deviceAdmin, UserManager.DISALLOW_FACTORY_RESET)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
policyManager.clearUserRestriction(deviceAdmin, UserManager.DISALLOW_SAFE_BOOT)
|
||||||
|
}
|
||||||
|
|
||||||
|
enableSystemApps()
|
||||||
|
stopSuspendingForAllApps()
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun enableSystemApps() {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// disabled system apps (all apps - enabled apps)
|
||||||
|
val allApps = context.packageManager.getInstalledApplications(
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1)
|
||||||
|
PackageManager.GET_UNINSTALLED_PACKAGES
|
||||||
|
else
|
||||||
|
PackageManager.MATCH_UNINSTALLED_PACKAGES
|
||||||
|
)
|
||||||
|
val enabledAppsPackages = context.packageManager.getInstalledApplications(0).map { it.packageName }.toSet()
|
||||||
|
|
||||||
|
allApps
|
||||||
|
.asSequence()
|
||||||
|
.filterNot { enabledAppsPackages.contains(it.packageName) }
|
||||||
|
.filter { it.flags and ApplicationInfo.FLAG_SYSTEM != 0 }
|
||||||
|
.map { it.packageName }
|
||||||
|
.forEach { policyManager.enableSystemApp(deviceAdmin, it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stopSuspendingForAllApps() {
|
||||||
|
setSuspendedApps(context.packageManager.getInstalledApplications(0).map { it.packageName }, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setLockTaskPackages(packageNames: List<String>): Boolean {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
if (policyManager.isDeviceOwnerApp(context.packageName)) {
|
||||||
|
policyManager.setLockTaskPackages(deviceAdmin, packageNames.toTypedArray())
|
||||||
|
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,157 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.integration.platform.android
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.pm.ResolveInfo
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.ContactsContract
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.provider.Telephony
|
||||||
|
import io.timelimit.android.data.model.App
|
||||||
|
import io.timelimit.android.data.model.AppRecommendation
|
||||||
|
|
||||||
|
object AndroidIntegrationApps {
|
||||||
|
private val mainIntent = Intent(Intent.ACTION_MAIN)
|
||||||
|
.addCategory(Intent.CATEGORY_LAUNCHER)
|
||||||
|
|
||||||
|
private val launcherIntent = Intent(Intent.ACTION_MAIN)
|
||||||
|
.addCategory(Intent.CATEGORY_DEFAULT)
|
||||||
|
.addCategory(Intent.CATEGORY_HOME)
|
||||||
|
|
||||||
|
val ignoredApps = HashSet<String>()
|
||||||
|
init {
|
||||||
|
ignoredApps.add("com.android.systemui")
|
||||||
|
ignoredApps.add("android")
|
||||||
|
ignoredApps.add("com.android.packageinstaller")
|
||||||
|
ignoredApps.add("com.google.android.packageinstaller")
|
||||||
|
ignoredApps.add("com.android.bluetooth")
|
||||||
|
ignoredApps.add("com.android.nfc")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLocalApps(context: Context): Collection<App> {
|
||||||
|
val packageManager = context.packageManager
|
||||||
|
|
||||||
|
val result = HashMap<String, App>()
|
||||||
|
|
||||||
|
// WHITELIST
|
||||||
|
// add launcher
|
||||||
|
add(map = result, resolveInfoList = packageManager.queryIntentActivities(launcherIntent, 0), recommendation = AppRecommendation.Whitelist, context = context)
|
||||||
|
// add settings
|
||||||
|
add(map = result, packageName = Intent(Settings.ACTION_SETTINGS).resolveActivity(packageManager)?.packageName, recommendation = AppRecommendation.Whitelist, context = context)
|
||||||
|
// add dialer
|
||||||
|
add(map = result, packageName = Intent(Intent.ACTION_DIAL).resolveActivity(packageManager)?.packageName, recommendation = AppRecommendation.Whitelist, context = context)
|
||||||
|
// add SMS
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
val smsApp: String? = Telephony.Sms.getDefaultSmsPackage(context)
|
||||||
|
|
||||||
|
if (smsApp != null) {
|
||||||
|
add(map = result, packageName = smsApp, recommendation = AppRecommendation.Whitelist, context = context)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
add(map = result, packageName = Intent(android.content.Intent.ACTION_VIEW).setType("vnd.android-dir/mms-sms").resolveActivity(packageManager)?.packageName, recommendation = AppRecommendation.Whitelist, context = context)
|
||||||
|
}
|
||||||
|
// add contacts
|
||||||
|
add(map = result, packageName = Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI).resolveActivity(packageManager)?.packageName, recommendation = AppRecommendation.Whitelist, context = context)
|
||||||
|
// add all apps with launcher icon
|
||||||
|
add(map = result, resolveInfoList = packageManager.queryIntentActivities(mainIntent, 0), recommendation = AppRecommendation.None, context = context)
|
||||||
|
|
||||||
|
val installedPackages = packageManager.getInstalledApplications(0)
|
||||||
|
|
||||||
|
for (applicationInfo in installedPackages) {
|
||||||
|
val packageName = applicationInfo.packageName
|
||||||
|
|
||||||
|
if (!result.containsKey(packageName) && !ignoredApps.contains(packageName)) {
|
||||||
|
result[packageName] = App(
|
||||||
|
packageName = packageName,
|
||||||
|
title = applicationInfo.loadLabel(packageManager).toString(),
|
||||||
|
isLaunchable = false,
|
||||||
|
recommendation = AppRecommendation.None
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.values
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun add(map: MutableMap<String, App>, resolveInfoList: List<ResolveInfo>, recommendation: AppRecommendation, context: Context) {
|
||||||
|
val packageManager = context.packageManager
|
||||||
|
|
||||||
|
for (info in resolveInfoList) {
|
||||||
|
val packageName = info.activityInfo.applicationInfo.packageName
|
||||||
|
if (ignoredApps.contains(packageName)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!map.containsKey(packageName)) {
|
||||||
|
map[packageName] = App(
|
||||||
|
packageName = packageName,
|
||||||
|
title = info.activityInfo.applicationInfo.loadLabel(packageManager).toString(),
|
||||||
|
isLaunchable = true,
|
||||||
|
recommendation = recommendation
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun add(map: MutableMap<String, App>, packageName: String?, recommendation: AppRecommendation, context: Context) {
|
||||||
|
val packageManager = context.packageManager
|
||||||
|
|
||||||
|
if (packageName == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ignoredApps.contains(packageName)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (map.containsKey(packageName)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val packageInfo = context.packageManager.getApplicationInfo(packageName, 0)
|
||||||
|
|
||||||
|
map[packageName] = App(
|
||||||
|
packageName = packageName,
|
||||||
|
title = packageInfo.loadLabel(packageManager).toString(),
|
||||||
|
isLaunchable = true,
|
||||||
|
recommendation = recommendation
|
||||||
|
)
|
||||||
|
} catch (ex: PackageManager.NameNotFoundException) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAppTitle(packageName: String, context: Context): String? {
|
||||||
|
try {
|
||||||
|
return context.packageManager.getApplicationInfo(packageName, 0).loadLabel(context.packageManager).toString()
|
||||||
|
} catch (ex: PackageManager.NameNotFoundException) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAppIcon(packageName: String, context: Context): Drawable? {
|
||||||
|
try {
|
||||||
|
return context.packageManager.getApplicationIcon(packageName)
|
||||||
|
} catch (ex: PackageManager.NameNotFoundException) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.integration.platform.android
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
|
||||||
|
object AppsChangeListener {
|
||||||
|
private val changeFilter = IntentFilter()
|
||||||
|
private val externalFilter = IntentFilter()
|
||||||
|
|
||||||
|
init {
|
||||||
|
changeFilter.addAction(Intent.ACTION_PACKAGE_ADDED)
|
||||||
|
changeFilter.addAction(Intent.ACTION_PACKAGE_CHANGED)
|
||||||
|
changeFilter.addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||||
|
changeFilter.addDataScheme("package")
|
||||||
|
|
||||||
|
externalFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE)
|
||||||
|
externalFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun registerBroadcastReceiver(context: Context, listener: BroadcastReceiver) {
|
||||||
|
context.registerReceiver(listener, changeFilter)
|
||||||
|
context.registerReceiver(listener, externalFilter)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,129 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.integration.platform.android
|
||||||
|
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.IBinder
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.coroutines.runAsync
|
||||||
|
import io.timelimit.android.integration.platform.AppStatusMessage
|
||||||
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
|
import io.timelimit.android.ui.MainActivity
|
||||||
|
|
||||||
|
class BackgroundService: Service() {
|
||||||
|
companion object {
|
||||||
|
private const val ACTION = "action"
|
||||||
|
private const val ACTION_SET_NOTIFICATION = "set_notification"
|
||||||
|
private const val ACTION_REVOKE_TEMPORARILY_ALLOWED_APPS = "revoke_temporarily_allowed_apps"
|
||||||
|
private const val EXTRA_NOTIFICATION = "notification"
|
||||||
|
|
||||||
|
fun setStatusMessage(status: AppStatusMessage?, context: Context) {
|
||||||
|
val intent = Intent(context, BackgroundService::class.java)
|
||||||
|
|
||||||
|
if (status != null) {
|
||||||
|
ContextCompat.startForegroundService(
|
||||||
|
context,
|
||||||
|
intent
|
||||||
|
.putExtra(ACTION, ACTION_SET_NOTIFICATION)
|
||||||
|
.putExtra(EXTRA_NOTIFICATION, status)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
context.stopService(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun prepareRevokeTemporarilyAllowed(context: Context) = Intent(context, BackgroundService::class.java)
|
||||||
|
.putExtra(ACTION, ACTION_REVOKE_TEMPORARILY_ALLOWED_APPS)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val notificationManager: NotificationManager by lazy {
|
||||||
|
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
}
|
||||||
|
|
||||||
|
private var didPostNotification = false
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
// init the app logic if not yet done
|
||||||
|
DefaultAppLogic.with(this)
|
||||||
|
|
||||||
|
// create the channel
|
||||||
|
NotificationChannels.createAppStatusChannel(notificationManager, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
if (intent != null) {
|
||||||
|
val action = intent.getStringExtra(ACTION)
|
||||||
|
|
||||||
|
if (action == ACTION_SET_NOTIFICATION) {
|
||||||
|
val appStatusMessage = intent.getParcelableExtra<AppStatusMessage>(EXTRA_NOTIFICATION)
|
||||||
|
|
||||||
|
val openAppIntent = PendingIntent.getActivity(
|
||||||
|
this,
|
||||||
|
PendingIntentIds.OPEN_MAIN_APP,
|
||||||
|
Intent(this, MainActivity::class.java),
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
)
|
||||||
|
|
||||||
|
val notification = NotificationCompat.Builder(this, NotificationChannels.APP_STATUS)
|
||||||
|
.setSmallIcon(R.drawable.ic_stat_timelapse)
|
||||||
|
.setContentTitle(appStatusMessage.title)
|
||||||
|
.setContentText(appStatusMessage.text)
|
||||||
|
.setContentIntent(openAppIntent)
|
||||||
|
.setWhen(0)
|
||||||
|
.setShowWhen(false)
|
||||||
|
.setSound(null)
|
||||||
|
.setOnlyAlertOnce(true)
|
||||||
|
.setLocalOnly(true)
|
||||||
|
.setAutoCancel(false)
|
||||||
|
.setOngoing(true)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
if (didPostNotification) {
|
||||||
|
notificationManager.notify(NotificationIds.APP_STATUS, notification)
|
||||||
|
} else {
|
||||||
|
startForeground(NotificationIds.APP_STATUS, notification)
|
||||||
|
didPostNotification = true
|
||||||
|
}
|
||||||
|
} else if (action == ACTION_REVOKE_TEMPORARILY_ALLOWED_APPS) {
|
||||||
|
runAsync {
|
||||||
|
DefaultAppLogic.with(this@BackgroundService).backgroundTaskLogic.resetTemporarilyAllowedApps()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return START_REDELIVER_INTENT
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
stopForeground(true)
|
||||||
|
didPostNotification = false
|
||||||
|
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent?): IBinder {
|
||||||
|
throw NotImplementedError()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.integration.platform.android
|
||||||
|
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import io.timelimit.android.R
|
||||||
|
|
||||||
|
object NotificationIds {
|
||||||
|
const val APP_STATUS = 1
|
||||||
|
const val NOTIFICATION_BLOCKED = 2
|
||||||
|
const val REVOKE_TEMPORARILY_ALLOWED_APPS = 3
|
||||||
|
const val APP_RESET = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
object NotificationChannels {
|
||||||
|
const val APP_STATUS = "app status"
|
||||||
|
const val BLOCKED_NOTIFICATIONS_NOTIFICATION = "notification blocked notification"
|
||||||
|
|
||||||
|
fun createAppStatusChannel(notificationManager: NotificationManager, context: Context) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
notificationManager.createNotificationChannel(
|
||||||
|
NotificationChannel(
|
||||||
|
APP_STATUS,
|
||||||
|
context.getString(R.string.notification_channel_app_status_title),
|
||||||
|
NotificationManager.IMPORTANCE_LOW
|
||||||
|
).apply {
|
||||||
|
description = context.getString(R.string.notification_channel_app_status_description)
|
||||||
|
enableLights(false)
|
||||||
|
setSound(null, null)
|
||||||
|
enableVibration(false)
|
||||||
|
setShowBadge(false)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createBlockedNotificationChannel(notificationManager: NotificationManager, context: Context) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
notificationManager.createNotificationChannel(
|
||||||
|
NotificationChannel(
|
||||||
|
NotificationChannels.BLOCKED_NOTIFICATIONS_NOTIFICATION,
|
||||||
|
context.getString(R.string.notification_channel_blocked_notification_title),
|
||||||
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
|
).apply {
|
||||||
|
description = context.getString(R.string.notification_channel_blocked_notification_text)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object PendingIntentIds {
|
||||||
|
const val OPEN_MAIN_APP = 1
|
||||||
|
const val REVOKE_TEMPORARILY_ALLOWED = 2
|
||||||
|
}
|
|
@ -0,0 +1,119 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.integration.platform.android
|
||||||
|
|
||||||
|
import android.annotation.TargetApi
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.ApplicationInfo
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.service.notification.NotificationListenerService
|
||||||
|
import android.service.notification.StatusBarNotification
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import io.timelimit.android.BuildConfig
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.coroutines.runAsync
|
||||||
|
import io.timelimit.android.livedata.waitForNonNullValue
|
||||||
|
import io.timelimit.android.logic.*
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
|
class NotificationListener: NotificationListenerService() {
|
||||||
|
companion object {
|
||||||
|
private const val LOG_TAG = "NotificationListenerLog"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val appLogic: AppLogic by lazy { DefaultAppLogic.with(this) }
|
||||||
|
private val blockingReasonUtil: BlockingReasonUtil by lazy { BlockingReasonUtil(appLogic) }
|
||||||
|
private val notificationManager: NotificationManager by lazy { getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager }
|
||||||
|
private val queryAppTitleCache: QueryAppTitleCache by lazy { QueryAppTitleCache(appLogic.platformIntegration) }
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
NotificationChannels.createBlockedNotificationChannel(notificationManager, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNotificationPosted(sbn: StatusBarNotification) {
|
||||||
|
super.onNotificationPosted(sbn)
|
||||||
|
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, sbn.packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
runAsync {
|
||||||
|
if (shouldRemoveNotification(sbn) == NotificationHandling.Replace) {
|
||||||
|
cancelNotification(sbn.key)
|
||||||
|
|
||||||
|
notificationManager.notify(
|
||||||
|
sbn.packageName,
|
||||||
|
NotificationIds.NOTIFICATION_BLOCKED,
|
||||||
|
NotificationCompat.Builder(this@NotificationListener, NotificationChannels.BLOCKED_NOTIFICATIONS_NOTIFICATION)
|
||||||
|
.setDefaults(NotificationCompat.DEFAULT_ALL)
|
||||||
|
.setSmallIcon(R.drawable.ic_stat_block)
|
||||||
|
.setContentTitle(getString(R.string.notification_filter_not_blocked_title))
|
||||||
|
.setContentText(queryAppTitleCache.query(sbn.packageName))
|
||||||
|
.setLocalOnly(true)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNotificationRemoved(sbn: StatusBarNotification) {
|
||||||
|
super.onNotificationRemoved(sbn)
|
||||||
|
|
||||||
|
// not interesting but required for old android versions
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun shouldRemoveNotification(sbn: StatusBarNotification): NotificationHandling {
|
||||||
|
if (sbn.packageName == packageName || sbn.isOngoing) {
|
||||||
|
return NotificationHandling.Keep
|
||||||
|
}
|
||||||
|
|
||||||
|
val blockingReason = blockingReasonUtil.getBlockingReason(sbn.packageName).waitForNonNullValue()
|
||||||
|
|
||||||
|
if (blockingReason == BlockingReason.None) {
|
||||||
|
return NotificationHandling.Keep
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSystemApp(sbn.packageName) && blockingReason == BlockingReason.NotPartOfAnCategory) {
|
||||||
|
return NotificationHandling.Keep
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "blocking notification of ${sbn.packageName} because $blockingReason")
|
||||||
|
}
|
||||||
|
|
||||||
|
return NotificationHandling.Replace
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isSystemApp(packageName: String): Boolean {
|
||||||
|
try {
|
||||||
|
val appInfo = packageManager.getApplicationInfo(packageName, 0)
|
||||||
|
|
||||||
|
return appInfo.flags and ApplicationInfo.FLAG_SYSTEM == ApplicationInfo.FLAG_SYSTEM
|
||||||
|
} catch (ex: PackageManager.NameNotFoundException) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class NotificationHandling {
|
||||||
|
Replace, Keep
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.integration.platform.android.foregroundapp
|
||||||
|
|
||||||
|
import android.app.ActivityManager
|
||||||
|
import android.content.Context
|
||||||
|
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
||||||
|
|
||||||
|
class CompatForegroundAppHelper(context: Context) : ForegroundAppHelper() {
|
||||||
|
private val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
||||||
|
|
||||||
|
override fun getForegroundAppPackage(): String? {
|
||||||
|
return try {
|
||||||
|
activityManager.getRunningTasks(1)[0].topActivity.packageName
|
||||||
|
} catch (ex: NullPointerException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPermissionStatus(): RuntimePermissionStatus {
|
||||||
|
return RuntimePermissionStatus.NotRequired
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.integration.platform.android.foregroundapp
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
||||||
|
|
||||||
|
abstract class ForegroundAppHelper {
|
||||||
|
abstract fun getForegroundAppPackage(): String?
|
||||||
|
abstract fun getPermissionStatus(): RuntimePermissionStatus
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val lock = Any()
|
||||||
|
private var instance: ForegroundAppHelper? = null
|
||||||
|
|
||||||
|
fun with(context: Context): ForegroundAppHelper {
|
||||||
|
if (instance == null) {
|
||||||
|
synchronized(lock) {
|
||||||
|
if (instance == null) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
instance = LollipopForegroundAppHelper(context.applicationContext)
|
||||||
|
} else {
|
||||||
|
instance = CompatForegroundAppHelper(context.applicationContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.integration.platform.android.foregroundapp
|
||||||
|
|
||||||
|
import android.annotation.TargetApi
|
||||||
|
import android.app.AppOpsManager
|
||||||
|
import android.app.usage.UsageEvents
|
||||||
|
import android.app.usage.UsageStatsManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
|
class LollipopForegroundAppHelper(private val context: Context) : ForegroundAppHelper() {
|
||||||
|
private val usageStatsManager = context.getSystemService(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) Context.USAGE_STATS_SERVICE else "usagestats") as UsageStatsManager
|
||||||
|
private val appOpsManager = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
|
||||||
|
|
||||||
|
@Throws(SecurityException::class)
|
||||||
|
override fun getForegroundAppPackage(): String? {
|
||||||
|
if (getPermissionStatus() == RuntimePermissionStatus.NotGranted) {
|
||||||
|
throw SecurityException()
|
||||||
|
}
|
||||||
|
|
||||||
|
val time = System.currentTimeMillis()
|
||||||
|
// query data for last 7 days
|
||||||
|
val usageEvents = usageStatsManager.queryEvents(time - 1000 * 60 * 60 * 24 * 7, time)
|
||||||
|
|
||||||
|
if (usageEvents != null) {
|
||||||
|
val event = UsageEvents.Event()
|
||||||
|
|
||||||
|
var lastTime: Long = 0
|
||||||
|
var lastPackage: String? = null
|
||||||
|
|
||||||
|
while (usageEvents.hasNextEvent()) {
|
||||||
|
usageEvents.getNextEvent(event)
|
||||||
|
|
||||||
|
if (event.eventType == UsageEvents.Event.MOVE_TO_FOREGROUND) {
|
||||||
|
if (event.timeStamp > lastTime) {
|
||||||
|
lastTime = event.timeStamp
|
||||||
|
lastPackage = event.packageName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastPackage
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPermissionStatus(): RuntimePermissionStatus {
|
||||||
|
if(appOpsManager.checkOpNoThrow("android:get_usage_stats",
|
||||||
|
android.os.Process.myUid(), context.packageName) == AppOpsManager.MODE_ALLOWED) {
|
||||||
|
return RuntimePermissionStatus.Granted
|
||||||
|
} else {
|
||||||
|
return RuntimePermissionStatus.NotGranted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.integration.platform.android.receiver
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
|
|
||||||
|
class BootReceiver : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
|
if (intent?.action == Intent.ACTION_BOOT_COMPLETED) {
|
||||||
|
// this starts the logic (if not yet done)
|
||||||
|
DefaultAppLogic.with(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.integration.platform.android.receiver
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
|
|
||||||
|
class UpdateReceiver : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
|
if (intent?.action == Intent.ACTION_MY_PACKAGE_REPLACED) {
|
||||||
|
// this starts the logic (if not yet done)
|
||||||
|
DefaultAppLogic.with(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.integration.platform.dummy
|
||||||
|
|
||||||
|
import io.timelimit.android.data.model.App
|
||||||
|
import io.timelimit.android.data.model.AppRecommendation
|
||||||
|
|
||||||
|
object DummyApps {
|
||||||
|
val taskmanagerLocalApp = App(
|
||||||
|
packageName = "com.demo.taskkiller",
|
||||||
|
title = "Task-Killer",
|
||||||
|
isLaunchable = true,
|
||||||
|
recommendation = AppRecommendation.Blacklist
|
||||||
|
)
|
||||||
|
|
||||||
|
val launcherLocalApp = App(
|
||||||
|
packageName = "com.demo.home",
|
||||||
|
title = "Launcher",
|
||||||
|
isLaunchable = true,
|
||||||
|
recommendation = AppRecommendation.Whitelist
|
||||||
|
)
|
||||||
|
|
||||||
|
val messagingLocalApp = App(
|
||||||
|
packageName = "com.demo.messaging",
|
||||||
|
title = "Messaging",
|
||||||
|
isLaunchable = true,
|
||||||
|
recommendation = AppRecommendation.None
|
||||||
|
)
|
||||||
|
|
||||||
|
val gameLocalApp = App(
|
||||||
|
packageName = "com.demo.game",
|
||||||
|
title = "Game",
|
||||||
|
isLaunchable = true,
|
||||||
|
recommendation = AppRecommendation.None
|
||||||
|
)
|
||||||
|
|
||||||
|
val all = listOf(taskmanagerLocalApp, launcherLocalApp, messagingLocalApp, gameLocalApp)
|
||||||
|
}
|
|
@ -0,0 +1,124 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.integration.platform.dummy
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import io.timelimit.android.data.model.App
|
||||||
|
import io.timelimit.android.integration.platform.*
|
||||||
|
|
||||||
|
class DummyIntegration(
|
||||||
|
maximumProtectionLevel: ProtectionLevel
|
||||||
|
): PlatformIntegration(maximumProtectionLevel) {
|
||||||
|
val localApps = ArrayList<App>(DummyApps.all)
|
||||||
|
var protectionLevel = ProtectionLevel.None
|
||||||
|
var foregroundAppPermission: RuntimePermissionStatus = RuntimePermissionStatus.NotRequired
|
||||||
|
var drawOverOtherApps: RuntimePermissionStatus = RuntimePermissionStatus.NotRequired
|
||||||
|
var notificationAccess: NewPermissionStatus = NewPermissionStatus.NotSupported
|
||||||
|
var foregroundApp: String? = null
|
||||||
|
var screenOn = false
|
||||||
|
var lastAppStatusMessage: AppStatusMessage? = null
|
||||||
|
var launchLockScreenForPackage: String? = null
|
||||||
|
var showRevokeTemporarilyAllowedNotification = false
|
||||||
|
|
||||||
|
override fun getLocalApps(): Collection<App> {
|
||||||
|
return localApps
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLocalAppTitle(packageName: String): String? {
|
||||||
|
return localApps.find { it.packageName == packageName }?.title
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAppIcon(packageName: String): Drawable? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCurrentProtectionLevel(): ProtectionLevel {
|
||||||
|
return protectionLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getForegroundAppPermissionStatus(): RuntimePermissionStatus {
|
||||||
|
return foregroundAppPermission
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDrawOverOtherAppsPermissionStatus(): RuntimePermissionStatus {
|
||||||
|
return drawOverOtherApps
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getNotificationAccessPermissionStatus(): NewPermissionStatus {
|
||||||
|
return notificationAccess
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun trySetLockScreenPassword(password: String): Boolean {
|
||||||
|
return false // it failed
|
||||||
|
}
|
||||||
|
override fun showOverlayMessage(text: String) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showAppLockScreen(currentPackageName: String) {
|
||||||
|
launchLockScreenForPackage = currentPackageName
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAndResetShowAppLockScreen(): String? {
|
||||||
|
try {
|
||||||
|
return launchLockScreenForPackage
|
||||||
|
} finally {
|
||||||
|
launchLockScreenForPackage = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getForegroundAppPackageName(): String? {
|
||||||
|
if (foregroundAppPermission == RuntimePermissionStatus.NotGranted) {
|
||||||
|
throw SecurityException()
|
||||||
|
}
|
||||||
|
|
||||||
|
return foregroundApp
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setAppStatusMessage(message: AppStatusMessage?) {
|
||||||
|
lastAppStatusMessage = message
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAppStatusMessage(): AppStatusMessage? {
|
||||||
|
return lastAppStatusMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
fun notifyLocalAppsChanged() {
|
||||||
|
installedAppsChangeListener?.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isScreenOn(): Boolean {
|
||||||
|
return screenOn
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setShowNotificationToRevokeTemporarilyAllowedApps(show: Boolean) {
|
||||||
|
showRevokeTemporarilyAllowedNotification = show
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun disableDeviceAdmin() {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setSuspendedApps(packageNames: List<String>, suspend: Boolean) = emptyList<String>()
|
||||||
|
|
||||||
|
override fun stopSuspendingForAllApps() {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setEnableSystemLockdown(enableLockdown: Boolean) = false
|
||||||
|
|
||||||
|
override fun setLockTaskPackages(packageNames: List<String>) = false
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.integration.time
|
||||||
|
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
|
class DummyTimeApi(var timeStepSizeInMillis: Long): TimeApi() {
|
||||||
|
private var currentTime: Long = System.currentTimeMillis()
|
||||||
|
private var currentUptime: Long = 0
|
||||||
|
private var scheduledActions = Collections.synchronizedList(ArrayList<ScheduledAction>())
|
||||||
|
var timeZone = TimeZone.getDefault()
|
||||||
|
|
||||||
|
override fun getCurrentTimeInMillis(): Long {
|
||||||
|
return currentTime
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCurrentTimeInMillis(time: Long) {
|
||||||
|
this.currentTime = time
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCurrentUptimeInMillis(): Long {
|
||||||
|
return currentUptime
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSystemTimeZone() = timeZone
|
||||||
|
|
||||||
|
override fun runDelayed(runnable: Runnable, delayInMillis: Long) {
|
||||||
|
scheduledActions.add(ScheduledAction(currentUptime + delayInMillis, runnable))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cancelScheduledAction(runnable: Runnable) {
|
||||||
|
scheduledActions.removeAll { it.action === runnable }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun emulateTimeAtOnce(timeInMillis: Long) {
|
||||||
|
if (timeInMillis <= 0) {
|
||||||
|
throw IllegalStateException()
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTime += timeInMillis
|
||||||
|
currentUptime += timeInMillis
|
||||||
|
|
||||||
|
synchronized(scheduledActions) {
|
||||||
|
val iterator = scheduledActions.iterator()
|
||||||
|
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
val action = iterator.next()
|
||||||
|
|
||||||
|
if (action.uptime <= currentUptime) {
|
||||||
|
action.action.run()
|
||||||
|
iterator.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun emulateTimePassing(timeInMillis: Long) {
|
||||||
|
var emulatedTime: Long = 0
|
||||||
|
|
||||||
|
while (emulatedTime < timeInMillis) {
|
||||||
|
val missingTime = timeInMillis - emulatedTime
|
||||||
|
|
||||||
|
if (missingTime >= timeStepSizeInMillis) {
|
||||||
|
emulateTimeAtOnce(timeStepSizeInMillis)
|
||||||
|
emulatedTime += timeStepSizeInMillis
|
||||||
|
} else {
|
||||||
|
emulateTimeAtOnce(missingTime)
|
||||||
|
emulatedTime += missingTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ScheduledAction (
|
||||||
|
val uptime: Long,
|
||||||
|
val action: Runnable
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.integration.time
|
||||||
|
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.os.SystemClock
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
object RealTimeApi: TimeApi() {
|
||||||
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
|
override fun getCurrentTimeInMillis(): Long {
|
||||||
|
return System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCurrentUptimeInMillis(): Long {
|
||||||
|
return SystemClock.uptimeMillis()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun runDelayed(runnable: Runnable, delayInMillis: Long) {
|
||||||
|
handler.postDelayed(runnable, delayInMillis)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cancelScheduledAction(runnable: Runnable) {
|
||||||
|
handler.removeCallbacks(runnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSystemTimeZone() = TimeZone.getDefault()
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.integration.time
|
||||||
|
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
|
abstract class TimeApi {
|
||||||
|
// normal clock - can be modified by the user at any time
|
||||||
|
abstract fun getCurrentTimeInMillis(): Long
|
||||||
|
// clock which starts at 0 at boot
|
||||||
|
abstract fun getCurrentUptimeInMillis(): Long
|
||||||
|
// function to run something delayed at the UI Thread
|
||||||
|
abstract fun runDelayed(runnable: Runnable, delayInMillis: Long)
|
||||||
|
abstract fun cancelScheduledAction(runnable: Runnable)
|
||||||
|
suspend fun sleep(timeInMillis: Long) = suspendCoroutine<Void?> {
|
||||||
|
runDelayed(Runnable {
|
||||||
|
it.resume(null)
|
||||||
|
}, timeInMillis)
|
||||||
|
}
|
||||||
|
abstract fun getSystemTimeZone(): TimeZone
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.livedata
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
|
||||||
|
fun LiveData<Boolean>.or(other: LiveData<Boolean>): LiveData<Boolean> {
|
||||||
|
return mergeLiveData(this, other).map {
|
||||||
|
(it.first != null && it.first == true) || ( it.second != null && it.second == true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun LiveData<Boolean>.and(other: LiveData<Boolean>): LiveData<Boolean> {
|
||||||
|
return mergeLiveData(this, other).map {
|
||||||
|
(it.first != null && it.first == true) && ( it.second != null && it.second == true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun LiveData<Boolean>.invert(): LiveData<Boolean> = this.map { !it }
|
20
app/src/main/java/io/timelimit/android/livedata/CastDown.kt
Normal file
20
app/src/main/java/io/timelimit/android/livedata/CastDown.kt
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.livedata
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
|
||||||
|
fun <T> LiveData<T>.castDown(): LiveData<T> = this
|
26
app/src/main/java/io/timelimit/android/livedata/FromValue.kt
Normal file
26
app/src/main/java/io/timelimit/android/livedata/FromValue.kt
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.livedata
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MediatorLiveData
|
||||||
|
|
||||||
|
fun <T> liveDataFromValue(value: T): LiveData<T> {
|
||||||
|
val result = MediatorLiveData<T>()
|
||||||
|
result.value = value
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
44
app/src/main/java/io/timelimit/android/livedata/FromView.kt
Normal file
44
app/src/main/java/io/timelimit/android/livedata/FromView.kt
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.livedata
|
||||||
|
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.TextWatcher
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
|
||||||
|
fun TextView.getTextLive(): LiveData<String> {
|
||||||
|
val result = MutableLiveData<String>()
|
||||||
|
|
||||||
|
result.value = this.text.toString()
|
||||||
|
|
||||||
|
this.addTextChangedListener(object: TextWatcher {
|
||||||
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun afterTextChanged(s: Editable?) {
|
||||||
|
result.value = s.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
55
app/src/main/java/io/timelimit/android/livedata/GetValue.kt
Normal file
55
app/src/main/java/io/timelimit/android/livedata/GetValue.kt
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.livedata
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
|
suspend fun <T> LiveData<T>.waitUntilValueMatches(check: (T?) -> Boolean): T? {
|
||||||
|
val liveData = this
|
||||||
|
var observer: Observer<T>? = null
|
||||||
|
|
||||||
|
fun removeObserver() {
|
||||||
|
val currentObserver = observer
|
||||||
|
|
||||||
|
if (currentObserver != null) {
|
||||||
|
liveData.removeObserver(currentObserver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return suspendCancellableCoroutine { continuation ->
|
||||||
|
continuation.invokeOnCancellation { removeObserver() }
|
||||||
|
|
||||||
|
observer = Observer { t ->
|
||||||
|
if (check(t)) {
|
||||||
|
removeObserver()
|
||||||
|
continuation.resume(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
liveData.observeForever(observer!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <T> LiveData<T>.waitForNullableValue(): T? {
|
||||||
|
return waitUntilValueMatches { true }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <T> LiveData<T>.waitForNonNullValue(): T {
|
||||||
|
return waitUntilValueMatches { it != null }!!
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.livedata
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MediatorLiveData
|
||||||
|
|
||||||
|
fun <T> LiveData<T>.ignoreUnchanged(): LiveData<T> {
|
||||||
|
val result = MediatorLiveData<T>()
|
||||||
|
var hadValue = false
|
||||||
|
|
||||||
|
result.addSource(this) {
|
||||||
|
if (it != result.value || !hadValue) {
|
||||||
|
hadValue = true
|
||||||
|
result.value = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.livedata
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import io.timelimit.android.async.Threads
|
||||||
|
|
||||||
|
fun <X> liveDataFromFunction(pollInterval: Long = 1000L, function: () -> X): LiveData<X> = object: LiveData<X>() {
|
||||||
|
val refresh = Runnable {
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refresh() {
|
||||||
|
value = function()
|
||||||
|
|
||||||
|
Threads.mainThreadHandler.postDelayed(refresh, pollInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActive() {
|
||||||
|
super.onActive()
|
||||||
|
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onInactive() {
|
||||||
|
super.onInactive()
|
||||||
|
|
||||||
|
Threads.mainThreadHandler.removeCallbacks(refresh)
|
||||||
|
}
|
||||||
|
}.ignoreUnchanged()
|
36
app/src/main/java/io/timelimit/android/livedata/Map.kt
Normal file
36
app/src/main/java/io/timelimit/android/livedata/Map.kt
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.livedata
|
||||||
|
|
||||||
|
import androidx.arch.core.util.Function
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.Transformations
|
||||||
|
|
||||||
|
fun <X, Y> LiveData<X>.map(function: Function<X, Y>): LiveData<Y> {
|
||||||
|
return Transformations.map(this, function)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <X, Y> LiveData<X>.map(function: (X) -> Y): LiveData<Y> {
|
||||||
|
return Transformations.map(this, function)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <X, Y> LiveData<X>.switchMap(function: Function<X, LiveData<Y>>): LiveData<Y> {
|
||||||
|
return Transformations.switchMap(this, function)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <X, Y> LiveData<X>.switchMap(function: (X) -> LiveData<Y>): LiveData<Y> {
|
||||||
|
return Transformations.switchMap(this, function)
|
||||||
|
}
|
140
app/src/main/java/io/timelimit/android/livedata/MergeLiveData.kt
Normal file
140
app/src/main/java/io/timelimit/android/livedata/MergeLiveData.kt
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.livedata
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MediatorLiveData
|
||||||
|
|
||||||
|
fun <T1, T2> mergeLiveData(d1: LiveData<T1>, d2: LiveData<T2>): LiveData<Pair<T1?, T2?>> {
|
||||||
|
val result = MediatorLiveData<Pair<T1?, T2?>>()
|
||||||
|
result.value = Pair(null, null)
|
||||||
|
|
||||||
|
result.addSource(d1) {
|
||||||
|
result.value = result.value!!.copy(first = it)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.addSource(d2) {
|
||||||
|
result.value = result.value!!.copy(second = it)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T1, T2, T3> mergeLiveData(d1: LiveData<T1>, d2: LiveData<T2>, d3: LiveData<T3>): LiveData<Triple<T1?, T2?, T3?>> {
|
||||||
|
val result = MediatorLiveData<Triple<T1?, T2?, T3?>>()
|
||||||
|
result.value = Triple(null, null, null)
|
||||||
|
|
||||||
|
result.addSource(d1) {
|
||||||
|
result.value = result.value!!.copy(first = it)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.addSource(d2) {
|
||||||
|
result.value = result.value!!.copy(second = it)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.addSource(d3) {
|
||||||
|
result.value = result.value!!.copy(third = it)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T1, T2, T3, T4> mergeLiveData(d1: LiveData<T1>, d2: LiveData<T2>, d3: LiveData<T3>, d4: LiveData<T4>): LiveData<FourTuple<T1?, T2?, T3?, T4?>> {
|
||||||
|
val result = MediatorLiveData<FourTuple<T1?, T2?, T3?, T4?>>()
|
||||||
|
result.value = FourTuple(null, null, null, null)
|
||||||
|
|
||||||
|
result.addSource(d1) {
|
||||||
|
result.value = result.value!!.copy(first = it)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.addSource(d2) {
|
||||||
|
result.value = result.value!!.copy(second = it)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.addSource(d3) {
|
||||||
|
result.value = result.value!!.copy(third = it)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.addSource(d4) {
|
||||||
|
result.value = result.value!!.copy(forth = it)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
data class FourTuple<A, B, C, D>(val first: A, val second: B, val third: C, val forth: D)
|
||||||
|
|
||||||
|
fun <T1, T2, T3, T4, T5> mergeLiveData(d1: LiveData<T1>, d2: LiveData<T2>, d3: LiveData<T3>, d4: LiveData<T4>, d5: LiveData<T5>): LiveData<FiveTuple<T1?, T2?, T3?, T4?, T5?>> {
|
||||||
|
val result = MediatorLiveData<FiveTuple<T1?, T2?, T3?, T4?, T5?>>()
|
||||||
|
result.value = FiveTuple(null, null, null, null, null)
|
||||||
|
|
||||||
|
result.addSource(d1) {
|
||||||
|
result.value = result.value!!.copy(first = it)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.addSource(d2) {
|
||||||
|
result.value = result.value!!.copy(second = it)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.addSource(d3) {
|
||||||
|
result.value = result.value!!.copy(third = it)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.addSource(d4) {
|
||||||
|
result.value = result.value!!.copy(forth = it)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.addSource(d5) {
|
||||||
|
result.value = result.value!!.copy(fifth = it)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
data class FiveTuple<A, B, C, D, E>(val first: A, val second: B, val third: C, val forth: D, val fifth: E)
|
||||||
|
|
||||||
|
fun <T1, T2, T3, T4, T5, T6> mergeLiveData(d1: LiveData<T1>, d2: LiveData<T2>, d3: LiveData<T3>, d4: LiveData<T4>, d5: LiveData<T5>, d6: LiveData<T6>): LiveData<SixTuple<T1?, T2?, T3?, T4?, T5?, T6?>> {
|
||||||
|
val result = MediatorLiveData<SixTuple<T1?, T2?, T3?, T4?, T5?, T6?>>()
|
||||||
|
result.value = SixTuple(null, null, null, null, null, null)
|
||||||
|
|
||||||
|
result.addSource(d1) {
|
||||||
|
result.value = result.value!!.copy(first = it)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.addSource(d2) {
|
||||||
|
result.value = result.value!!.copy(second = it)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.addSource(d3) {
|
||||||
|
result.value = result.value!!.copy(third = it)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.addSource(d4) {
|
||||||
|
result.value = result.value!!.copy(forth = it)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.addSource(d5) {
|
||||||
|
result.value = result.value!!.copy(fifth = it)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.addSource(d6) {
|
||||||
|
result.value = result.value!!.copy(sixth = it)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SixTuple<A, B, C, D, E, F>(val first: A, val second: B, val third: C, val forth: D, val fifth: E, val sixth: F)
|
|
@ -0,0 +1,118 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.livedata
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
abstract class LiveDataCache {
|
||||||
|
abstract fun reportLoopDone()
|
||||||
|
abstract fun removeAllItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
class SingleItemLiveDataCache<T>(private val liveData: LiveData<T>): LiveDataCache() {
|
||||||
|
private val dummyObserver = Observer<T> {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isObserving = false
|
||||||
|
private var wasUsed = false
|
||||||
|
|
||||||
|
fun read(): LiveData<T> {
|
||||||
|
if (!isObserving) {
|
||||||
|
liveData.observeForever(dummyObserver)
|
||||||
|
isObserving = true
|
||||||
|
}
|
||||||
|
|
||||||
|
wasUsed = true
|
||||||
|
|
||||||
|
return liveData
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeAllItems() {
|
||||||
|
if (isObserving) {
|
||||||
|
liveData.removeObserver(dummyObserver)
|
||||||
|
isObserving = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun reportLoopDone() {
|
||||||
|
if (isObserving && !wasUsed) {
|
||||||
|
removeAllItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
wasUsed = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class MultiKeyLiveDataCache<R, K>: LiveDataCache() {
|
||||||
|
class ItemWrapper<R>(val value: LiveData<R>, var used: Boolean)
|
||||||
|
|
||||||
|
private val items = ConcurrentHashMap<K, ItemWrapper<R>>()
|
||||||
|
|
||||||
|
private val dummyObserver = Observer<R> {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract fun createValue(key: K): LiveData<R>
|
||||||
|
|
||||||
|
fun get(key: K): LiveData<R> {
|
||||||
|
val oldItem = items[key]
|
||||||
|
|
||||||
|
if (oldItem != null) {
|
||||||
|
oldItem.used = true
|
||||||
|
|
||||||
|
return oldItem.value
|
||||||
|
} else {
|
||||||
|
val newItem = ItemWrapper(createValue(key), true)
|
||||||
|
newItem.value.observeForever(dummyObserver)
|
||||||
|
|
||||||
|
items[key] = newItem
|
||||||
|
|
||||||
|
return newItem.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun reportLoopDone() {
|
||||||
|
items.forEach {
|
||||||
|
if (it.value.used) {
|
||||||
|
it.value.used = false
|
||||||
|
} else {
|
||||||
|
it.value.value.removeObserver(dummyObserver)
|
||||||
|
items.remove(it.key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeAllItems() {
|
||||||
|
items.forEach {
|
||||||
|
it.value.value.removeObserver(dummyObserver)
|
||||||
|
}
|
||||||
|
|
||||||
|
items.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LiveDataCaches(private val caches: Array<LiveDataCache>) {
|
||||||
|
fun reportLoopDone() {
|
||||||
|
caches.forEach { it.reportLoopDone() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeAllItems() {
|
||||||
|
caches.forEach { it.removeAllItems() }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.logic
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import io.timelimit.android.coroutines.runAsync
|
||||||
|
import io.timelimit.android.data.RoomDatabase
|
||||||
|
import io.timelimit.android.data.backup.DatabaseBackup
|
||||||
|
import io.timelimit.android.integration.platform.android.AndroidIntegration
|
||||||
|
import io.timelimit.android.integration.time.RealTimeApi
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
|
||||||
|
object AndroidAppLogic {
|
||||||
|
private var instance: AppLogic? = null
|
||||||
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
|
fun with(context: Context): AppLogic {
|
||||||
|
val safeContext = context.applicationContext
|
||||||
|
|
||||||
|
if (Looper.getMainLooper() == Looper.myLooper()) {
|
||||||
|
// at the UI thread
|
||||||
|
if (instance == null) {
|
||||||
|
val isInitialized = MutableLiveData<Boolean>().apply { value = false }
|
||||||
|
|
||||||
|
instance = AppLogic(
|
||||||
|
platformIntegration = AndroidIntegration(safeContext),
|
||||||
|
timeApi = RealTimeApi,
|
||||||
|
database = RoomDatabase.with(safeContext),
|
||||||
|
context = safeContext,
|
||||||
|
isInitialized = isInitialized
|
||||||
|
)
|
||||||
|
|
||||||
|
runAsync {
|
||||||
|
DatabaseBackup.with(safeContext).tryRestoreDatabaseBackupAsyncAndWait()
|
||||||
|
isInitialized.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// at a background thread
|
||||||
|
if (instance == null) {
|
||||||
|
val latch = CountDownLatch(1)
|
||||||
|
|
||||||
|
handler.post {
|
||||||
|
with(context)
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
latch.await()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance!!
|
||||||
|
}
|
||||||
|
}
|
76
app/src/main/java/io/timelimit/android/logic/AppLogic.kt
Normal file
76
app/src/main/java/io/timelimit/android/logic/AppLogic.kt
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.logic
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.Transformations
|
||||||
|
import io.timelimit.android.data.Database
|
||||||
|
import io.timelimit.android.data.model.Device
|
||||||
|
import io.timelimit.android.data.model.User
|
||||||
|
import io.timelimit.android.integration.platform.PlatformIntegration
|
||||||
|
import io.timelimit.android.integration.time.TimeApi
|
||||||
|
import io.timelimit.android.livedata.ignoreUnchanged
|
||||||
|
import io.timelimit.android.livedata.liveDataFromValue
|
||||||
|
import io.timelimit.android.livedata.switchMap
|
||||||
|
|
||||||
|
class AppLogic(
|
||||||
|
val platformIntegration: PlatformIntegration,
|
||||||
|
val timeApi: TimeApi,
|
||||||
|
val database: Database,
|
||||||
|
val context: Context,
|
||||||
|
val isInitialized: LiveData<Boolean>
|
||||||
|
) {
|
||||||
|
val enable = MutableLiveData<Boolean>().apply { value = true }
|
||||||
|
|
||||||
|
val deviceId = database.config().getOwnDeviceId()
|
||||||
|
|
||||||
|
val deviceEntry = Transformations.switchMap<String?, Device?> (deviceId) {
|
||||||
|
if (it == null) {
|
||||||
|
liveDataFromValue(null)
|
||||||
|
} else {
|
||||||
|
database.device().getDeviceById(it)
|
||||||
|
}
|
||||||
|
}.ignoreUnchanged()
|
||||||
|
|
||||||
|
val deviceEntryIfEnabled = enable.switchMap {
|
||||||
|
if (it == null || it == false) {
|
||||||
|
liveDataFromValue(null as Device?)
|
||||||
|
} else {
|
||||||
|
deviceEntry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val deviceUserId: LiveData<String> = Transformations.map(deviceEntry) { it?.currentUserId ?: "" }
|
||||||
|
|
||||||
|
val deviceUserEntry = deviceUserId.switchMap {
|
||||||
|
if (it == "") {
|
||||||
|
liveDataFromValue(null as User?)
|
||||||
|
} else {
|
||||||
|
database.user().getUserByIdLive(it)
|
||||||
|
}
|
||||||
|
}.ignoreUnchanged()
|
||||||
|
|
||||||
|
val backgroundTaskLogic = BackgroundTaskLogic(this)
|
||||||
|
val appSetupLogic = AppSetupLogic(this)
|
||||||
|
private val syncAppsLogic = SyncInstalledAppsLogic(this)
|
||||||
|
val manipulationLogic = ManipulationLogic(this)
|
||||||
|
|
||||||
|
fun shutdown() {
|
||||||
|
enable.value = false
|
||||||
|
}
|
||||||
|
}
|
185
app/src/main/java/io/timelimit/android/logic/AppSetupLogic.kt
Normal file
185
app/src/main/java/io/timelimit/android/logic/AppSetupLogic.kt
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.logic
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.jaredrummler.android.device.DeviceName
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.async.Threads
|
||||||
|
import io.timelimit.android.coroutines.executeAndWait
|
||||||
|
import io.timelimit.android.crypto.PasswordHashing
|
||||||
|
import io.timelimit.android.data.IdGenerator
|
||||||
|
import io.timelimit.android.data.backup.DatabaseBackup
|
||||||
|
import io.timelimit.android.data.customtypes.ImmutableBitmask
|
||||||
|
import io.timelimit.android.data.model.*
|
||||||
|
import io.timelimit.android.integration.platform.NewPermissionStatus
|
||||||
|
import io.timelimit.android.integration.platform.ProtectionLevel
|
||||||
|
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
||||||
|
import io.timelimit.android.ui.user.create.DefaultCategories
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class AppSetupLogic(private val appLogic: AppLogic) {
|
||||||
|
suspend fun setupForLocalUse(parentPassword: String, context: Context) {
|
||||||
|
Threads.database.executeAndWait(Runnable {
|
||||||
|
run {
|
||||||
|
// assert that the device is not yet configured
|
||||||
|
val oldDeviceId = appLogic.database.config().getOwnDeviceIdSync()
|
||||||
|
|
||||||
|
if (oldDeviceId != null) {
|
||||||
|
throw IllegalStateException("already configured")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val ownDeviceId = IdGenerator.generateId()
|
||||||
|
val parentUserId = IdGenerator.generateId()
|
||||||
|
val childUserId = IdGenerator.generateId()
|
||||||
|
val allowedAppsCategoryId = IdGenerator.generateId()
|
||||||
|
val allowedGamesCategoryId = IdGenerator.generateId()
|
||||||
|
|
||||||
|
appLogic.database.beginTransaction()
|
||||||
|
try {
|
||||||
|
run {
|
||||||
|
// just for safety: delete everything
|
||||||
|
appLogic.database.deleteAllData()
|
||||||
|
}
|
||||||
|
|
||||||
|
run {
|
||||||
|
// set device id
|
||||||
|
appLogic.database.config().setOwnDeviceIdSync(ownDeviceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
val timeZone = appLogic.timeApi.getSystemTimeZone().id
|
||||||
|
|
||||||
|
run {
|
||||||
|
// add device
|
||||||
|
val deviceName = DeviceName.getDeviceName()
|
||||||
|
|
||||||
|
val device = Device(
|
||||||
|
id = ownDeviceId,
|
||||||
|
name = deviceName,
|
||||||
|
model = deviceName,
|
||||||
|
addedAt = appLogic.timeApi.getCurrentTimeInMillis(),
|
||||||
|
currentUserId = childUserId,
|
||||||
|
currentProtectionLevel = ProtectionLevel.None,
|
||||||
|
highestProtectionLevel = ProtectionLevel.None,
|
||||||
|
currentNotificationAccessPermission = NewPermissionStatus.NotGranted,
|
||||||
|
highestNotificationAccessPermission = NewPermissionStatus.NotGranted,
|
||||||
|
currentUsageStatsPermission = RuntimePermissionStatus.NotGranted,
|
||||||
|
highestUsageStatsPermission = RuntimePermissionStatus.NotGranted,
|
||||||
|
currentAppVersion = 0,
|
||||||
|
highestAppVersion = 0,
|
||||||
|
manipulationTriedDisablingDeviceAdmin = false,
|
||||||
|
hadManipulation = false
|
||||||
|
)
|
||||||
|
|
||||||
|
appLogic.database.device().addDeviceSync(device)
|
||||||
|
}
|
||||||
|
|
||||||
|
run {
|
||||||
|
// add child
|
||||||
|
|
||||||
|
val child = User(
|
||||||
|
id = childUserId,
|
||||||
|
name = context.getString(R.string.setup_username_child),
|
||||||
|
password = "",
|
||||||
|
type = UserType.Child,
|
||||||
|
timeZone = timeZone,
|
||||||
|
disableLimitsUntil = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
appLogic.database.user().addUserSync(child)
|
||||||
|
}
|
||||||
|
|
||||||
|
run {
|
||||||
|
// add parent
|
||||||
|
|
||||||
|
val parent = User(
|
||||||
|
id = parentUserId,
|
||||||
|
name = context.getString(R.string.setup_username_parent),
|
||||||
|
password = PasswordHashing.hashSync(parentPassword),
|
||||||
|
type = UserType.Parent,
|
||||||
|
timeZone = timeZone,
|
||||||
|
disableLimitsUntil = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
appLogic.database.user().addUserSync(parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
val installedApps = appLogic.platformIntegration.getLocalApps()
|
||||||
|
|
||||||
|
// add installed apps
|
||||||
|
appLogic.database.app().addAppsSync(installedApps)
|
||||||
|
|
||||||
|
val defaultCategories = DefaultCategories.with(context)
|
||||||
|
|
||||||
|
// NOTE: the default config is created at the AddUserModel and at the AppSetupLogic
|
||||||
|
run {
|
||||||
|
// add starter categories
|
||||||
|
appLogic.database.category().addCategory(Category(
|
||||||
|
id = allowedAppsCategoryId,
|
||||||
|
childId = childUserId,
|
||||||
|
title = defaultCategories.allowedAppsTitle,
|
||||||
|
blockedMinutesInWeek = ImmutableBitmask((BitSet())),
|
||||||
|
extraTimeInMillis = 0,
|
||||||
|
temporarilyBlocked = false
|
||||||
|
))
|
||||||
|
|
||||||
|
appLogic.database.category().addCategory(Category(
|
||||||
|
id = allowedGamesCategoryId,
|
||||||
|
childId = childUserId,
|
||||||
|
title = defaultCategories.allowedGamesTitle,
|
||||||
|
blockedMinutesInWeek = defaultCategories.allowedGamesBlockedTimes,
|
||||||
|
extraTimeInMillis = 0,
|
||||||
|
temporarilyBlocked = false
|
||||||
|
))
|
||||||
|
|
||||||
|
// add default allowed apps
|
||||||
|
appLogic.database.categoryApp().addCategoryAppsSync(
|
||||||
|
installedApps
|
||||||
|
.filter { it.recommendation == AppRecommendation.Whitelist }
|
||||||
|
.map {
|
||||||
|
CategoryApp(
|
||||||
|
categoryId = allowedAppsCategoryId,
|
||||||
|
packageName = it.packageName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// add default time limit rules
|
||||||
|
defaultCategories.generateGamesTimeLimitRules(allowedGamesCategoryId).forEach { rule ->
|
||||||
|
appLogic.database.timeLimitRules().addTimeLimitRule(rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appLogic.database.setTransactionSuccessful()
|
||||||
|
} finally {
|
||||||
|
appLogic.database.endTransaction()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
DatabaseBackup.with(appLogic.context).tryCreateDatabaseBackupAsync()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun dangerousResetApp() {
|
||||||
|
Threads.database.executeAndWait(Runnable {
|
||||||
|
// this is already wrapped in a transaction
|
||||||
|
appLogic.database.deleteAllData()
|
||||||
|
})
|
||||||
|
|
||||||
|
// delete the old config
|
||||||
|
DatabaseBackup.with(appLogic.context).tryCreateDatabaseBackupAsync()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,463 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.logic
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import android.util.SparseArray
|
||||||
|
import android.util.SparseLongArray
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import io.timelimit.android.BuildConfig
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.async.Threads
|
||||||
|
import io.timelimit.android.coroutines.executeAndWait
|
||||||
|
import io.timelimit.android.coroutines.runAsync
|
||||||
|
import io.timelimit.android.coroutines.runAsyncExpectForever
|
||||||
|
import io.timelimit.android.data.backup.DatabaseBackup
|
||||||
|
import io.timelimit.android.data.model.*
|
||||||
|
import io.timelimit.android.date.DateInTimezone
|
||||||
|
import io.timelimit.android.date.getMinuteOfWeek
|
||||||
|
import io.timelimit.android.integration.platform.AppStatusMessage
|
||||||
|
import io.timelimit.android.integration.platform.ProtectionLevel
|
||||||
|
import io.timelimit.android.integration.platform.android.AndroidIntegrationApps
|
||||||
|
import io.timelimit.android.livedata.*
|
||||||
|
import io.timelimit.android.sync.actions.UpdateDeviceStatusAction
|
||||||
|
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||||
|
import io.timelimit.android.util.TimeTextUtil
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
|
companion object {
|
||||||
|
private const val CHECK_PERMISSION_INTERVAL = 10 * 1000L // all 10 seconds
|
||||||
|
private const val BACKGROUND_SERVICE_INTERVAL = 100L // all 100 ms
|
||||||
|
private const val MAX_USED_TIME_PER_ROUND = 1000 // 1 second
|
||||||
|
private const val LOG_TAG = "BackgroundTaskLogic"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val temporarilyAllowedApps = appLogic.database.temporarilyAllowedApp().getTemporarilyAllowedApps()
|
||||||
|
|
||||||
|
init {
|
||||||
|
runAsyncExpectForever { backgroundServiceLoop() }
|
||||||
|
runAsyncExpectForever { syncDeviceStatusLoop() }
|
||||||
|
runAsyncExpectForever { backupDatabaseLoop() }
|
||||||
|
runAsync {
|
||||||
|
// this is effective after an reboot
|
||||||
|
|
||||||
|
if (appLogic.deviceEntryIfEnabled.waitForNullableValue() != null) {
|
||||||
|
appLogic.platformIntegration.setEnableSystemLockdown(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appLogic.deviceEntryIfEnabled
|
||||||
|
.map { it?.id }
|
||||||
|
.ignoreUnchanged()
|
||||||
|
.observeForever {
|
||||||
|
_ ->
|
||||||
|
|
||||||
|
runAsync {
|
||||||
|
syncInstalledAppVersion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
temporarilyAllowedApps.map { it.isNotEmpty() }.ignoreUnchanged().observeForever {
|
||||||
|
appLogic.platformIntegration.setShowNotificationToRevokeTemporarilyAllowedApps(it!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val deviceUserEntryLive = SingleItemLiveDataCache(appLogic.deviceUserEntry.ignoreUnchanged())
|
||||||
|
private val childCategories = object: MultiKeyLiveDataCache<List<Category>, String?>() {
|
||||||
|
// key = child id
|
||||||
|
override fun createValue(key: String?): LiveData<List<Category>> {
|
||||||
|
if (key == null) {
|
||||||
|
// this should rarely happen
|
||||||
|
return liveDataFromValue(Collections.emptyList())
|
||||||
|
} else {
|
||||||
|
return appLogic.database.category().getCategoriesByChildId(key).ignoreUnchanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private val appCategories = object: MultiKeyLiveDataCache<CategoryApp?, Pair<String, List<String>>>() {
|
||||||
|
// key = package name, category ids
|
||||||
|
override fun createValue(key: Pair<String, List<String>>): LiveData<CategoryApp?> {
|
||||||
|
return appLogic.database.categoryApp().getCategoryApp(key.second, key.first)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private val timeLimitRules = object: MultiKeyLiveDataCache<List<TimeLimitRule>, String>() {
|
||||||
|
override fun createValue(key: String): LiveData<List<TimeLimitRule>> {
|
||||||
|
return appLogic.database.timeLimitRules().getTimeLimitRulesByCategory(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private val usedTimesOfCategoryAndWeekByFirstDayOfWeek = object: MultiKeyLiveDataCache<SparseArray<UsedTimeItem>, Pair<String, Int>>() {
|
||||||
|
override fun createValue(key: Pair<String, Int>): LiveData<SparseArray<UsedTimeItem>> {
|
||||||
|
return appLogic.database.usedTimes().getUsedTimesOfWeek(key.first, key.second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val liveDataCaches = LiveDataCaches(arrayOf(
|
||||||
|
deviceUserEntryLive,
|
||||||
|
childCategories,
|
||||||
|
appCategories,
|
||||||
|
timeLimitRules,
|
||||||
|
usedTimesOfCategoryAndWeekByFirstDayOfWeek
|
||||||
|
))
|
||||||
|
|
||||||
|
private var usedTimeUpdateHelper: UsedTimeItemBatchUpdateHelper? = null
|
||||||
|
private var previousMainLogicExecutionTime = 0
|
||||||
|
private var previousMainLoopEndTime = 0L
|
||||||
|
|
||||||
|
private val appTitleCache = QueryAppTitleCache(appLogic.platformIntegration)
|
||||||
|
|
||||||
|
private suspend fun backgroundServiceLoop() {
|
||||||
|
while (true) {
|
||||||
|
// app must be enabled
|
||||||
|
if (!appLogic.enable.waitForNonNullValue()) {
|
||||||
|
usedTimeUpdateHelper?.commit(appLogic)
|
||||||
|
liveDataCaches.removeAllItems()
|
||||||
|
appLogic.platformIntegration.setAppStatusMessage(null)
|
||||||
|
appLogic.enable.waitUntilValueMatches { it == true }
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// device must be used by a child
|
||||||
|
val deviceUserEntry = deviceUserEntryLive.read().waitForNullableValue()
|
||||||
|
|
||||||
|
if (deviceUserEntry == null || deviceUserEntry.type != UserType.Child) {
|
||||||
|
usedTimeUpdateHelper?.commit(appLogic)
|
||||||
|
liveDataCaches.removeAllItems()
|
||||||
|
appLogic.platformIntegration.setAppStatusMessage(null)
|
||||||
|
deviceUserEntryLive.read().waitUntilValueMatches { it != null && it.type == UserType.Child }
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// loop logic
|
||||||
|
try {
|
||||||
|
// get the current time
|
||||||
|
val nowTimestamp = appLogic.timeApi.getCurrentTimeInMillis()
|
||||||
|
|
||||||
|
// get the categories
|
||||||
|
val categories = childCategories.get(deviceUserEntry.id).waitForNonNullValue()
|
||||||
|
val temporarilyAllowedApps = temporarilyAllowedApps.waitForNonNullValue()
|
||||||
|
|
||||||
|
// get the current status
|
||||||
|
val isScreenOn = appLogic.platformIntegration.isScreenOn()
|
||||||
|
|
||||||
|
if (!isScreenOn) {
|
||||||
|
if (temporarilyAllowedApps.isNotEmpty()) {
|
||||||
|
resetTemporarilyAllowedApps()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val foregroundAppPackageName = appLogic.platformIntegration.getForegroundAppPackageName()
|
||||||
|
// the following is not executed if the permission is missing
|
||||||
|
|
||||||
|
if (foregroundAppPackageName == BuildConfig.APPLICATION_ID) {
|
||||||
|
// this app itself runs now -> no need for an status message
|
||||||
|
usedTimeUpdateHelper?.commit(appLogic)
|
||||||
|
appLogic.platformIntegration.setAppStatusMessage(null)
|
||||||
|
} else if (foregroundAppPackageName != null && AndroidIntegrationApps.ignoredApps.contains(foregroundAppPackageName)) {
|
||||||
|
usedTimeUpdateHelper?.commit(appLogic)
|
||||||
|
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||||
|
appTitleCache.query(foregroundAppPackageName),
|
||||||
|
appLogic.context.getString(R.string.background_logic_whitelisted)
|
||||||
|
))
|
||||||
|
} else if (foregroundAppPackageName != null && temporarilyAllowedApps.contains(foregroundAppPackageName)) {
|
||||||
|
usedTimeUpdateHelper?.commit(appLogic)
|
||||||
|
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||||
|
appTitleCache.query(foregroundAppPackageName),
|
||||||
|
appLogic.context.getString(R.string.background_logic_temporarily_allowed)
|
||||||
|
))
|
||||||
|
} else if (foregroundAppPackageName != null) {
|
||||||
|
val appCategory = appCategories.get(Pair(foregroundAppPackageName, categories.map { it.id })).waitForNullableValue()
|
||||||
|
val category = categories.find { it.id == appCategory?.categoryId }
|
||||||
|
|
||||||
|
if (category == null) {
|
||||||
|
usedTimeUpdateHelper?.commit(appLogic)
|
||||||
|
|
||||||
|
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||||
|
title = appTitleCache.query(foregroundAppPackageName),
|
||||||
|
text = appLogic.context.getString(R.string.background_logic_opening_lockscreen)
|
||||||
|
))
|
||||||
|
appLogic.platformIntegration.setSuspendedApps(listOf(foregroundAppPackageName), true)
|
||||||
|
appLogic.platformIntegration.showAppLockScreen(foregroundAppPackageName)
|
||||||
|
} else if (category.temporarilyBlocked) {
|
||||||
|
usedTimeUpdateHelper?.commit(appLogic)
|
||||||
|
|
||||||
|
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||||
|
title = appTitleCache.query(foregroundAppPackageName),
|
||||||
|
text = appLogic.context.getString(R.string.background_logic_opening_lockscreen)
|
||||||
|
))
|
||||||
|
appLogic.platformIntegration.showAppLockScreen(foregroundAppPackageName)
|
||||||
|
} else {
|
||||||
|
val nowTimezone = TimeZone.getTimeZone(deviceUserEntry.timeZone)
|
||||||
|
|
||||||
|
val nowDate = DateInTimezone.newInstance(nowTimestamp, nowTimezone)
|
||||||
|
val minuteOfWeek = getMinuteOfWeek(nowTimestamp, nowTimezone)
|
||||||
|
|
||||||
|
// disable time limits temporarily feature
|
||||||
|
if (nowTimestamp < deviceUserEntry.disableLimitsUntil) {
|
||||||
|
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||||
|
title = appTitleCache.query(foregroundAppPackageName),
|
||||||
|
text = appLogic.context.getString(R.string.background_logic_limits_disabled)
|
||||||
|
))
|
||||||
|
} else if (
|
||||||
|
// check blocked time areas
|
||||||
|
(category.blockedMinutesInWeek.read(minuteOfWeek))
|
||||||
|
) {
|
||||||
|
usedTimeUpdateHelper?.commit(appLogic)
|
||||||
|
|
||||||
|
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||||
|
title = appTitleCache.query(foregroundAppPackageName),
|
||||||
|
text = appLogic.context.getString(R.string.background_logic_opening_lockscreen)
|
||||||
|
))
|
||||||
|
appLogic.platformIntegration.showAppLockScreen(foregroundAppPackageName)
|
||||||
|
} else {
|
||||||
|
// check time limits
|
||||||
|
val rules = timeLimitRules.get(category.id).waitForNonNullValue()
|
||||||
|
|
||||||
|
if (rules.isEmpty()) {
|
||||||
|
// unlimited
|
||||||
|
usedTimeUpdateHelper?.commit(appLogic)
|
||||||
|
|
||||||
|
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||||
|
category.title + " - " + appTitleCache.query(foregroundAppPackageName),
|
||||||
|
appLogic.context.getString(R.string.background_logic_no_timelimit)
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
val usedTimes = usedTimesOfCategoryAndWeekByFirstDayOfWeek.get(Pair(category.id, nowDate.dayOfEpoch - nowDate.dayOfWeek)).waitForNonNullValue()
|
||||||
|
|
||||||
|
val newUsedTimeItemBatchUpdateHelper = UsedTimeItemBatchUpdateHelper.eventuallyUpdateInstance(
|
||||||
|
date = nowDate,
|
||||||
|
categoryId = category.id,
|
||||||
|
oldInstance = usedTimeUpdateHelper,
|
||||||
|
usedTimeItemForDay = usedTimes.get(nowDate.dayOfWeek),
|
||||||
|
logic = appLogic
|
||||||
|
)
|
||||||
|
usedTimeUpdateHelper = newUsedTimeItemBatchUpdateHelper
|
||||||
|
|
||||||
|
val usedTimesSparseArray = SparseLongArray()
|
||||||
|
|
||||||
|
for (i in 0..6) {
|
||||||
|
val usedTimesItem = usedTimes[i]?.usedMillis
|
||||||
|
|
||||||
|
if (newUsedTimeItemBatchUpdateHelper.date.dayOfWeek == i) {
|
||||||
|
usedTimesSparseArray.put(i, newUsedTimeItemBatchUpdateHelper.getTotalUsedTime())
|
||||||
|
} else {
|
||||||
|
usedTimesSparseArray.put(i, (if (usedTimesItem != null) usedTimesItem else 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val remaining = RemainingTime.getRemainingTime(
|
||||||
|
nowDate.dayOfWeek, usedTimesSparseArray, rules,
|
||||||
|
Math.max(0, category.extraTimeInMillis - newUsedTimeItemBatchUpdateHelper.getCachedExtraTimeToSubtract())
|
||||||
|
)
|
||||||
|
|
||||||
|
if (remaining == null) {
|
||||||
|
// unlimited
|
||||||
|
|
||||||
|
usedTimeUpdateHelper?.commit(appLogic)
|
||||||
|
|
||||||
|
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||||
|
category.title + " - " + appTitleCache.query(foregroundAppPackageName),
|
||||||
|
appLogic.context.getString(R.string.background_logic_no_timelimit)
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
// time limited
|
||||||
|
if (remaining.includingExtraTime > 0) {
|
||||||
|
if (remaining.default == 0L) {
|
||||||
|
// using extra time
|
||||||
|
|
||||||
|
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||||
|
category.title + " - " + appTitleCache.query(foregroundAppPackageName),
|
||||||
|
appLogic.context.getString(R.string.background_logic_using_extra_time, TimeTextUtil.remaining(remaining.includingExtraTime.toInt(), appLogic.context))
|
||||||
|
))
|
||||||
|
|
||||||
|
if (isScreenOn) {
|
||||||
|
newUsedTimeItemBatchUpdateHelper.addUsedTime(
|
||||||
|
Math.min(previousMainLogicExecutionTime, MAX_USED_TIME_PER_ROUND), // never save more than a second of used time
|
||||||
|
true,
|
||||||
|
appLogic
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// using normal contingent
|
||||||
|
|
||||||
|
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||||
|
category.title + " - " + appTitleCache.query(foregroundAppPackageName),
|
||||||
|
TimeTextUtil.remaining(remaining.default.toInt(), appLogic.context)
|
||||||
|
))
|
||||||
|
|
||||||
|
if (isScreenOn) {
|
||||||
|
newUsedTimeItemBatchUpdateHelper.addUsedTime(
|
||||||
|
Math.min(previousMainLogicExecutionTime, MAX_USED_TIME_PER_ROUND), // never save more than a second of used time
|
||||||
|
false,
|
||||||
|
appLogic
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// there is not time anymore
|
||||||
|
|
||||||
|
newUsedTimeItemBatchUpdateHelper.commit(appLogic)
|
||||||
|
|
||||||
|
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||||
|
title = appTitleCache.query(foregroundAppPackageName),
|
||||||
|
text = appLogic.context.getString(R.string.background_logic_opening_lockscreen)
|
||||||
|
))
|
||||||
|
appLogic.platformIntegration.showAppLockScreen(foregroundAppPackageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||||
|
appLogic.context.getString(R.string.background_logic_idle_title),
|
||||||
|
appLogic.context.getString(R.string.background_logic_idle_text)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
} catch (ex: SecurityException) {
|
||||||
|
// this is handled by an other main loop (with a delay)
|
||||||
|
|
||||||
|
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||||
|
appLogic.context.getString(R.string.background_logic_error),
|
||||||
|
appLogic.context.getString(R.string.background_logic_error_permission)
|
||||||
|
))
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.w(LOG_TAG, "exception during running main loop", ex)
|
||||||
|
}
|
||||||
|
|
||||||
|
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||||
|
appLogic.context.getString(R.string.background_logic_error),
|
||||||
|
appLogic.context.getString(R.string.background_logic_error_internal)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
liveDataCaches.reportLoopDone()
|
||||||
|
|
||||||
|
// delay before running next time
|
||||||
|
val endTime = appLogic.timeApi.getCurrentUptimeInMillis()
|
||||||
|
previousMainLogicExecutionTime = (endTime - previousMainLoopEndTime).toInt()
|
||||||
|
previousMainLoopEndTime = endTime
|
||||||
|
|
||||||
|
val timeToWait = Math.max(10, BACKGROUND_SERVICE_INTERVAL - previousMainLogicExecutionTime)
|
||||||
|
appLogic.timeApi.sleep(timeToWait)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun syncInstalledAppVersion() {
|
||||||
|
val currentAppVersion = BuildConfig.VERSION_CODE
|
||||||
|
val deviceEntry = appLogic.deviceEntry.waitForNullableValue()
|
||||||
|
|
||||||
|
if (deviceEntry != null) {
|
||||||
|
if (deviceEntry.currentAppVersion != currentAppVersion) {
|
||||||
|
ApplyActionUtil.applyAppLogicAction(
|
||||||
|
UpdateDeviceStatusAction(
|
||||||
|
newProtectionLevel = null,
|
||||||
|
newUsageStatsPermissionStatus = null,
|
||||||
|
newNotificationAccessPermission = null,
|
||||||
|
newAppVersion = currentAppVersion
|
||||||
|
),
|
||||||
|
appLogic
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun syncDeviceStatusAsync() {
|
||||||
|
runAsync {
|
||||||
|
syncDeviceStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun syncDeviceStatusLoop() {
|
||||||
|
while (true) {
|
||||||
|
appLogic.deviceEntryIfEnabled.waitUntilValueMatches { it != null }
|
||||||
|
|
||||||
|
syncDeviceStatus()
|
||||||
|
|
||||||
|
appLogic.timeApi.sleep(CHECK_PERMISSION_INTERVAL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val syncDeviceStatusLock = Mutex()
|
||||||
|
|
||||||
|
private suspend fun syncDeviceStatus() {
|
||||||
|
syncDeviceStatusLock.withLock {
|
||||||
|
val deviceEntry = appLogic.deviceEntry.waitForNullableValue()
|
||||||
|
|
||||||
|
if (deviceEntry != null) {
|
||||||
|
val protectionLevel = appLogic.platformIntegration.getCurrentProtectionLevel()
|
||||||
|
val usageStatsPermission = appLogic.platformIntegration.getForegroundAppPermissionStatus()
|
||||||
|
val notificationAccess = appLogic.platformIntegration.getNotificationAccessPermissionStatus()
|
||||||
|
|
||||||
|
val emptyChanges = UpdateDeviceStatusAction(
|
||||||
|
newProtectionLevel = null,
|
||||||
|
newUsageStatsPermissionStatus = null,
|
||||||
|
newNotificationAccessPermission = null,
|
||||||
|
newAppVersion = null
|
||||||
|
)
|
||||||
|
|
||||||
|
var changes = emptyChanges
|
||||||
|
|
||||||
|
if (protectionLevel != deviceEntry.currentProtectionLevel) {
|
||||||
|
changes = changes.copy(
|
||||||
|
newProtectionLevel = protectionLevel
|
||||||
|
)
|
||||||
|
|
||||||
|
if (protectionLevel == ProtectionLevel.DeviceOwner) {
|
||||||
|
appLogic.platformIntegration.setEnableSystemLockdown(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usageStatsPermission != deviceEntry.currentUsageStatsPermission) {
|
||||||
|
changes = changes.copy(
|
||||||
|
newUsageStatsPermissionStatus = usageStatsPermission
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notificationAccess != deviceEntry.currentNotificationAccessPermission) {
|
||||||
|
changes = changes.copy(
|
||||||
|
newNotificationAccessPermission = notificationAccess
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changes != emptyChanges) {
|
||||||
|
ApplyActionUtil.applyAppLogicAction(changes, appLogic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun resetTemporarilyAllowedApps() {
|
||||||
|
Threads.database.executeAndWait(Runnable {
|
||||||
|
appLogic.database.temporarilyAllowedApp().removeAllTemporarilyAllowedAppsSync()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun backupDatabaseLoop() {
|
||||||
|
appLogic.timeApi.sleep(1000 * 60 * 5 /* 5 minutes */)
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
DatabaseBackup.with(appLogic.context).tryCreateDatabaseBackupAsync()
|
||||||
|
|
||||||
|
appLogic.timeApi.sleep(1000 * 60 * 60 * 3 /* 3 hours */)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
235
app/src/main/java/io/timelimit/android/logic/BlockingReason.kt
Normal file
235
app/src/main/java/io/timelimit/android/logic/BlockingReason.kt
Normal file
|
@ -0,0 +1,235 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.logic
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import android.util.SparseLongArray
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.Transformations
|
||||||
|
import io.timelimit.android.BuildConfig
|
||||||
|
import io.timelimit.android.data.model.Category
|
||||||
|
import io.timelimit.android.data.model.TimeLimitRule
|
||||||
|
import io.timelimit.android.data.model.User
|
||||||
|
import io.timelimit.android.data.model.UserType
|
||||||
|
import io.timelimit.android.date.DateInTimezone
|
||||||
|
import io.timelimit.android.date.getMinuteOfWeek
|
||||||
|
import io.timelimit.android.integration.platform.android.AndroidIntegrationApps
|
||||||
|
import io.timelimit.android.integration.time.TimeApi
|
||||||
|
import io.timelimit.android.livedata.*
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
enum class BlockingReason {
|
||||||
|
None,
|
||||||
|
NotPartOfAnCategory,
|
||||||
|
TemporarilyBlocked,
|
||||||
|
BlockedAtThisTime,
|
||||||
|
TimeOver,
|
||||||
|
TimeOverExtraTimeCanBeUsedLater
|
||||||
|
}
|
||||||
|
|
||||||
|
class BlockingReasonUtil(private val appLogic: AppLogic) {
|
||||||
|
companion object {
|
||||||
|
private const val LOG_TAG = "BlockingReason"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBlockingReason(packageName: String): LiveData<BlockingReason> {
|
||||||
|
// check precondition that the app is running
|
||||||
|
|
||||||
|
return appLogic.enable.switchMap {
|
||||||
|
enabled ->
|
||||||
|
|
||||||
|
if (enabled == null || enabled == false) {
|
||||||
|
liveDataFromValue(BlockingReason.None)
|
||||||
|
} else {
|
||||||
|
appLogic.deviceUserEntry.switchMap {
|
||||||
|
user ->
|
||||||
|
|
||||||
|
if (user == null || user.type != UserType.Child) {
|
||||||
|
liveDataFromValue(BlockingReason.None)
|
||||||
|
} else {
|
||||||
|
getBlockingReasonStep2(packageName, user, TimeZone.getTimeZone(user.timeZone))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getBlockingReasonStep2(packageName: String, child: User, timeZone: TimeZone): LiveData<BlockingReason> {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "step 2")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check internal whitelist
|
||||||
|
if (packageName == BuildConfig.APPLICATION_ID) {
|
||||||
|
return liveDataFromValue(BlockingReason.None)
|
||||||
|
} else if (AndroidIntegrationApps.ignoredApps.contains(packageName)) {
|
||||||
|
return liveDataFromValue(BlockingReason.None)
|
||||||
|
} else {
|
||||||
|
return getBlockingReasonStep3(packageName, child, timeZone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getBlockingReasonStep3(packageName: String, child: User, timeZone: TimeZone): LiveData<BlockingReason> {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "step 3")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check temporarily allowed Apps
|
||||||
|
return appLogic.database.temporarilyAllowedApp().getTemporarilyAllowedApps().switchMap {
|
||||||
|
temporarilyAllowedApps ->
|
||||||
|
|
||||||
|
if (temporarilyAllowedApps.contains(packageName)) {
|
||||||
|
liveDataFromValue(BlockingReason.None)
|
||||||
|
} else {
|
||||||
|
getBlockingReasonStep4(packageName, child, timeZone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getBlockingReasonStep4(packageName: String, child: User, timeZone: TimeZone): LiveData<BlockingReason> {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "step 4")
|
||||||
|
}
|
||||||
|
|
||||||
|
return appLogic.database.category().getCategoriesByChildId(child.id).switchMap {
|
||||||
|
childCategories ->
|
||||||
|
|
||||||
|
Transformations.map(appLogic.database.categoryApp().getCategoryApp(childCategories.map { it.id }, packageName)) {
|
||||||
|
categoryApp ->
|
||||||
|
|
||||||
|
if (categoryApp == null) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
childCategories.find { it.id == categoryApp.categoryId }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.switchMap {
|
||||||
|
categoryEntry ->
|
||||||
|
|
||||||
|
if (categoryEntry == null) {
|
||||||
|
liveDataFromValue(BlockingReason.NotPartOfAnCategory)
|
||||||
|
} else if (categoryEntry.temporarilyBlocked) {
|
||||||
|
liveDataFromValue(BlockingReason.TemporarilyBlocked)
|
||||||
|
} else {
|
||||||
|
getBlockingReasonStep4Point5(categoryEntry, child, timeZone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getBlockingReasonStep4Point5(category: Category, child: User, timeZone: TimeZone): LiveData<BlockingReason> {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "step 4.5")
|
||||||
|
}
|
||||||
|
|
||||||
|
val areLimitsDisabled: LiveData<Boolean>
|
||||||
|
|
||||||
|
if (child.disableLimitsUntil == 0L) {
|
||||||
|
areLimitsDisabled = liveDataFromValue(false)
|
||||||
|
} else {
|
||||||
|
areLimitsDisabled = timeInMillis.map { timeInMillis ->
|
||||||
|
child.disableLimitsUntil > timeInMillis
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return areLimitsDisabled.switchMap {
|
||||||
|
limitsDisabled ->
|
||||||
|
|
||||||
|
if (limitsDisabled) {
|
||||||
|
liveDataFromValue(BlockingReason.None)
|
||||||
|
} else {
|
||||||
|
getBlockingReasonStep5(category, timeZone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getBlockingReasonStep5(category: Category, timeZone: TimeZone): LiveData<BlockingReason> {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "step 5")
|
||||||
|
}
|
||||||
|
|
||||||
|
return Transformations.switchMap(getMinuteOfWeekLive(appLogic.timeApi, timeZone)) {
|
||||||
|
trustedMinuteOfWeek ->
|
||||||
|
|
||||||
|
if (category.blockedMinutesInWeek.dataNotToModify.isEmpty) {
|
||||||
|
getBlockingReasonStep6(category, timeZone)
|
||||||
|
} else if (category.blockedMinutesInWeek.read(trustedMinuteOfWeek)) {
|
||||||
|
liveDataFromValue(BlockingReason.BlockedAtThisTime)
|
||||||
|
} else {
|
||||||
|
getBlockingReasonStep6(category, timeZone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getBlockingReasonStep6(category: Category, timeZone: TimeZone): LiveData<BlockingReason> {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "step 6")
|
||||||
|
}
|
||||||
|
|
||||||
|
return getDateLive(appLogic.timeApi, timeZone).switchMap {
|
||||||
|
nowTrustedDate ->
|
||||||
|
|
||||||
|
appLogic.database.timeLimitRules().getTimeLimitRulesByCategory(category.id).switchMap {
|
||||||
|
rules ->
|
||||||
|
|
||||||
|
if (rules.isEmpty()) {
|
||||||
|
liveDataFromValue(BlockingReason.None)
|
||||||
|
} else {
|
||||||
|
getBlockingReasonStep7(category, nowTrustedDate, rules)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getBlockingReasonStep7(category: Category, nowTrustedDate: DateInTimezone, rules: List<TimeLimitRule>): LiveData<BlockingReason> {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "step 7")
|
||||||
|
}
|
||||||
|
|
||||||
|
return appLogic.database.usedTimes().getUsedTimesOfWeek(category.id, nowTrustedDate.dayOfEpoch - nowTrustedDate.dayOfWeek).map {
|
||||||
|
usedTimes ->
|
||||||
|
val usedTimesSparseArray = SparseLongArray()
|
||||||
|
|
||||||
|
for (i in 0..6) {
|
||||||
|
val usedTimesItem = usedTimes[i]?.usedMillis
|
||||||
|
usedTimesSparseArray.put(i, (if (usedTimesItem != null) usedTimesItem else 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
val remaining = RemainingTime.getRemainingTime(nowTrustedDate.dayOfWeek, usedTimesSparseArray, rules, category.extraTimeInMillis)
|
||||||
|
|
||||||
|
if (remaining == null || remaining.includingExtraTime > 0) {
|
||||||
|
BlockingReason.None
|
||||||
|
} else {
|
||||||
|
if (category.extraTimeInMillis > 0) {
|
||||||
|
BlockingReason.TimeOverExtraTimeCanBeUsedLater
|
||||||
|
} else {
|
||||||
|
BlockingReason.TimeOver
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val timeInMillis: LiveData<Long> = liveDataFromFunction {
|
||||||
|
appLogic.timeApi.getCurrentTimeInMillis()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMinuteOfWeekLive(api: TimeApi, timeZone: TimeZone): LiveData<Int> = liveDataFromFunction {
|
||||||
|
getMinuteOfWeek(api.getCurrentTimeInMillis(), timeZone)
|
||||||
|
}.ignoreUnchanged()
|
||||||
|
|
||||||
|
private fun getDateLive(api: TimeApi, timeZone: TimeZone): LiveData<DateInTimezone> = liveDataFromFunction {
|
||||||
|
DateInTimezone.newInstance(api.getCurrentTimeInMillis(), timeZone)
|
||||||
|
}.ignoreUnchanged()
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.logic
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
object DefaultAppLogic {
|
||||||
|
private var instance: AppLogic? = null
|
||||||
|
|
||||||
|
fun with(context: Context): AppLogic {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = AndroidAppLogic.with(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance!!
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.logic
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import io.timelimit.android.async.Threads
|
||||||
|
import io.timelimit.android.coroutines.executeAndWait
|
||||||
|
import io.timelimit.android.coroutines.runAsync
|
||||||
|
import io.timelimit.android.data.transaction
|
||||||
|
import io.timelimit.android.ui.MainActivity
|
||||||
|
import io.timelimit.android.ui.manipulation.UnlockAfterManipulationActivity
|
||||||
|
|
||||||
|
class ManipulationLogic(val appLogic: AppLogic) {
|
||||||
|
companion object {
|
||||||
|
private const val LOG_TAG = "ManipulationLogic"
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
runAsync {
|
||||||
|
Threads.database.executeAndWait {
|
||||||
|
if (appLogic.database.config().wasDeviceLockedSync()) {
|
||||||
|
showManipulationScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun lockDeviceSync() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
if (appLogic.platformIntegration.setLockTaskPackages(listOf(appLogic.context.packageName))) {
|
||||||
|
appLogic.database.config().setWasDeviceLockedSync(true)
|
||||||
|
|
||||||
|
showManipulationScreen()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (lockDeviceSync("opentimelimit1234")) {
|
||||||
|
appLogic.database.config().setWasDeviceLockedSync(true)
|
||||||
|
|
||||||
|
showManipulationScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun lockDeviceSync(password: String) = appLogic.platformIntegration.trySetLockScreenPassword(password)
|
||||||
|
|
||||||
|
private fun showManipulationScreen() {
|
||||||
|
appLogic.context.startActivity(
|
||||||
|
Intent(appLogic.context, UnlockAfterManipulationActivity::class.java)
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showManipulationUnlockedScreen() {
|
||||||
|
appLogic.context.startActivity(
|
||||||
|
Intent(appLogic.context, MainActivity::class.java)
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unlockDeviceSync() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
appLogic.database.config().setWasDeviceLockedSync(false)
|
||||||
|
} else {
|
||||||
|
if (lockDeviceSync("")) {
|
||||||
|
appLogic.database.config().setWasDeviceLockedSync(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reportManualUnlock() {
|
||||||
|
Threads.database.execute {
|
||||||
|
appLogic.database.transaction().use { transaction ->
|
||||||
|
if (appLogic.database.config().getOwnDeviceIdSync() != null) {
|
||||||
|
if (appLogic.database.config().wasDeviceLockedSync()) {
|
||||||
|
appLogic.database.config().setWasDeviceLockedSync(false)
|
||||||
|
|
||||||
|
showManipulationUnlockedScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.setSuccess()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.logic
|
||||||
|
|
||||||
|
import io.timelimit.android.integration.platform.PlatformIntegration
|
||||||
|
|
||||||
|
class QueryAppTitleCache(val platformIntegration: PlatformIntegration) {
|
||||||
|
private var lastPackageName: String? = null
|
||||||
|
private var lastAppTitle: String? = null
|
||||||
|
|
||||||
|
fun query(packageName: String): String {
|
||||||
|
if (packageName == lastPackageName) {
|
||||||
|
return lastAppTitle!!
|
||||||
|
} else {
|
||||||
|
val title = platformIntegration.getLocalAppTitle(packageName)
|
||||||
|
|
||||||
|
lastAppTitle = when {
|
||||||
|
title != null -> title
|
||||||
|
else -> packageName
|
||||||
|
}
|
||||||
|
lastPackageName = packageName
|
||||||
|
|
||||||
|
return lastAppTitle!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.logic
|
||||||
|
|
||||||
|
import android.util.SparseLongArray
|
||||||
|
import io.timelimit.android.data.model.TimeLimitRule
|
||||||
|
|
||||||
|
data class RemainingTime(val includingExtraTime: Long, val default: Long) {
|
||||||
|
init {
|
||||||
|
if (includingExtraTime < 0 || default < 0) {
|
||||||
|
throw IllegalStateException("time is < 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includingExtraTime < default) {
|
||||||
|
throw IllegalStateException("extra time < default time")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private fun getRulesRelatedToDay(dayOfWeek: Int, rules: List<TimeLimitRule>): List<TimeLimitRule> {
|
||||||
|
return rules.filter { (it.dayMask.toInt() and (1 shl dayOfWeek)) != 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getRemainingTime(dayOfWeek: Int, usedTimes: SparseLongArray, rules: List<TimeLimitRule>, extraTime: Long): RemainingTime? {
|
||||||
|
if (extraTime < 0) {
|
||||||
|
throw IllegalStateException("extra time < 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
val relatedRules = getRulesRelatedToDay(dayOfWeek, rules)
|
||||||
|
val withoutExtraTime = getRemainingTime(usedTimes, relatedRules, false)
|
||||||
|
val withExtraTime = getRemainingTime(usedTimes, relatedRules, true)
|
||||||
|
|
||||||
|
if (withoutExtraTime == null && withExtraTime == null) {
|
||||||
|
// no rules
|
||||||
|
return null
|
||||||
|
} else if (withoutExtraTime != null && withExtraTime != null) {
|
||||||
|
// with rules for extra time
|
||||||
|
val additionalTimeWithExtraTime = withExtraTime - withoutExtraTime
|
||||||
|
|
||||||
|
if (additionalTimeWithExtraTime < 0) {
|
||||||
|
throw IllegalStateException("additional time with extra time < 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
return RemainingTime(
|
||||||
|
includingExtraTime = withoutExtraTime + Math.min(extraTime, additionalTimeWithExtraTime),
|
||||||
|
default = withoutExtraTime
|
||||||
|
)
|
||||||
|
} else if (withoutExtraTime != null) {
|
||||||
|
// without rules for extra time
|
||||||
|
return RemainingTime(
|
||||||
|
includingExtraTime = withoutExtraTime + extraTime,
|
||||||
|
default = withoutExtraTime
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
throw IllegalStateException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getRemainingTime(usedTimes: SparseLongArray, relatedRules: List<TimeLimitRule>, assumeMaximalExtraTime: Boolean): Long? {
|
||||||
|
return relatedRules.filter { (!assumeMaximalExtraTime) || it.applyToExtraTimeUsage }.map {
|
||||||
|
var usedTime = 0L
|
||||||
|
|
||||||
|
for (day in 0..6) {
|
||||||
|
if ((it.dayMask.toInt() and (1 shl day)) != 0) {
|
||||||
|
usedTime += usedTimes[day]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val maxTime = it.maximumTimeInMillis
|
||||||
|
val remaining = Math.max(0, maxTime - usedTime)
|
||||||
|
|
||||||
|
remaining
|
||||||
|
}.min()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.logic
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import io.timelimit.android.coroutines.runAsyncExpectForever
|
||||||
|
import io.timelimit.android.data.model.UserType
|
||||||
|
import io.timelimit.android.livedata.*
|
||||||
|
import io.timelimit.android.sync.actions.AddInstalledAppsAction
|
||||||
|
import io.timelimit.android.sync.actions.InstalledApp
|
||||||
|
import io.timelimit.android.sync.actions.RemoveInstalledAppsAction
|
||||||
|
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
|
||||||
|
class SyncInstalledAppsLogic(val appLogic: AppLogic) {
|
||||||
|
private val doSyncLock = Mutex()
|
||||||
|
private var requestSync = MutableLiveData<Boolean>().apply { value = false }
|
||||||
|
|
||||||
|
private fun requestSync() {
|
||||||
|
requestSync.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
appLogic.platformIntegration.installedAppsChangeListener = Runnable { requestSync() }
|
||||||
|
appLogic.deviceEntryIfEnabled.map { it?.id + it?.currentUserId }.ignoreUnchanged().observeForever { requestSync() }
|
||||||
|
|
||||||
|
runAsyncExpectForever { syncLoop() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun syncLoop() {
|
||||||
|
while (true) {
|
||||||
|
requestSync.waitUntilValueMatches { it == true }
|
||||||
|
requestSync.value = false
|
||||||
|
|
||||||
|
doSyncNow()
|
||||||
|
|
||||||
|
// maximal 1 time per 5 seconds
|
||||||
|
appLogic.timeApi.sleep(5 * 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun doSyncNow() {
|
||||||
|
doSyncLock.withLock {
|
||||||
|
val userEntry = appLogic.deviceUserEntry.waitForNullableValue()
|
||||||
|
|
||||||
|
if (userEntry == null || userEntry.type != UserType.Child) {
|
||||||
|
return@withLock
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentlyInstalled = appLogic.platformIntegration.getLocalApps().associateBy { app -> app.packageName }
|
||||||
|
val currentlySaved = appLogic.database.app().getApps().waitForNonNullValue().associateBy { app -> app.packageName }
|
||||||
|
|
||||||
|
// skip all items for removal which are still saved locally
|
||||||
|
val itemsToRemove = HashMap(currentlySaved)
|
||||||
|
currentlyInstalled.forEach { (packageName, _) -> itemsToRemove.remove(packageName) }
|
||||||
|
|
||||||
|
// only add items which are not the same locally
|
||||||
|
val itemsToAdd = currentlyInstalled.filter { (packageName, app) -> currentlySaved[packageName] != app }
|
||||||
|
|
||||||
|
// save the changes
|
||||||
|
if (itemsToRemove.isNotEmpty()) {
|
||||||
|
ApplyActionUtil.applyAppLogicAction(
|
||||||
|
RemoveInstalledAppsAction(packageNames = itemsToRemove.keys.toList()),
|
||||||
|
appLogic
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemsToAdd.isNotEmpty()) {
|
||||||
|
ApplyActionUtil.applyAppLogicAction(
|
||||||
|
AddInstalledAppsAction(
|
||||||
|
apps = itemsToAdd.map {
|
||||||
|
(_, app) ->
|
||||||
|
|
||||||
|
InstalledApp(
|
||||||
|
packageName = app.packageName,
|
||||||
|
title = app.title,
|
||||||
|
recommendation = app.recommendation,
|
||||||
|
isLaunchable = app.isLaunchable
|
||||||
|
)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
appLogic
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
37
app/src/main/java/io/timelimit/android/logic/TestAppLogic.kt
Normal file
37
app/src/main/java/io/timelimit/android/logic/TestAppLogic.kt
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.logic
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import io.timelimit.android.data.RoomDatabase
|
||||||
|
import io.timelimit.android.integration.platform.ProtectionLevel
|
||||||
|
import io.timelimit.android.integration.platform.dummy.DummyIntegration
|
||||||
|
import io.timelimit.android.integration.time.DummyTimeApi
|
||||||
|
import io.timelimit.android.livedata.liveDataFromValue
|
||||||
|
|
||||||
|
class TestAppLogic(maximumProtectionLevel: ProtectionLevel, context: Context) {
|
||||||
|
val platformIntegration = DummyIntegration(maximumProtectionLevel)
|
||||||
|
val timeApi = DummyTimeApi(100)
|
||||||
|
val database = RoomDatabase.createInMemoryInstance(context)
|
||||||
|
|
||||||
|
val logic = AppLogic(
|
||||||
|
platformIntegration = platformIntegration,
|
||||||
|
timeApi = timeApi,
|
||||||
|
database = database,
|
||||||
|
context = context,
|
||||||
|
isInitialized = liveDataFromValue(true)
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.logic
|
||||||
|
|
||||||
|
import io.timelimit.android.data.Database
|
||||||
|
import io.timelimit.android.data.model.UsedTimeItem
|
||||||
|
import io.timelimit.android.date.DateInTimezone
|
||||||
|
import io.timelimit.android.livedata.waitForNullableValue
|
||||||
|
import io.timelimit.android.sync.actions.AddUsedTimeAction
|
||||||
|
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||||
|
|
||||||
|
class UsedTimeItemBatchUpdateHelper(val date: DateInTimezone, val categoryId: String, var cachedItem: UsedTimeItem?) {
|
||||||
|
companion object {
|
||||||
|
suspend fun eventuallyUpdateInstance(
|
||||||
|
date: DateInTimezone,
|
||||||
|
categoryId: String,
|
||||||
|
oldInstance: UsedTimeItemBatchUpdateHelper?,
|
||||||
|
usedTimeItemForDay: UsedTimeItem?,
|
||||||
|
logic: AppLogic
|
||||||
|
): UsedTimeItemBatchUpdateHelper {
|
||||||
|
if (oldInstance != null && oldInstance.date == date && oldInstance.categoryId == categoryId) {
|
||||||
|
if (oldInstance.cachedItem != usedTimeItemForDay) {
|
||||||
|
oldInstance.cachedItem = usedTimeItemForDay
|
||||||
|
}
|
||||||
|
|
||||||
|
return oldInstance
|
||||||
|
} else {
|
||||||
|
if (oldInstance != null) {
|
||||||
|
oldInstance.commit(logic)
|
||||||
|
}
|
||||||
|
|
||||||
|
return UsedTimeItemBatchUpdateHelper(
|
||||||
|
date = date,
|
||||||
|
categoryId = categoryId,
|
||||||
|
cachedItem = usedTimeItemForDay
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var timeToAdd = 0
|
||||||
|
private var extraTimeToSubtract = 0
|
||||||
|
|
||||||
|
suspend fun addUsedTime(time: Int, subtractExtraTime: Boolean, appLogic: AppLogic) {
|
||||||
|
timeToAdd += time
|
||||||
|
|
||||||
|
if (subtractExtraTime) {
|
||||||
|
extraTimeToSubtract += time
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.max(timeToAdd, extraTimeToSubtract) > 1000 * 10 /* 10 seconds */) {
|
||||||
|
commit(appLogic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTotalUsedTime(): Long {
|
||||||
|
val cachedItem = cachedItem
|
||||||
|
|
||||||
|
return (if (cachedItem == null) 0 else cachedItem.usedMillis) + timeToAdd
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCachedExtraTimeToSubtract(): Int {
|
||||||
|
return extraTimeToSubtract
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun queryCurrentStatusFromDatabase(database: Database) {
|
||||||
|
cachedItem = database.usedTimes().getUsedTimeItem(categoryId, date.dayOfEpoch).waitForNullableValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun commit(logic: AppLogic) {
|
||||||
|
if (timeToAdd == 0) {
|
||||||
|
// do nothing
|
||||||
|
} else {
|
||||||
|
ApplyActionUtil.applyAppLogicAction(
|
||||||
|
AddUsedTimeAction(
|
||||||
|
categoryId = categoryId,
|
||||||
|
timeToAdd = timeToAdd,
|
||||||
|
dayOfEpoch = date.dayOfEpoch,
|
||||||
|
extraTimeToSubtract = extraTimeToSubtract
|
||||||
|
),
|
||||||
|
logic
|
||||||
|
)
|
||||||
|
|
||||||
|
timeToAdd = 0
|
||||||
|
extraTimeToSubtract = 0
|
||||||
|
|
||||||
|
queryCurrentStatusFromDatabase(logic.database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
255
app/src/main/java/io/timelimit/android/sync/actions/Actions.kt
Normal file
255
app/src/main/java/io/timelimit/android/sync/actions/Actions.kt
Normal file
|
@ -0,0 +1,255 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.sync.actions
|
||||||
|
|
||||||
|
import io.timelimit.android.data.IdGenerator
|
||||||
|
import io.timelimit.android.data.customtypes.ImmutableBitmask
|
||||||
|
import io.timelimit.android.data.model.AppRecommendation
|
||||||
|
import io.timelimit.android.data.model.TimeLimitRule
|
||||||
|
import io.timelimit.android.data.model.UserType
|
||||||
|
import io.timelimit.android.integration.platform.NewPermissionStatus
|
||||||
|
import io.timelimit.android.integration.platform.ProtectionLevel
|
||||||
|
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
||||||
|
import io.timelimit.android.sync.validation.ListValidation
|
||||||
|
|
||||||
|
// Tip: [Ctrl] + [A] and [Ctrl] + [Shift] + [Minus] make this file easy to read
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The actions describe things that happen.
|
||||||
|
* The same actions (should) result in the same state if applied in the same order.
|
||||||
|
* This actions are used for the remote control and monitoring.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
// base types
|
||||||
|
sealed class Action
|
||||||
|
|
||||||
|
sealed class AppLogicAction: Action()
|
||||||
|
sealed class ParentAction: Action()
|
||||||
|
|
||||||
|
//
|
||||||
|
// now the concrete actions
|
||||||
|
//
|
||||||
|
|
||||||
|
data class AddUsedTimeAction(val categoryId: String, val dayOfEpoch: Int, val timeToAdd: Int, val extraTimeToSubtract: Int): AppLogicAction() {
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(categoryId)
|
||||||
|
|
||||||
|
if (dayOfEpoch < 0) {
|
||||||
|
throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeToAdd < 0) {
|
||||||
|
throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extraTimeToSubtract < 0) {
|
||||||
|
throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class InstalledApp(val packageName: String, val title: String, val isLaunchable: Boolean, val recommendation: AppRecommendation)
|
||||||
|
data class AddInstalledAppsAction(val apps: List<InstalledApp>): AppLogicAction() {
|
||||||
|
init {
|
||||||
|
ListValidation.assertNotEmptyListWithoutDuplicates(apps.map { it.packageName })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data class RemoveInstalledAppsAction(val packageNames: List<String>): AppLogicAction() {
|
||||||
|
init {
|
||||||
|
ListValidation.assertNotEmptyListWithoutDuplicates(packageNames)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class AddCategoryAppsAction(val categoryId: String, val packageNames: List<String>): ParentAction() {
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(categoryId)
|
||||||
|
ListValidation.assertNotEmptyListWithoutDuplicates(packageNames)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data class RemoveCategoryAppsAction(val categoryId: String, val packageNames: List<String>): ParentAction() {
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(categoryId)
|
||||||
|
ListValidation.assertNotEmptyListWithoutDuplicates(packageNames)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class CreateCategoryAction(val childId: String, val categoryId: String, val title: String): ParentAction() {
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(categoryId)
|
||||||
|
IdGenerator.assertIdValid(childId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data class DeleteCategoryAction(val categoryId: String): ParentAction() {
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(categoryId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data class UpdateCategoryTitleAction(val categoryId: String, val newTitle: String): ParentAction() {
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(categoryId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data class SetCategoryExtraTimeAction(val categoryId: String, val newExtraTime: Long): ParentAction() {
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(categoryId)
|
||||||
|
|
||||||
|
if (newExtraTime < 0) {
|
||||||
|
throw IllegalArgumentException("newExtraTime must be >= 0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data class IncrementCategoryExtraTimeAction(val categoryId: String, val addedExtraTime: Long): ParentAction() {
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(categoryId)
|
||||||
|
|
||||||
|
if (addedExtraTime <= 0) {
|
||||||
|
throw IllegalArgumentException("addedExtraTime must be more than zero")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data class UpdateCategoryTemporarilyBlockedAction(val categoryId: String, val blocked: Boolean): ParentAction() {
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(categoryId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeviceDao
|
||||||
|
|
||||||
|
data class UpdateDeviceStatusAction(
|
||||||
|
val newProtectionLevel: ProtectionLevel?,
|
||||||
|
val newUsageStatsPermissionStatus: RuntimePermissionStatus?,
|
||||||
|
val newNotificationAccessPermission: NewPermissionStatus?,
|
||||||
|
val newAppVersion: Int?
|
||||||
|
): AppLogicAction() {
|
||||||
|
init {
|
||||||
|
if (newAppVersion != null && newAppVersion < 0) {
|
||||||
|
throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class IgnoreManipulationAction(
|
||||||
|
val deviceId: String,
|
||||||
|
val ignoreDeviceAdminManipulation: Boolean,
|
||||||
|
val ignoreDeviceAdminManipulationAttempt: Boolean,
|
||||||
|
val ignoreAppDowngrade: Boolean,
|
||||||
|
val ignoreNotificationAccessManipulation: Boolean,
|
||||||
|
val ignoreUsageStatsAccessManipulation: Boolean,
|
||||||
|
val ignoreHadManipulation: Boolean
|
||||||
|
): ParentAction() {
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(deviceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
val isEmpty = (!ignoreDeviceAdminManipulation) &&
|
||||||
|
(!ignoreDeviceAdminManipulationAttempt) &&
|
||||||
|
(!ignoreAppDowngrade) &&
|
||||||
|
(!ignoreNotificationAccessManipulation) &&
|
||||||
|
(!ignoreUsageStatsAccessManipulation) &&
|
||||||
|
(!ignoreHadManipulation)
|
||||||
|
}
|
||||||
|
|
||||||
|
object TriedDisablingDeviceAdminAction: AppLogicAction()
|
||||||
|
|
||||||
|
data class SetDeviceUserAction(val deviceId: String, val userId: String): ParentAction() {
|
||||||
|
// user id can be an empty string
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(deviceId)
|
||||||
|
|
||||||
|
if (userId != "") {
|
||||||
|
IdGenerator.assertIdValid(userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class UpdateCategoryBlockedTimesAction(val categoryId: String, val blockedTimes: ImmutableBitmask): ParentAction() {
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(categoryId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class CreateTimeLimitRuleAction(val rule: TimeLimitRule): ParentAction()
|
||||||
|
|
||||||
|
data class UpdateTimeLimitRuleAction(val ruleId: String, val dayMask: Byte, val maximumTimeInMillis: Int, val applyToExtraTimeUsage: Boolean): ParentAction() {
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(ruleId)
|
||||||
|
|
||||||
|
if (maximumTimeInMillis < 0) {
|
||||||
|
throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dayMask < 0 || dayMask > (1 or 2 or 4 or 8 or 16 or 32 or 64)) {
|
||||||
|
throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DeleteTimeLimitRuleAction(val ruleId: String): ParentAction() {
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(ruleId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserDao
|
||||||
|
data class AddUserAction(val name: String, val userType: UserType, val password: String?, val userId: String, val timeZone: String): ParentAction() {
|
||||||
|
init {
|
||||||
|
if (userType == UserType.Parent) {
|
||||||
|
password!!
|
||||||
|
}
|
||||||
|
|
||||||
|
IdGenerator.assertIdValid(userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ChangeParentPasswordAction(
|
||||||
|
val parentUserId: String,
|
||||||
|
val newPassword: String
|
||||||
|
): ParentAction() {
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(parentUserId)
|
||||||
|
|
||||||
|
if (newPassword.isEmpty()) {
|
||||||
|
throw IllegalArgumentException("missing required parameter")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class RemoveUserAction(val userId: String): ParentAction() {
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SetUserDisableLimitsUntilAction(val childId: String, val timestamp: Long): ParentAction() {
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(childId)
|
||||||
|
|
||||||
|
if (timestamp < 0) {
|
||||||
|
throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class UpdateDeviceNameAction(val deviceId: String, val name: String): ParentAction() {
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(deviceId)
|
||||||
|
|
||||||
|
if (name.isBlank()) {
|
||||||
|
throw IllegalArgumentException("new device name must not be blank")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.sync.actions
|
||||||
|
|
||||||
|
import io.timelimit.android.data.Database
|
||||||
|
import io.timelimit.android.data.model.UserType
|
||||||
|
|
||||||
|
object DatabaseValidation {
|
||||||
|
fun assertCategoryExists(database: Database, categoryId: String) {
|
||||||
|
database.category().getCategoryByIdSync(categoryId)
|
||||||
|
?:throw IllegalArgumentException("category with the specified id does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assertChildExists(database: Database, childId: String) {
|
||||||
|
val userEntry = database.user().getUserByIdSync(childId)
|
||||||
|
|
||||||
|
if (userEntry == null || userEntry.type != UserType.Child) {
|
||||||
|
throw IllegalArgumentException("child with the specified id does not exist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assertUserExists(database: Database, userId: String) {
|
||||||
|
database.user().getUserByIdSync(userId)
|
||||||
|
?:throw IllegalArgumentException("user with the specified id does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assertTimelimitRuleExists(database: Database, timeLimitRuleId: String) {
|
||||||
|
database.timeLimitRules().getTimeLimitRuleByIdSync(timeLimitRuleId)
|
||||||
|
?:throw IllegalArgumentException("time limit rule with the specified id does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assertDeviceExists(database: Database, devcieId: String) {
|
||||||
|
database.device().getDeviceByIdSync(devcieId)
|
||||||
|
?:throw IllegalArgumentException("device does not exist")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.sync.actions.apply
|
||||||
|
|
||||||
|
import io.timelimit.android.async.Threads
|
||||||
|
import io.timelimit.android.coroutines.executeAndWait
|
||||||
|
import io.timelimit.android.data.Database
|
||||||
|
import io.timelimit.android.data.transaction
|
||||||
|
import io.timelimit.android.integration.platform.PlatformIntegration
|
||||||
|
import io.timelimit.android.logic.AppLogic
|
||||||
|
import io.timelimit.android.logic.ManipulationLogic
|
||||||
|
import io.timelimit.android.sync.actions.AddCategoryAppsAction
|
||||||
|
import io.timelimit.android.sync.actions.AppLogicAction
|
||||||
|
import io.timelimit.android.sync.actions.ParentAction
|
||||||
|
import io.timelimit.android.sync.actions.SetDeviceUserAction
|
||||||
|
import io.timelimit.android.sync.actions.dispatch.LocalDatabaseAppLogicActionDispatcher
|
||||||
|
import io.timelimit.android.sync.actions.dispatch.LocalDatabaseParentActionDispatcher
|
||||||
|
|
||||||
|
object ApplyActionUtil {
|
||||||
|
suspend fun applyAppLogicAction(action: AppLogicAction, appLogic: AppLogic) {
|
||||||
|
applyAppLogicAction(action, appLogic.database, appLogic.manipulationLogic)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun applyAppLogicAction(action: AppLogicAction, database: Database, manipulationLogic: ManipulationLogic) {
|
||||||
|
Threads.database.executeAndWait {
|
||||||
|
database.transaction().use {
|
||||||
|
LocalDatabaseAppLogicActionDispatcher.dispatchAppLogicActionSync(action, database.config().getOwnDeviceIdSync()!!, database, manipulationLogic)
|
||||||
|
|
||||||
|
database.setTransactionSuccessful()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun applyParentAction(action: ParentAction, database: Database, platformIntegration: PlatformIntegration) {
|
||||||
|
Threads.database.executeAndWait {
|
||||||
|
database.transaction().use {
|
||||||
|
LocalDatabaseParentActionDispatcher.dispatchParentActionSync(action, database)
|
||||||
|
|
||||||
|
// disable suspending the assigned app
|
||||||
|
if (action is AddCategoryAppsAction) {
|
||||||
|
val thisDeviceId = database.config().getOwnDeviceIdSync()!!
|
||||||
|
val thisDeviceEntry = database.device().getDeviceByIdSync(thisDeviceId)!!
|
||||||
|
|
||||||
|
if (thisDeviceEntry.currentUserId != "") {
|
||||||
|
val userCategories = database.category().getCategoriesByChildIdSync(thisDeviceEntry.currentUserId)
|
||||||
|
|
||||||
|
if (userCategories.find { category -> category.id == action.categoryId } != null) {
|
||||||
|
platformIntegration.setSuspendedApps(action.packageNames, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action is SetDeviceUserAction) {
|
||||||
|
val thisDeviceId = database.config().getOwnDeviceIdSync()!!
|
||||||
|
|
||||||
|
if (action.deviceId == thisDeviceId) {
|
||||||
|
platformIntegration.stopSuspendingForAllApps()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
database.setTransactionSuccessful()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,180 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.sync.actions.dispatch
|
||||||
|
|
||||||
|
import io.timelimit.android.data.Database
|
||||||
|
import io.timelimit.android.data.model.App
|
||||||
|
import io.timelimit.android.data.model.UsedTimeItem
|
||||||
|
import io.timelimit.android.integration.platform.NewPermissionStatusUtil
|
||||||
|
import io.timelimit.android.integration.platform.ProtectionLevelUtil
|
||||||
|
import io.timelimit.android.integration.platform.RuntimePermissionStatusUtil
|
||||||
|
import io.timelimit.android.logic.ManipulationLogic
|
||||||
|
import io.timelimit.android.sync.actions.*
|
||||||
|
|
||||||
|
object LocalDatabaseAppLogicActionDispatcher {
|
||||||
|
fun dispatchAppLogicActionSync(action: AppLogicAction, deviceId: String, database: Database, manipulationLogic: ManipulationLogic) {
|
||||||
|
DatabaseValidation.assertDeviceExists(database, deviceId)
|
||||||
|
|
||||||
|
database.beginTransaction()
|
||||||
|
|
||||||
|
try {
|
||||||
|
when(action) {
|
||||||
|
is AddUsedTimeAction -> {
|
||||||
|
DatabaseValidation.assertCategoryExists(database, action.categoryId)
|
||||||
|
|
||||||
|
// try to update
|
||||||
|
val updatedRows = database.usedTimes().addUsedTime(
|
||||||
|
categoryId = action.categoryId,
|
||||||
|
timeToAdd = action.timeToAdd,
|
||||||
|
dayOfEpoch = action.dayOfEpoch
|
||||||
|
)
|
||||||
|
|
||||||
|
if (updatedRows == 0) {
|
||||||
|
// create new entry
|
||||||
|
|
||||||
|
database.usedTimes().insertUsedTime(UsedTimeItem(
|
||||||
|
categoryId = action.categoryId,
|
||||||
|
dayOfEpoch = action.dayOfEpoch,
|
||||||
|
usedMillis = action.timeToAdd.toLong()
|
||||||
|
))
|
||||||
|
} // required to make this compile
|
||||||
|
|
||||||
|
|
||||||
|
if (action.extraTimeToSubtract != 0) {
|
||||||
|
database.category().subtractCategoryExtraTime(
|
||||||
|
categoryId = action.categoryId,
|
||||||
|
removedExtraTime = action.extraTimeToSubtract
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// required to make this compile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is AddInstalledAppsAction -> {
|
||||||
|
database.app().addAppsSync(
|
||||||
|
action.apps.map {
|
||||||
|
App(
|
||||||
|
packageName = it.packageName,
|
||||||
|
title = it.title,
|
||||||
|
isLaunchable = it.isLaunchable,
|
||||||
|
recommendation = it.recommendation
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is RemoveInstalledAppsAction -> {
|
||||||
|
database.app().removeAppsByPackageNamesSync(
|
||||||
|
packageNames = action.packageNames
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is UpdateDeviceStatusAction -> {
|
||||||
|
var device = database.device().getDeviceByIdSync(deviceId)!!
|
||||||
|
|
||||||
|
if (action.newProtectionLevel != null) {
|
||||||
|
if (device.currentProtectionLevel != action.newProtectionLevel) {
|
||||||
|
device = device.copy(
|
||||||
|
currentProtectionLevel = action.newProtectionLevel
|
||||||
|
)
|
||||||
|
|
||||||
|
if (ProtectionLevelUtil.toInt(action.newProtectionLevel) > ProtectionLevelUtil.toInt(device.highestProtectionLevel)) {
|
||||||
|
device = device.copy(
|
||||||
|
highestProtectionLevel = action.newProtectionLevel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device.currentProtectionLevel != device.highestProtectionLevel) {
|
||||||
|
device = device.copy(hadManipulation = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.newUsageStatsPermissionStatus != null) {
|
||||||
|
if (device.currentUsageStatsPermission != action.newUsageStatsPermissionStatus) {
|
||||||
|
device = device.copy(
|
||||||
|
currentUsageStatsPermission = action.newUsageStatsPermissionStatus
|
||||||
|
)
|
||||||
|
|
||||||
|
if (RuntimePermissionStatusUtil.toInt(action.newUsageStatsPermissionStatus) > RuntimePermissionStatusUtil.toInt(device.highestUsageStatsPermission)) {
|
||||||
|
device = device.copy(
|
||||||
|
highestUsageStatsPermission = action.newUsageStatsPermissionStatus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device.currentUsageStatsPermission != device.highestUsageStatsPermission) {
|
||||||
|
device = device.copy(hadManipulation = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.newNotificationAccessPermission != null) {
|
||||||
|
if (device.currentNotificationAccessPermission != action.newNotificationAccessPermission) {
|
||||||
|
device = device.copy(
|
||||||
|
currentNotificationAccessPermission = action.newNotificationAccessPermission
|
||||||
|
)
|
||||||
|
|
||||||
|
if (NewPermissionStatusUtil.toInt(action.newNotificationAccessPermission) > NewPermissionStatusUtil.toInt(device.highestNotificationAccessPermission)) {
|
||||||
|
device = device.copy(
|
||||||
|
highestNotificationAccessPermission = action.newNotificationAccessPermission
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device.currentNotificationAccessPermission != device.highestNotificationAccessPermission) {
|
||||||
|
device = device.copy(hadManipulation = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.newAppVersion != null) {
|
||||||
|
if (device.currentAppVersion != action.newAppVersion) {
|
||||||
|
device = device.copy(
|
||||||
|
currentAppVersion = action.newAppVersion,
|
||||||
|
highestAppVersion = Math.max(device.highestAppVersion, action.newAppVersion)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (device.currentAppVersion != device.highestAppVersion) {
|
||||||
|
device = device.copy(hadManipulation = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
database.device().updateDeviceEntry(device)
|
||||||
|
|
||||||
|
if (device.hasActiveManipulationWarning) {
|
||||||
|
manipulationLogic.lockDeviceSync()
|
||||||
|
}
|
||||||
|
|
||||||
|
null
|
||||||
|
}
|
||||||
|
is TriedDisablingDeviceAdminAction -> {
|
||||||
|
database.device().updateDeviceEntry(
|
||||||
|
database.device().getDeviceByIdSync(
|
||||||
|
database.config().getOwnDeviceIdSync()!!
|
||||||
|
)!!.copy(
|
||||||
|
manipulationTriedDisablingDeviceAdmin = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
manipulationLogic.lockDeviceSync()
|
||||||
|
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}.let { }
|
||||||
|
|
||||||
|
database.setTransactionSuccessful()
|
||||||
|
} finally {
|
||||||
|
database.endTransaction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,273 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.sync.actions.dispatch
|
||||||
|
|
||||||
|
import io.timelimit.android.data.Database
|
||||||
|
import io.timelimit.android.data.customtypes.ImmutableBitmask
|
||||||
|
import io.timelimit.android.data.model.Category
|
||||||
|
import io.timelimit.android.data.model.CategoryApp
|
||||||
|
import io.timelimit.android.data.model.User
|
||||||
|
import io.timelimit.android.data.model.UserType
|
||||||
|
import io.timelimit.android.sync.actions.*
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
object LocalDatabaseParentActionDispatcher {
|
||||||
|
fun dispatchParentActionSync(action: ParentAction, database: Database) {
|
||||||
|
database.beginTransaction()
|
||||||
|
|
||||||
|
try {
|
||||||
|
when (action) {
|
||||||
|
is AddCategoryAppsAction -> {
|
||||||
|
// validate that the category exists
|
||||||
|
val categoryEntry = database.category().getCategoryByIdSync(action.categoryId)
|
||||||
|
?: throw IllegalArgumentException("category with the specified id does not exist")
|
||||||
|
|
||||||
|
// remove same apps from other categories of the same child
|
||||||
|
val allCategoriesOfChild = database.category().getCategoriesByChildIdSync(categoryEntry.childId)
|
||||||
|
|
||||||
|
database.categoryApp().removeCategoryAppsSyncByCategoryIds(
|
||||||
|
packageNames = action.packageNames,
|
||||||
|
categoryIds = allCategoriesOfChild.map { it.id }
|
||||||
|
)
|
||||||
|
|
||||||
|
// add the apps to the new category
|
||||||
|
database.categoryApp().addCategoryAppsSync(
|
||||||
|
action.packageNames.map {
|
||||||
|
CategoryApp(
|
||||||
|
categoryId = action.categoryId,
|
||||||
|
packageName = it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is RemoveCategoryAppsAction -> {
|
||||||
|
DatabaseValidation.assertCategoryExists(database, action.categoryId)
|
||||||
|
|
||||||
|
// remove the apps from the category
|
||||||
|
database.categoryApp().removeCategoryAppsSyncByCategoryIds(
|
||||||
|
packageNames = action.packageNames,
|
||||||
|
categoryIds = listOf(action.categoryId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is CreateCategoryAction -> {
|
||||||
|
DatabaseValidation.assertChildExists(database, action.childId)
|
||||||
|
|
||||||
|
// create the category
|
||||||
|
database.category().addCategory(Category(
|
||||||
|
id = action.categoryId,
|
||||||
|
childId = action.childId,
|
||||||
|
title = action.title,
|
||||||
|
// nothing blocked by default
|
||||||
|
blockedMinutesInWeek = ImmutableBitmask(BitSet()),
|
||||||
|
extraTimeInMillis = 0,
|
||||||
|
temporarilyBlocked = false
|
||||||
|
))
|
||||||
|
}
|
||||||
|
is DeleteCategoryAction -> {
|
||||||
|
DatabaseValidation.assertCategoryExists(database, action.categoryId)
|
||||||
|
|
||||||
|
// delete all related data and the category
|
||||||
|
database.timeLimitRules().deleteTimeLimitRulesByCategory(action.categoryId)
|
||||||
|
database.usedTimes().deleteUsedTimeItems(action.categoryId)
|
||||||
|
database.categoryApp().deleteCategoryAppsByCategoryId(action.categoryId)
|
||||||
|
database.category().deleteCategory(action.categoryId)
|
||||||
|
}
|
||||||
|
is UpdateCategoryTitleAction -> {
|
||||||
|
DatabaseValidation.assertCategoryExists(database, action.categoryId)
|
||||||
|
|
||||||
|
database.category().updateCategoryTitle(
|
||||||
|
categoryId = action.categoryId,
|
||||||
|
newTitle = action.newTitle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is SetCategoryExtraTimeAction -> {
|
||||||
|
DatabaseValidation.assertCategoryExists(database, action.categoryId)
|
||||||
|
|
||||||
|
if (action.newExtraTime < 0) {
|
||||||
|
throw IllegalArgumentException("invalid new extra time")
|
||||||
|
}
|
||||||
|
|
||||||
|
database.category().updateCategoryExtraTime(action.categoryId, action.newExtraTime)
|
||||||
|
}
|
||||||
|
is IncrementCategoryExtraTimeAction -> {
|
||||||
|
DatabaseValidation.assertCategoryExists(database, action.categoryId)
|
||||||
|
|
||||||
|
if (action.addedExtraTime < 0) {
|
||||||
|
throw IllegalArgumentException("invalid added extra time")
|
||||||
|
}
|
||||||
|
|
||||||
|
database.category().incrementCategoryExtraTime(action.categoryId, action.addedExtraTime)
|
||||||
|
}
|
||||||
|
is UpdateCategoryTemporarilyBlockedAction -> {
|
||||||
|
DatabaseValidation.assertCategoryExists(database, action.categoryId)
|
||||||
|
|
||||||
|
database.category().updateCategoryTemporarilyBlocked(action.categoryId, action.blocked)
|
||||||
|
}
|
||||||
|
is DeleteTimeLimitRuleAction -> {
|
||||||
|
DatabaseValidation.assertTimelimitRuleExists(database, action.ruleId)
|
||||||
|
|
||||||
|
database.timeLimitRules().deleteTimeLimitRuleByIdSync(action.ruleId)
|
||||||
|
}
|
||||||
|
is AddUserAction -> {
|
||||||
|
database.user().addUserSync(User(
|
||||||
|
id = action.userId,
|
||||||
|
name = action.name,
|
||||||
|
type = action.userType,
|
||||||
|
timeZone = action.timeZone,
|
||||||
|
password = if (action.password == null) "" else action.password,
|
||||||
|
disableLimitsUntil = 0
|
||||||
|
))
|
||||||
|
}
|
||||||
|
is UpdateCategoryBlockedTimesAction -> {
|
||||||
|
DatabaseValidation.assertCategoryExists(database, action.categoryId)
|
||||||
|
|
||||||
|
database.category().updateCategoryBlockedTimes(action.categoryId, action.blockedTimes)
|
||||||
|
}
|
||||||
|
is CreateTimeLimitRuleAction -> {
|
||||||
|
DatabaseValidation.assertCategoryExists(database, action.rule.categoryId)
|
||||||
|
|
||||||
|
database.timeLimitRules().addTimeLimitRule(action.rule)
|
||||||
|
}
|
||||||
|
is UpdateTimeLimitRuleAction -> {
|
||||||
|
val oldRule = database.timeLimitRules().getTimeLimitRuleByIdSync(action.ruleId)!!
|
||||||
|
|
||||||
|
database.timeLimitRules().updateTimeLimitRule(oldRule.copy(
|
||||||
|
maximumTimeInMillis = action.maximumTimeInMillis,
|
||||||
|
dayMask = action.dayMask,
|
||||||
|
applyToExtraTimeUsage = action.applyToExtraTimeUsage
|
||||||
|
))
|
||||||
|
}
|
||||||
|
is SetDeviceUserAction -> {
|
||||||
|
DatabaseValidation.assertDeviceExists(database, action.deviceId)
|
||||||
|
|
||||||
|
if (action.userId != "") {
|
||||||
|
DatabaseValidation.assertUserExists(database, action.userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
database.device().updateDeviceUser(
|
||||||
|
deviceId = action.deviceId,
|
||||||
|
userId = action.userId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is SetUserDisableLimitsUntilAction -> {
|
||||||
|
val affectedRows = database.user().updateDisableChildUserLimitsUntil(
|
||||||
|
childId = action.childId,
|
||||||
|
timestamp = action.timestamp
|
||||||
|
)
|
||||||
|
|
||||||
|
if (affectedRows == 0) {
|
||||||
|
throw IllegalArgumentException("provided user id does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
null
|
||||||
|
}
|
||||||
|
is UpdateDeviceNameAction -> {
|
||||||
|
val affectedRows = database.device().updateDeviceName(
|
||||||
|
deviceId = action.deviceId,
|
||||||
|
name = action.name
|
||||||
|
)
|
||||||
|
|
||||||
|
if (affectedRows == 0) {
|
||||||
|
throw IllegalArgumentException("provided device id was invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
null
|
||||||
|
}
|
||||||
|
is RemoveUserAction -> {
|
||||||
|
// authentication is not checked locally, only at the server
|
||||||
|
|
||||||
|
val userToDelete = database.user().getUserByIdSync(action.userId)!!
|
||||||
|
|
||||||
|
if (userToDelete.type == UserType.Parent) {
|
||||||
|
val currentParents = database.user().getParentUsersSync()
|
||||||
|
|
||||||
|
if (currentParents.size <= 1) {
|
||||||
|
throw IllegalStateException("would delete last parent")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userToDelete.type == UserType.Child) {
|
||||||
|
val categories = database.category().getCategoriesByChildIdSync(userToDelete.id)
|
||||||
|
|
||||||
|
categories.forEach {
|
||||||
|
category ->
|
||||||
|
|
||||||
|
dispatchParentActionSync(
|
||||||
|
DeleteCategoryAction(
|
||||||
|
categoryId = category.id
|
||||||
|
),
|
||||||
|
database
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
database.device().unassignCurrentUserFromAllDevices(action.userId)
|
||||||
|
|
||||||
|
database.user().deleteUsersByIds(listOf(action.userId))
|
||||||
|
}
|
||||||
|
is ChangeParentPasswordAction -> {
|
||||||
|
val userEntry = database.user().getUserByIdSync(action.parentUserId)
|
||||||
|
|
||||||
|
if (userEntry == null || userEntry.type != UserType.Parent) {
|
||||||
|
throw IllegalArgumentException("invalid user entry")
|
||||||
|
}
|
||||||
|
|
||||||
|
// the client does not have the data to check the integrity
|
||||||
|
|
||||||
|
database.user().updateUserSync(
|
||||||
|
userEntry.copy(
|
||||||
|
password = action.newPassword
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is IgnoreManipulationAction -> {
|
||||||
|
val originalDeviceEntry = database.device().getDeviceByIdSync(action.deviceId)!!
|
||||||
|
var deviceEntry = originalDeviceEntry
|
||||||
|
|
||||||
|
if (action.ignoreDeviceAdminManipulation) {
|
||||||
|
deviceEntry = deviceEntry.copy(highestProtectionLevel = deviceEntry.currentProtectionLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.ignoreDeviceAdminManipulationAttempt) {
|
||||||
|
deviceEntry = deviceEntry.copy(manipulationTriedDisablingDeviceAdmin = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.ignoreAppDowngrade) {
|
||||||
|
deviceEntry = deviceEntry.copy(highestAppVersion = deviceEntry.currentAppVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.ignoreNotificationAccessManipulation) {
|
||||||
|
deviceEntry = deviceEntry.copy(highestNotificationAccessPermission = deviceEntry.currentNotificationAccessPermission)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.ignoreUsageStatsAccessManipulation) {
|
||||||
|
deviceEntry = deviceEntry.copy(highestUsageStatsPermission = deviceEntry.currentUsageStatsPermission)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.ignoreHadManipulation) {
|
||||||
|
deviceEntry = deviceEntry.copy(hadManipulation = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
database.device().updateDeviceEntry(deviceEntry)
|
||||||
|
}
|
||||||
|
}.let { }
|
||||||
|
|
||||||
|
database.setTransactionSuccessful()
|
||||||
|
} finally {
|
||||||
|
database.endTransaction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.sync.validation
|
||||||
|
|
||||||
|
object ListValidation {
|
||||||
|
fun assertNotEmptyListWithoutDuplicates(list: List<String>) {
|
||||||
|
if (list.isEmpty()) {
|
||||||
|
throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (list.distinct().size != list.size) {
|
||||||
|
throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
151
app/src/main/java/io/timelimit/android/ui/MainActivity.kt
Normal file
151
app/src/main/java/io/timelimit/android/ui/MainActivity.kt
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.ui
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.MenuItem
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.lifecycle.Transformations
|
||||||
|
import androidx.lifecycle.ViewModelProviders
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.async.Threads
|
||||||
|
import io.timelimit.android.extensions.showSafe
|
||||||
|
import io.timelimit.android.livedata.ignoreUnchanged
|
||||||
|
import io.timelimit.android.livedata.liveDataFromValue
|
||||||
|
import io.timelimit.android.livedata.map
|
||||||
|
import io.timelimit.android.livedata.switchMap
|
||||||
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
|
import io.timelimit.android.ui.login.NewLoginFragment
|
||||||
|
import io.timelimit.android.ui.main.ActivityViewModel
|
||||||
|
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
||||||
|
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
||||||
|
import io.timelimit.android.ui.overview.main.MainFragment
|
||||||
|
import io.timelimit.android.ui.setup.SetupTermsFragment
|
||||||
|
|
||||||
|
class MainActivity : AppCompatActivity(), ActivityViewModelHolder {
|
||||||
|
companion object {
|
||||||
|
private const val AUTH_DIALOG_TAG = "adt"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val currentNavigatorFragment = MutableLiveData<Fragment>()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_main)
|
||||||
|
|
||||||
|
// prepare livedata
|
||||||
|
val customTitle = currentNavigatorFragment.switchMap {
|
||||||
|
if (it != null && it is FragmentWithCustomTitle) {
|
||||||
|
it.getCustomTitle()
|
||||||
|
} else {
|
||||||
|
liveDataFromValue(null as String?)
|
||||||
|
}
|
||||||
|
}.ignoreUnchanged()
|
||||||
|
|
||||||
|
val title = Transformations.map(customTitle) {
|
||||||
|
if (it == null) {
|
||||||
|
getString(R.string.app_name)
|
||||||
|
} else {
|
||||||
|
it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// up button
|
||||||
|
val shouldShowBackButtonForNavigatorFragment = currentNavigatorFragment.map { fragment ->
|
||||||
|
(!(fragment is MainFragment)) && (!(fragment is SetupTermsFragment))
|
||||||
|
}
|
||||||
|
|
||||||
|
val shouldShowUpButton = shouldShowBackButtonForNavigatorFragment
|
||||||
|
|
||||||
|
shouldShowUpButton.observe(this, Observer { supportActionBar!!.setDisplayHomeAsUpEnabled(it) })
|
||||||
|
|
||||||
|
// init if not yet done
|
||||||
|
DefaultAppLogic.with(this)
|
||||||
|
|
||||||
|
val fragmentContainer = supportFragmentManager.findFragmentById(R.id.nav_host)!!
|
||||||
|
val fragmentContainerManager = fragmentContainer.childFragmentManager
|
||||||
|
getNavController().addOnDestinationChangedListener { _, _, _ ->
|
||||||
|
Threads.mainThreadHandler.post {
|
||||||
|
currentNavigatorFragment.value = fragmentContainerManager.fragments.first()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
title.observe(this, Observer { setTitle(it) })
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem) = when {
|
||||||
|
item.itemId == android.R.id.home -> {
|
||||||
|
onBackPressed()
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
|
||||||
|
if (!isChangingConfigurations) {
|
||||||
|
getActivityViewModel().logOut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent?) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
|
||||||
|
getNavController().popBackStack(R.id.overviewFragment, true)
|
||||||
|
getNavController().handleDeepLink(
|
||||||
|
getNavController().createDeepLink()
|
||||||
|
.setDestination(R.id.overviewFragment)
|
||||||
|
.createTaskStackBuilder()
|
||||||
|
.intents
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getActivityViewModel(): ActivityViewModel {
|
||||||
|
return ViewModelProviders.of(this).get(ActivityViewModel::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getNavHostFragment(): NavHostFragment {
|
||||||
|
return supportFragmentManager.findFragmentById(R.id.nav_host) as NavHostFragment
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getNavController(): NavController {
|
||||||
|
return getNavHostFragment().navController
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
if (currentNavigatorFragment.value is SetupTermsFragment) {
|
||||||
|
// hack to prevent the user from going to the launch screen of the App if it is not set up
|
||||||
|
finish()
|
||||||
|
} else {
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showAuthenticationScreen() {
|
||||||
|
if (supportFragmentManager.findFragmentByTag(AUTH_DIALOG_TAG) == null) {
|
||||||
|
NewLoginFragment().showSafe(supportFragmentManager, AUTH_DIALOG_TAG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.ui.diagnose
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.format.DateFormat
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import io.timelimit.android.databinding.DiagnoseClockFragmentBinding
|
||||||
|
import io.timelimit.android.date.CalendarCache
|
||||||
|
import io.timelimit.android.date.DateInTimezone
|
||||||
|
import io.timelimit.android.date.getMinuteOfWeek
|
||||||
|
import io.timelimit.android.livedata.liveDataFromFunction
|
||||||
|
import io.timelimit.android.livedata.map
|
||||||
|
import io.timelimit.android.livedata.switchMap
|
||||||
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class DiagnoseClockFragment : Fragment() {
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
val binding = DiagnoseClockFragmentBinding.inflate(inflater, container, false)
|
||||||
|
val logic = DefaultAppLogic.with(context!!)
|
||||||
|
|
||||||
|
val timeZone = logic.deviceUserEntry.map { TimeZone.getTimeZone(it?.timeZone) ?: logic.timeApi.getSystemTimeZone() }
|
||||||
|
val timestamp = liveDataFromFunction { logic.timeApi.getCurrentTimeInMillis() }
|
||||||
|
val dateInTimezone = timeZone.switchMap { tz -> timestamp.map { ts -> DateInTimezone.newInstance(ts, tz) } }
|
||||||
|
val minuteOfWeek = timeZone.switchMap { tz -> timestamp.map { ts -> getMinuteOfWeek(ts, tz) } }
|
||||||
|
val timeOfDay = timeZone.switchMap { tz -> timestamp.map { ts ->
|
||||||
|
val calendar = CalendarCache.getCalendar()
|
||||||
|
|
||||||
|
calendar.firstDayOfWeek = Calendar.MONDAY
|
||||||
|
|
||||||
|
calendar.timeZone = tz
|
||||||
|
calendar.timeInMillis = ts
|
||||||
|
|
||||||
|
String.format("%2d:%2d", calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE))
|
||||||
|
}}
|
||||||
|
val dateString = timeZone.switchMap { tz -> timestamp.map { ts ->
|
||||||
|
DateFormat.getDateFormat(context).apply {
|
||||||
|
setTimeZone(tz)
|
||||||
|
}.format(Date(ts))
|
||||||
|
}}
|
||||||
|
|
||||||
|
timestamp.observe(this, androidx.lifecycle.Observer { binding.epochalSeconds = it / 1000 })
|
||||||
|
timeZone.observe(this, androidx.lifecycle.Observer { binding.timeZone = it.displayName })
|
||||||
|
timeOfDay.observe(this, androidx.lifecycle.Observer { binding.timeOfDay = it })
|
||||||
|
dateString.observe(this, androidx.lifecycle.Observer { binding.dateString = it })
|
||||||
|
dateInTimezone.observe(this, androidx.lifecycle.Observer {
|
||||||
|
binding.dayOfWeek = it.dayOfWeek
|
||||||
|
binding.dayOfEpoch = it.dayOfEpoch.toLong()
|
||||||
|
})
|
||||||
|
minuteOfWeek.observe(this, androidx.lifecycle.Observer {
|
||||||
|
binding.minuteOfWeek = it
|
||||||
|
})
|
||||||
|
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* 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 version 3 of the License.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.ui.diagnose
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.navigation.Navigation
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.databinding.FragmentDiagnoseMainBinding
|
||||||
|
import io.timelimit.android.extensions.safeNavigate
|
||||||
|
|
||||||
|
class DiagnoseMainFragment : Fragment() {
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
val binding = FragmentDiagnoseMainBinding.inflate(inflater, container, false)
|
||||||
|
val navigation = Navigation.findNavController(container!!)
|
||||||
|
|
||||||
|
binding.diagnoseClockButton.setOnClickListener {
|
||||||
|
navigation.safeNavigate(
|
||||||
|
DiagnoseMainFragmentDirections.actionDiagnoseMainFragmentToDiagnoseClockFragment(),
|
||||||
|
R.id.diagnoseMainFragment
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue