mirror of
https://codeberg.org/timelimit/opentimelimit-android.git
synced 2025-10-03 01:39:21 +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