Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-15 18:08:00 +01:00
parent d327c31227
commit 0b2aca0925
638 changed files with 76461 additions and 0 deletions

674
LICENSE Normal file
View file

@ -0,0 +1,674 @@
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.
{one line to give the program's name and a brief idea of what it does.}
Copyright (C) {year} {name of author}
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 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:
{project} Copyright (C) {year} {fullname}
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>.

58
README.MD Normal file
View file

@ -0,0 +1,58 @@
![](docs/images/logo.png)
[![Downloads](https://img.shields.io/badge/dynamic/json?color=green&label=Downloads&query=totalString&url=https%3A%2F%2Fraw.githubusercontent.com%2Ftopjohnwu%2Fmagisk-files%2Fcount%2Fcount.json&cacheSeconds=1800)](https://raw.githubusercontent.com/topjohnwu/magisk-files/count/count.json)
#### This is not an officially supported Google product
## Introduction
Magisk is a suite of open source software for customizing Android, supporting devices higher than Android 6.0.<br>
Some highlight features:
- **MagiskSU**: Provide root access for applications
- **Magisk Modules**: Modify read-only partitions by installing modules
- **MagiskBoot**: The most complete tool for unpacking and repacking Android boot images
- **Zygisk**: Run code in every Android applications' processes
## Downloads
[Github](https://github.com/topjohnwu/Magisk/releases) is the only source where you can get official Magisk information and downloads.
## Useful Links
- [Installation Instruction](https://topjohnwu.github.io/Magisk/install.html)
- [Building and Development](https://topjohnwu.github.io/Magisk/build.html)
- [Magisk Documentation](https://topjohnwu.github.io/Magisk/)
- [Zygisk module sample](https://github.com/topjohnwu/zygisk-module-sample)
## Bug Reports
**Only bug reports from Debug builds will be accepted.**
For installation issues, upload both boot image and install logs.<br>
For Magisk issues, upload boot logcat or dmesg.<br>
For Magisk app crashes, record and upload the logcat when the crash occurs.
## Translation Contributions
Default string resources for the Magisk app and its stub APK are located here:
- `app/core/src/main/res/values/strings.xml`
- `app/stub/src/main/res/values/strings.xml`
Translate each and place them in the respective locations (`[module]/src/main/res/values-[lang]/strings.xml`).
## License
Magisk, including all git submodules are 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/>.

7
app/.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
/dict.txt
# Gradle
.gradle
.kotlin
/local.properties
/build

1
app/apk/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

59
app/apk/build.gradle.kts Normal file
View file

@ -0,0 +1,59 @@
plugins {
id("com.android.application")
kotlin("android")
kotlin("plugin.parcelize")
kotlin("kapt")
id("androidx.navigation.safeargs.kotlin")
}
setupMainApk()
kapt {
correctErrorTypes = true
useBuildCache = true
mapDiagnosticLocations = true
javacOptions {
option("-Xmaxerrs", "1000")
}
}
android {
buildFeatures {
dataBinding = true
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
}
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
}
}
}
dependencies {
implementation(project(":core"))
coreLibraryDesugaring(libs.jdk.libs)
implementation(libs.indeterminate.checkbox)
implementation(libs.rikka.layoutinflater)
implementation(libs.rikka.insets)
implementation(libs.rikka.recyclerview)
implementation(libs.navigation.fragment.ktx)
implementation(libs.navigation.ui.ktx)
implementation(libs.constraintlayout)
implementation(libs.swiperefreshlayout)
implementation(libs.recyclerview)
implementation(libs.transition)
implementation(libs.fragment.ktx)
implementation(libs.appcompat)
implementation(libs.material)
// Make sure kapt runs with a proper kotlin-stdlib
kapt(kotlin("stdlib"))
}

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application android:localeConfig="@xml/locale_config">
<activity
android:name=".ui.MainActivity"
android:exported="true"
android:theme="@style/SplashTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.APPLICATION_PREFERENCES" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name=".ui.surequest.SuRequestActivity"
android:directBootAware="true"
android:exported="false"
android:taskAffinity=""
tools:ignore="AppLinkUrlError">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,22 @@
package com.topjohnwu.magisk.arch
import androidx.annotation.MainThread
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
abstract class AsyncLoadViewModel : BaseViewModel() {
private var loadingJob: Job? = null
@MainThread
fun startLoading() {
if (loadingJob?.isActive == true) {
// Prevent multiple jobs from running at the same time
return
}
loadingJob = viewModelScope.launch { doLoadWork() }
}
protected abstract suspend fun doLoadWork()
}

View file

@ -0,0 +1,96 @@
package com.topjohnwu.magisk.arch
import android.os.Bundle
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.MenuProvider
import androidx.databinding.DataBindingUtil
import androidx.databinding.OnRebindCallback
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.navigation.NavDirections
import com.topjohnwu.magisk.BR
abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHolder {
val activity get() = getActivity() as? NavigationActivity<*>
protected lateinit var binding: Binding
protected abstract val layoutRes: Int
private val navigation get() = activity?.navigation
open val snackbarView: View? get() = null
open val snackbarAnchorView: View? get() = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startObserveLiveData()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate<Binding>(inflater, layoutRes, container, false).also {
it.setVariable(BR.viewModel, viewModel)
it.lifecycleOwner = viewLifecycleOwner
}
if (this is MenuProvider) {
activity?.addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.STARTED)
}
savedInstanceState?.let { viewModel.onRestoreState(it) }
return binding.root
}
override fun onSaveInstanceState(outState: Bundle) {
viewModel.onSaveState(outState)
}
override fun onStart() {
super.onStart()
activity?.supportActionBar?.subtitle = null
}
override fun onEventDispatched(event: ViewEvent) = when(event) {
is ContextExecutor -> event(requireContext())
is ActivityExecutor -> activity?.let { event(it) } ?: Unit
is FragmentExecutor -> event(this)
else -> Unit
}
open fun onKeyEvent(event: KeyEvent): Boolean {
return false
}
open fun onBackPressed(): Boolean = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.addOnRebindCallback(object : OnRebindCallback<Binding>() {
override fun onPreBind(binding: Binding): Boolean {
this@BaseFragment.onPreBind(binding)
return true
}
})
}
override fun onResume() {
super.onResume()
viewModel.let {
if (it is AsyncLoadViewModel)
it.startLoading()
}
}
protected open fun onPreBind(binding: Binding) {
(binding.root as? ViewGroup)?.startAnimations()
}
fun NavDirections.navigate() {
navigation?.currentDestination?.getAction(actionId)?.let { navigation!!.navigate(this) }
}
}

View file

@ -0,0 +1,83 @@
package com.topjohnwu.magisk.arch
import android.Manifest.permission.POST_NOTIFICATIONS
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.annotation.SuppressLint
import android.os.Bundle
import androidx.databinding.PropertyChangeRegistry
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.navigation.NavDirections
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.databinding.ObservableHost
import com.topjohnwu.magisk.events.BackPressEvent
import com.topjohnwu.magisk.events.DialogBuilder
import com.topjohnwu.magisk.events.DialogEvent
import com.topjohnwu.magisk.events.NavigationEvent
import com.topjohnwu.magisk.events.PermissionEvent
import com.topjohnwu.magisk.events.SnackbarEvent
abstract class BaseViewModel : ViewModel(), ObservableHost {
override var callbacks: PropertyChangeRegistry? = null
private val _viewEvents = MutableLiveData<ViewEvent>()
val viewEvents: LiveData<ViewEvent> get() = _viewEvents
open fun onSaveState(state: Bundle) {}
open fun onRestoreState(state: Bundle) {}
open fun onNetworkChanged(network: Boolean) {}
fun withPermission(permission: String, callback: (Boolean) -> Unit) {
PermissionEvent(permission, callback).publish()
}
inline fun withExternalRW(crossinline callback: () -> Unit) {
withPermission(WRITE_EXTERNAL_STORAGE) {
if (!it) {
SnackbarEvent(R.string.external_rw_permission_denied).publish()
} else {
callback()
}
}
}
@SuppressLint("InlinedApi")
inline fun withInstallPermission(crossinline callback: () -> Unit) {
withPermission(REQUEST_INSTALL_PACKAGES) {
if (!it) {
SnackbarEvent(R.string.install_unknown_denied).publish()
} else {
callback()
}
}
}
@SuppressLint("InlinedApi")
inline fun withPostNotificationPermission(crossinline callback: () -> Unit) {
withPermission(POST_NOTIFICATIONS) {
if (!it) {
SnackbarEvent(R.string.post_notifications_denied).publish()
} else {
callback()
}
}
}
fun back() = BackPressEvent().publish()
fun ViewEvent.publish() {
_viewEvents.postValue(this)
}
fun DialogBuilder.show() {
DialogEvent(this).publish()
}
fun NavDirections.navigate(pop: Boolean = false) {
_viewEvents.postValue(NavigationEvent(this, pop))
}
}

View file

@ -0,0 +1,50 @@
package com.topjohnwu.magisk.arch
import android.content.ContentResolver
import android.view.KeyEvent
import androidx.databinding.ViewDataBinding
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.navOptions
import com.topjohnwu.magisk.utils.AccessibilityUtils
abstract class NavigationActivity<Binding : ViewDataBinding> : UIActivity<Binding>() {
abstract val navHostId: Int
private val navHostFragment by lazy {
supportFragmentManager.findFragmentById(navHostId) as NavHostFragment
}
protected val currentFragment get() =
navHostFragment.childFragmentManager.fragments.getOrNull(0) as? BaseFragment<*>
val navigation: NavController get() = navHostFragment.navController
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
return if (binded && currentFragment?.onKeyEvent(event) == true) true else super.dispatchKeyEvent(event)
}
override fun onBackPressed() {
if (binded) {
if (currentFragment?.onBackPressed() == false) {
super.onBackPressed()
}
}
}
companion object {
fun navigate(directions: NavDirections, navigation: NavController, cr: ContentResolver) {
if (AccessibilityUtils.isAnimationEnabled(cr)) {
navigation.navigate(directions)
} else {
navigation.navigate(directions, navOptions {})
}
}
}
fun NavDirections.navigate() {
navigate(this, navigation, contentResolver)
}
}

View file

@ -0,0 +1,141 @@
package com.topjohnwu.magisk.arch
import android.content.Context
import android.content.res.Resources
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.res.use
import androidx.core.view.WindowCompat
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.transition.AutoTransition
import androidx.transition.TransitionManager
import com.google.android.material.snackbar.Snackbar
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.base.ActivityExtension
import com.topjohnwu.magisk.core.base.IActivityExtension
import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.ktx.reflectField
import com.topjohnwu.magisk.core.wrap
import rikka.insets.WindowInsetsHelper
import rikka.layoutinflater.view.LayoutInflaterFactory
abstract class UIActivity<Binding : ViewDataBinding>
: AppCompatActivity(), ViewModelHolder, IActivityExtension {
protected lateinit var binding: Binding
protected abstract val layoutRes: Int
override val extension = ActivityExtension(this)
protected val binded get() = ::binding.isInitialized
open val snackbarView get() = binding.root
open val snackbarAnchorView: View? get() = null
init {
AppCompatDelegate.setDefaultNightMode(Config.darkTheme)
}
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base.wrap())
}
override fun onCreate(savedInstanceState: Bundle?) {
layoutInflater.factory2 = LayoutInflaterFactory(delegate)
.addOnViewCreatedListener(WindowInsetsHelper.LISTENER)
extension.onCreate(savedInstanceState)
if (isRunningAsStub) {
// Overwrite private members to avoid nasty "false" stack traces being logged
val delegate = delegate
val clz = delegate.javaClass
clz.reflectField("mActivityHandlesConfigFlagsChecked").set(delegate, true)
clz.reflectField("mActivityHandlesConfigFlags").set(delegate, 0)
}
super.onCreate(savedInstanceState)
startObserveLiveData()
// We need to set the window background explicitly since for whatever reason it's not
// propagated upstream
obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground))
.use { it.getDrawable(0) }
.also { window.setBackgroundDrawable(it) }
WindowCompat.setDecorFitsSystemWindows(window, false)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
window?.decorView?.post {
// If navigation bar is short enough (gesture navigation enabled), make it transparent
if ((window.decorView.rootWindowInsets?.systemWindowInsetBottom
?: 0) < Resources.getSystem().displayMetrics.density * 40) {
window.navigationBarColor = Color.TRANSPARENT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
window.navigationBarDividerColor = Color.TRANSPARENT
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false
window.isStatusBarContrastEnforced = false
}
}
}
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
extension.onSaveInstanceState(outState)
}
fun setContentView() {
binding = DataBindingUtil.setContentView<Binding>(this, layoutRes).also {
it.setVariable(BR.viewModel, viewModel)
it.lifecycleOwner = this
}
}
fun setAccessibilityDelegate(delegate: View.AccessibilityDelegate?) {
binding.root.rootView.accessibilityDelegate = delegate
}
fun showSnackbar(
message: CharSequence,
length: Int = Snackbar.LENGTH_SHORT,
builder: Snackbar.() -> Unit = {}
) = Snackbar.make(snackbarView, message, length)
.setAnchorView(snackbarAnchorView).apply(builder).show()
override fun onResume() {
super.onResume()
viewModel.let {
if (it is AsyncLoadViewModel)
it.startLoading()
}
}
override fun onEventDispatched(event: ViewEvent) = when (event) {
is ContextExecutor -> event(this)
is ActivityExecutor -> event(this)
else -> Unit
}
}
fun ViewGroup.startAnimations() {
val transition = AutoTransition()
.setInterpolator(FastOutSlowInInterpolator())
.setDuration(400)
.excludeTarget(R.id.main_toolbar, true)
TransitionManager.beginDelayedTransition(
this,
transition
)
}

View file

@ -0,0 +1,21 @@
package com.topjohnwu.magisk.arch
import android.content.Context
/**
* Class for passing events from ViewModels to Activities/Fragments
* (see https://medium.com/google-developers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150)
*/
abstract class ViewEvent
interface ContextExecutor {
operator fun invoke(context: Context)
}
interface ActivityExecutor {
operator fun invoke(activity: UIActivity<*>)
}
interface FragmentExecutor {
operator fun invoke(fragment: BaseFragment<*>)
}

View file

@ -0,0 +1,49 @@
package com.topjohnwu.magisk.arch
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStoreOwner
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.ui.home.HomeViewModel
import com.topjohnwu.magisk.ui.install.InstallViewModel
import com.topjohnwu.magisk.ui.log.LogViewModel
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
import com.topjohnwu.magisk.ui.surequest.SuRequestViewModel
interface ViewModelHolder : LifecycleOwner, ViewModelStoreOwner {
val viewModel: BaseViewModel
fun startObserveLiveData() {
viewModel.viewEvents.observe(this, this::onEventDispatched)
Info.isConnected.observe(this, viewModel::onNetworkChanged)
}
/**
* Called for all [ViewEvent]s published by associated viewModel.
*/
fun onEventDispatched(event: ViewEvent) {}
}
object VMFactory : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return when (modelClass) {
HomeViewModel::class.java -> HomeViewModel(ServiceLocator.networkService)
LogViewModel::class.java -> LogViewModel(ServiceLocator.logRepo)
SuperuserViewModel::class.java -> SuperuserViewModel(ServiceLocator.policyDB)
InstallViewModel::class.java ->
InstallViewModel(ServiceLocator.networkService, ServiceLocator.markwon)
SuRequestViewModel::class.java ->
SuRequestViewModel(ServiceLocator.policyDB, ServiceLocator.timeoutPrefs)
else -> modelClass.newInstance()
} as T
}
}
inline fun <reified VM : ViewModel> ViewModelHolder.viewModel() =
lazy(LazyThreadSafetyMode.NONE) {
ViewModelProvider(this, VMFactory)[VM::class.java]
}

View file

@ -0,0 +1,346 @@
package com.topjohnwu.magisk.databinding
import android.animation.ValueAnimator
import android.content.res.ColorStateList
import android.graphics.Paint
import android.graphics.drawable.Drawable
import android.text.Spanned
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.Spinner
import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.appcompat.widget.Toolbar
import androidx.cardview.widget.CardView
import androidx.core.view.isGone
import androidx.core.view.isInvisible
import androidx.core.view.updateLayoutParams
import androidx.core.widget.ImageViewCompat
import androidx.databinding.BindingAdapter
import androidx.databinding.InverseBindingAdapter
import androidx.databinding.InverseBindingListener
import androidx.databinding.InverseMethod
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
import com.google.android.material.chip.Chip
import com.google.android.material.slider.Slider
import com.google.android.material.textfield.TextInputLayout
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.model.su.SuPolicy
import com.topjohnwu.magisk.utils.TextHolder
import com.topjohnwu.superuser.internal.UiThreadHandler
import com.topjohnwu.widget.IndeterminateCheckBox
import kotlin.math.roundToInt
@BindingAdapter("gone")
fun setGone(view: View, gone: Boolean) {
view.isGone = gone
}
@BindingAdapter("invisible")
fun setInvisible(view: View, invisible: Boolean) {
view.isInvisible = invisible
}
@BindingAdapter("goneUnless")
fun setGoneUnless(view: View, goneUnless: Boolean) {
setGone(view, goneUnless.not())
}
@BindingAdapter("invisibleUnless")
fun setInvisibleUnless(view: View, invisibleUnless: Boolean) {
setInvisible(view, invisibleUnless.not())
}
@BindingAdapter("markdownText")
fun setMarkdownText(tv: TextView, markdown: Spanned) {
ServiceLocator.markwon.setParsedMarkdown(tv, markdown)
}
@BindingAdapter("onNavigationClick")
fun setOnNavigationClickedListener(view: Toolbar, listener: View.OnClickListener) {
view.setNavigationOnClickListener(listener)
}
@BindingAdapter("srcCompat")
fun setImageResource(view: ImageView, @DrawableRes resId: Int) {
view.setImageResource(resId)
}
@BindingAdapter("srcCompat")
fun setImageResource(view: ImageView, drawable: Drawable) {
view.setImageDrawable(drawable)
}
@BindingAdapter("onTouch")
fun setOnTouchListener(view: View, listener: View.OnTouchListener) {
view.setOnTouchListener(listener)
}
@BindingAdapter("scrollToLast")
fun setScrollToLast(view: RecyclerView, shouldScrollToLast: Boolean) {
fun scrollToLast() = UiThreadHandler.handler.postDelayed({
view.scrollToPosition(view.adapter?.itemCount?.minus(1) ?: 0)
}, 30)
fun wait(callback: () -> Unit) {
UiThreadHandler.handler.postDelayed(callback, 1000)
}
fun RecyclerView.Adapter<*>.setListener() {
val observer = object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
scrollToLast()
}
}
registerAdapterDataObserver(observer)
view.setTag(R.id.recyclerScrollListener, observer)
}
fun RecyclerView.Adapter<*>.removeListener() {
val observer =
view.getTag(R.id.recyclerScrollListener) as? RecyclerView.AdapterDataObserver ?: return
unregisterAdapterDataObserver(observer)
}
fun trySetListener(): Unit = view.adapter?.setListener() ?: wait { trySetListener() }
if (shouldScrollToLast) {
trySetListener()
} else {
view.adapter?.removeListener()
}
}
@BindingAdapter("isEnabled")
fun setEnabled(view: View, isEnabled: Boolean) {
view.isEnabled = isEnabled
}
@BindingAdapter("error")
fun TextInputLayout.setErrorString(error: String) {
val newError = error.let { if (it.isEmpty()) null else it }
if (this.error == null && newError == null) return
this.error = newError
}
// md2
@BindingAdapter(
"android:layout_marginLeft",
"android:layout_marginTop",
"android:layout_marginRight",
"android:layout_marginBottom",
"android:layout_marginStart",
"android:layout_marginEnd",
requireAll = false
)
fun View.setMargins(
marginLeft: Int?,
marginTop: Int?,
marginRight: Int?,
marginBottom: Int?,
marginStart: Int?,
marginEnd: Int?
) = updateLayoutParams<ViewGroup.MarginLayoutParams> {
marginLeft?.let { leftMargin = it }
marginTop?.let { topMargin = it }
marginRight?.let { rightMargin = it }
marginBottom?.let { bottomMargin = it }
marginStart?.let { this.marginStart = it }
marginEnd?.let { this.marginEnd = it }
}
@BindingAdapter("nestedScrollingEnabled")
fun RecyclerView.setNestedScrolling(enabled: Boolean) {
isNestedScrollingEnabled = enabled
}
@BindingAdapter("isSelected")
fun View.isSelected(isSelected: Boolean) {
this.isSelected = isSelected
}
@BindingAdapter("dividerVertical", "dividerHorizontal", requireAll = false)
fun RecyclerView.setDividers(dividerVertical: Drawable?, dividerHorizontal: Drawable?) {
if (dividerHorizontal != null) {
DividerItemDecoration(context, LinearLayoutManager.HORIZONTAL).apply {
setDrawable(dividerHorizontal)
}.let { addItemDecoration(it) }
}
if (dividerVertical != null) {
DividerItemDecoration(context, LinearLayoutManager.VERTICAL).apply {
setDrawable(dividerVertical)
}.let { addItemDecoration(it) }
}
}
@BindingAdapter("icon")
fun Button.setIconRes(res: Int) {
(this as MaterialButton).setIconResource(res)
}
@BindingAdapter("icon")
fun Button.setIcon(drawable: Drawable) {
(this as MaterialButton).icon = drawable
}
@BindingAdapter("strokeWidth")
fun MaterialCardView.setCardStrokeWidthBound(stroke: Float) {
strokeWidth = stroke.roundToInt()
}
@BindingAdapter("onMenuClick")
fun Toolbar.setOnMenuClickListener(listener: Toolbar.OnMenuItemClickListener) {
setOnMenuItemClickListener(listener)
}
@BindingAdapter("onCloseClicked")
fun Chip.setOnCloseClickedListenerBinding(listener: View.OnClickListener) {
setOnCloseIconClickListener(listener)
}
@BindingAdapter("progressAnimated")
fun ProgressBar.setProgressAnimated(newProgress: Int) {
val animator = tag as? ValueAnimator
animator?.cancel()
ValueAnimator.ofInt(progress, newProgress).apply {
interpolator = FastOutSlowInInterpolator()
addUpdateListener { progress = it.animatedValue as Int }
tag = this
}.start()
}
@BindingAdapter("android:text")
fun TextView.setTextSafe(text: Int) {
if (text == 0) this.text = null else setText(text)
}
@BindingAdapter("android:onLongClick")
fun View.setOnLongClickListenerBinding(listener: () -> Unit) {
setOnLongClickListener {
listener()
true
}
}
@BindingAdapter("strikeThrough")
fun TextView.setStrikeThroughEnabled(useStrikeThrough: Boolean) {
paintFlags = if (useStrikeThrough) {
paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
} else {
paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv()
}
}
@BindingAdapter("spanCount")
fun RecyclerView.setSpanCount(count: Int) {
when (val lama = layoutManager) {
is GridLayoutManager -> lama.spanCount = count
is StaggeredGridLayoutManager -> lama.spanCount = count
}
}
@BindingAdapter("state")
fun setState(view: IndeterminateCheckBox, state: Boolean?) {
if (view.state != state)
view.state = state
}
@InverseBindingAdapter(attribute = "state")
fun getState(view: IndeterminateCheckBox) = view.state
@BindingAdapter("stateAttrChanged")
fun setListeners(
view: IndeterminateCheckBox,
attrChange: InverseBindingListener
) {
view.setOnStateChangedListener { _, _ ->
attrChange.onChange()
}
}
@BindingAdapter("cardBackgroundColorAttr")
fun CardView.setCardBackgroundColorAttr(attr: Int) {
val tv = TypedValue()
context.theme.resolveAttribute(attr, tv, true)
setCardBackgroundColor(tv.data)
}
@BindingAdapter("tint")
fun ImageView.setTint(color: Int) {
ImageViewCompat.setImageTintList(this, ColorStateList.valueOf(color))
}
@BindingAdapter("tintAttr")
fun ImageView.setTintAttr(attr: Int) {
val tv = TypedValue()
context.theme.resolveAttribute(attr, tv, true)
ImageViewCompat.setImageTintList(this, ColorStateList.valueOf(tv.data))
}
@BindingAdapter("textColorAttr")
fun TextView.setTextColorAttr(attr: Int) {
val tv = TypedValue()
context.theme.resolveAttribute(attr, tv, true)
setTextColor(tv.data)
}
@BindingAdapter("android:text")
fun TextView.setText(text: TextHolder) {
this.text = text.getText(context.resources)
}
@BindingAdapter("items", "layout")
fun Spinner.setAdapter(items: Array<Any>, layoutRes: Int) {
adapter = ArrayAdapter(context, layoutRes, items)
}
@BindingAdapter("labelFormatter")
fun Slider.setLabelFormatter(formatter: (Float) -> Int) {
setLabelFormatter { value -> resources.getString(formatter(value)) }
}
@InverseBindingAdapter(attribute = "android:value")
fun Slider.getValueBinding() = value
@BindingAdapter("android:valueAttrChanged")
fun Slider.setListener(attrChange: InverseBindingListener) {
addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
override fun onStartTrackingTouch(slider: Slider) = Unit
override fun onStopTrackingTouch(slider: Slider) = attrChange.onChange()
})
}
@InverseMethod("sliderValueToPolicy")
fun policyToSliderValue(policy: Int): Float {
return when (policy) {
SuPolicy.DENY -> 1f
SuPolicy.RESTRICT -> 2f
SuPolicy.ALLOW -> 3f
else -> 1f
}
}
fun sliderValueToPolicy(value: Float): Int {
return when (value) {
1f -> SuPolicy.DENY
2f -> SuPolicy.RESTRICT
3f -> SuPolicy.ALLOW
else -> SuPolicy.DENY
}
}

View file

@ -0,0 +1,156 @@
package com.topjohnwu.magisk.databinding
import androidx.annotation.MainThread
import androidx.annotation.WorkerThread
import androidx.databinding.ListChangeRegistry
import androidx.databinding.ObservableList
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListUpdateCallback
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.AbstractList
// Only expose the immutable List types
interface DiffList<T : DiffItem<*>> : List<T> {
fun calculateDiff(newItems: List<T>): DiffUtil.DiffResult
@MainThread
fun update(newItems: List<T>, diffResult: DiffUtil.DiffResult)
@WorkerThread
suspend fun update(newItems: List<T>)
}
interface FilterList<T : DiffItem<*>> : List<T> {
fun filter(filter: (T) -> Boolean)
@MainThread
fun set(newItems: List<T>)
}
fun <T : DiffItem<*>> diffList(): DiffList<T> = DiffObservableList()
fun <T : DiffItem<*>> filterList(scope: CoroutineScope): FilterList<T> =
FilterableDiffObservableList(scope)
private open class DiffObservableList<T : DiffItem<*>>
: AbstractList<T>(), ObservableList<T>, DiffList<T>, ListUpdateCallback {
protected var list: List<T> = emptyList()
private val listeners = ListChangeRegistry()
override val size: Int get() = list.size
override fun get(index: Int) = list[index]
override fun calculateDiff(newItems: List<T>): DiffUtil.DiffResult {
return doCalculateDiff(list, newItems)
}
protected fun doCalculateDiff(oldItems: List<T>, newItems: List<T>): DiffUtil.DiffResult {
return DiffUtil.calculateDiff(object : DiffUtil.Callback() {
override fun getOldListSize() = oldItems.size
override fun getNewListSize() = newItems.size
@Suppress("UNCHECKED_CAST")
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition]
return (oldItem as DiffItem<Any>).itemSameAs(newItem)
}
@Suppress("UNCHECKED_CAST")
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition]
return (oldItem as DiffItem<Any>).contentSameAs(newItem)
}
}, true)
}
@MainThread
override fun update(newItems: List<T>, diffResult: DiffUtil.DiffResult) {
list = ArrayList(newItems)
diffResult.dispatchUpdatesTo(this)
}
@WorkerThread
override suspend fun update(newItems: List<T>) {
val diffResult = calculateDiff(newItems)
withContext(Dispatchers.Main) {
update(newItems, diffResult)
}
}
override fun addOnListChangedCallback(listener: ObservableList.OnListChangedCallback<out ObservableList<T>>) {
listeners.add(listener)
}
override fun removeOnListChangedCallback(listener: ObservableList.OnListChangedCallback<out ObservableList<T>>) {
listeners.remove(listener)
}
override fun onChanged(position: Int, count: Int, payload: Any?) {
listeners.notifyChanged(this, position, count)
}
override fun onMoved(fromPosition: Int, toPosition: Int) {
listeners.notifyMoved(this, fromPosition, toPosition, 1)
}
override fun onInserted(position: Int, count: Int) {
modCount += 1
listeners.notifyInserted(this, position, count)
}
override fun onRemoved(position: Int, count: Int) {
modCount += 1
listeners.notifyRemoved(this, position, count)
}
}
private class FilterableDiffObservableList<T : DiffItem<*>>(
private val scope: CoroutineScope
) : DiffObservableList<T>(), FilterList<T> {
private var sublist: List<T> = emptyList()
private var job: Job? = null
private var lastFilter: ((T) -> Boolean)? = null
// ---
override fun filter(filter: (T) -> Boolean) {
lastFilter = filter
job?.cancel()
job = scope.launch(Dispatchers.Default) {
val oldList = sublist
val newList = list.filter(filter)
val diff = doCalculateDiff(oldList, newList)
withContext(Dispatchers.Main) {
sublist = newList
diff.dispatchUpdatesTo(this@FilterableDiffObservableList)
}
}
}
// ---
override fun get(index: Int): T {
return sublist[index]
}
override val size: Int
get() = sublist.size
@MainThread
override fun set(newItems: List<T>) {
onRemoved(0, sublist.size)
list = newItems
sublist = emptyList()
lastFilter?.let { filter(it) }
}
}

View file

@ -0,0 +1,162 @@
package com.topjohnwu.magisk.databinding
import androidx.databinding.ListChangeRegistry
import androidx.databinding.ObservableList
import androidx.databinding.ObservableList.OnListChangedCallback
import java.util.AbstractList
@Suppress("UNCHECKED_CAST")
class MergeObservableList<T> : AbstractList<T>(), ObservableList<T> {
private val lists: MutableList<List<T>> = mutableListOf()
private val listeners = ListChangeRegistry()
private val callback = Callback<T>()
override fun addOnListChangedCallback(callback: OnListChangedCallback<out ObservableList<T>>) {
listeners.add(callback)
}
override fun removeOnListChangedCallback(callback: OnListChangedCallback<out ObservableList<T>>) {
listeners.remove(callback)
}
override fun get(index: Int): T {
if (index < 0)
throw IndexOutOfBoundsException()
var idx = index
for (list in lists) {
val size = list.size
if (idx < size) {
return list[idx]
}
idx -= size
}
throw IndexOutOfBoundsException()
}
override val size: Int
get() = lists.fold(0) { i, it -> i + it.size }
fun insertItem(obj: T): MergeObservableList<T> {
val idx = size
lists.add(listOf(obj))
++modCount
listeners.notifyInserted(this, idx, 1)
return this
}
fun insertList(list: List<T>): MergeObservableList<T> {
val idx = size
lists.add(list)
++modCount
(list as? ObservableList<T>)?.addOnListChangedCallback(callback)
if (list.isNotEmpty())
listeners.notifyInserted(this, idx, list.size)
return this
}
fun removeItem(obj: T): Boolean {
var idx = 0
for ((i, list) in lists.withIndex()) {
if (list !is ObservableList<*>) {
if (obj == list[0]) {
lists.removeAt(i)
++modCount
listeners.notifyRemoved(this, idx, 1)
return true
}
}
idx += list.size
}
return false
}
fun removeList(listToRemove: List<T>): Boolean {
var idx = 0
for ((i, list) in lists.withIndex()) {
if (listToRemove === list) {
(list as? ObservableList<T>)?.removeOnListChangedCallback(callback)
lists.removeAt(i)
++modCount
listeners.notifyRemoved(this, idx, list.size)
return true
}
idx += list.size
}
return false
}
override fun clear() {
val sz = size
for (list in lists) {
if (list is ObservableList) {
list.removeOnListChangedCallback(callback)
}
}
++modCount
lists.clear()
if (sz > 0)
listeners.notifyRemoved(this, 0, sz)
}
private fun subIndexToIndex(subList: List<*>, index: Int): Int {
if (index < 0)
throw IndexOutOfBoundsException()
var idx = 0
for (list in lists) {
if (subList === list) {
return idx + index
}
idx += list.size
}
throw IllegalArgumentException()
}
inner class Callback<T> : OnListChangedCallback<ObservableList<T>>() {
override fun onChanged(sender: ObservableList<T>) {
++modCount
listeners.notifyChanged(this@MergeObservableList)
}
override fun onItemRangeChanged(
sender: ObservableList<T>,
positionStart: Int,
itemCount: Int
) {
listeners.notifyChanged(this@MergeObservableList,
subIndexToIndex(sender, positionStart), itemCount)
}
override fun onItemRangeInserted(
sender: ObservableList<T>,
positionStart: Int,
itemCount: Int
) {
++modCount
listeners.notifyInserted(this@MergeObservableList,
subIndexToIndex(sender, positionStart), itemCount)
}
override fun onItemRangeMoved(
sender: ObservableList<T>,
fromPosition: Int,
toPosition: Int,
itemCount: Int
) {
val idx = subIndexToIndex(sender, 0)
listeners.notifyMoved(this@MergeObservableList,
idx + fromPosition, idx + toPosition, itemCount)
}
override fun onItemRangeRemoved(
sender: ObservableList<T>,
positionStart: Int,
itemCount: Int
) {
++modCount
listeners.notifyRemoved(this@MergeObservableList,
subIndexToIndex(sender, positionStart), itemCount)
}
}
}

View file

@ -0,0 +1,97 @@
package com.topjohnwu.magisk.databinding
import androidx.databinding.Observable
import androidx.databinding.PropertyChangeRegistry
/**
* Modified from https://github.com/skoumalcz/teanity/blob/1.2/core/src/main/java/com/skoumal/teanity/observable/Notifyable.kt
*
* Interface that allows user to be observed via DataBinding or manually by assigning listeners.
*
* @see [androidx.databinding.Observable]
* */
interface ObservableHost : Observable {
var callbacks: PropertyChangeRegistry?
/**
* Notifies all observers that something has changed. By default implementation this method is
* synchronous, hence observers will never be notified in undefined order. Observers might
* choose to refresh the view completely, which is beyond the scope of this function.
* */
fun notifyChange() {
synchronized(this) {
callbacks ?: return
}.notifyCallbacks(this, 0, null)
}
/**
* Notifies all observers about field with [fieldId] has been changed. This will happen
* synchronously before or after [notifyChange] has been called. It will never be called during
* the execution of aforementioned method.
* */
fun notifyPropertyChanged(fieldId: Int) {
synchronized(this) {
callbacks ?: return
}.notifyCallbacks(this, fieldId, null)
}
override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
synchronized(this) {
callbacks ?: PropertyChangeRegistry().also { callbacks = it }
}.add(callback)
}
override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
synchronized(this) {
callbacks ?: return
}.remove(callback)
}
}
fun ObservableHost.addOnPropertyChangedCallback(
fieldId: Int,
removeAfterChanged: Boolean = false,
callback: () -> Unit
) {
addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
if (fieldId == propertyId) {
callback()
if (removeAfterChanged)
removeOnPropertyChangedCallback(this)
}
}
})
}
/**
* Injects boilerplate implementation for {@literal @}[androidx.databinding.Bindable] field setters.
*
* # Examples:
* ```kotlin
* @get:Bindable
* var myField = defaultValue
* set(value) = set(value, field, { field = it }, BR.myField) {
* doSomething(it)
* }
* ```
* */
inline fun <reified T> ObservableHost.set(
new: T, old: T, setter: (T) -> Unit, fieldId: Int, afterChanged: (T) -> Unit = {}) {
if (old != new) {
setter(new)
notifyPropertyChanged(fieldId)
afterChanged(new)
}
}
inline fun <reified T> ObservableHost.set(
new: T, old: T, setter: (T) -> Unit, vararg fieldIds: Int, afterChanged: (T) -> Unit = {}) {
if (old != new) {
setter(new)
fieldIds.forEach { notifyPropertyChanged(it) }
afterChanged(new)
}
}

View file

@ -0,0 +1,35 @@
package com.topjohnwu.magisk.databinding
import androidx.databinding.PropertyChangeRegistry
import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.RecyclerView
abstract class RvItem {
abstract val layoutRes: Int
}
abstract class ObservableRvItem : RvItem(), ObservableHost {
override var callbacks: PropertyChangeRegistry? = null
}
interface ItemWrapper<E> {
val item: E
}
interface ViewAwareItem {
fun onBind(binding: ViewDataBinding, recyclerView: RecyclerView)
}
interface DiffItem<T : Any> {
fun itemSameAs(other: T): Boolean {
if (this === other) return true
return when (this) {
is ItemWrapper<*> -> item == (other as ItemWrapper<*>).item
is Comparable<*> -> compareValues(this, other as Comparable<*>) == 0
else -> this == other
}
}
fun contentSameAs(other: T) = true
}

View file

@ -0,0 +1,121 @@
package com.topjohnwu.magisk.databinding
import android.annotation.SuppressLint
import android.util.SparseArray
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.databinding.BindingAdapter
import androidx.databinding.DataBindingUtil
import androidx.databinding.ObservableList
import androidx.databinding.ObservableList.OnListChangedCallback
import androidx.databinding.ViewDataBinding
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.recyclerview.widget.RecyclerView
import com.topjohnwu.magisk.BR
class RvItemAdapter<T: RvItem>(
val items: List<T>,
val extraBindings: SparseArray<*>?
) : RecyclerView.Adapter<RvItemAdapter.ViewHolder>() {
private var lifecycleOwner: LifecycleOwner? = null
private var recyclerView: RecyclerView? = null
private val observer by lazy(LazyThreadSafetyMode.NONE) { ListObserver<T>() }
override fun onAttachedToRecyclerView(rv: RecyclerView) {
lifecycleOwner = rv.findViewTreeLifecycleOwner()
recyclerView = rv
if (items is ObservableList)
items.addOnListChangedCallback(observer)
}
override fun onDetachedFromRecyclerView(rv: RecyclerView) {
lifecycleOwner = null
recyclerView = null
if (items is ObservableList)
items.removeOnListChangedCallback(observer)
}
override fun onCreateViewHolder(parent: ViewGroup, layoutRes: Int): ViewHolder {
val inflator = LayoutInflater.from(parent.context)
return ViewHolder(DataBindingUtil.inflate(inflator, layoutRes, parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
holder.binding.setVariable(BR.item, item)
extraBindings?.let {
for (i in 0 until it.size()) {
holder.binding.setVariable(it.keyAt(i), it.valueAt(i))
}
}
holder.binding.lifecycleOwner = lifecycleOwner
holder.binding.executePendingBindings()
recyclerView?.let {
if (item is ViewAwareItem)
item.onBind(holder.binding, it)
}
}
override fun getItemCount() = items.size
override fun getItemViewType(position: Int) = items[position].layoutRes
class ViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root)
inner class ListObserver<T: RvItem> : OnListChangedCallback<ObservableList<T>>() {
@SuppressLint("NotifyDataSetChanged")
override fun onChanged(sender: ObservableList<T>) {
notifyDataSetChanged()
}
override fun onItemRangeChanged(
sender: ObservableList<T>,
positionStart: Int,
itemCount: Int
) {
notifyItemRangeChanged(positionStart, itemCount)
}
override fun onItemRangeInserted(
sender: ObservableList<T>?,
positionStart: Int,
itemCount: Int
) {
notifyItemRangeInserted(positionStart, itemCount)
}
override fun onItemRangeMoved(
sender: ObservableList<T>?,
fromPosition: Int,
toPosition: Int,
itemCount: Int
) {
for (i in 0 until itemCount) {
notifyItemMoved(fromPosition + i, toPosition + i)
}
}
override fun onItemRangeRemoved(
sender: ObservableList<T>?,
positionStart: Int,
itemCount: Int
) {
notifyItemRangeRemoved(positionStart, itemCount)
}
}
}
inline fun bindExtra(body: (SparseArray<Any?>) -> Unit) = SparseArray<Any?>().also(body)
@BindingAdapter("items", "extraBindings", requireAll = false)
fun <T: RvItem> RecyclerView.setAdapter(items: List<T>?, extraBindings: SparseArray<*>?) {
if (items != null) {
val rva = (adapter as? RvItemAdapter<*>)
if (rva == null || rva.items !== items || rva.extraBindings !== extraBindings) {
adapter = RvItemAdapter(items, extraBindings)
}
}
}

View file

@ -0,0 +1,41 @@
package com.topjohnwu.magisk.dialog
import android.app.Activity
import androidx.appcompat.app.AppCompatDelegate
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.events.DialogBuilder
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.magisk.core.R as CoreR
class DarkThemeDialog : DialogBuilder {
override fun build(dialog: MagiskDialog) {
val activity = dialog.ownerActivity!!
dialog.apply {
setTitle(CoreR.string.settings_dark_mode_title)
setMessage(CoreR.string.settings_dark_mode_message)
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = CoreR.string.settings_dark_mode_light
icon = R.drawable.ic_day
onClick { selectTheme(AppCompatDelegate.MODE_NIGHT_NO, activity) }
}
setButton(MagiskDialog.ButtonType.NEUTRAL) {
text = CoreR.string.settings_dark_mode_system
icon = R.drawable.ic_day_night
onClick { selectTheme(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM, activity) }
}
setButton(MagiskDialog.ButtonType.NEGATIVE) {
text = CoreR.string.settings_dark_mode_dark
icon = R.drawable.ic_night
onClick { selectTheme(AppCompatDelegate.MODE_NIGHT_YES, activity) }
}
}
}
private fun selectTheme(mode: Int, activity: Activity) {
Config.darkTheme = mode
(activity as UIActivity<*>).delegate.localNightMode = mode
}
}

View file

@ -0,0 +1,65 @@
package com.topjohnwu.magisk.dialog
import android.widget.Toast
import androidx.core.os.postDelayed
import androidx.lifecycle.lifecycleScope
import com.topjohnwu.magisk.core.BuildConfig
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.ktx.reboot
import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
import com.topjohnwu.magisk.events.DialogBuilder
import com.topjohnwu.magisk.ui.home.HomeViewModel
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.superuser.internal.UiThreadHandler
import kotlinx.coroutines.launch
class EnvFixDialog(private val vm: HomeViewModel, private val code: Int) : DialogBuilder {
override fun build(dialog: MagiskDialog) {
dialog.apply {
setTitle(R.string.env_fix_title)
setMessage(R.string.env_fix_msg)
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = android.R.string.ok
doNotDismiss = true
onClick {
dialog.apply {
setTitle(R.string.setup_title)
setMessage(R.string.setup_msg)
resetButtons()
setCancelable(false)
}
dialog.activity.lifecycleScope.launch {
MagiskInstaller.FixEnv().exec { success ->
dialog.dismiss()
context.toast(
if (success) R.string.reboot_delay_toast else R.string.setup_fail,
Toast.LENGTH_LONG
)
if (success)
UiThreadHandler.handler.postDelayed(5000) { reboot() }
}
}
}
}
setButton(MagiskDialog.ButtonType.NEGATIVE) {
text = android.R.string.cancel
}
}
if (code == 2 || // No rules block, module policy not loaded
Info.env.versionCode != BuildConfig.APP_VERSION_CODE ||
Info.env.versionString != BuildConfig.APP_VERSION_NAME) {
dialog.setMessage(R.string.env_full_fix_msg)
dialog.setButton(MagiskDialog.ButtonType.POSITIVE) {
text = android.R.string.ok
onClick {
vm.onMagiskPressed()
dialog.dismiss()
}
}
}
}
}

View file

@ -0,0 +1,33 @@
package com.topjohnwu.magisk.dialog
import android.net.Uri
import com.topjohnwu.magisk.MainDirections
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.events.DialogBuilder
import com.topjohnwu.magisk.ui.module.ModuleViewModel
import com.topjohnwu.magisk.view.MagiskDialog
class LocalModuleInstallDialog(
private val viewModel: ModuleViewModel,
private val uri: Uri,
private val displayName: String
) : DialogBuilder {
override fun build(dialog: MagiskDialog) {
dialog.apply {
setTitle(R.string.confirm_install_title)
setMessage(context.getString(R.string.confirm_install, displayName))
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = android.R.string.ok
onClick {
viewModel.apply {
MainDirections.actionFlashFragment(Const.Value.FLASH_ZIP, uri).navigate()
}
}
}
setButton(MagiskDialog.ButtonType.NEGATIVE) {
text = android.R.string.cancel
}
}
}
}

View file

@ -0,0 +1,34 @@
package com.topjohnwu.magisk.dialog
import com.topjohnwu.magisk.core.AppContext
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.download.DownloadEngine
import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.view.MagiskDialog
import java.io.File
class ManagerInstallDialog : MarkDownDialog() {
override suspend fun getMarkdownText(): String {
val text = Info.update.note
// Cache the changelog
File(AppContext.cacheDir, "${Info.update.versionCode}.md").writeText(text)
return text
}
override fun build(dialog: MagiskDialog) {
super.build(dialog)
dialog.apply {
setCancelable(true)
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = R.string.install
onClick { DownloadEngine.startWithActivity(activity, Subject.App()) }
}
setButton(MagiskDialog.ButtonType.NEGATIVE) {
text = android.R.string.cancel
}
}
}
}

View file

@ -0,0 +1,39 @@
package com.topjohnwu.magisk.dialog
import android.view.LayoutInflater
import android.widget.TextView
import androidx.annotation.CallSuper
import androidx.lifecycle.lifecycleScope
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.events.DialogBuilder
import com.topjohnwu.magisk.view.MagiskDialog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.IOException
import com.topjohnwu.magisk.core.R as CoreR
abstract class MarkDownDialog : DialogBuilder {
abstract suspend fun getMarkdownText(): String
@CallSuper
override fun build(dialog: MagiskDialog) {
with(dialog) {
val view = LayoutInflater.from(context).inflate(R.layout.markdown_window_md2, null)
setView(view)
val tv = view.findViewById<TextView>(R.id.md_txt)
activity.lifecycleScope.launch {
try {
val text = withContext(Dispatchers.IO) { getMarkdownText() }
ServiceLocator.markwon.setMarkdown(tv, text)
} catch (e: IOException) {
Timber.e(e)
tv.setText(CoreR.string.download_file_error)
}
}
}
}
}

View file

@ -0,0 +1,59 @@
package com.topjohnwu.magisk.dialog
import android.content.Context
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.download.DownloadEngine
import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.core.model.module.OnlineModule
import com.topjohnwu.magisk.ui.flash.FlashFragment
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.magisk.view.Notifications
import kotlinx.parcelize.Parcelize
class OnlineModuleInstallDialog(private val item: OnlineModule) : MarkDownDialog() {
private val svc get() = ServiceLocator.networkService
override suspend fun getMarkdownText(): String {
val str = svc.fetchString(item.changelog)
return if (str.length > 1000) str.substring(0, 1000) else str
}
@Parcelize
class Module(
override val module: OnlineModule,
override val autoLaunch: Boolean,
override val notifyId: Int = Notifications.nextId()
) : Subject.Module() {
override fun pendingIntent(context: Context) = FlashFragment.installIntent(context, file)
}
override fun build(dialog: MagiskDialog) {
super.build(dialog)
dialog.apply {
fun download(install: Boolean) {
DownloadEngine.startWithActivity(activity, Module(item, install))
}
val title = context.getString(R.string.repo_install_title,
item.name, item.version, item.versionCode)
setTitle(title)
setCancelable(true)
setButton(MagiskDialog.ButtonType.NEGATIVE) {
text = R.string.download
onClick { download(false) }
}
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = R.string.install
onClick { download(true) }
}
setButton(MagiskDialog.ButtonType.NEUTRAL) {
text = android.R.string.cancel
}
}
}
}

View file

@ -0,0 +1,19 @@
package com.topjohnwu.magisk.dialog
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.events.DialogBuilder
import com.topjohnwu.magisk.view.MagiskDialog
class SecondSlotWarningDialog : DialogBuilder {
override fun build(dialog: MagiskDialog) {
dialog.apply {
setTitle(android.R.string.dialog_alert_title)
setMessage(R.string.install_inactive_slot_msg)
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = android.R.string.ok
}
setCancelable(true)
}
}
}

View file

@ -0,0 +1,25 @@
package com.topjohnwu.magisk.dialog
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.events.DialogBuilder
import com.topjohnwu.magisk.view.MagiskDialog
class SuperuserRevokeDialog(
private val appName: String,
private val onSuccess: () -> Unit
) : DialogBuilder {
override fun build(dialog: MagiskDialog) {
dialog.apply {
setTitle(R.string.su_revoke_title)
setMessage(R.string.su_revoke_msg, appName)
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = android.R.string.ok
onClick { onSuccess() }
}
setButton(MagiskDialog.ButtonType.NEGATIVE) {
text = android.R.string.cancel
}
}
}
}

View file

@ -0,0 +1,57 @@
package com.topjohnwu.magisk.dialog
import android.app.ProgressDialog
import android.widget.Toast
import androidx.lifecycle.lifecycleScope
import com.topjohnwu.magisk.arch.NavigationActivity
import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
import com.topjohnwu.magisk.events.DialogBuilder
import com.topjohnwu.magisk.ui.flash.FlashFragment
import com.topjohnwu.magisk.view.MagiskDialog
import kotlinx.coroutines.launch
class UninstallDialog : DialogBuilder {
override fun build(dialog: MagiskDialog) {
dialog.apply {
setTitle(R.string.uninstall_magisk_title)
setMessage(R.string.uninstall_magisk_msg)
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = R.string.restore_img
onClick { restore(dialog.activity) }
}
setButton(MagiskDialog.ButtonType.NEGATIVE) {
text = R.string.complete_uninstall
onClick { completeUninstall(dialog) }
}
}
}
@Suppress("DEPRECATION")
private fun restore(activity: UIActivity<*>) {
val dialog = ProgressDialog(activity).apply {
setMessage(activity.getString(R.string.restore_img_msg))
show()
}
activity.lifecycleScope.launch {
MagiskInstaller.Restore().exec { success ->
dialog.dismiss()
if (success) {
activity.toast(R.string.restore_done, Toast.LENGTH_SHORT)
} else {
activity.toast(R.string.restore_fail, Toast.LENGTH_LONG)
}
}
}
}
private fun completeUninstall(dialog: MagiskDialog) {
(dialog.ownerActivity as NavigationActivity<*>)
.navigation.navigate(FlashFragment.uninstall())
}
}

View file

@ -0,0 +1,124 @@
package com.topjohnwu.magisk.events
import android.content.Context
import android.view.View
import androidx.annotation.StringRes
import androidx.navigation.NavDirections
import com.google.android.material.snackbar.Snackbar
import com.topjohnwu.magisk.arch.ActivityExecutor
import com.topjohnwu.magisk.arch.ContextExecutor
import com.topjohnwu.magisk.arch.NavigationActivity
import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.arch.ViewEvent
import com.topjohnwu.magisk.core.base.ContentResultCallback
import com.topjohnwu.magisk.core.base.relaunch
import com.topjohnwu.magisk.utils.TextHolder
import com.topjohnwu.magisk.utils.asText
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.magisk.view.Shortcuts
class PermissionEvent(
private val permission: String,
private val callback: (Boolean) -> Unit
) : ViewEvent(), ActivityExecutor {
override fun invoke(activity: UIActivity<*>) =
activity.withPermission(permission, callback)
}
class BackPressEvent : ViewEvent(), ActivityExecutor {
override fun invoke(activity: UIActivity<*>) {
activity.onBackPressed()
}
}
class DieEvent : ViewEvent(), ActivityExecutor {
override fun invoke(activity: UIActivity<*>) {
activity.finish()
}
}
class ShowUIEvent(private val delegate: View.AccessibilityDelegate?)
: ViewEvent(), ActivityExecutor {
override fun invoke(activity: UIActivity<*>) {
activity.setContentView()
activity.setAccessibilityDelegate(delegate)
}
}
class RecreateEvent : ViewEvent(), ActivityExecutor {
override fun invoke(activity: UIActivity<*>) {
activity.relaunch()
}
}
class AuthEvent(
private val callback: () -> Unit
) : ViewEvent(), ActivityExecutor {
override fun invoke(activity: UIActivity<*>) {
activity.withAuthentication { if (it) callback() }
}
}
class GetContentEvent(
private val type: String,
private val callback: ContentResultCallback
) : ViewEvent(), ActivityExecutor {
override fun invoke(activity: UIActivity<*>) {
activity.getContent(type, callback)
}
}
class NavigationEvent(
private val directions: NavDirections,
private val pop: Boolean
) : ViewEvent(), ActivityExecutor {
override fun invoke(activity: UIActivity<*>) {
(activity as? NavigationActivity<*>)?.apply {
if (pop) navigation.popBackStack()
directions.navigate()
}
}
}
class AddHomeIconEvent : ViewEvent(), ContextExecutor {
override fun invoke(context: Context) {
Shortcuts.addHomeIcon(context)
}
}
class SnackbarEvent(
private val msg: TextHolder,
private val length: Int = Snackbar.LENGTH_SHORT,
private val builder: Snackbar.() -> Unit = {}
) : ViewEvent(), ActivityExecutor {
constructor(
@StringRes res: Int,
length: Int = Snackbar.LENGTH_SHORT,
builder: Snackbar.() -> Unit = {}
) : this(res.asText(), length, builder)
constructor(
msg: String,
length: Int = Snackbar.LENGTH_SHORT,
builder: Snackbar.() -> Unit = {}
) : this(msg.asText(), length, builder)
override fun invoke(activity: UIActivity<*>) {
activity.showSnackbar(msg.getText(activity.resources), length, builder)
}
}
class DialogEvent(
private val builder: DialogBuilder
) : ViewEvent(), ActivityExecutor {
override fun invoke(activity: UIActivity<*>) {
MagiskDialog(activity).apply(builder::build).show()
}
}
interface DialogBuilder {
fun build(dialog: MagiskDialog)
}

View file

@ -0,0 +1,278 @@
package com.topjohnwu.magisk.ui
import android.Manifest
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import android.view.WindowManager
import android.widget.Toast
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.view.forEach
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavDirections
import com.topjohnwu.magisk.MainDirections
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseViewModel
import com.topjohnwu.magisk.arch.NavigationActivity
import com.topjohnwu.magisk.arch.startAnimations
import com.topjohnwu.magisk.arch.viewModel
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.base.SplashController
import com.topjohnwu.magisk.core.base.SplashScreenHost
import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.core.model.module.LocalModule
import com.topjohnwu.magisk.core.tasks.AppMigration
import com.topjohnwu.magisk.databinding.ActivityMainMd2Binding
import com.topjohnwu.magisk.ui.home.HomeFragmentDirections
import com.topjohnwu.magisk.ui.theme.Theme
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.magisk.view.Shortcuts
import kotlinx.coroutines.launch
import java.io.File
import com.topjohnwu.magisk.core.R as CoreR
class MainViewModel : BaseViewModel()
class MainActivity : NavigationActivity<ActivityMainMd2Binding>(), SplashScreenHost {
override val layoutRes = R.layout.activity_main_md2
override val viewModel by viewModel<MainViewModel>()
override val navHostId: Int = R.id.main_nav_host
override val splashController = SplashController(this)
override val snackbarView: View
get() {
val fragmentOverride = currentFragment?.snackbarView
return fragmentOverride ?: super.snackbarView
}
override val snackbarAnchorView: View?
get() {
val fragmentAnchor = currentFragment?.snackbarAnchorView
return when {
fragmentAnchor?.isVisible == true -> fragmentAnchor
binding.mainNavigation.isVisible -> return binding.mainNavigation
else -> null
}
}
private var isRootFragment = true
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(Theme.selected.themeRes)
splashController.preOnCreate()
super.onCreate(savedInstanceState)
splashController.onCreate(savedInstanceState)
}
override fun onResume() {
super.onResume()
splashController.onResume()
}
@SuppressLint("InlinedApi")
override fun onCreateUi(savedInstanceState: Bundle?) {
setContentView()
showUnsupportedMessage()
askForHomeShortcut()
// Ask permission to post notifications for background update check
if (Config.checkUpdate) {
withPermission(Manifest.permission.POST_NOTIFICATIONS) {
Config.checkUpdate = it
}
}
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
navigation.addOnDestinationChangedListener { _, destination, _ ->
isRootFragment = when (destination.id) {
R.id.homeFragment,
R.id.modulesFragment,
R.id.superuserFragment,
R.id.logFragment -> true
else -> false
}
setDisplayHomeAsUpEnabled(!isRootFragment)
requestNavigationHidden(!isRootFragment)
binding.mainNavigation.menu.forEach {
if (it.itemId == destination.id) {
it.isChecked = true
}
}
}
setSupportActionBar(binding.mainToolbar)
binding.mainNavigation.setOnItemSelectedListener {
getScreen(it.itemId)?.navigate()
true
}
binding.mainNavigation.setOnItemReselectedListener {
// https://issuetracker.google.com/issues/124538620
}
binding.mainNavigation.menu.apply {
findItem(R.id.superuserFragment)?.isEnabled = Info.showSuperUser
findItem(R.id.modulesFragment)?.isEnabled = Info.env.isActive && LocalModule.loaded()
}
val section =
if (intent.action == Intent.ACTION_APPLICATION_PREFERENCES)
Const.Nav.SETTINGS
else
intent.getStringExtra(Const.Key.OPEN_SECTION)
getScreen(section)?.navigate()
if (!isRootFragment) {
requestNavigationHidden(requiresAnimation = savedInstanceState == null)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> onBackPressed()
else -> return super.onOptionsItemSelected(item)
}
return true
}
fun setDisplayHomeAsUpEnabled(isEnabled: Boolean) {
binding.mainToolbar.startAnimations()
when {
isEnabled -> binding.mainToolbar.setNavigationIcon(R.drawable.ic_back_md2)
else -> binding.mainToolbar.navigationIcon = null
}
}
internal fun requestNavigationHidden(hide: Boolean = true, requiresAnimation: Boolean = true) {
val bottomView = binding.mainNavigation
if (requiresAnimation) {
bottomView.isVisible = true
bottomView.isHidden = hide
} else {
bottomView.isGone = hide
}
}
fun invalidateToolbar() {
//binding.mainToolbar.startAnimations()
binding.mainToolbar.invalidate()
}
private fun getScreen(name: String?): NavDirections? {
return when (name) {
Const.Nav.SUPERUSER -> MainDirections.actionSuperuserFragment()
Const.Nav.MODULES -> MainDirections.actionModuleFragment()
Const.Nav.SETTINGS -> HomeFragmentDirections.actionHomeFragmentToSettingsFragment()
else -> null
}
}
private fun getScreen(id: Int): NavDirections? {
return when (id) {
R.id.homeFragment -> MainDirections.actionHomeFragment()
R.id.modulesFragment -> MainDirections.actionModuleFragment()
R.id.superuserFragment -> MainDirections.actionSuperuserFragment()
R.id.logFragment -> MainDirections.actionLogFragment()
else -> null
}
}
@SuppressLint("InlinedApi")
override fun showInvalidStateMessage(): Unit = runOnUiThread {
MagiskDialog(this).apply {
setTitle(CoreR.string.unsupport_nonroot_stub_title)
setMessage(CoreR.string.unsupport_nonroot_stub_msg)
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = CoreR.string.install
onClick {
withPermission(REQUEST_INSTALL_PACKAGES) {
if (!it) {
toast(CoreR.string.install_unknown_denied, Toast.LENGTH_SHORT)
showInvalidStateMessage()
} else {
lifecycleScope.launch {
AppMigration.restore(this@MainActivity)
}
}
}
}
}
setCancelable(false)
show()
}
}
private fun showUnsupportedMessage() {
if (Info.env.isUnsupported) {
MagiskDialog(this).apply {
setTitle(CoreR.string.unsupport_magisk_title)
setMessage(CoreR.string.unsupport_magisk_msg, Const.Version.MIN_VERSION)
setButton(MagiskDialog.ButtonType.POSITIVE) { text = android.R.string.ok }
setCancelable(false)
}.show()
}
if (!Info.isEmulator && Info.env.isActive && System.getenv("PATH")
?.split(':')
?.filterNot { File("$it/magisk").exists() }
?.any { File("$it/su").exists() } == true) {
MagiskDialog(this).apply {
setTitle(CoreR.string.unsupport_general_title)
setMessage(CoreR.string.unsupport_other_su_msg)
setButton(MagiskDialog.ButtonType.POSITIVE) { text = android.R.string.ok }
setCancelable(false)
}.show()
}
if (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0) {
MagiskDialog(this).apply {
setTitle(CoreR.string.unsupport_general_title)
setMessage(CoreR.string.unsupport_system_app_msg)
setButton(MagiskDialog.ButtonType.POSITIVE) { text = android.R.string.ok }
setCancelable(false)
}.show()
}
if (applicationInfo.flags and ApplicationInfo.FLAG_EXTERNAL_STORAGE != 0) {
MagiskDialog(this).apply {
setTitle(CoreR.string.unsupport_general_title)
setMessage(CoreR.string.unsupport_external_storage_msg)
setButton(MagiskDialog.ButtonType.POSITIVE) { text = android.R.string.ok }
setCancelable(false)
}.show()
}
}
private fun askForHomeShortcut() {
if (isRunningAsStub && !Config.askedHome &&
ShortcutManagerCompat.isRequestPinShortcutSupported(this)) {
// Ask and show dialog
Config.askedHome = true
MagiskDialog(this).apply {
setTitle(CoreR.string.add_shortcut_title)
setMessage(CoreR.string.add_shortcut_msg)
setButton(MagiskDialog.ButtonType.NEGATIVE) {
text = android.R.string.cancel
}
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = android.R.string.ok
onClick {
Shortcuts.addHomeIcon(this@MainActivity)
}
}
setCancelable(true)
}.show()
}
}
}

View file

@ -0,0 +1,118 @@
package com.topjohnwu.magisk.ui.deny
import android.annotation.SuppressLint
import android.content.pm.ApplicationInfo
import android.content.pm.ComponentInfo
import android.content.pm.PackageManager
import android.content.pm.PackageManager.GET_ACTIVITIES
import android.content.pm.PackageManager.GET_PROVIDERS
import android.content.pm.PackageManager.GET_RECEIVERS
import android.content.pm.PackageManager.GET_SERVICES
import android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
import android.content.pm.ServiceInfo
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import androidx.core.os.ProcessCompat
import com.topjohnwu.magisk.core.ktx.getLabel
import java.util.Locale
import java.util.TreeSet
class CmdlineListItem(line: String) {
val packageName: String
val process: String
init {
val split = line.split(Regex("\\|"), 2)
packageName = split[0]
process = split.getOrElse(1) { packageName }
}
}
const val ISOLATED_MAGIC = "isolated"
@SuppressLint("InlinedApi")
class AppProcessInfo(
private val info: ApplicationInfo,
pm: PackageManager,
denyList: List<CmdlineListItem>
) : Comparable<AppProcessInfo> {
private val denyList = denyList.filter {
it.packageName == info.packageName || it.packageName == ISOLATED_MAGIC
}
val label = info.getLabel(pm)
val iconImage: Drawable = runCatching { info.loadIcon(pm) }.getOrDefault(pm.defaultActivityIcon)
val packageName: String get() = info.packageName
val processes = fetchProcesses(pm)
override fun compareTo(other: AppProcessInfo) = comparator.compare(this, other)
fun isSystemApp() = info.flags and ApplicationInfo.FLAG_SYSTEM != 0
fun isApp() = ProcessCompat.isApplicationUid(info.uid)
private fun createProcess(name: String, pkg: String = info.packageName) =
ProcessInfo(name, pkg, denyList.any { it.process == name && it.packageName == pkg })
private fun ComponentInfo.getProcName(): String = processName
?: applicationInfo.processName
?: applicationInfo.packageName
private val ServiceInfo.isIsolated get() = (flags and ServiceInfo.FLAG_ISOLATED_PROCESS) != 0
private val ServiceInfo.useAppZygote get() = (flags and ServiceInfo.FLAG_USE_APP_ZYGOTE) != 0
private fun Array<out ComponentInfo>?.toProcessList() =
orEmpty().map { createProcess(it.getProcName()) }
private fun Array<ServiceInfo>?.toProcessList() = orEmpty().map {
if (it.isIsolated) {
if (it.useAppZygote) {
val proc = info.processName ?: info.packageName
createProcess("${proc}_zygote")
} else {
val proc = if (SDK_INT >= Build.VERSION_CODES.Q)
"${it.getProcName()}:${it.name}" else it.getProcName()
createProcess(proc, ISOLATED_MAGIC)
}
} else {
createProcess(it.getProcName())
}
}
private fun fetchProcesses(pm: PackageManager): Collection<ProcessInfo> {
val flag = MATCH_DISABLED_COMPONENTS or MATCH_UNINSTALLED_PACKAGES or
GET_ACTIVITIES or GET_SERVICES or GET_RECEIVERS or GET_PROVIDERS
val packageInfo = try {
pm.getPackageInfo(info.packageName, flag)
} catch (e: Exception) {
// Exceed binder data transfer limit, parse the package locally
pm.getPackageArchiveInfo(info.sourceDir, flag) ?: return emptyList()
}
val processSet = TreeSet<ProcessInfo>(compareBy({ it.name }, { it.isIsolated }))
processSet += packageInfo.activities.toProcessList()
processSet += packageInfo.services.toProcessList()
processSet += packageInfo.receivers.toProcessList()
processSet += packageInfo.providers.toProcessList()
return processSet
}
companion object {
private val comparator = compareBy<AppProcessInfo>(
{ it.label.lowercase(Locale.ROOT) },
{ it.info.packageName }
)
}
}
data class ProcessInfo(
val name: String,
val packageName: String,
var isEnabled: Boolean
) {
val isIsolated = packageName == ISOLATED_MAGIC
val isAppZygote = name.endsWith("_zygote")
}

View file

@ -0,0 +1,99 @@
package com.topjohnwu.magisk.ui.deny
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.appcompat.widget.SearchView
import androidx.core.view.MenuProvider
import androidx.recyclerview.widget.RecyclerView
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseFragment
import com.topjohnwu.magisk.arch.viewModel
import com.topjohnwu.magisk.core.ktx.hideKeyboard
import com.topjohnwu.magisk.databinding.FragmentDenyMd2Binding
import rikka.recyclerview.addEdgeSpacing
import rikka.recyclerview.addItemSpacing
import rikka.recyclerview.fixEdgeEffect
import com.topjohnwu.magisk.core.R as CoreR
class DenyListFragment : BaseFragment<FragmentDenyMd2Binding>(), MenuProvider {
override val layoutRes = R.layout.fragment_deny_md2
override val viewModel by viewModel<DenyListViewModel>()
private lateinit var searchView: SearchView
override fun onStart() {
super.onStart()
activity?.setTitle(CoreR.string.denylist)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.appList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
if (newState != RecyclerView.SCROLL_STATE_IDLE) activity?.hideKeyboard()
}
})
binding.appList.apply {
addEdgeSpacing(top = R.dimen.l_50, bottom = R.dimen.l1)
addItemSpacing(R.dimen.l1, R.dimen.l_50, R.dimen.l1)
fixEdgeEffect()
}
}
override fun onPreBind(binding: FragmentDenyMd2Binding) = Unit
override fun onBackPressed(): Boolean {
if (searchView.isIconfiedByDefault && !searchView.isIconified) {
searchView.isIconified = true
return true
}
return super.onBackPressed()
}
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_deny_md2, menu)
searchView = menu.findItem(R.id.action_search).actionView as SearchView
searchView.queryHint = searchView.context.getString(CoreR.string.hide_filter_hint)
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
viewModel.query = query ?: ""
return true
}
override fun onQueryTextChange(newText: String?): Boolean {
viewModel.query = newText ?: ""
return true
}
})
}
override fun onMenuItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_show_system -> {
val check = !item.isChecked
viewModel.isShowSystem = check
item.isChecked = check
return true
}
R.id.action_show_OS -> {
val check = !item.isChecked
viewModel.isShowOS = check
item.isChecked = check
return true
}
}
return super.onOptionsItemSelected(item)
}
override fun onPrepareMenu(menu: Menu) {
val showSystem = menu.findItem(R.id.action_show_system)
val showOS = menu.findItem(R.id.action_show_OS)
showOS.isEnabled = showSystem.isChecked
}
}

View file

@ -0,0 +1,130 @@
package com.topjohnwu.magisk.ui.deny
import android.view.View
import android.view.ViewGroup
import androidx.databinding.Bindable
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.startAnimations
import com.topjohnwu.magisk.databinding.DiffItem
import com.topjohnwu.magisk.databinding.ObservableRvItem
import com.topjohnwu.magisk.databinding.addOnPropertyChangedCallback
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.superuser.Shell
import kotlin.math.roundToInt
class DenyListRvItem(
val info: AppProcessInfo
) : ObservableRvItem(), DiffItem<DenyListRvItem>, Comparable<DenyListRvItem> {
override val layoutRes get() = R.layout.item_hide_md2
val processes = info.processes.map { ProcessRvItem(it) }
@get:Bindable
var isExpanded = false
set(value) = set(value, field, { field = it }, BR.expanded)
var itemsChecked = 0
set(value) = set(value, field, { field = it }, BR.checkedPercent)
val isChecked get() = itemsChecked != 0
@get:Bindable
val checkedPercent get() = (itemsChecked.toFloat() / processes.size * 100).roundToInt()
private var _state: Boolean? = false
set(value) = set(value, field, { field = it }, BR.state)
@get:Bindable
var state: Boolean?
get() = _state
set(value) = set(value, _state, { _state = it }, BR.state) {
if (value == true) {
processes
.filterNot { it.isEnabled }
.filter { isExpanded || it.defaultSelection }
.forEach { it.toggle() }
} else {
Shell.cmd("magisk --denylist rm ${info.packageName}").submit()
processes.filter { it.isEnabled }.forEach {
if (it.process.isIsolated) {
it.toggle()
} else {
it.isEnabled = !it.isEnabled
notifyPropertyChanged(BR.enabled)
}
}
}
}
init {
processes.forEach { it.addOnPropertyChangedCallback(BR.enabled) { recalculateChecked() } }
addOnPropertyChangedCallback(BR.expanded) { recalculateChecked() }
recalculateChecked()
}
fun toggleExpand(v: View) {
(v.parent as? ViewGroup)?.startAnimations()
isExpanded = !isExpanded
}
private fun recalculateChecked() {
itemsChecked = processes.count { it.isEnabled }
_state = if (isExpanded) {
when (itemsChecked) {
0 -> false
processes.size -> true
else -> null
}
} else {
val defaultProcesses = processes.filter { it.defaultSelection }
when (defaultProcesses.count { it.isEnabled }) {
0 -> false
defaultProcesses.size -> true
else -> null
}
}
}
override fun compareTo(other: DenyListRvItem) = comparator.compare(this, other)
companion object {
private val comparator = compareBy<DenyListRvItem>(
{ it.itemsChecked == 0 },
{ it.info }
)
}
}
class ProcessRvItem(
val process: ProcessInfo
) : ObservableRvItem(), DiffItem<ProcessRvItem> {
override val layoutRes get() = R.layout.item_hide_process_md2
val displayName = if (process.isIsolated) "(isolated) ${process.name}" else process.name
@get:Bindable
var isEnabled
get() = process.isEnabled
set(value) = set(value, process.isEnabled, { process.isEnabled = it }, BR.enabled) {
val arg = if (it) "add" else "rm"
val (name, pkg) = process
Shell.cmd("magisk --denylist $arg $pkg \'$name\'").submit()
}
fun toggle() {
isEnabled = !isEnabled
}
val defaultSelection get() =
process.isIsolated || process.isAppZygote || process.name == process.packageName
override fun itemSameAs(other: ProcessRvItem) =
process.name == other.process.name && process.packageName == other.process.packageName
override fun contentSameAs(other: ProcessRvItem) =
process.isEnabled == other.process.isEnabled
}

View file

@ -0,0 +1,89 @@
package com.topjohnwu.magisk.ui.deny
import android.annotation.SuppressLint
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
import androidx.databinding.Bindable
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
import com.topjohnwu.magisk.core.AppContext
import com.topjohnwu.magisk.core.ktx.concurrentMap
import com.topjohnwu.magisk.databinding.bindExtra
import com.topjohnwu.magisk.databinding.filterList
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.toCollection
import kotlinx.coroutines.withContext
class DenyListViewModel : AsyncLoadViewModel() {
var isShowSystem = false
set(value) {
field = value
doQuery(query)
}
var isShowOS = false
set(value) {
field = value
doQuery(query)
}
var query = ""
set(value) {
field = value
doQuery(value)
}
val items = filterList<DenyListRvItem>(viewModelScope)
val extraBindings = bindExtra {
it.put(BR.viewModel, this)
}
@get:Bindable
var loading = true
private set(value) = set(value, field, { field = it }, BR.loading)
@SuppressLint("InlinedApi")
override suspend fun doLoadWork() {
loading = true
val apps = withContext(Dispatchers.Default) {
val pm = AppContext.packageManager
val denyList = Shell.cmd("magisk --denylist ls").exec().out
.map { CmdlineListItem(it) }
val apps = pm.getInstalledApplications(MATCH_UNINSTALLED_PACKAGES).run {
asFlow()
.filter { AppContext.packageName != it.packageName }
.concurrentMap { AppProcessInfo(it, pm, denyList) }
.filter { it.processes.isNotEmpty() }
.concurrentMap { DenyListRvItem(it) }
.toCollection(ArrayList(size))
}
apps.sort()
apps
}
items.set(apps)
doQuery(query)
}
private fun doQuery(s: String) {
items.filter {
fun filterSystem() = isShowSystem || !it.info.isSystemApp()
fun filterOS() = (isShowSystem && isShowOS) || it.info.isApp()
fun filterQuery(): Boolean {
fun inName() = it.info.label.contains(s, true)
fun inPackage() = it.info.packageName.contains(s, true)
fun inProcesses() = it.processes.any { p -> p.process.name.contains(s, true) }
return inName() || inPackage() || inProcesses()
}
(it.isChecked || (filterSystem() && filterOS())) && filterQuery()
}
loading = false
}
}

View file

@ -0,0 +1,38 @@
package com.topjohnwu.magisk.ui.flash
import android.view.View
import android.widget.TextView
import androidx.core.view.updateLayoutParams
import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.RecyclerView
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.DiffItem
import com.topjohnwu.magisk.databinding.ItemWrapper
import com.topjohnwu.magisk.databinding.RvItem
import com.topjohnwu.magisk.databinding.ViewAwareItem
import kotlin.math.max
class ConsoleItem(
override val item: String
) : RvItem(), ViewAwareItem, DiffItem<ConsoleItem>, ItemWrapper<String> {
override val layoutRes = R.layout.item_console_md2
private var parentWidth = -1
override fun onBind(binding: ViewDataBinding, recyclerView: RecyclerView) {
if (parentWidth < 0)
parentWidth = (recyclerView.parent as View).width
val view = binding.root as TextView
view.measure(0, 0)
// We want our recyclerView at least as wide as screen
val desiredWidth = max(view.measuredWidth, parentWidth)
view.updateLayoutParams { width = desiredWidth }
if (recyclerView.width < desiredWidth) {
recyclerView.requestLayout()
}
}
}

View file

@ -0,0 +1,149 @@
package com.topjohnwu.magisk.ui.flash
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.ActivityInfo
import android.net.Uri
import android.os.Bundle
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.core.view.MenuProvider
import androidx.core.view.isVisible
import androidx.navigation.NavDeepLinkBuilder
import com.topjohnwu.magisk.MainDirections
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseFragment
import com.topjohnwu.magisk.arch.viewModel
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.cmp
import com.topjohnwu.magisk.databinding.FragmentFlashMd2Binding
import com.topjohnwu.magisk.ui.MainActivity
import com.topjohnwu.magisk.core.R as CoreR
class FlashFragment : BaseFragment<FragmentFlashMd2Binding>(), MenuProvider {
override val layoutRes = R.layout.fragment_flash_md2
override val viewModel by viewModel<FlashViewModel>()
override val snackbarView: View get() = binding.snackbarContainer
override val snackbarAnchorView: View?
get() = if (binding.restartBtn.isShown) binding.restartBtn else super.snackbarAnchorView
private var defaultOrientation = -1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.args = FlashFragmentArgs.fromBundle(requireArguments())
}
override fun onStart() {
super.onStart()
activity?.setTitle(CoreR.string.flash_screen_title)
viewModel.state.observe(this) {
activity?.supportActionBar?.setSubtitle(
when (it) {
FlashViewModel.State.FLASHING -> CoreR.string.flashing
FlashViewModel.State.SUCCESS -> CoreR.string.done
FlashViewModel.State.FAILED -> CoreR.string.failure
}
)
if (it == FlashViewModel.State.SUCCESS && viewModel.showReboot) {
binding.restartBtn.apply {
if (!this.isVisible) this.show()
if (!this.isFocused) this.requestFocus()
}
}
}
}
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_flash, menu)
}
override fun onMenuItemSelected(item: MenuItem): Boolean {
return viewModel.onMenuItemClicked(item)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
defaultOrientation = activity?.requestedOrientation ?: -1
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
if (savedInstanceState == null) {
viewModel.startFlashing()
}
}
@SuppressLint("WrongConstant")
override fun onDestroyView() {
if (defaultOrientation != -1) {
activity?.requestedOrientation = defaultOrientation
}
super.onDestroyView()
}
override fun onKeyEvent(event: KeyEvent): Boolean {
return when (event.keyCode) {
KeyEvent.KEYCODE_VOLUME_UP,
KeyEvent.KEYCODE_VOLUME_DOWN -> true
else -> false
}
}
override fun onBackPressed(): Boolean {
if (viewModel.flashing.value == true)
return true
return super.onBackPressed()
}
override fun onPreBind(binding: FragmentFlashMd2Binding) = Unit
companion object {
private fun createIntent(context: Context, args: FlashFragmentArgs) =
NavDeepLinkBuilder(context)
.setGraph(R.navigation.main)
.setComponentName(MainActivity::class.java.cmp(context.packageName))
.setDestination(R.id.flashFragment)
.setArguments(args.toBundle())
.createPendingIntent()
private fun flashType(isSecondSlot: Boolean) =
if (isSecondSlot) Const.Value.FLASH_INACTIVE_SLOT else Const.Value.FLASH_MAGISK
/* Flashing is understood as installing / flashing magisk itself */
fun flash(isSecondSlot: Boolean) = MainDirections.actionFlashFragment(
action = flashType(isSecondSlot)
)
/* Patching is understood as injecting img files with magisk */
fun patch(uri: Uri) = MainDirections.actionFlashFragment(
action = Const.Value.PATCH_FILE,
additionalData = uri
)
/* Uninstalling is understood as removing magisk entirely */
fun uninstall() = MainDirections.actionFlashFragment(
action = Const.Value.UNINSTALL
)
/* Installing is understood as flashing modules / zips */
fun installIntent(context: Context, file: Uri) = FlashFragmentArgs(
action = Const.Value.FLASH_ZIP,
additionalData = file,
).let { createIntent(context, it) }
fun install(file: Uri) = MainDirections.actionFlashFragment(
action = Const.Value.FLASH_ZIP,
additionalData = file,
)
}
}

View file

@ -0,0 +1,122 @@
package com.topjohnwu.magisk.ui.flash
import android.view.MenuItem
import androidx.databinding.Bindable
import androidx.databinding.ObservableArrayList
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseViewModel
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.ktx.reboot
import com.topjohnwu.magisk.core.ktx.synchronized
import com.topjohnwu.magisk.core.ktx.timeFormatStandard
import com.topjohnwu.magisk.core.ktx.toTime
import com.topjohnwu.magisk.core.tasks.FlashZip
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.superuser.CallbackList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class FlashViewModel : BaseViewModel() {
enum class State {
FLASHING, SUCCESS, FAILED
}
private val _state = MutableLiveData(State.FLASHING)
val state: LiveData<State> get() = _state
val flashing = state.map { it == State.FLASHING }
@get:Bindable
var showReboot = Info.isRooted
set(value) = set(value, field, { field = it }, BR.showReboot)
val items = ObservableArrayList<ConsoleItem>()
lateinit var args: FlashFragmentArgs
private val logItems = mutableListOf<String>().synchronized()
private val outItems = object : CallbackList<String>() {
override fun onAddElement(e: String?) {
e ?: return
items.add(ConsoleItem(e))
logItems.add(e)
}
}
fun startFlashing() {
val (action, uri) = args
viewModelScope.launch {
val result = when (action) {
Const.Value.FLASH_ZIP -> {
uri ?: return@launch
FlashZip(uri, outItems, logItems).exec()
}
Const.Value.UNINSTALL -> {
showReboot = false
MagiskInstaller.Uninstall(outItems, logItems).exec()
}
Const.Value.FLASH_MAGISK -> {
if (Info.isEmulator)
MagiskInstaller.Emulator(outItems, logItems).exec()
else
MagiskInstaller.Direct(outItems, logItems).exec()
}
Const.Value.FLASH_INACTIVE_SLOT -> {
showReboot = false
MagiskInstaller.SecondSlot(outItems, logItems).exec()
}
Const.Value.PATCH_FILE -> {
uri ?: return@launch
showReboot = false
MagiskInstaller.Patch(uri, outItems, logItems).exec()
}
else -> {
back()
return@launch
}
}
onResult(result)
}
}
private fun onResult(success: Boolean) {
_state.value = if (success) State.SUCCESS else State.FAILED
}
fun onMenuItemClicked(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_save -> savePressed()
}
return true
}
private fun savePressed() = withExternalRW {
viewModelScope.launch(Dispatchers.IO) {
val name = "magisk_install_log_%s.log".format(
System.currentTimeMillis().toTime(timeFormatStandard)
)
val file = MediaStoreUtils.getFile(name)
file.uri.outputStream().bufferedWriter().use { writer ->
synchronized(logItems) {
logItems.forEach {
writer.write(it)
writer.newLine()
}
}
}
SnackbarEvent(file.toString()).publish()
}
}
fun restartPressed() = reboot()
}

View file

@ -0,0 +1,127 @@
package com.topjohnwu.magisk.ui.home
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.databinding.RvItem
import com.topjohnwu.magisk.core.R as CoreR
interface Dev {
val name: String
}
private interface JohnImpl : Dev {
override val name get() = "topjohnwu"
}
private interface VvbImpl : Dev {
override val name get() = "vvb2060"
}
private interface YUImpl : Dev {
override val name get() = "yujincheng08"
}
private interface RikkaImpl : Dev {
override val name get() = "RikkaW"
}
private interface CanyieImpl : Dev {
override val name get() = "canyie"
}
sealed class DeveloperItem : Dev {
abstract val items: List<IconLink>
val handle get() = "@${name}"
object John : DeveloperItem(), JohnImpl {
override val items =
listOf(
object : IconLink.Twitter(), JohnImpl {},
IconLink.Github.Project
)
}
object Vvb : DeveloperItem(), VvbImpl {
override val items =
listOf<IconLink>(
object : IconLink.Twitter(), VvbImpl {},
object : IconLink.Github.User(), VvbImpl {}
)
}
object YU : DeveloperItem(), YUImpl {
override val items =
listOf<IconLink>(
object : IconLink.Twitter() { override val name = "shanasaimoe" },
object : IconLink.Github.User(), YUImpl {},
object : IconLink.Sponsor(), YUImpl {}
)
}
object Rikka : DeveloperItem(), RikkaImpl {
override val items =
listOf<IconLink>(
object : IconLink.Twitter() { override val name = "rikkawww" },
object : IconLink.Github.User(), RikkaImpl {}
)
}
object Canyie : DeveloperItem(), CanyieImpl {
override val items =
listOf<IconLink>(
object : IconLink.Twitter() { override val name = "canyie2977" },
object : IconLink.Github.User(), CanyieImpl {}
)
}
}
sealed class IconLink : RvItem() {
abstract val icon: Int
abstract val title: Int
abstract val link: String
override val layoutRes get() = R.layout.item_icon_link
abstract class PayPal : IconLink(), Dev {
override val icon get() = CoreR.drawable.ic_paypal
override val title get() = CoreR.string.paypal
override val link get() = "https://paypal.me/$name"
object Project : PayPal() {
override val name: String get() = "magiskdonate"
}
}
object Patreon : IconLink() {
override val icon get() = CoreR.drawable.ic_patreon
override val title get() = CoreR.string.patreon
override val link get() = Const.Url.PATREON_URL
}
abstract class Twitter : IconLink(), Dev {
override val icon get() = CoreR.drawable.ic_twitter
override val title get() = CoreR.string.twitter
override val link get() = "https://twitter.com/$name"
}
abstract class Github : IconLink() {
override val icon get() = CoreR.drawable.ic_github
override val title get() = CoreR.string.github
abstract class User : Github(), Dev {
override val link get() = "https://github.com/$name"
}
object Project : Github() {
override val link get() = Const.Url.SOURCE_CODE_URL
}
}
abstract class Sponsor : IconLink(), Dev {
override val icon get() = CoreR.drawable.ic_favorite
override val title get() = CoreR.string.github
override val link get() = "https://github.com/sponsors/$name"
}
}

View file

@ -0,0 +1,90 @@
package com.topjohnwu.magisk.ui.home
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.MenuProvider
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseFragment
import com.topjohnwu.magisk.arch.viewModel
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.download.DownloadEngine
import com.topjohnwu.magisk.databinding.FragmentHomeMd2Binding
import com.topjohnwu.magisk.core.R as CoreR
import androidx.navigation.findNavController
import com.topjohnwu.magisk.arch.NavigationActivity
class HomeFragment : BaseFragment<FragmentHomeMd2Binding>(), MenuProvider {
override val layoutRes = R.layout.fragment_home_md2
override val viewModel by viewModel<HomeViewModel>()
override fun onStart() {
super.onStart()
activity?.setTitle(CoreR.string.section_home)
DownloadEngine.observeProgress(this, viewModel::onProgressUpdate)
}
private fun checkTitle(text: TextView, icon: ImageView) {
text.post {
if (text.layout?.getEllipsisCount(0) != 0) {
with (icon) {
layoutParams.width = 0
layoutParams.height = 0
requestLayout()
}
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
super.onCreateView(inflater, container, savedInstanceState)
// If titles are squished, hide icons
with(binding.homeMagiskWrapper) {
checkTitle(homeMagiskTitle, homeMagiskIcon)
}
with(binding.homeManagerWrapper) {
checkTitle(homeManagerTitle, homeManagerIcon)
}
return binding.root
}
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_home_md2, menu)
if (!Info.isRooted)
menu.removeItem(R.id.action_reboot)
}
override fun onMenuItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_settings ->
activity?.let {
NavigationActivity.navigate(
HomeFragmentDirections.actionHomeFragmentToSettingsFragment(),
it.findNavController(R.id.main_nav_host),
it.contentResolver,
)
}
R.id.action_reboot -> activity?.let { RebootMenu.inflate(it).show() }
else -> return super.onOptionsItemSelected(item)
}
return true
}
override fun onResume() {
super.onResume()
viewModel.stateManagerProgress = 0
}
}

View file

@ -0,0 +1,166 @@
package com.topjohnwu.magisk.ui.home
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.widget.Toast
import androidx.core.net.toUri
import androidx.databinding.Bindable
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.ActivityExecutor
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
import com.topjohnwu.magisk.arch.ContextExecutor
import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.arch.ViewEvent
import com.topjohnwu.magisk.core.BuildConfig
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.core.download.Subject.App
import com.topjohnwu.magisk.core.ktx.await
import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.core.repository.NetworkService
import com.topjohnwu.magisk.databinding.bindExtra
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.dialog.EnvFixDialog
import com.topjohnwu.magisk.dialog.ManagerInstallDialog
import com.topjohnwu.magisk.dialog.UninstallDialog
import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.magisk.utils.asText
import com.topjohnwu.superuser.Shell
import kotlin.math.roundToInt
import com.topjohnwu.magisk.core.R as CoreR
class HomeViewModel(
private val svc: NetworkService
) : AsyncLoadViewModel() {
enum class State {
LOADING, INVALID, OUTDATED, UP_TO_DATE
}
val magiskTitleBarrierIds =
intArrayOf(R.id.home_magisk_icon, R.id.home_magisk_title, R.id.home_magisk_button)
val appTitleBarrierIds =
intArrayOf(R.id.home_manager_icon, R.id.home_manager_title, R.id.home_manager_button)
@get:Bindable
var isNoticeVisible = Config.safetyNotice
set(value) = set(value, field, { field = it }, BR.noticeVisible)
val magiskState
get() = when {
Info.isRooted && Info.env.isUnsupported -> State.OUTDATED
!Info.env.isActive -> State.INVALID
Info.env.versionCode < BuildConfig.APP_VERSION_CODE -> State.OUTDATED
else -> State.UP_TO_DATE
}
@get:Bindable
var appState = State.LOADING
set(value) = set(value, field, { field = it }, BR.appState)
val magiskInstalledVersion
get() = Info.env.run {
if (isActive)
("$versionString ($versionCode)" + if (isDebug) " (D)" else "").asText()
else
CoreR.string.not_available.asText()
}
@get:Bindable
var managerRemoteVersion = CoreR.string.loading.asText()
set(value) = set(value, field, { field = it }, BR.managerRemoteVersion)
val managerInstalledVersion
get() = "${BuildConfig.APP_VERSION_NAME} (${BuildConfig.APP_VERSION_CODE})" +
if (BuildConfig.DEBUG) " (D)" else ""
@get:Bindable
var stateManagerProgress = 0
set(value) = set(value, field, { field = it }, BR.stateManagerProgress)
val extraBindings = bindExtra {
it.put(BR.viewModel, this)
}
companion object {
private var checkedEnv = false
}
override suspend fun doLoadWork() {
appState = State.LOADING
Info.fetchUpdate(svc)?.apply {
appState = when {
BuildConfig.APP_VERSION_CODE < versionCode -> State.OUTDATED
else -> State.UP_TO_DATE
}
val isDebug = Config.updateChannel == Config.Value.DEBUG_CHANNEL
managerRemoteVersion =
("$version (${versionCode})" + if (isDebug) " (D)" else "").asText()
} ?: run {
appState = State.INVALID
managerRemoteVersion = CoreR.string.not_available.asText()
}
ensureEnv()
}
override fun onNetworkChanged(network: Boolean) = startLoading()
fun onProgressUpdate(progress: Float, subject: Subject) {
if (subject is App)
stateManagerProgress = progress.times(100f).roundToInt()
}
fun onLinkPressed(link: String) = object : ViewEvent(), ContextExecutor {
override fun invoke(context: Context) {
val intent = Intent(Intent.ACTION_VIEW, link.toUri())
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try {
context.startActivity(intent)
} catch (e: ActivityNotFoundException) {
context.toast(CoreR.string.open_link_failed_toast, Toast.LENGTH_SHORT)
}
}
}.publish()
fun onDeletePressed() = UninstallDialog().show()
fun onManagerPressed() = when (appState) {
State.LOADING -> SnackbarEvent(CoreR.string.loading).publish()
State.INVALID -> SnackbarEvent(CoreR.string.no_connection).publish()
else -> withExternalRW {
withInstallPermission {
ManagerInstallDialog().show()
}
}
}
fun onMagiskPressed() = withExternalRW {
HomeFragmentDirections.actionHomeFragmentToInstallFragment().navigate()
}
fun hideNotice() {
Config.safetyNotice = false
isNoticeVisible = false
}
private suspend fun ensureEnv() {
if (magiskState == State.INVALID || checkedEnv) return
val cmd = "env_check ${Info.env.versionString} ${Info.env.versionCode}"
val code = Shell.cmd(cmd).await().code
if (code != 0) {
EnvFixDialog(this, code).show()
}
checkedEnv = true
}
val showTest = false
fun onTestPressed() = object : ViewEvent(), ActivityExecutor {
override fun invoke(activity: UIActivity<*>) {
/* Entry point to trigger test events within the app */
}
}.publish()
}

View file

@ -0,0 +1,52 @@
package com.topjohnwu.magisk.ui.home
import android.app.Activity
import android.os.Build
import android.os.PowerManager
import android.view.ContextThemeWrapper
import android.view.MenuItem
import android.widget.PopupMenu
import androidx.core.content.getSystemService
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.ktx.reboot as systemReboot
object RebootMenu {
private fun reboot(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_reboot_normal -> systemReboot()
R.id.action_reboot_userspace -> systemReboot("userspace")
R.id.action_reboot_bootloader -> systemReboot("bootloader")
R.id.action_reboot_download -> systemReboot("download")
R.id.action_reboot_edl -> systemReboot("edl")
R.id.action_reboot_recovery -> systemReboot("recovery")
R.id.action_reboot_safe_mode -> {
val status = !item.isChecked
item.isChecked = status
Config.bootloop = if (status) 2 else 0
}
else -> Unit
}
return true
}
fun inflate(activity: Activity): PopupMenu {
val themeWrapper = ContextThemeWrapper(activity, R.style.Foundation_PopupMenu)
val menu = PopupMenu(themeWrapper, activity.findViewById(R.id.action_reboot))
activity.menuInflater.inflate(R.menu.menu_reboot, menu.menu)
menu.setOnMenuItemClickListener(RebootMenu::reboot)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
activity.getSystemService<PowerManager>()?.isRebootingUserspaceSupported == true) {
menu.menu.findItem(R.id.action_reboot_userspace).isVisible = true
}
if (Const.Version.atLeast_28_0()) {
menu.menu.findItem(R.id.action_reboot_safe_mode).isChecked = Config.bootloop >= 2
} else {
menu.menu.findItem(R.id.action_reboot_safe_mode).isVisible = false
}
return menu
}
}

View file

@ -0,0 +1,18 @@
package com.topjohnwu.magisk.ui.install
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseFragment
import com.topjohnwu.magisk.arch.viewModel
import com.topjohnwu.magisk.databinding.FragmentInstallMd2Binding
import com.topjohnwu.magisk.core.R as CoreR
class InstallFragment : BaseFragment<FragmentInstallMd2Binding>() {
override val layoutRes = R.layout.fragment_install_md2
override val viewModel by viewModel<InstallViewModel>()
override fun onStart() {
super.onStart()
requireActivity().setTitle(CoreR.string.install)
}
}

View file

@ -0,0 +1,147 @@
package com.topjohnwu.magisk.ui.install
import android.net.Uri
import android.os.Bundle
import android.os.Parcelable
import android.text.Spanned
import android.text.SpannedString
import android.widget.Toast
import androidx.databinding.Bindable
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseViewModel
import com.topjohnwu.magisk.core.AppContext
import com.topjohnwu.magisk.core.BuildConfig.APP_VERSION_CODE
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.base.ContentResultCallback
import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.core.repository.NetworkService
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.dialog.SecondSlotWarningDialog
import com.topjohnwu.magisk.events.GetContentEvent
import com.topjohnwu.magisk.ui.flash.FlashFragment
import io.noties.markwon.Markwon
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import timber.log.Timber
import java.io.File
import java.io.IOException
import com.topjohnwu.magisk.core.R as CoreR
class InstallViewModel(svc: NetworkService, markwon: Markwon) : BaseViewModel() {
val isRooted get() = Info.isRooted
val skipOptions = Info.isEmulator || (Info.isSAR && !Info.isFDE && Info.ramdisk)
val noSecondSlot = !isRooted || !Info.isAB || Info.isEmulator
@get:Bindable
var step = if (skipOptions) 1 else 0
set(value) = set(value, field, { field = it }, BR.step)
private var methodId = -1
@get:Bindable
var method
get() = methodId
set(value) = set(value, methodId, { methodId = it }, BR.method) {
when (it) {
R.id.method_patch -> {
GetContentEvent("*/*", UriCallback()).publish()
}
R.id.method_inactive_slot -> {
SecondSlotWarningDialog().show()
}
}
}
val data: LiveData<Uri?> get() = uri
@get:Bindable
var notes: Spanned = SpannedString("")
set(value) = set(value, field, { field = it }, BR.notes)
init {
viewModelScope.launch(Dispatchers.IO) {
try {
val noteFile = File(AppContext.cacheDir, "${APP_VERSION_CODE}.md")
val noteText = when {
noteFile.exists() -> noteFile.readText()
else -> {
val note = svc.fetchUpdate(APP_VERSION_CODE)?.note.orEmpty()
if (note.isEmpty()) return@launch
noteFile.writeText(note)
note
}
}
val spanned = markwon.toMarkdown(noteText)
withContext(Dispatchers.Main) {
notes = spanned
}
} catch (e: IOException) {
Timber.e(e)
}
}
}
fun install() {
when (method) {
R.id.method_patch -> FlashFragment.patch(data.value!!).navigate(true)
R.id.method_direct -> FlashFragment.flash(false).navigate(true)
R.id.method_inactive_slot -> FlashFragment.flash(true).navigate(true)
else -> error("Unknown value")
}
}
override fun onSaveState(state: Bundle) {
state.putParcelable(
INSTALL_STATE_KEY, InstallState(
methodId,
step,
Config.keepVerity,
Config.keepEnc,
Config.recovery
)
)
}
override fun onRestoreState(state: Bundle) {
state.getParcelable<InstallState>(INSTALL_STATE_KEY)?.let {
methodId = it.method
step = it.step
Config.keepVerity = it.keepVerity
Config.keepEnc = it.keepEnc
Config.recovery = it.recovery
}
}
@Parcelize
class UriCallback : ContentResultCallback {
override fun onActivityLaunch() {
AppContext.toast(CoreR.string.patch_file_msg, Toast.LENGTH_LONG)
}
override fun onActivityResult(result: Uri) {
uri.value = result
}
}
@Parcelize
class InstallState(
val method: Int,
val step: Int,
val keepVerity: Boolean,
val keepEnc: Boolean,
val recovery: Boolean,
) : Parcelable
companion object {
private const val INSTALL_STATE_KEY = "install_state"
private val uri = MutableLiveData<Uri?>()
}
}

View file

@ -0,0 +1,97 @@
package com.topjohnwu.magisk.ui.log
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.widget.HorizontalScrollView
import androidx.core.view.MenuProvider
import androidx.core.view.isVisible
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseFragment
import com.topjohnwu.magisk.arch.viewModel
import com.topjohnwu.magisk.databinding.FragmentLogMd2Binding
import com.topjohnwu.magisk.ui.MainActivity
import com.topjohnwu.magisk.utils.AccessibilityUtils
import com.topjohnwu.magisk.utils.MotionRevealHelper
import rikka.recyclerview.addEdgeSpacing
import rikka.recyclerview.addItemSpacing
import rikka.recyclerview.fixEdgeEffect
import com.topjohnwu.magisk.core.R as CoreR
class LogFragment : BaseFragment<FragmentLogMd2Binding>(), MenuProvider {
override val layoutRes = R.layout.fragment_log_md2
override val viewModel by viewModel<LogViewModel>()
override val snackbarView: View?
get() = if (isMagiskLogVisible) binding.logFilterSuperuser.snackbarContainer
else super.snackbarView
override val snackbarAnchorView get() = binding.logFilterToggle
private var actionSave: MenuItem? = null
private var isMagiskLogVisible
get() = binding.logFilter.isVisible
set(value) {
MotionRevealHelper.withViews(binding.logFilter, binding.logFilterToggle, value)
actionSave?.isVisible = !value
with(activity as MainActivity) {
invalidateToolbar()
requestNavigationHidden(value)
setDisplayHomeAsUpEnabled(value)
}
}
override fun onStart() {
super.onStart()
activity?.setTitle(CoreR.string.logs)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.logFilterToggle.setOnClickListener {
isMagiskLogVisible = true
}
binding.logFilterSuperuser.logSuperuser.apply {
addEdgeSpacing(bottom = R.dimen.l1)
addItemSpacing(R.dimen.l1, R.dimen.l_50, R.dimen.l1)
fixEdgeEffect()
}
if (!AccessibilityUtils.isAnimationEnabled(requireContext().contentResolver)) {
val scrollView = view.findViewById<HorizontalScrollView>(R.id.log_scroll_magisk)
scrollView.setOverScrollMode(View.OVER_SCROLL_NEVER)
}
}
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_log_md2, menu)
actionSave = menu.findItem(R.id.action_save)?.also {
it.isVisible = !isMagiskLogVisible
}
}
override fun onMenuItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_save -> viewModel.saveMagiskLog()
R.id.action_clear ->
if (!isMagiskLogVisible) viewModel.clearMagiskLog()
else viewModel.clearLog()
}
return super.onOptionsItemSelected(item)
}
override fun onPreBind(binding: FragmentLogMd2Binding) = Unit
override fun onBackPressed(): Boolean {
if (binding.logFilter.isVisible) {
isMagiskLogVisible = false
return true
}
return super.onBackPressed()
}
}

View file

@ -0,0 +1,28 @@
package com.topjohnwu.magisk.ui.log
import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.textview.MaterialTextView
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.DiffItem
import com.topjohnwu.magisk.databinding.ItemWrapper
import com.topjohnwu.magisk.databinding.ObservableRvItem
import com.topjohnwu.magisk.databinding.ViewAwareItem
class LogRvItem(
override val item: String
) : ObservableRvItem(), DiffItem<LogRvItem>, ItemWrapper<String>, ViewAwareItem {
override val layoutRes = R.layout.item_log_textview
override fun onBind(binding: ViewDataBinding, recyclerView: RecyclerView) {
val view = binding.root as MaterialTextView
view.measure(0, 0)
val desiredWidth = view.measuredWidth
val layoutParams = view.layoutParams
layoutParams.width = desiredWidth
if (recyclerView.width < desiredWidth) {
recyclerView.requestLayout()
}
}
}

View file

@ -0,0 +1,114 @@
package com.topjohnwu.magisk.ui.log
import android.system.Os
import androidx.databinding.Bindable
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
import com.topjohnwu.magisk.core.BuildConfig
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.ktx.timeFormatStandard
import com.topjohnwu.magisk.core.ktx.toTime
import com.topjohnwu.magisk.core.repository.LogRepository
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.databinding.bindExtra
import com.topjohnwu.magisk.databinding.diffList
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.magisk.view.TextItem
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.FileInputStream
class LogViewModel(
private val repo: LogRepository
) : AsyncLoadViewModel() {
@get:Bindable
var loading = true
private set(value) = set(value, field, { field = it }, BR.loading)
// --- empty view
val itemEmpty = TextItem(R.string.log_data_none)
val itemMagiskEmpty = TextItem(R.string.log_data_magisk_none)
// --- su log
val items = diffList<SuLogRvItem>()
val extraBindings = bindExtra {
it.put(BR.viewModel, this)
}
// --- magisk log
val logs = diffList<LogRvItem>()
var magiskLogRaw = " "
override suspend fun doLoadWork() {
loading = true
val (suLogs, suDiff) = withContext(Dispatchers.Default) {
magiskLogRaw = repo.fetchMagiskLogs()
val newLogs = magiskLogRaw.split('\n').map { LogRvItem(it) }
logs.update(newLogs)
val suLogs = repo.fetchSuLogs().map { SuLogRvItem(it) }
suLogs to items.calculateDiff(suLogs)
}
items.firstOrNull()?.isTop = false
items.lastOrNull()?.isBottom = false
items.update(suLogs, suDiff)
items.firstOrNull()?.isTop = true
items.lastOrNull()?.isBottom = true
loading = false
}
fun saveMagiskLog() = withExternalRW {
viewModelScope.launch(Dispatchers.IO) {
val filename = "magisk_log_%s.log".format(
System.currentTimeMillis().toTime(timeFormatStandard))
val logFile = MediaStoreUtils.getFile(filename)
logFile.uri.outputStream().bufferedWriter().use { file ->
file.write("---Detected Device Info---\n\n")
file.write("isAB=${Info.isAB}\n")
file.write("isSAR=${Info.isSAR}\n")
file.write("ramdisk=${Info.ramdisk}\n")
val uname = Os.uname()
file.write("kernel=${uname.sysname} ${uname.machine} ${uname.release} ${uname.version}\n")
file.write("\n\n---System Properties---\n\n")
ProcessBuilder("getprop").start()
.inputStream.reader().use { it.copyTo(file) }
file.write("\n\n---Environment Variables---\n\n")
System.getenv().forEach { (key, value) -> file.write("${key}=${value}\n") }
file.write("\n\n---System MountInfo---\n\n")
FileInputStream("/proc/self/mountinfo").reader().use { it.copyTo(file) }
file.write("\n---Magisk Logs---\n")
file.write("${Info.env.versionString} (${Info.env.versionCode})\n\n")
if (Info.env.isActive) file.write(magiskLogRaw)
file.write("\n---Manager Logs---\n")
file.write("${BuildConfig.APP_VERSION_NAME} (${BuildConfig.APP_VERSION_CODE})\n\n")
ProcessBuilder("logcat", "-d").start()
.inputStream.reader().use { it.copyTo(file) }
}
SnackbarEvent(logFile.toString()).publish()
}
}
fun clearMagiskLog() = repo.clearMagiskLogs {
SnackbarEvent(R.string.logs_cleared).publish()
startLoading()
}
fun clearLog() = viewModelScope.launch {
repo.clearLogs()
SnackbarEvent(R.string.logs_cleared).publish()
startLoading()
}
}

View file

@ -0,0 +1,54 @@
package com.topjohnwu.magisk.ui.log
import androidx.databinding.Bindable
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.AppContext
import com.topjohnwu.magisk.core.ktx.timeDateFormat
import com.topjohnwu.magisk.core.ktx.toTime
import com.topjohnwu.magisk.core.model.su.SuLog
import com.topjohnwu.magisk.databinding.DiffItem
import com.topjohnwu.magisk.databinding.ObservableRvItem
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.core.R as CoreR
class SuLogRvItem(val log: SuLog) : ObservableRvItem(), DiffItem<SuLogRvItem> {
override val layoutRes = R.layout.item_log_access_md2
val info = genInfo()
@get:Bindable
var isTop = false
set(value) = set(value, field, { field = it }, BR.top)
@get:Bindable
var isBottom = false
set(value) = set(value, field, { field = it }, BR.bottom)
override fun itemSameAs(other: SuLogRvItem) = log.appName == other.log.appName
private fun genInfo(): String {
val res = AppContext.resources
val sb = StringBuilder()
val date = log.time.toTime(timeDateFormat)
val toUid = res.getString(CoreR.string.target_uid, log.toUid)
val fromPid = res.getString(CoreR.string.pid, log.fromPid)
sb.append("$date\n$toUid $fromPid")
if (log.target != -1) {
val pid = if (log.target == 0) "magiskd" else log.target.toString()
val target = res.getString(CoreR.string.target_pid, pid)
sb.append(" $target")
}
if (log.context.isNotEmpty()) {
val context = res.getString(CoreR.string.selinux_context, log.context)
sb.append("\n$context")
}
if (log.gids.isNotEmpty()) {
val gids = res.getString(CoreR.string.supp_group, log.gids)
sb.append("\n$gids")
}
sb.append("\n${log.command}")
return sb.toString()
}
}

View file

@ -0,0 +1,108 @@
package com.topjohnwu.magisk.ui.module
import android.annotation.SuppressLint
import android.content.pm.ActivityInfo
import android.os.Bundle
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewTreeObserver
import android.widget.Toast
import androidx.core.view.MenuProvider
import androidx.core.view.isVisible
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseFragment
import com.topjohnwu.magisk.arch.viewModel
import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.databinding.FragmentActionMd2Binding
import com.topjohnwu.magisk.core.R as CoreR
class ActionFragment : BaseFragment<FragmentActionMd2Binding>(), MenuProvider {
override val layoutRes = R.layout.fragment_action_md2
override val viewModel by viewModel<ActionViewModel>()
override val snackbarView: View get() = binding.snackbarContainer
private var defaultOrientation = -1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.args = ActionFragmentArgs.fromBundle(requireArguments())
}
override fun onStart() {
super.onStart()
activity?.setTitle(viewModel.args.name)
binding.closeBtn.setOnClickListener {
activity?.onBackPressed()
}
viewModel.state.observe(this) {
if (it != ActionViewModel.State.RUNNING) {
binding.closeBtn.apply {
if (!this.isVisible) this.show()
if (!this.isFocused) this.requestFocus()
}
}
if (it != ActionViewModel.State.SUCCESS) return@observe
view?.viewTreeObserver?.addOnWindowFocusChangeListener(
object : ViewTreeObserver.OnWindowFocusChangeListener {
override fun onWindowFocusChanged(hasFocus: Boolean) {
if (hasFocus) return
view?.viewTreeObserver?.removeOnWindowFocusChangeListener(this)
view?.context?.apply {
toast(
getString(CoreR.string.done_action, viewModel.args.name),
Toast.LENGTH_SHORT
)
}
viewModel.back()
}
}
)
}
}
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_flash, menu)
}
override fun onMenuItemSelected(item: MenuItem): Boolean {
return viewModel.onMenuItemClicked(item)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
defaultOrientation = activity?.requestedOrientation ?: -1
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
if (savedInstanceState == null) {
viewModel.startRunAction()
}
}
@SuppressLint("WrongConstant")
override fun onDestroyView() {
if (defaultOrientation != -1) {
activity?.requestedOrientation = defaultOrientation
}
super.onDestroyView()
}
override fun onKeyEvent(event: KeyEvent): Boolean {
return when (event.keyCode) {
KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN -> true
else -> false
}
}
override fun onBackPressed(): Boolean {
if (viewModel.state.value == ActionViewModel.State.RUNNING) return true
return super.onBackPressed()
}
override fun onPreBind(binding: FragmentActionMd2Binding) = Unit
}

View file

@ -0,0 +1,88 @@
package com.topjohnwu.magisk.ui.module
import android.view.MenuItem
import androidx.databinding.ObservableArrayList
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseViewModel
import com.topjohnwu.magisk.core.ktx.synchronized
import com.topjohnwu.magisk.core.ktx.timeFormatStandard
import com.topjohnwu.magisk.core.ktx.toTime
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.magisk.ui.flash.ConsoleItem
import com.topjohnwu.superuser.CallbackList
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.IOException
class ActionViewModel : BaseViewModel() {
enum class State {
RUNNING, SUCCESS, FAILED
}
private val _state = MutableLiveData(State.RUNNING)
val state: LiveData<State> get() = _state
val items = ObservableArrayList<ConsoleItem>()
lateinit var args: ActionFragmentArgs
private val logItems = mutableListOf<String>().synchronized()
private val outItems = object : CallbackList<String>() {
override fun onAddElement(e: String?) {
e ?: return
items.add(ConsoleItem(e))
logItems.add(e)
}
}
fun startRunAction() = viewModelScope.launch {
onResult(withContext(Dispatchers.IO) {
try {
Shell.cmd("run_action \'${args.id}\'")
.to(outItems, logItems)
.exec().isSuccess
} catch (e: IOException) {
Timber.e(e)
false
}
})
}
private fun onResult(success: Boolean) {
_state.value = if (success) State.SUCCESS else State.FAILED
}
fun onMenuItemClicked(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_save -> savePressed()
}
return true
}
private fun savePressed() = withExternalRW {
viewModelScope.launch(Dispatchers.IO) {
val name = "%s_action_log_%s.log".format(
args.name,
System.currentTimeMillis().toTime(timeFormatStandard)
)
val file = MediaStoreUtils.getFile(name)
file.uri.outputStream().bufferedWriter().use { writer ->
synchronized(logItems) {
logItems.forEach {
writer.write(it)
writer.newLine()
}
}
}
SnackbarEvent(file.toString()).publish()
}
}
}

View file

@ -0,0 +1,45 @@
package com.topjohnwu.magisk.ui.module
import android.os.Bundle
import android.view.View
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseFragment
import com.topjohnwu.magisk.arch.viewModel
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName
import com.topjohnwu.magisk.databinding.FragmentModuleMd2Binding
import rikka.recyclerview.addEdgeSpacing
import rikka.recyclerview.addInvalidateItemDecorationsObserver
import rikka.recyclerview.addItemSpacing
import rikka.recyclerview.fixEdgeEffect
import com.topjohnwu.magisk.core.R as CoreR
class ModuleFragment : BaseFragment<FragmentModuleMd2Binding>() {
override val layoutRes = R.layout.fragment_module_md2
override val viewModel by viewModel<ModuleViewModel>()
override fun onStart() {
super.onStart()
activity?.title = resources.getString(CoreR.string.modules)
viewModel.data.observe(this) {
it ?: return@observe
val displayName = runCatching { it.displayName }.getOrNull() ?: return@observe
viewModel.requestInstallLocalModule(it, displayName)
viewModel.data.value = null
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.moduleList.apply {
addEdgeSpacing(top = R.dimen.l_50, bottom = R.dimen.l1)
addItemSpacing(R.dimen.l1, R.dimen.l_50, R.dimen.l1)
fixEdgeEffect()
post { addInvalidateItemDecorationsObserver() }
}
}
override fun onPreBind(binding: FragmentModuleMd2Binding) = Unit
}

View file

@ -0,0 +1,78 @@
package com.topjohnwu.magisk.ui.module
import androidx.databinding.Bindable
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.model.module.LocalModule
import com.topjohnwu.magisk.databinding.DiffItem
import com.topjohnwu.magisk.databinding.ItemWrapper
import com.topjohnwu.magisk.databinding.ObservableRvItem
import com.topjohnwu.magisk.databinding.RvItem
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.utils.TextHolder
import com.topjohnwu.magisk.utils.asText
import com.topjohnwu.magisk.core.R as CoreR
object InstallModule : RvItem(), DiffItem<InstallModule> {
override val layoutRes = R.layout.item_module_download
}
class LocalModuleRvItem(
override val item: LocalModule
) : ObservableRvItem(), DiffItem<LocalModuleRvItem>, ItemWrapper<LocalModule> {
override val layoutRes = R.layout.item_module_md2
val showNotice: Boolean
val showAction: Boolean
val noticeText: TextHolder
init {
val isZygisk = item.isZygisk
val isRiru = item.isRiru
val zygiskUnloaded = isZygisk && item.zygiskUnloaded
showNotice = zygiskUnloaded ||
(Info.isZygiskEnabled && isRiru) ||
(!Info.isZygiskEnabled && isZygisk)
showAction = item.hasAction && !showNotice
noticeText =
when {
zygiskUnloaded -> CoreR.string.zygisk_module_unloaded.asText()
isRiru -> CoreR.string.suspend_text_riru.asText(CoreR.string.zygisk.asText())
else -> CoreR.string.suspend_text_zygisk.asText(CoreR.string.zygisk.asText())
}
}
@get:Bindable
var isEnabled = item.enable
set(value) = set(value, field, { field = it }, BR.enabled, BR.updateReady) {
item.enable = value
}
@get:Bindable
var isRemoved = item.remove
set(value) = set(value, field, { field = it }, BR.removed, BR.updateReady) {
item.remove = value
}
@get:Bindable
val showUpdate get() = item.updateInfo != null
@get:Bindable
val updateReady get() = item.outdated && !isRemoved && isEnabled
val isUpdated = item.updated
fun fetchedUpdateInfo() {
notifyPropertyChanged(BR.showUpdate)
notifyPropertyChanged(BR.updateReady)
}
fun delete() {
isRemoved = !isRemoved
}
override fun itemSameAs(other: LocalModuleRvItem): Boolean = item.id == other.item.id
}

View file

@ -0,0 +1,108 @@
package com.topjohnwu.magisk.ui.module
import android.net.Uri
import androidx.databinding.Bindable
import androidx.lifecycle.MutableLiveData
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.MainDirections
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.base.ContentResultCallback
import com.topjohnwu.magisk.core.model.module.LocalModule
import com.topjohnwu.magisk.core.model.module.OnlineModule
import com.topjohnwu.magisk.databinding.MergeObservableList
import com.topjohnwu.magisk.databinding.RvItem
import com.topjohnwu.magisk.databinding.bindExtra
import com.topjohnwu.magisk.databinding.diffList
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.dialog.LocalModuleInstallDialog
import com.topjohnwu.magisk.dialog.OnlineModuleInstallDialog
import com.topjohnwu.magisk.events.GetContentEvent
import com.topjohnwu.magisk.events.SnackbarEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import com.topjohnwu.magisk.core.R as CoreR
class ModuleViewModel : AsyncLoadViewModel() {
val bottomBarBarrierIds = intArrayOf(R.id.module_update, R.id.module_remove)
private val itemsInstalled = diffList<LocalModuleRvItem>()
val items = MergeObservableList<RvItem>()
val extraBindings = bindExtra {
it.put(BR.viewModel, this)
}
val data get() = uri
@get:Bindable
var loading = true
private set(value) = set(value, field, { field = it }, BR.loading)
override suspend fun doLoadWork() {
loading = true
val moduleLoaded = Info.env.isActive &&
withContext(Dispatchers.IO) { LocalModule.loaded() }
if (moduleLoaded) {
loadInstalled()
if (items.isEmpty()) {
items.insertItem(InstallModule)
.insertList(itemsInstalled)
}
}
loading = false
loadUpdateInfo()
}
override fun onNetworkChanged(network: Boolean) = startLoading()
private suspend fun loadInstalled() {
withContext(Dispatchers.Default) {
val installed = LocalModule.installed().map { LocalModuleRvItem(it) }
itemsInstalled.update(installed)
}
}
private suspend fun loadUpdateInfo() {
withContext(Dispatchers.IO) {
itemsInstalled.forEach {
if (it.item.fetch())
it.fetchedUpdateInfo()
}
}
}
fun downloadPressed(item: OnlineModule?) =
if (item != null && Info.isConnected.value == true) {
withExternalRW { OnlineModuleInstallDialog(item).show() }
} else {
SnackbarEvent(CoreR.string.no_connection).publish()
}
fun installPressed() = withExternalRW {
GetContentEvent("application/zip", UriCallback()).publish()
}
fun requestInstallLocalModule(uri: Uri, displayName: String) {
LocalModuleInstallDialog(this, uri, displayName).show()
}
@Parcelize
class UriCallback : ContentResultCallback {
override fun onActivityResult(result: Uri) {
uri.value = result
}
}
fun runAction(id: String, name: String) {
MainDirections.actionActionFragment(id, name).navigate()
}
companion object {
private val uri = MutableLiveData<Uri?>()
}
}

View file

@ -0,0 +1,145 @@
package com.topjohnwu.magisk.ui.settings
import android.content.Context
import android.content.res.Resources
import android.view.View
import androidx.databinding.Bindable
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.ktx.activity
import com.topjohnwu.magisk.databinding.ObservableRvItem
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.utils.TextHolder
import com.topjohnwu.magisk.view.MagiskDialog
sealed class BaseSettingsItem : ObservableRvItem() {
interface Handler {
fun onItemPressed(view: View, item: BaseSettingsItem, andThen: () -> Unit)
fun onItemAction(view: View, item: BaseSettingsItem)
}
override val layoutRes get() = R.layout.item_settings
open val icon: Int get() = 0
open val title: TextHolder get() = TextHolder.EMPTY
@get:Bindable
open val description: TextHolder get() = TextHolder.EMPTY
@get:Bindable
var isEnabled = true
set(value) = set(value, field, { field = it }, BR.enabled, BR.description)
open fun onPressed(view: View, handler: Handler) {
handler.onItemPressed(view, this) {
handler.onItemAction(view, this)
}
}
open fun refresh() {}
// Only for toggle
open val showSwitch get() = false
@get:Bindable
open val isChecked get() = false
fun onToggle(view: View, handler: Handler, checked: Boolean) =
set(checked, isChecked, { onPressed(view, handler) })
abstract class Value<T> : BaseSettingsItem() {
/**
* Represents last agreed-upon value by the validation process and the user for current
* child. Be very aware that this shouldn't be **set** unless both sides agreed that _that_
* is the new value.
* */
abstract var value: T
protected set
}
abstract class Toggle : Value<Boolean>() {
override val showSwitch get() = true
override val isChecked get() = value
override fun onPressed(view: View, handler: Handler) {
// Make sure the checked state is synced
notifyPropertyChanged(BR.checked)
handler.onItemPressed(view, this) {
value = !value
notifyPropertyChanged(BR.checked)
handler.onItemAction(view, this)
}
}
}
abstract class Input : Value<String>() {
@get:Bindable
abstract val inputResult: String?
override fun onPressed(view: View, handler: Handler) {
handler.onItemPressed(view, this) {
MagiskDialog(view.activity).apply {
setTitle(title.getText(view.resources))
setView(getView(view.context))
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = android.R.string.ok
onClick {
inputResult?.let { result ->
doNotDismiss = false
value = result
handler.onItemAction(view, this@Input)
return@onClick
}
doNotDismiss = true
}
}
setButton(MagiskDialog.ButtonType.NEGATIVE) {
text = android.R.string.cancel
}
}.show()
}
}
abstract fun getView(context: Context): View
}
abstract class Selector : Value<Int>() {
open val entryRes get() = -1
open val descriptionRes get() = entryRes
open fun entries(res: Resources) = res.getArrayOrEmpty(entryRes)
open fun descriptions(res: Resources) = res.getArrayOrEmpty(descriptionRes)
override val description = object : TextHolder() {
override fun getText(resources: Resources): CharSequence {
return descriptions(resources).getOrElse(value) { "" }
}
}
private fun Resources.getArrayOrEmpty(id: Int): Array<String> =
runCatching { getStringArray(id) }.getOrDefault(emptyArray())
override fun onPressed(view: View, handler: Handler) {
handler.onItemPressed(view, this) {
MagiskDialog(view.activity).apply {
setTitle(title.getText(view.resources))
setButton(MagiskDialog.ButtonType.NEGATIVE) {
text = android.R.string.cancel
}
setListItems(entries(view.resources)) {
if (value != it) {
value = it
notifyPropertyChanged(BR.description)
handler.onItemAction(view, this@Selector)
}
}
}.show()
}
}
}
abstract class Blank : BaseSettingsItem()
abstract class Section : BaseSettingsItem() {
override val layoutRes = R.layout.item_settings_section
}
}

View file

@ -0,0 +1,40 @@
package com.topjohnwu.magisk.ui.settings
import android.os.Bundle
import android.view.View
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseFragment
import com.topjohnwu.magisk.arch.viewModel
import com.topjohnwu.magisk.databinding.FragmentSettingsMd2Binding
import rikka.recyclerview.addEdgeSpacing
import rikka.recyclerview.addItemSpacing
import rikka.recyclerview.fixEdgeEffect
import com.topjohnwu.magisk.core.R as CoreR
class SettingsFragment : BaseFragment<FragmentSettingsMd2Binding>() {
override val layoutRes = R.layout.fragment_settings_md2
override val viewModel by viewModel<SettingsViewModel>()
override val snackbarView: View get() = binding.snackbarContainer
override fun onStart() {
super.onStart()
activity?.title = resources.getString(CoreR.string.settings)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.settingsList.apply {
addEdgeSpacing(bottom = R.dimen.l1)
addItemSpacing(R.dimen.l1, R.dimen.l_50, R.dimen.l1)
fixEdgeEffect()
}
}
override fun onResume() {
super.onResume()
viewModel.items.forEach { it.refresh() }
}
}

View file

@ -0,0 +1,333 @@
package com.topjohnwu.magisk.ui.settings
import android.content.Context
import android.content.res.Resources
import android.os.Build
import android.view.LayoutInflater
import android.view.View
import androidx.databinding.Bindable
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.BuildConfig
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.ktx.activity
import com.topjohnwu.magisk.core.tasks.AppMigration
import com.topjohnwu.magisk.core.utils.LocaleSetting
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.databinding.DialogSettingsAppNameBinding
import com.topjohnwu.magisk.databinding.DialogSettingsDownloadPathBinding
import com.topjohnwu.magisk.databinding.DialogSettingsUpdateChannelBinding
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.utils.TextHolder
import com.topjohnwu.magisk.utils.asText
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.superuser.Shell
import com.topjohnwu.magisk.core.R as CoreR
// --- Customization
object Customization : BaseSettingsItem.Section() {
override val title = CoreR.string.settings_customization.asText()
}
object Language : BaseSettingsItem.Selector() {
private val names: Array<String> get() = LocaleSetting.available.names
private val tags: Array<String> get() = LocaleSetting.available.tags
override var value
get() = tags.indexOf(Config.locale)
set(value) {
Config.locale = tags[value]
}
override val title = CoreR.string.language.asText()
override fun entries(res: Resources) = names
override fun descriptions(res: Resources) = names
}
object LanguageSystem : BaseSettingsItem.Blank() {
override val title = CoreR.string.language.asText()
override val description: TextHolder
get() {
val locale = LocaleSetting.instance.appLocale
return locale?.getDisplayName(locale)?.asText() ?: CoreR.string.system_default.asText()
}
}
object Theme : BaseSettingsItem.Blank() {
override val icon = R.drawable.ic_paint
override val title = CoreR.string.section_theme.asText()
}
// --- App
object AppSettings : BaseSettingsItem.Section() {
override val title = CoreR.string.home_app_title.asText()
}
object Hide : BaseSettingsItem.Input() {
override val title = CoreR.string.settings_hide_app_title.asText()
override val description = CoreR.string.settings_hide_app_summary.asText()
override var value = ""
override val inputResult
get() = if (isError) null else result
@get:Bindable
var result = "Settings"
set(value) = set(value, field, { field = it }, BR.result, BR.error)
val maxLength
get() = AppMigration.MAX_LABEL_LENGTH
@get:Bindable
val isError
get() = result.length > maxLength || result.isBlank()
override fun getView(context: Context) = DialogSettingsAppNameBinding
.inflate(LayoutInflater.from(context)).also { it.data = this }.root
}
object Restore : BaseSettingsItem.Blank() {
override val title = CoreR.string.settings_restore_app_title.asText()
override val description = CoreR.string.settings_restore_app_summary.asText()
override fun onPressed(view: View, handler: Handler) {
handler.onItemPressed(view, this) {
MagiskDialog(view.activity).apply {
setTitle(CoreR.string.settings_restore_app_title)
setMessage(CoreR.string.restore_app_confirmation)
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = android.R.string.ok
onClick {
handler.onItemAction(view, this@Restore)
}
}
setButton(MagiskDialog.ButtonType.NEGATIVE) {
text = android.R.string.cancel
}
setCancelable(true)
show()
}
}
}
}
object AddShortcut : BaseSettingsItem.Blank() {
override val title = CoreR.string.add_shortcut_title.asText()
override val description = CoreR.string.setting_add_shortcut_summary.asText()
}
object DownloadPath : BaseSettingsItem.Input() {
override var value
get() = Config.downloadDir
set(value) {
Config.downloadDir = value
notifyPropertyChanged(BR.description)
}
override val title = CoreR.string.settings_download_path_title.asText()
override val description get() = MediaStoreUtils.fullPath(value).asText()
override var inputResult: String = value
set(value) = set(value, field, { field = it }, BR.inputResult, BR.path)
@get:Bindable
val path get() = MediaStoreUtils.fullPath(inputResult)
override fun getView(context: Context) = DialogSettingsDownloadPathBinding
.inflate(LayoutInflater.from(context)).also { it.data = this }.root
}
object UpdateChannel : BaseSettingsItem.Selector() {
override var value
get() = Config.updateChannel
set(value) {
Config.updateChannel = value
Info.resetUpdate()
}
override val title = CoreR.string.settings_update_channel_title.asText()
override val entryRes = CoreR.array.update_channel
}
object UpdateChannelUrl : BaseSettingsItem.Input() {
override val title = CoreR.string.settings_update_custom.asText()
override val description get() = value.asText()
override var value
get() = Config.customChannelUrl
set(value) {
Config.customChannelUrl = value
Info.resetUpdate()
notifyPropertyChanged(BR.description)
}
override var inputResult: String = value
set(value) = set(value, field, { field = it }, BR.inputResult)
override fun refresh() {
isEnabled = UpdateChannel.value == Config.Value.CUSTOM_CHANNEL
}
override fun getView(context: Context) = DialogSettingsUpdateChannelBinding
.inflate(LayoutInflater.from(context)).also { it.data = this }.root
}
object UpdateChecker : BaseSettingsItem.Toggle() {
override val title = CoreR.string.settings_check_update_title.asText()
override val description = CoreR.string.settings_check_update_summary.asText()
override var value by Config::checkUpdate
}
object DoHToggle : BaseSettingsItem.Toggle() {
override val title = CoreR.string.settings_doh_title.asText()
override val description = CoreR.string.settings_doh_description.asText()
override var value by Config::doh
}
object SystemlessHosts : BaseSettingsItem.Blank() {
override val title = CoreR.string.settings_hosts_title.asText()
override val description = CoreR.string.settings_hosts_summary.asText()
}
object RandNameToggle : BaseSettingsItem.Toggle() {
override val title = CoreR.string.settings_random_name_title.asText()
override val description = CoreR.string.settings_random_name_description.asText()
override var value by Config::randName
}
// --- Magisk
object Magisk : BaseSettingsItem.Section() {
override val title = CoreR.string.magisk.asText()
}
object Zygisk : BaseSettingsItem.Toggle() {
override val title = CoreR.string.zygisk.asText()
override val description get() =
if (mismatch) CoreR.string.reboot_apply_change.asText()
else CoreR.string.settings_zygisk_summary.asText()
override var value
get() = Config.zygisk
set(value) {
Config.zygisk = value
notifyPropertyChanged(BR.description)
}
val mismatch get() = value != Info.isZygiskEnabled
}
object DenyList : BaseSettingsItem.Toggle() {
override val title = CoreR.string.settings_denylist_title.asText()
override val description get() = CoreR.string.settings_denylist_summary.asText()
override var value = Config.denyList
set(value) {
field = value
val cmd = if (value) "enable" else "disable"
Shell.cmd("magisk --denylist $cmd").submit { result ->
if (result.isSuccess) {
Config.denyList = value
} else {
field = !value
notifyPropertyChanged(BR.checked)
}
}
}
}
object DenyListConfig : BaseSettingsItem.Blank() {
override val title = CoreR.string.settings_denylist_config_title.asText()
override val description = CoreR.string.settings_denylist_config_summary.asText()
}
// --- Superuser
object Tapjack : BaseSettingsItem.Toggle() {
override val title = CoreR.string.settings_su_tapjack_title.asText()
override val description = CoreR.string.settings_su_tapjack_summary.asText()
override var value by Config::suTapjack
}
object Authentication : BaseSettingsItem.Toggle() {
override val title = CoreR.string.settings_su_auth_title.asText()
override var description = CoreR.string.settings_su_auth_summary.asText()
override var value by Config::suAuth
override fun refresh() {
isEnabled = Info.isDeviceSecure
if (!isEnabled) {
description = CoreR.string.settings_su_auth_insecure.asText()
}
}
}
object Superuser : BaseSettingsItem.Section() {
override val title = CoreR.string.superuser.asText()
}
object AccessMode : BaseSettingsItem.Selector() {
override val title = CoreR.string.superuser_access.asText()
override val entryRes = CoreR.array.su_access
override var value by Config::rootMode
}
object MultiuserMode : BaseSettingsItem.Selector() {
override val title = CoreR.string.multiuser_mode.asText()
override val entryRes = CoreR.array.multiuser_mode
override val descriptionRes = CoreR.array.multiuser_summary
override var value by Config::suMultiuserMode
override fun refresh() {
isEnabled = Const.USER_ID == 0
}
}
object MountNamespaceMode : BaseSettingsItem.Selector() {
override val title = CoreR.string.mount_namespace_mode.asText()
override val entryRes = CoreR.array.namespace
override val descriptionRes = CoreR.array.namespace_summary
override var value by Config::suMntNamespaceMode
}
object AutomaticResponse : BaseSettingsItem.Selector() {
override val title = CoreR.string.auto_response.asText()
override val entryRes = CoreR.array.auto_response
override var value by Config::suAutoResponse
}
object RequestTimeout : BaseSettingsItem.Selector() {
override val title = CoreR.string.request_timeout.asText()
override val entryRes = CoreR.array.request_timeout
private val entryValues = listOf(10, 15, 20, 30, 45, 60)
override var value = entryValues.indexOfFirst { it == Config.suDefaultTimeout }
set(value) {
field = value
Config.suDefaultTimeout = entryValues[value]
}
}
object SUNotification : BaseSettingsItem.Selector() {
override val title = CoreR.string.superuser_notification.asText()
override val entryRes = CoreR.array.su_notification
override var value by Config::suNotification
}
object Reauthenticate : BaseSettingsItem.Toggle() {
override val title = CoreR.string.settings_su_reauth_title.asText()
override val description = CoreR.string.settings_su_reauth_summary.asText()
override var value by Config::suReAuth
override fun refresh() {
isEnabled = Build.VERSION.SDK_INT < Build.VERSION_CODES.O
}
}
object Restrict : BaseSettingsItem.Toggle() {
override val title = CoreR.string.settings_su_restrict_title.asText()
override val description = CoreR.string.settings_su_restrict_summary.asText()
override var value by Config::suRestrict
}

View file

@ -0,0 +1,138 @@
package com.topjohnwu.magisk.ui.settings
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
import android.view.View
import android.widget.Toast
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.arch.BaseViewModel
import com.topjohnwu.magisk.core.AppContext
import com.topjohnwu.magisk.core.BuildConfig
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.ktx.activity
import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.core.tasks.AppMigration
import com.topjohnwu.magisk.core.utils.LocaleSetting
import com.topjohnwu.magisk.core.utils.RootUtils
import com.topjohnwu.magisk.databinding.bindExtra
import com.topjohnwu.magisk.events.AddHomeIconEvent
import com.topjohnwu.magisk.events.AuthEvent
import com.topjohnwu.magisk.events.SnackbarEvent
import kotlinx.coroutines.launch
class SettingsViewModel : BaseViewModel(), BaseSettingsItem.Handler {
val items = createItems()
val extraBindings = bindExtra {
it.put(BR.handler, this)
}
private fun createItems(): List<BaseSettingsItem> {
val context = AppContext
val hidden = context.packageName != BuildConfig.APP_PACKAGE_NAME
// Customization
val list = mutableListOf(
Customization,
Theme, if (LocaleSetting.useLocaleManager) LanguageSystem else Language
)
if (isRunningAsStub && ShortcutManagerCompat.isRequestPinShortcutSupported(context))
list.add(AddShortcut)
// Manager
list.addAll(listOf(
AppSettings,
UpdateChannel, UpdateChannelUrl, DoHToggle, UpdateChecker, DownloadPath, RandNameToggle
))
if (Info.env.isActive && Const.USER_ID == 0) {
if (hidden) list.add(Restore) else list.add(Hide)
}
// Magisk
if (Info.env.isActive) {
list.addAll(listOf(
Magisk,
SystemlessHosts
))
if (Const.Version.atLeast_24_0()) {
list.addAll(listOf(Zygisk, DenyList, DenyListConfig))
}
}
// Superuser
if (Info.showSuperUser) {
list.addAll(listOf(
Superuser,
Tapjack, Authentication, AccessMode, MultiuserMode, MountNamespaceMode,
AutomaticResponse, RequestTimeout, SUNotification
))
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
// Re-authenticate is not feasible on 8.0+
list.add(Reauthenticate)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// Can hide overlay windows on 12.0+
list.remove(Tapjack)
}
if (Const.Version.atLeast_30_1()) {
list.add(Restrict)
}
}
return list
}
override fun onItemPressed(view: View, item: BaseSettingsItem, doAction: () -> Unit) {
when (item) {
DownloadPath -> withExternalRW(doAction)
UpdateChecker -> withPostNotificationPermission(doAction)
Authentication -> AuthEvent(doAction).publish()
AutomaticResponse -> if (Config.suAuth) AuthEvent(doAction).publish() else doAction()
else -> doAction()
}
}
override fun onItemAction(view: View, item: BaseSettingsItem) {
when (item) {
Theme -> SettingsFragmentDirections.actionSettingsFragmentToThemeFragment().navigate()
LanguageSystem -> launchAppLocaleSettings(view.activity)
AddShortcut -> AddHomeIconEvent().publish()
SystemlessHosts -> createHosts()
DenyListConfig -> SettingsFragmentDirections.actionSettingsFragmentToDenyFragment().navigate()
UpdateChannel -> openUrlIfNecessary(view)
is Hide -> viewModelScope.launch { AppMigration.hide(view.activity, item.value) }
Restore -> viewModelScope.launch { AppMigration.restore(view.activity) }
Zygisk -> if (Zygisk.mismatch) SnackbarEvent(R.string.reboot_apply_change).publish()
else -> Unit
}
}
private fun launchAppLocaleSettings(activity: Activity) {
val intent = Intent(Settings.ACTION_APP_LOCALE_SETTINGS)
intent.data = Uri.fromParts("package", activity.packageName, null)
activity.startActivity(intent)
}
private fun openUrlIfNecessary(view: View) {
UpdateChannelUrl.refresh()
if (UpdateChannelUrl.isEnabled && UpdateChannelUrl.value.isBlank()) {
UpdateChannelUrl.onPressed(view, this)
}
}
private fun createHosts() {
viewModelScope.launch {
RootUtils.addSystemlessHosts()
AppContext.toast(R.string.settings_hosts_toast, Toast.LENGTH_SHORT)
}
}
}

View file

@ -0,0 +1,102 @@
package com.topjohnwu.magisk.ui.superuser
import android.graphics.drawable.Drawable
import androidx.databinding.Bindable
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.model.su.SuPolicy
import com.topjohnwu.magisk.databinding.DiffItem
import com.topjohnwu.magisk.databinding.ItemWrapper
import com.topjohnwu.magisk.databinding.ObservableRvItem
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.core.R as CoreR
class PolicyRvItem(
private val viewModel: SuperuserViewModel,
override val item: SuPolicy,
val packageName: String,
private val isSharedUid: Boolean,
val icon: Drawable,
val appName: String
) : ObservableRvItem(), DiffItem<PolicyRvItem>, ItemWrapper<SuPolicy> {
override val layoutRes = R.layout.item_policy_md2
val title get() = if (isSharedUid) "[SharedUID] $appName" else appName
private inline fun <reified T> setImpl(new: T, old: T, setter: (T) -> Unit) {
if (old != new) {
setter(new)
}
}
@get:Bindable
var isExpanded = false
set(value) = set(value, field, { field = it }, BR.expanded)
val showSlider = Config.suRestrict || item.policy == SuPolicy.RESTRICT
@get:Bindable
var isEnabled
get() = item.policy >= SuPolicy.ALLOW
set(value) = setImpl(value, isEnabled) {
notifyPropertyChanged(BR.enabled)
viewModel.updatePolicy(this, if (it) SuPolicy.ALLOW else SuPolicy.DENY)
}
@get:Bindable
var sliderValue
get() = item.policy
set(value) = setImpl(value, sliderValue) {
notifyPropertyChanged(BR.sliderValue)
notifyPropertyChanged(BR.enabled)
viewModel.updatePolicy(this, it)
}
val sliderValueToPolicyString: (Float) -> Int = { value ->
when (value.toInt()) {
1 -> CoreR.string.deny
2 -> CoreR.string.restrict
3 -> CoreR.string.grant
else -> CoreR.string.deny
}
}
@get:Bindable
var shouldNotify
get() = item.notification
private set(value) = setImpl(value, shouldNotify) {
item.notification = it
viewModel.updateNotify(this)
}
@get:Bindable
var shouldLog
get() = item.logging
private set(value) = setImpl(value, shouldLog) {
item.logging = it
viewModel.updateLogging(this)
}
fun toggleExpand() {
isExpanded = !isExpanded
}
fun toggleNotify() {
shouldNotify = !shouldNotify
}
fun toggleLog() {
shouldLog = !shouldLog
}
fun revoke() {
viewModel.deletePressed(this)
}
override fun itemSameAs(other: PolicyRvItem) = packageName == other.packageName
override fun contentSameAs(other: PolicyRvItem) = item.policy == other.item.policy
}

View file

@ -0,0 +1,36 @@
package com.topjohnwu.magisk.ui.superuser
import android.os.Bundle
import android.view.View
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseFragment
import com.topjohnwu.magisk.arch.viewModel
import com.topjohnwu.magisk.databinding.FragmentSuperuserMd2Binding
import rikka.recyclerview.addEdgeSpacing
import rikka.recyclerview.addItemSpacing
import rikka.recyclerview.fixEdgeEffect
import com.topjohnwu.magisk.core.R as CoreR
class SuperuserFragment : BaseFragment<FragmentSuperuserMd2Binding>() {
override val layoutRes = R.layout.fragment_superuser_md2
override val viewModel by viewModel<SuperuserViewModel>()
override fun onStart() {
super.onStart()
activity?.title = resources.getString(CoreR.string.superuser)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.superuserList.apply {
addEdgeSpacing(top = R.dimen.l_50, bottom = R.dimen.l1)
addItemSpacing(R.dimen.l1, R.dimen.l_50, R.dimen.l1)
fixEdgeEffect()
}
}
override fun onPreBind(binding: FragmentSuperuserMd2Binding) {}
}

View file

@ -0,0 +1,180 @@
package com.topjohnwu.magisk.ui.superuser
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
import android.os.Process
import androidx.databinding.Bindable
import androidx.databinding.ObservableArrayList
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
import com.topjohnwu.magisk.core.AppContext
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.data.magiskdb.PolicyDao
import com.topjohnwu.magisk.core.ktx.getLabel
import com.topjohnwu.magisk.core.model.su.SuPolicy
import com.topjohnwu.magisk.databinding.MergeObservableList
import com.topjohnwu.magisk.databinding.RvItem
import com.topjohnwu.magisk.databinding.bindExtra
import com.topjohnwu.magisk.databinding.diffList
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.dialog.SuperuserRevokeDialog
import com.topjohnwu.magisk.events.AuthEvent
import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.magisk.utils.asText
import com.topjohnwu.magisk.view.TextItem
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.Locale
class SuperuserViewModel(
private val db: PolicyDao
) : AsyncLoadViewModel() {
private val itemNoData = TextItem(R.string.superuser_policy_none)
private val itemsHelpers = ObservableArrayList<TextItem>()
private val itemsPolicies = diffList<PolicyRvItem>()
val items = MergeObservableList<RvItem>()
.insertList(itemsHelpers)
.insertList(itemsPolicies)
val extraBindings = bindExtra {
it.put(BR.listener, this)
}
@get:Bindable
var loading = true
private set(value) = set(value, field, { field = it }, BR.loading)
@SuppressLint("InlinedApi")
override suspend fun doLoadWork() {
if (!Info.showSuperUser) {
loading = false
return
}
loading = true
withContext(Dispatchers.IO) {
db.deleteOutdated()
db.delete(AppContext.applicationInfo.uid)
val policies = ArrayList<PolicyRvItem>()
val pm = AppContext.packageManager
for (policy in db.fetchAll()) {
val pkgs =
if (policy.uid == Process.SYSTEM_UID) arrayOf("android")
else pm.getPackagesForUid(policy.uid)
if (pkgs == null) {
db.delete(policy.uid)
continue
}
val map = pkgs.mapNotNull { pkg ->
try {
val info = pm.getPackageInfo(pkg, MATCH_UNINSTALLED_PACKAGES)
PolicyRvItem(
this@SuperuserViewModel, policy,
info.packageName,
info.sharedUserId != null,
info.applicationInfo?.loadIcon(pm) ?: pm.defaultActivityIcon,
info.applicationInfo?.getLabel(pm) ?: info.packageName
)
} catch (e: PackageManager.NameNotFoundException) {
null
}
}
if (map.isEmpty()) {
db.delete(policy.uid)
continue
}
policies.addAll(map)
}
policies.sortWith(compareBy(
{ it.appName.lowercase(Locale.ROOT) },
{ it.packageName }
))
itemsPolicies.update(policies)
}
if (itemsPolicies.isNotEmpty())
itemsHelpers.clear()
else if (itemsHelpers.isEmpty())
itemsHelpers.add(itemNoData)
loading = false
}
// ---
fun deletePressed(item: PolicyRvItem) {
fun updateState() = viewModelScope.launch {
db.delete(item.item.uid)
val list = ArrayList(itemsPolicies)
list.removeAll { it.item.uid == item.item.uid }
itemsPolicies.update(list)
if (list.isEmpty() && itemsHelpers.isEmpty()) {
itemsHelpers.add(itemNoData)
}
}
if (Config.suAuth) {
AuthEvent { updateState() }.publish()
} else {
SuperuserRevokeDialog(item.title) { updateState() }.show()
}
}
fun updateNotify(item: PolicyRvItem) {
viewModelScope.launch {
db.update(item.item)
val res = when {
item.item.notification -> R.string.su_snack_notif_on
else -> R.string.su_snack_notif_off
}
itemsPolicies.forEach {
if (it.item.uid == item.item.uid) {
it.notifyPropertyChanged(BR.shouldNotify)
}
}
SnackbarEvent(res.asText(item.appName)).publish()
}
}
fun updateLogging(item: PolicyRvItem) {
viewModelScope.launch {
db.update(item.item)
val res = when {
item.item.logging -> R.string.su_snack_log_on
else -> R.string.su_snack_log_off
}
itemsPolicies.forEach {
if (it.item.uid == item.item.uid) {
it.notifyPropertyChanged(BR.shouldLog)
}
}
SnackbarEvent(res.asText(item.appName)).publish()
}
}
fun updatePolicy(item: PolicyRvItem, policy: Int) {
val items = itemsPolicies.filter { it.item.uid == item.item.uid }
fun updateState() {
viewModelScope.launch {
val res = if (policy >= SuPolicy.ALLOW) R.string.su_snack_grant else R.string.su_snack_deny
item.item.policy = policy
db.update(item.item)
items.forEach {
it.notifyPropertyChanged(BR.enabled)
it.notifyPropertyChanged(BR.sliderValue)
}
SnackbarEvent(res.asText(item.appName)).publish()
}
}
if (Config.suAuth) {
AuthEvent { updateState() }.publish()
} else {
updateState()
}
}
}

View file

@ -0,0 +1,69 @@
package com.topjohnwu.magisk.ui.surequest
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.res.Resources
import android.os.Build
import android.os.Bundle
import android.view.Window
import android.view.WindowManager
import androidx.lifecycle.lifecycleScope
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.arch.viewModel
import com.topjohnwu.magisk.core.base.UntrackedActivity
import com.topjohnwu.magisk.core.su.SuCallbackHandler
import com.topjohnwu.magisk.core.su.SuCallbackHandler.REQUEST
import com.topjohnwu.magisk.databinding.ActivityRequestBinding
import com.topjohnwu.magisk.ui.theme.Theme
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
open class SuRequestActivity : UIActivity<ActivityRequestBinding>(), UntrackedActivity {
override val layoutRes: Int = R.layout.activity_request
override val viewModel: SuRequestViewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
supportRequestWindowFeature(Window.FEATURE_NO_TITLE)
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
window.addFlags(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
window.setHideOverlayWindows(true)
}
setTheme(Theme.selected.themeRes)
super.onCreate(savedInstanceState)
if (intent.action == Intent.ACTION_VIEW) {
val action = intent.getStringExtra("action")
if (action == REQUEST) {
viewModel.handleRequest(intent)
} else {
lifecycleScope.launch {
withContext(Dispatchers.IO) {
SuCallbackHandler.run(this@SuRequestActivity, action, intent.extras)
}
finish()
}
}
} else {
finish()
}
}
override fun getTheme(): Resources.Theme {
val theme = super.getTheme()
theme.applyStyle(R.style.Foundation_Floating, true)
return theme
}
override fun onBackPressed() {
viewModel.denyPressed()
}
override fun finish() {
super.finishAndRemoveTask()
}
}

View file

@ -0,0 +1,199 @@
package com.topjohnwu.magisk.ui.surequest
import android.annotation.SuppressLint
import android.content.Intent
import android.content.SharedPreferences
import android.content.res.Resources
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.os.CountDownTimer
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.view.accessibility.AccessibilityNodeProvider
import android.widget.Toast
import androidx.databinding.Bindable
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.arch.BaseViewModel
import com.topjohnwu.magisk.core.AppContext
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.data.magiskdb.PolicyDao
import com.topjohnwu.magisk.core.ktx.getLabel
import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.core.model.su.SuPolicy.Companion.ALLOW
import com.topjohnwu.magisk.core.model.su.SuPolicy.Companion.DENY
import com.topjohnwu.magisk.core.su.SuRequestHandler
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.events.AuthEvent
import com.topjohnwu.magisk.events.DieEvent
import com.topjohnwu.magisk.events.ShowUIEvent
import com.topjohnwu.magisk.utils.TextHolder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.concurrent.TimeUnit.SECONDS
class SuRequestViewModel(
policyDB: PolicyDao,
private val timeoutPrefs: SharedPreferences
) : BaseViewModel() {
lateinit var icon: Drawable
lateinit var title: String
lateinit var packageName: String
@get:Bindable
val denyText = DenyText()
@get:Bindable
var selectedItemPosition = 0
set(value) = set(value, field, { field = it }, BR.selectedItemPosition)
@get:Bindable
var grantEnabled = false
set(value) = set(value, field, { field = it }, BR.grantEnabled)
@SuppressLint("ClickableViewAccessibility")
val grantTouchListener = View.OnTouchListener { _: View, event: MotionEvent ->
// Filter obscured touches by consuming them.
if (event.flags and MotionEvent.FLAG_WINDOW_IS_OBSCURED != 0
|| event.flags and MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED != 0) {
if (event.action == MotionEvent.ACTION_UP) {
AppContext.toast(R.string.touch_filtered_warning, Toast.LENGTH_SHORT)
}
return@OnTouchListener Config.suTapjack
}
false
}
private val handler = SuRequestHandler(AppContext.packageManager, policyDB)
private val millis = SECONDS.toMillis(Config.suDefaultTimeout.toLong())
private var timer = SuTimer(millis, 1000)
private var initialized = false
fun grantPressed() {
cancelTimer()
if (Config.suAuth) {
AuthEvent { respond(ALLOW) }.publish()
} else {
respond(ALLOW)
}
}
fun denyPressed() {
respond(DENY)
}
fun spinnerTouched(): Boolean {
cancelTimer()
return false
}
fun handleRequest(intent: Intent) {
viewModelScope.launch(Dispatchers.Default) {
if (handler.start(intent))
showDialog()
else
DieEvent().publish()
}
}
private fun showDialog() {
val pm = handler.pm
val info = handler.pkgInfo
val app = info.applicationInfo
if (app == null) {
// The request is not coming from an app process, and the UID is a
// shared UID. We have no way to know where this request comes from.
icon = pm.defaultActivityIcon
title = "[SharedUID] ${info.sharedUserId}"
packageName = info.sharedUserId.toString()
} else {
val prefix = if (info.sharedUserId == null) "" else "[SharedUID] "
icon = app.loadIcon(pm)
title = "$prefix${app.getLabel(pm)}"
packageName = info.packageName
}
selectedItemPosition = timeoutPrefs.getInt(packageName, 0)
// Set timer
timer.start()
// Actually show the UI
ShowUIEvent(if (Config.suTapjack) EmptyAccessibilityDelegate else null).publish()
initialized = true
}
private fun respond(action: Int) {
if (!initialized) {
// ignore the response until showDialog done
return
}
timer.cancel()
val pos = selectedItemPosition
timeoutPrefs.edit().putInt(packageName, pos).apply()
viewModelScope.launch {
handler.respond(action, Config.Value.TIMEOUT_LIST[pos])
// Kill activity after response
DieEvent().publish()
}
}
private fun cancelTimer() {
timer.cancel()
denyText.seconds = 0
}
private inner class SuTimer(
private val millis: Long,
interval: Long
) : CountDownTimer(millis, interval) {
override fun onTick(remains: Long) {
if (!grantEnabled && remains <= millis - 1000) {
grantEnabled = true
}
denyText.seconds = (remains / 1000).toInt() + 1
}
override fun onFinish() {
denyText.seconds = 0
respond(DENY)
}
}
inner class DenyText : TextHolder() {
var seconds = 0
set(value) = set(value, field, { field = it }, BR.denyText)
override fun getText(resources: Resources): CharSequence {
return if (seconds != 0)
"${resources.getString(R.string.deny)} ($seconds)"
else
resources.getString(R.string.deny)
}
}
// Invisible for accessibility services
object EmptyAccessibilityDelegate : View.AccessibilityDelegate() {
override fun sendAccessibilityEvent(host: View, eventType: Int) {}
override fun performAccessibilityAction(host: View, action: Int, args: Bundle?) = true
override fun sendAccessibilityEventUnchecked(host: View, event: AccessibilityEvent) {}
override fun dispatchPopulateAccessibilityEvent(host: View, event: AccessibilityEvent) = true
override fun onPopulateAccessibilityEvent(host: View, event: AccessibilityEvent) {}
override fun onInitializeAccessibilityEvent(host: View, event: AccessibilityEvent) {}
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo) {}
override fun addExtraDataToAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo, extraDataKey: String, arguments: Bundle?) {}
override fun onRequestSendAccessibilityEvent(host: ViewGroup, child: View, event: AccessibilityEvent): Boolean = false
override fun getAccessibilityNodeProvider(host: View): AccessibilityNodeProvider? = null
}
}

View file

@ -0,0 +1,50 @@
package com.topjohnwu.magisk.ui.theme
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
enum class Theme(
val themeName: String,
val themeRes: Int
) {
Piplup(
themeName = "Piplup",
themeRes = R.style.ThemeFoundationMD2_Piplup
),
PiplupAmoled(
themeName = "AMOLED",
themeRes = R.style.ThemeFoundationMD2_Amoled
),
Rayquaza(
themeName = "Rayquaza",
themeRes = R.style.ThemeFoundationMD2_Rayquaza
),
Zapdos(
themeName = "Zapdos",
themeRes = R.style.ThemeFoundationMD2_Zapdos
),
Charmeleon(
themeName = "Charmeleon",
themeRes = R.style.ThemeFoundationMD2_Charmeleon
),
Mew(
themeName = "Mew",
themeRes = R.style.ThemeFoundationMD2_Mew
),
Salamence(
themeName = "Salamence",
themeRes = R.style.ThemeFoundationMD2_Salamence
),
Fraxure(
themeName = "Fraxure (Legacy)",
themeRes = R.style.ThemeFoundationMD2_Fraxure
);
val isSelected get() = Config.themeOrdinal == ordinal
companion object {
val selected get() = values().getOrNull(Config.themeOrdinal) ?: Piplup
}
}

View file

@ -0,0 +1,68 @@
package com.topjohnwu.magisk.ui.theme
import android.os.Bundle
import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseFragment
import com.topjohnwu.magisk.arch.viewModel
import com.topjohnwu.magisk.databinding.FragmentThemeMd2Binding
import com.topjohnwu.magisk.databinding.ItemThemeBindingImpl
import com.topjohnwu.magisk.core.R as CoreR
class ThemeFragment : BaseFragment<FragmentThemeMd2Binding>() {
override val layoutRes = R.layout.fragment_theme_md2
override val viewModel by viewModel<ThemeViewModel>()
private fun <T> Array<T>.paired(): List<Pair<T, T?>> {
val iterator = iterator()
if (!iterator.hasNext()) return emptyList()
val result = mutableListOf<Pair<T, T?>>()
while (iterator.hasNext()) {
val a = iterator.next()
val b = if (iterator.hasNext()) iterator.next() else null
result.add(a to b)
}
return result
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
super.onCreateView(inflater, container, savedInstanceState)
for ((a, b) in Theme.values().paired()) {
val c = inflater.inflate(R.layout.item_theme_container, null, false)
val left = c.findViewById<FrameLayout>(R.id.left)
val right = c.findViewById<FrameLayout>(R.id.right)
for ((theme, view) in listOf(a to left, b to right)) {
theme ?: continue
val themed = ContextThemeWrapper(activity, theme.themeRes)
ItemThemeBindingImpl.inflate(LayoutInflater.from(themed), view, true).also {
it.setVariable(BR.viewModel, viewModel)
it.setVariable(BR.theme, theme)
it.lifecycleOwner = viewLifecycleOwner
}
}
binding.themeContainer.addView(c)
}
return binding.root
}
override fun onStart() {
super.onStart()
activity?.title = getString(CoreR.string.section_theme)
}
}

View file

@ -0,0 +1,23 @@
package com.topjohnwu.magisk.ui.theme
import com.topjohnwu.magisk.arch.BaseViewModel
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.dialog.DarkThemeDialog
import com.topjohnwu.magisk.events.RecreateEvent
import com.topjohnwu.magisk.view.TappableHeadlineItem
class ThemeViewModel : BaseViewModel(), TappableHeadlineItem.Listener {
val themeHeadline = TappableHeadlineItem.ThemeMode
override fun onItemPressed(item: TappableHeadlineItem) = when (item) {
is TappableHeadlineItem.ThemeMode -> DarkThemeDialog().show()
}
fun saveTheme(theme: Theme) {
if (!theme.isSelected) {
Config.themeOrdinal = theme.ordinal
RecreateEvent().publish()
}
}
}

View file

@ -0,0 +1,14 @@
package com.topjohnwu.magisk.utils
import android.content.ContentResolver
import android.provider.Settings
class AccessibilityUtils {
companion object {
fun isAnimationEnabled(cr: ContentResolver): Boolean {
return !(Settings.Global.getFloat(cr, Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f) == 0.0f
&& Settings.Global.getFloat(cr, Settings.Global.TRANSITION_ANIMATION_SCALE, 1.0f) == 0.0f
&& Settings.Global.getFloat(cr, Settings.Global.WINDOW_ANIMATION_SCALE, 1.0f) == 0.0f)
}
}
}

View file

@ -0,0 +1,86 @@
package com.topjohnwu.magisk.utils
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.view.View
import androidx.core.animation.addListener
import androidx.core.text.layoutDirection
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.core.view.marginBottom
import androidx.core.view.marginEnd
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import com.google.android.material.circularreveal.CircularRevealCompat
import com.google.android.material.circularreveal.CircularRevealWidget
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.topjohnwu.magisk.core.utils.LocaleSetting
import kotlin.math.hypot
object MotionRevealHelper {
fun <CV> withViews(
revealable: CV,
fab: FloatingActionButton,
expanded: Boolean
) where CV : CircularRevealWidget, CV : View {
revealable.revealInfo = revealable.createRevealInfo(!expanded)
val revealInfo = revealable.createRevealInfo(expanded)
val revealAnim = revealable.createRevealAnim(revealInfo)
val moveAnim = fab.createMoveAnim(revealInfo)
AnimatorSet().also {
if (expanded) {
it.play(revealAnim).after(moveAnim)
} else {
it.play(moveAnim).after(revealAnim)
}
}.start()
}
private fun <CV> CV.createRevealAnim(
revealInfo: CircularRevealWidget.RevealInfo
): Animator where CV : CircularRevealWidget, CV : View =
CircularRevealCompat.createCircularReveal(
this,
revealInfo.centerX,
revealInfo.centerY,
revealInfo.radius
).apply {
addListener(onStart = {
isVisible = true
}, onEnd = {
if (revealInfo.radius == 0f) {
isInvisible = true
}
})
}
private fun FloatingActionButton.createMoveAnim(
revealInfo: CircularRevealWidget.RevealInfo
): Animator = AnimatorSet().also {
it.interpolator = FastOutSlowInInterpolator()
it.addListener(onStart = { show() }, onEnd = { if (revealInfo.radius != 0f) hide() })
val rtlMod =
if (LocaleSetting.instance.currentLocale.layoutDirection == View.LAYOUT_DIRECTION_RTL)
1f else -1f
val maxX = revealInfo.centerX - marginEnd - measuredWidth / 2f
val targetX = if (revealInfo.radius == 0f) 0f else maxX * rtlMod
val moveX = ObjectAnimator.ofFloat(this, View.TRANSLATION_X, targetX)
val maxY = revealInfo.centerY - marginBottom - measuredHeight / 2f
val targetY = if (revealInfo.radius == 0f) 0f else -maxY
val moveY = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, targetY)
it.playTogether(moveX, moveY)
}
private fun View.createRevealInfo(expanded: Boolean): CircularRevealWidget.RevealInfo {
val cX = measuredWidth / 2f
val cY = measuredHeight / 2f - paddingBottom
return CircularRevealWidget.RevealInfo(cX, cY, if (expanded) hypot(cX, cY) else 0f)
}
}

View file

@ -0,0 +1,46 @@
package com.topjohnwu.magisk.utils
import android.content.res.Resources
abstract class TextHolder {
open val isEmpty: Boolean get() = false
abstract fun getText(resources: Resources): CharSequence
// ---
class String(
private val value: CharSequence
) : TextHolder() {
override val isEmpty get() = value.isEmpty()
override fun getText(resources: Resources) = value
}
open class Resource(
protected val value: Int
) : TextHolder() {
override val isEmpty get() = value == 0
override fun getText(resources: Resources) = resources.getString(value)
}
class ResourceArgs(
value: Int,
private vararg val params: Any
) : Resource(value) {
override fun getText(resources: Resources): kotlin.String {
// Replace TextHolder with strings
val args = params.map { if (it is TextHolder) it.getText(resources) else it }
return resources.getString(value, *args.toTypedArray())
}
}
// ---
companion object {
val EMPTY = String("")
}
}
fun Int.asText(): TextHolder = TextHolder.Resource(this)
fun Int.asText(vararg params: Any): TextHolder = TextHolder.ResourceArgs(this, *params)
fun CharSequence.asText(): TextHolder = TextHolder.String(this)

View file

@ -0,0 +1,232 @@
package com.topjohnwu.magisk.view
import android.app.Activity
import android.content.DialogInterface
import android.content.res.ColorStateList
import android.graphics.drawable.Drawable
import android.graphics.drawable.InsetDrawable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatDialog
import androidx.appcompat.content.res.AppCompatResources
import androidx.databinding.Bindable
import androidx.databinding.PropertyChangeRegistry
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.color.MaterialColors
import com.google.android.material.shape.MaterialShapeDrawable
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.databinding.DialogMagiskBaseBinding
import com.topjohnwu.magisk.databinding.DiffItem
import com.topjohnwu.magisk.databinding.ItemWrapper
import com.topjohnwu.magisk.databinding.ObservableHost
import com.topjohnwu.magisk.databinding.RvItem
import com.topjohnwu.magisk.databinding.bindExtra
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.databinding.setAdapter
import com.topjohnwu.magisk.view.MagiskDialog.DialogClickListener
typealias DialogButtonClickListener = (DialogInterface) -> Unit
class MagiskDialog(
context: Activity, theme: Int = 0
) : AppCompatDialog(context, theme) {
private val binding: DialogMagiskBaseBinding =
DialogMagiskBaseBinding.inflate(LayoutInflater.from(context))
private val data = Data()
val activity: UIActivity<*> get() = ownerActivity as UIActivity<*>
init {
binding.setVariable(BR.data, data)
setCancelable(true)
setOwnerActivity(context)
}
inner class Data : ObservableHost {
override var callbacks: PropertyChangeRegistry? = null
@get:Bindable
var icon: Drawable? = null
set(value) = set(value, field, { field = it }, BR.icon)
@get:Bindable
var title: CharSequence = ""
set(value) = set(value, field, { field = it }, BR.title)
@get:Bindable
var message: CharSequence = ""
set(value) = set(value, field, { field = it }, BR.message)
val buttonPositive = ButtonViewModel()
val buttonNeutral = ButtonViewModel()
val buttonNegative = ButtonViewModel()
}
enum class ButtonType {
POSITIVE, NEUTRAL, NEGATIVE
}
interface Button {
var icon: Int
var text: Any
var isEnabled: Boolean
var doNotDismiss: Boolean
fun onClick(listener: DialogButtonClickListener)
}
inner class ButtonViewModel : Button, ObservableHost {
override var callbacks: PropertyChangeRegistry? = null
@get:Bindable
override var icon = 0
set(value) = set(value, field, { field = it }, BR.icon, BR.gone)
@get:Bindable
var message: String = ""
set(value) = set(value, field, { field = it }, BR.message, BR.gone)
override var text: Any
get() = message
set(value) {
message = when (value) {
is Int -> context.getText(value)
else -> value
}.toString()
}
@get:Bindable
val gone get() = icon == 0 && message.isEmpty()
@get:Bindable
override var isEnabled = true
set(value) = set(value, field, { field = it }, BR.enabled)
override var doNotDismiss = false
private var onClickAction: DialogButtonClickListener = {}
override fun onClick(listener: DialogButtonClickListener) {
onClickAction = listener
}
fun clicked() {
onClickAction(this@MagiskDialog)
if (!doNotDismiss) {
dismiss()
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
super.setContentView(binding.root)
val default = MaterialColors.getColor(context, com.google.android.material.R.attr.colorSurface, javaClass.canonicalName)
val surfaceColor = MaterialColors.getColor(context, R.attr.colorSurfaceSurfaceVariant, default)
val materialShapeDrawable = MaterialShapeDrawable(context, null, androidx.appcompat.R.attr.alertDialogStyle, com.google.android.material.R.style.MaterialAlertDialog_MaterialComponents)
materialShapeDrawable.initializeElevationOverlay(context)
materialShapeDrawable.fillColor = ColorStateList.valueOf(surfaceColor)
materialShapeDrawable.elevation = context.resources.getDimension(R.dimen.margin_generic)
materialShapeDrawable.setCornerSize(context.resources.getDimension(R.dimen.l_50))
val inset = context.resources.getDimensionPixelSize(com.google.android.material.R.dimen.appcompat_dialog_background_inset)
window?.apply {
setBackgroundDrawable(InsetDrawable(materialShapeDrawable, inset, inset, inset, inset))
setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
}
}
override fun setTitle(@StringRes titleId: Int) { data.title = context.getString(titleId) }
override fun setTitle(title: CharSequence?) { data.title = title ?: "" }
fun setMessage(@StringRes msgId: Int, vararg args: Any) {
data.message = context.getString(msgId, *args)
}
fun setMessage(message: CharSequence) { data.message = message }
fun setIcon(@DrawableRes drawableRes: Int) {
data.icon = AppCompatResources.getDrawable(context, drawableRes)
}
fun setIcon(drawable: Drawable) { data.icon = drawable }
fun setButton(buttonType: ButtonType, builder: Button.() -> Unit) {
val button = when (buttonType) {
ButtonType.POSITIVE -> data.buttonPositive
ButtonType.NEUTRAL -> data.buttonNeutral
ButtonType.NEGATIVE -> data.buttonNegative
}
button.apply(builder)
}
class DialogItem(
override val item: CharSequence,
val position: Int
) : RvItem(), DiffItem<DialogItem>, ItemWrapper<CharSequence> {
override val layoutRes = R.layout.item_list_single_line
}
fun interface DialogClickListener {
fun onClick(position: Int)
}
fun setListItems(
list: Array<out CharSequence>,
listener: DialogClickListener
) = setView(
RecyclerView(context).also {
it.isNestedScrollingEnabled = false
it.layoutManager = LinearLayoutManager(context)
val items = list.mapIndexed { i, cs -> DialogItem(cs, i) }
val extraBindings = bindExtra { sa ->
sa.put(BR.listener, DialogClickListener { pos ->
listener.onClick(pos)
dismiss()
})
}
it.setAdapter(items, extraBindings)
}
)
fun setView(view: View) {
binding.dialogBaseContainer.removeAllViews()
binding.dialogBaseContainer.addView(
view,
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
}
fun resetButtons() {
ButtonType.values().forEach {
setButton(it) {
text = ""
icon = 0
isEnabled = true
doNotDismiss = false
onClick {}
}
}
}
// Prevent calling setContentView
@Deprecated("Please use setView(view)", level = DeprecationLevel.ERROR)
override fun setContentView(layoutResID: Int) {}
@Deprecated("Please use setView(view)", level = DeprecationLevel.ERROR)
override fun setContentView(view: View) {}
@Deprecated("Please use setView(view)", level = DeprecationLevel.ERROR)
override fun setContentView(view: View, params: ViewGroup.LayoutParams?) {}
}

View file

@ -0,0 +1,30 @@
package com.topjohnwu.magisk.view
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.DiffItem
import com.topjohnwu.magisk.databinding.RvItem
import com.topjohnwu.magisk.core.R as CoreR
sealed class TappableHeadlineItem : RvItem(), DiffItem<TappableHeadlineItem> {
abstract val title: Int
abstract val icon: Int
override val layoutRes = R.layout.item_tappable_headline
// --- listener
interface Listener {
fun onItemPressed(item: TappableHeadlineItem)
}
// --- objects
object ThemeMode : TappableHeadlineItem() {
override val title = CoreR.string.settings_dark_mode_title
override val icon = R.drawable.ic_day_night
}
}

View file

@ -0,0 +1,10 @@
package com.topjohnwu.magisk.view
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.DiffItem
import com.topjohnwu.magisk.databinding.ItemWrapper
import com.topjohnwu.magisk.databinding.RvItem
class TextItem(override val item: Int) : RvItem(), DiffItem<TextItem>, ItemWrapper<Int> {
override val layoutRes = R.layout.item_text
}

View file

@ -0,0 +1,135 @@
package com.topjohnwu.magisk.widget;
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.animation.StateListAnimator;
import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.customview.view.AbsSavedState;
import androidx.interpolator.view.animation.FastOutLinearInInterpolator;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.topjohnwu.magisk.R;
public class ConcealableBottomNavigationView extends BottomNavigationView {
private static final int[] STATE_SET = {
R.attr.state_hidden
};
private boolean isHidden;
public ConcealableBottomNavigationView(@NonNull Context context) {
this(context, null);
}
public ConcealableBottomNavigationView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, com.google.android.material.R.attr.bottomNavigationStyle);
}
public ConcealableBottomNavigationView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, com.google.android.material.R.style.Widget_Design_BottomNavigationView);
}
public ConcealableBottomNavigationView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
private void recreateAnimator(int height) {
Animator toHidden = ObjectAnimator.ofFloat(this, "translationY", height);
toHidden.setDuration(175);
toHidden.setInterpolator(new FastOutLinearInInterpolator());
Animator toUnhidden = ObjectAnimator.ofFloat(this, "translationY", 0);
toUnhidden.setDuration(225);
toUnhidden.setInterpolator(new FastOutLinearInInterpolator());
StateListAnimator animator = new StateListAnimator();
animator.addState(STATE_SET, toHidden);
animator.addState(new int[]{}, toUnhidden);
setStateListAnimator(animator);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
recreateAnimator(getMeasuredHeight());
}
@Override
protected int[] onCreateDrawableState(int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (isHidden()) {
mergeDrawableStates(drawableState, STATE_SET);
}
return drawableState;
}
public boolean isHidden() {
return isHidden;
}
public void setHidden(boolean raised) {
if (isHidden != raised) {
isHidden = raised;
refreshDrawableState();
}
}
@NonNull
@Override
protected Parcelable onSaveInstanceState() {
SavedState state = new SavedState(super.onSaveInstanceState());
state.isHidden = isHidden();
return state;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
final SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
if (ss.isHidden) {
setHidden(isHidden);
}
}
static class SavedState extends AbsSavedState {
public boolean isHidden;
public SavedState(Parcel source) {
super(source, ConcealableBottomNavigationView.class.getClassLoader());
isHidden = source.readByte() != 0;
}
public SavedState(Parcelable superState) {
super(superState);
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeByte(isHidden ? (byte) 1 : (byte) 0);
}
public static final Creator<SavedState> CREATOR = new Creator<>() {
@Override
public SavedState createFromParcel(Parcel source) {
return new SavedState(source);
}
@Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
}

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="@android:integer/config_shortAnimTime"
android:fromAlpha="0"
android:toAlpha="1" />
<scale
android:duration="@android:integer/config_shortAnimTime"
android:fromXScale="0.9"
android:fromYScale="0.9"
android:pivotX="50%p"
android:pivotY="50%p"
android:toXScale="1"
android:toYScale="1" />
</set>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="@android:integer/config_shortAnimTime"
android:fromAlpha="0"
android:toAlpha="1" />
<scale
android:duration="@android:integer/config_shortAnimTime"
android:fromXScale="1.1"
android:fromYScale="1.1"
android:pivotX="50%p"
android:pivotY="50%p"
android:toXScale="1"
android:toYScale="1" />
</set>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="@android:integer/config_shortAnimTime"
android:fromAlpha="1"
android:toAlpha="0" />
<scale
android:duration="@android:integer/config_shortAnimTime"
android:fromXScale="1"
android:fromYScale="1"
android:pivotX="50%p"
android:pivotY="50%p"
android:toXScale="1.1"
android:toYScale="1.1" />
</set>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="@android:integer/config_shortAnimTime"
android:fromAlpha="1"
android:toAlpha="0" />
<scale
android:duration="@android:integer/config_shortAnimTime"
android:fromXScale="1"
android:fromYScale="1"
android:pivotX="50%p"
android:pivotY="50%p"
android:toXScale="0.9"
android:toYScale="0.9" />
</set>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorSurfaceVariant" android:state_enabled="true" />
<item android:alpha="0.68" android:color="?colorSurfaceVariant" />
</selector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorDisabled" android:state_enabled="false" />
<item android:color="?colorError" />
</selector>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorDisabledVariant" android:state_enabled="false" />
<item android:color="?colorSecondary" android:state_checked="true" />
<item android:color="?colorOnSurfaceVariant" />
</selector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorDisabled" android:state_enabled="false" />
<item android:color="?colorOnPrimary" />
</selector>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorError" android:state_selected="true" />
<item android:color="?colorDisabled" android:state_enabled="false" />
<item android:color="?colorPrimary" />
</selector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorDisabled" android:state_enabled="false" />
<item android:color="?colorPrimary" />
</selector>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorPrimary" android:state_selected="true" />
<item android:color="?colorPrimary" android:state_checked="true" />
<item android:color="?colorDisabled" android:state_enabled="false" />
<item android:color="?colorOnSurfaceVariant" />
</selector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorDisabled" android:state_enabled="false" />
<item android:color="?colorOnSurface" />
</selector>

View file

@ -0,0 +1,28 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path_1"
android:fillColor="#000000"
android:fillType="evenOdd"
android:pathData="M 20 8 L 17.19 8 C 16.74 7.22 16.12 6.55 15.37 6.04 L 17 4.41 L 15.59 3 L 13.42 5.17 C 12.96 5.06 12.49 5 12 5 C 11.51 5 11.04 5.06 10.59 5.17 L 8.41 3 L 7 4.41 L 8.62 6.04 C 7.88 6.55 7.26 7.22 6.81 8 L 4 8 L 4 10 L 6.09 10 C 6.04 10.33 6 10.66 6 11 L 6 12 L 4 12 L 4 14 L 6 14 L 6 15 C 6 15.34 6.04 15.67 6.09 16 L 4 16 L 4 18 L 6.81 18 C 7.85 19.79 9.78 21 12 21 C 14.22 21 16.15 19.79 17.19 18 L 20 18 L 20 16 L 17.91 16 C 17.96 15.67 18 15.34 18 15 L 18 14 L 20 14 L 20 12 L 18 12 L 18 11 C 18 10.66 17.96 10.33 17.91 10 L 20 10 L 20 8 Z M 14 16 L 10 16 L 10 14 L 14 14 L 14 16 Z M 14 12 L 10 12 L 10 10 L 14 10 L 14 12 Z" />
</vector>
</aapt:attr>
<target android:name="path_1">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="300"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="M 20 8 L 20 8 L 17.19 8 C 16.74 7.22 16.12 6.55 15.37 6.04 L 17 4.41 L 15.59 3 L 13.42 5.17 C 12.96 5.06 12.49 5 12 5 C 11.51 5 11.04 5.06 10.59 5.17 L 8.41 3 L 7 4.41 L 8.62 6.04 C 7.88 6.55 7.26 7.22 6.81 8 L 4 8 L 4 10 L 6.09 10 C 6.04 10.33 6 10.66 6 11 L 6 12 L 4 12 L 4 14 L 6 14 L 6 15 C 6 15.34 6.04 15.67 6.09 16 L 4 16 L 4 18 L 6.81 18 C 7.85 19.79 9.78 21 12 21 C 14.22 21 16.15 19.79 17.19 18 L 20 18 L 20 16 L 17.91 16 C 17.96 15.67 18 15.34 18 15 L 18 14 L 20 14 L 20 12 L 18 12 L 18 11 C 18 10.66 17.96 10.33 17.91 10 L 20 10 L 20 8 M 14 16 C 14 15.43 14 14.859 14 14.289 L 14 14 C 13.869 14 13.739 14 13.608 14 C 12.405 14 11.203 14 10 14 C 10 14.509 10 15.017 10 15.526 C 10 15.684 10 15.842 10 16 L 10.33 16 C 10.392 16 10.454 16 10.515 16 C 11.677 16 12.838 16 14 16 C 14 16 14 16 14 16 M 14 10 L 14 12 L 14 12 L 10 12 L 10 10 L 14 10 M 12 15 L 12 15 L 12 15 L 12 15 L 12 15 L 12 15"
android:valueTo="M 20 8 L 18.595 8 L 17.19 8 C 16.74 7.2 16.12 6.5 15.37 6 L 17 4.41 L 15.59 3 L 13.42 5.17 C 12.96 5.06 12.5 5 12 5 C 11.5 5 11.05 5.06 10.59 5.17 L 8.41 3 L 7 4.41 L 8.62 6 C 7.87 6.5 7.26 7.21 6.81 8 L 4 8 L 4 10 L 6.09 10 C 6.03 10.33 6 10.66 6 11 L 6 12 L 4 12 L 4 14 L 6 14 L 6 15 C 6 15.34 6.03 15.67 6.09 16 L 4 16 L 4 18 L 6.81 18 C 8.47 20.87 12.14 21.84 15 20.18 C 15.91 19.66 16.67 18.9 17.19 18 L 20 18 L 20 16 L 17.91 16 C 17.97 15.67 18 15.34 18 15 L 18 14 L 20 14 L 20 12 L 18 12 L 18 11 C 18 10.66 17.97 10.33 17.91 10 L 20 10 L 20 8 M 14.828 17.828 C 15.578 17.079 16 16.06 16 15 L 16 11 C 16 9.94 15.578 8.921 14.828 8.172 C 14.079 7.422 13.06 7 12 7 C 10.94 7 9.921 7.422 9.172 8.172 C 8.422 8.921 8 9.94 8 11 L 8 15 C 8 16.06 8.422 17.079 9.172 17.828 C 9.921 18.578 10.94 19 12 19 C 13.06 19 14.079 18.578 14.828 17.828 M 14 10 L 14 11 L 14 12 L 10 12 L 10 10 L 14 10 M 10 14 L 14 14 L 14 16 L 10 16 L 10 14 L 10 14"
android:valueType="pathType" />
</aapt:attr>
</target>
</animated-vector>

View file

@ -0,0 +1,28 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path_1"
android:fillColor="#000000"
android:fillType="evenOdd"
android:pathData="M 20 8 L 18.595 8 L 17.19 8 C 16.74 7.2 16.12 6.5 15.37 6 L 17 4.41 L 15.59 3 L 13.42 5.17 C 12.96 5.06 12.5 5 12 5 C 11.5 5 11.05 5.06 10.59 5.17 L 8.41 3 L 7 4.41 L 8.62 6 C 7.87 6.5 7.26 7.21 6.81 8 L 4 8 L 4 10 L 6.09 10 C 6.03 10.33 6 10.66 6 11 L 6 12 L 4 12 L 4 14 L 6 14 L 6 15 C 6 15.34 6.03 15.67 6.09 16 L 4 16 L 4 18 L 6.81 18 C 8.47 20.87 12.14 21.84 15 20.18 C 15.91 19.66 16.67 18.9 17.19 18 L 20 18 L 20 16 L 17.91 16 C 17.97 15.67 18 15.34 18 15 L 18 14 L 20 14 L 20 12 L 18 12 L 18 11 C 18 10.66 17.97 10.33 17.91 10 L 20 10 L 20 8 M 14.828 17.828 C 15.578 17.079 16 16.06 16 15 L 16 11 C 16 9.94 15.578 8.921 14.828 8.172 C 14.079 7.422 13.06 7 12 7 C 10.94 7 9.921 7.422 9.172 8.172 C 8.422 8.921 8 9.94 8 11 L 8 15 C 8 16.06 8.422 17.079 9.172 17.828 C 9.921 18.578 10.94 19 12 19 C 13.06 19 14.079 18.578 14.828 17.828 M 14 10 L 14 11 L 14 12 L 10 12 L 10 10 L 14 10 M 10 14 L 14 14 L 14 16 L 10 16 L 10 14 L 10 14" />
</vector>
</aapt:attr>
<target android:name="path_1">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="300"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="M 20 8 L 18.595 8 L 17.19 8 C 16.74 7.2 16.12 6.5 15.37 6 L 17 4.41 L 15.59 3 L 13.42 5.17 C 12.96 5.06 12.5 5 12 5 C 11.5 5 11.05 5.06 10.59 5.17 L 8.41 3 L 7 4.41 L 8.62 6 C 7.87 6.5 7.26 7.21 6.81 8 L 4 8 L 4 10 L 6.09 10 C 6.03 10.33 6 10.66 6 11 L 6 12 L 4 12 L 4 14 L 6 14 L 6 15 C 6 15.34 6.03 15.67 6.09 16 L 4 16 L 4 18 L 6.81 18 C 8.47 20.87 12.14 21.84 15 20.18 C 15.91 19.66 16.67 18.9 17.19 18 L 20 18 L 20 16 L 17.91 16 C 17.97 15.67 18 15.34 18 15 L 18 14 L 20 14 L 20 12 L 18 12 L 18 11 C 18 10.66 17.97 10.33 17.91 10 L 20 10 L 20 8 M 14.828 17.828 C 15.578 17.079 16 16.06 16 15 L 16 11 C 16 9.94 15.578 8.921 14.828 8.172 C 14.079 7.422 13.06 7 12 7 C 10.94 7 9.921 7.422 9.172 8.172 C 8.422 8.921 8 9.94 8 11 L 8 15 C 8 16.06 8.422 17.079 9.172 17.828 C 9.921 18.578 10.94 19 12 19 C 13.06 19 14.079 18.578 14.828 17.828 M 14 10 L 14 11 L 14 12 L 10 12 L 10 10 L 14 10 M 10 14 L 14 14 L 14 16 L 10 16 L 10 14 L 10 14"
android:valueTo="M 20 8 L 20 8 L 17.19 8 C 16.74 7.22 16.12 6.55 15.37 6.04 L 17 4.41 L 15.59 3 L 13.42 5.17 C 12.96 5.06 12.49 5 12 5 C 11.51 5 11.04 5.06 10.59 5.17 L 8.41 3 L 7 4.41 L 8.62 6.04 C 7.88 6.55 7.26 7.22 6.81 8 L 4 8 L 4 10 L 6.09 10 C 6.04 10.33 6 10.66 6 11 L 6 12 L 4 12 L 4 14 L 6 14 L 6 15 C 6 15.34 6.04 15.67 6.09 16 L 4 16 L 4 18 L 6.81 18 C 7.85 19.79 9.78 21 12 21 C 14.22 21 16.15 19.79 17.19 18 L 20 18 L 20 16 L 17.91 16 C 17.96 15.67 18 15.34 18 15 L 18 14 L 20 14 L 20 12 L 18 12 L 18 11 C 18 10.66 17.96 10.33 17.91 10 L 20 10 L 20 8 M 14 16 C 14 15.43 14 14.859 14 14.289 L 14 14 C 13.869 14 13.739 14 13.608 14 C 12.405 14 11.203 14 10 14 C 10 14.509 10 15.017 10 15.526 C 10 15.684 10 15.842 10 16 L 10.33 16 C 10.392 16 10.454 16 10.515 16 C 11.677 16 12.838 16 14 16 C 14 16 14 16 14 16 M 14 10 L 14 12 L 14 12 L 10 12 L 10 10 L 14 10 M 12 15 L 12 15 L 12 15 L 12 15 L 12 15 L 12 15"
android:valueType="pathType" />
</aapt:attr>
</target>
</animated-vector>

View file

@ -0,0 +1,27 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path"
android:fillColor="#000000"
android:pathData="M 12 2 C 6.5 2 2 6.5 2 12 C 2 17.5 6.5 22 12 22 C 17.5 22 22 17.5 22 12 C 22 6.5 17.5 2 12 2 M 12 20 C 7.59 20 4 16.41 4 12 C 4 7.59 7.59 4 12 4 C 16.41 4 20 7.59 20 12 C 20 16.41 16.41 20 12 20 M 16.59 7.58 L 10 14.17 L 7.41 11.59 L 6 13 L 10 17 L 18 9 L 16.59 7.58 Z" />
</vector>
</aapt:attr>
<target android:name="path">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="500"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="M 12 2 C 9.217 2 6.689 3.152 4.872 5.004 C 3.098 6.811 2 9.283 2 12 C 2 14.744 3.12 17.24 4.927 19.052 C 6.74 20.87 9.244 22 12 22 C 13.911 22 15.701 21.457 17.224 20.517 C 18.628 19.651 19.804 18.448 20.638 17.024 C 21.503 15.545 22 13.828 22 12 C 22 10.2 21.518 8.507 20.677 7.044 C 19.755 5.441 18.402 4.114 16.779 3.224 C 15.357 2.444 13.728 2 12 2 M 12 20 C 7.59 20 4 16.41 4 12 C 4 7.59 7.59 4 12 4 C 16.41 4 20 7.59 20 12 C 20 16.41 16.41 20 12 20 M 6 13 L 10 17 L 18 9 L 16.59 7.58 L 16.59 7.58 L 10 14.17 L 7.41 11.59 L 6 13"
android:valueTo="M 12 2 C 9.349 2 6.804 3.054 4.929 4.929 C 3.054 6.804 2 9.349 2 12 C 2 14.651 3.054 17.196 4.929 19.071 C 6.804 20.946 9.349 22 12 22 C 13.755 22 15.48 21.538 17 20.66 C 18.52 19.783 19.783 18.52 20.66 17 C 21.538 15.48 22 13.755 22 12 C 22 10.245 21.538 8.52 20.66 7 C 19.783 5.48 18.52 4.217 17 3.34 C 15.48 2.462 13.755 2 12 2 M 12 20 C 7.59 20 4 16.41 4 12 C 4 7.59 7.59 4 12 4 C 16.41 4 20 7.59 20 12 C 20 16.41 16.41 20 12 20 M 7 13 L 7 13 L 17 13 L 17 11 L 17 11 L 7 11 L 7 11 L 7 11"
android:valueType="pathType" />
</aapt:attr>
</target>
</animated-vector>

View file

@ -0,0 +1,27 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path_1"
android:fillColor="#000000"
android:pathData="M 12 20 C 7.59 20 4 16.41 4 12 C 4 7.59 7.59 4 12 4 C 16.41 4 20 7.59 20 12 C 20 16.41 16.41 20 12 20 M 12 2 C 9.349 2 6.804 3.054 4.929 4.929 C 3.054 6.804 2 9.349 2 12 C 2 14.651 3.054 17.196 4.929 19.071 C 6.804 20.946 9.349 22 12 22 C 13.755 22 15.48 21.538 17 20.66 C 18.52 19.783 19.783 18.52 20.66 17 C 21.538 15.48 22 13.755 22 12 C 22 10.245 21.538 8.52 20.66 7 C 19.783 5.48 18.52 4.217 17 3.34 C 15.48 2.462 13.755 2 12 2 M 7 13 L 17 13 L 17 11 L 7 11" />
</vector>
</aapt:attr>
<target android:name="path_1">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="500"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="M 12 2 C 9.349 2 6.804 3.054 4.929 4.929 C 3.054 6.804 2 9.349 2 12 C 2 14.651 3.054 17.196 4.929 19.071 C 6.804 20.946 9.349 22 12 22 C 13.755 22 15.48 21.538 17 20.66 C 18.52 19.783 19.783 18.52 20.66 17 C 21.538 15.48 22 13.755 22 12 C 22 10.245 21.538 8.52 20.66 7 C 19.783 5.48 18.52 4.217 17 3.34 C 15.48 2.462 13.755 2 12 2 M 12 20 C 7.59 20 4 16.41 4 12 C 4 7.59 7.59 4 12 4 C 16.41 4 20 7.59 20 12 C 20 16.41 16.41 20 12 20 M 7 13 L 7 13 L 17 13 L 17 11 L 17 11 L 7 11 L 7 11 L 7 11"
android:valueTo="M 12 2 C 9.217 2 6.689 3.152 4.872 5.004 C 3.098 6.811 2 9.283 2 12 C 2 14.856 3.213 17.442 5.149 19.268 C 6.942 20.96 9.356 22 12 22 C 14.061 22 15.982 21.368 17.578 20.288 C 19.114 19.249 20.349 17.796 21.119 16.092 C 21.685 14.841 22 13.456 22 12 C 22 10.122 21.475 8.361 20.566 6.856 C 19.691 5.408 18.46 4.197 16.997 3.347 C 15.524 2.491 13.817 2 12 2 M 12 20 C 7.59 20 4 16.41 4 12 C 4 7.59 7.59 4 12 4 C 16.41 4 20 7.59 20 12 C 20 16.41 16.41 20 12 20 M 6 13 L 10 17 L 18 9 L 16.59 7.58 L 16.59 7.58 L 10 14.17 L 7.41 11.59 L 6 13"
android:valueType="pathType" />
</aapt:attr>
</target>
</animated-vector>

View file

@ -0,0 +1,29 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path_3"
android:fillColor="#000"
android:fillType="evenOdd"
android:pathData="M 12 3 L 20 9 L 20 21 L 15 21 L 15 14 L 9 14 L 9 21 L 4 21 L 4 9 L 12 3 Z"
android:strokeWidth="1" />
</vector>
</aapt:attr>
<target android:name="path_3">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="300"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="M 9 14 L 9 21 L 4 21 L 4 9 L 12 3 L 12 3 L 20 9 L 20 21 L 15 21 L 15 14 L 9 14 M 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4"
android:valueTo="M 9 13 L 9 19 L 6 19 L 6 10 L 12 5.5 L 15 7.75 L 18 10 L 18 19 L 15 19 L 15 13 L 9 13 M 4 21 L 4 9 L 12 3 L 20 9 L 20 21 L 4 21 L 4 21"
android:valueType="pathType" />
</aapt:attr>
</target>
</animated-vector>

View file

@ -0,0 +1,29 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path_3"
android:fillColor="#000"
android:fillType="evenOdd"
android:pathData="M 9 13 L 9 19 L 6 19 L 6 10 L 12 5.5 L 15 7.75 L 18 10 L 18 19 L 15 19 L 15 13 L 9 13 M 4 21 L 4 9 L 12 3 L 20 9 L 20 21 L 4 21 L 4 21"
android:strokeWidth="1" />
</vector>
</aapt:attr>
<target android:name="path_3">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="300"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="M 9 13 L 9 19 L 6 19 L 6 10 L 12 5.5 L 15 7.75 L 18 10 L 18 19 L 15 19 L 15 13 L 9 13 M 4 21 L 4 9 L 12 3 L 20 9 L 20 21 L 4 21 L 4 21"
android:valueTo="M 9 14 L 9 21 L 4 21 L 4 9 L 12 3 L 12 3 L 20 9 L 20 21 L 15 21 L 15 14 L 9 14 M 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4"
android:valueType="pathType" />
</aapt:attr>
</target>
</animated-vector>

View file

@ -0,0 +1,28 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="outlined"
android:fillColor="#000000"
android:fillType="evenOdd"
android:pathData="M 23 13.5 C 23 14.163 22.736 14.799 22.268 15.268 C 21.799 15.736 21.163 16 20.5 16 C 20 16 19.5 16 19 16 L 19 20 C 19 20.53 18.789 21.039 18.414 21.414 C 18.039 21.789 17.53 22 17 22 L 15.1 22 L 13.2 22 C 13.2 21.5 13.2 21 13.2 20.5 C 13.2 19 12 17.8 10.5 17.8 C 9 17.8 7.8 19 7.8 20.5 L 7.8 22 L 4 22 C 3.47 22 2.961 21.789 2.586 21.414 C 2.211 21.039 2 20.53 2 20 L 2 16.2 L 3.5 16.2 C 5 16.2 6.2 15 6.2 13.5 C 6.2 12 5 10.8 3.5 10.8 L 2 10.8 L 2 7 C 2 6.47 2.211 5.961 2.586 5.586 C 2.961 5.211 3.47 5 4 5 L 8 5 C 8 4.5 8 4 8 3.5 C 8 2.837 8.264 2.201 8.732 1.732 C 9.201 1.264 9.837 1 10.5 1 C 11.163 1 11.799 1.264 12.268 1.732 C 12.736 2.201 13 2.837 13 3.5 C 13 4 13 4.5 13 5 L 17 5 C 17.55 5 18.05 5.223 18.413 5.584 C 18.775 5.945 19 6.445 19 7 L 19 11 C 19.5 11 20 11 20.5 11 C 20.5 11 20.5 11 20.5 11 C 21.163 11 21.799 11.264 22.268 11.732 C 22.736 12.201 23 12.837 23 13.5 M 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12" />
</vector>
</aapt:attr>
<target android:name="outlined">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="300"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="M 23 13.5 C 23 14.163 22.736 14.799 22.268 15.268 C 21.799 15.736 21.163 16 20.5 16 C 20 16 19.5 16 19 16 L 19 20 C 19 20.53 18.789 21.039 18.414 21.414 C 18.039 21.789 17.53 22 17 22 L 15.1 22 L 13.2 22 C 13.2 21.5 13.2 21 13.2 20.5 C 13.2 19 12 17.8 10.5 17.8 C 9 17.8 7.8 19 7.8 20.5 L 7.8 22 L 4 22 C 3.47 22 2.961 21.789 2.586 21.414 C 2.211 21.039 2 20.53 2 20 L 2 16.2 L 3.5 16.2 C 5 16.2 6.2 15 6.2 13.5 C 6.2 12 5 10.8 3.5 10.8 L 2 10.8 L 2 7 C 2 6.47 2.211 5.961 2.586 5.586 C 2.961 5.211 3.47 5 4 5 L 8 5 C 8 4.5 8 4 8 3.5 C 8 2.837 8.264 2.201 8.732 1.732 C 9.201 1.264 9.837 1 10.5 1 C 11.163 1 11.799 1.264 12.268 1.732 C 12.736 2.201 13 2.837 13 3.5 C 13 4 13 4.5 13 5 L 17 5 C 17.55 5 18.05 5.223 18.413 5.584 C 18.775 5.945 19 6.445 19 7 L 19 11 C 19.5 11 20 11 20.5 11 C 20.5 11 20.5 11 20.5 11 C 21.163 11 21.799 11.264 22.268 11.732 C 22.736 12.201 23 12.837 23 13.5 M 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12"
android:valueTo="M 22 13.5 C 22 14.087 21.856 14.64 21.6 15.126 C 21.344 15.612 20.978 16.03 20.533 16.347 C 20.089 16.664 19.567 16.88 19 16.96 L 19 20 C 19 20.53 18.789 21.039 18.414 21.414 C 18.039 21.789 17.53 22 17 22 L 13.2 22 L 13.2 21.7 C 13.2 20.984 12.915 20.297 12.409 19.791 C 11.903 19.285 11.216 19 10.5 19 C 9 19 7.8 20.21 7.8 21.7 L 7.8 22 L 4 22 C 3.47 22 2.961 21.789 2.586 21.414 C 2.211 21.039 2 20.53 2 20 L 2 16.2 L 2.3 16.2 C 3.79 16.2 5 15 5 13.5 C 5 12 3.79 10.8 2.3 10.8 L 2 10.8 L 2 7 C 2 6.47 2.211 5.961 2.586 5.586 C 2.961 5.211 3.47 5 4 5 L 7.04 5 C 7.12 4.433 7.336 3.911 7.653 3.467 C 7.97 3.022 8.388 2.656 8.874 2.4 C 9.36 2.144 9.913 2 10.5 2 C 11.087 2 11.64 2.144 12.126 2.4 C 12.612 2.656 13.03 3.022 13.347 3.467 C 13.664 3.911 13.88 4.433 13.96 5 L 17 5 C 17.53 5 18.039 5.211 18.414 5.586 C 18.789 5.961 19 6.47 19 7 L 19 10.04 C 19.425 10.1 19.825 10.236 20.186 10.434 C 20.547 10.633 20.869 10.893 21.137 11.2 C 21.406 11.508 21.622 11.863 21.77 12.251 C 21.919 12.639 22 13.06 22 13.5 M 17 12 L 18.5 12 C 18.898 12 19.279 12.158 19.561 12.439 C 19.842 12.721 20 13.102 20 13.5 C 20 13.898 19.842 14.279 19.561 14.561 C 19.279 14.842 18.898 15 18.5 15 L 17 15 L 17 15 L 17 20 L 14.88 20 C 14.2 18.25 12.5 17 10.5 17 C 8.5 17 6.8 18.25 6.12 20 L 4 20 L 4 17.88 C 5.75 17.2 7 15.5 7 13.5 C 7 11.5 5.76 9.8 4 9.12 L 4 7 L 9 7 L 9 5.5 C 9 5.102 9.158 4.721 9.439 4.439 C 9.721 4.158 10.102 4 10.5 4 C 10.898 4 11.279 4.158 11.561 4.439 C 11.842 4.721 12 5.102 12 5.5 L 12 7 L 17 7 L 17 12"
android:valueType="pathType" />
</aapt:attr>
</target>
</animated-vector>

View file

@ -0,0 +1,28 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="outlined"
android:fillColor="#000000"
android:fillType="evenOdd"
android:pathData="M 22 13.5 C 22 15.26 20.7 16.72 19 16.96 L 19 20 C 19 20.53 18.789 21.039 18.414 21.414 C 18.039 21.789 17.53 22 17 22 L 13.2 22 L 13.2 21.7 C 13.2 20.984 12.915 20.297 12.409 19.791 C 11.903 19.285 11.216 19 10.5 19 C 9 19 7.8 20.21 7.8 21.7 L 7.8 22 L 4 22 C 3.47 22 2.961 21.789 2.586 21.414 C 2.211 21.039 2 20.53 2 20 L 2 16.2 L 2.3 16.2 C 3.79 16.2 5 15 5 13.5 C 5 12 3.79 10.8 2.3 10.8 L 2 10.8 L 2 7 C 2 6.47 2.211 5.961 2.586 5.586 C 2.961 5.211 3.47 5 4 5 L 7.04 5 C 7.28 3.3 8.74 2 10.5 2 C 12.26 2 13.72 3.3 13.96 5 L 17 5 C 17.53 5 18.039 5.211 18.414 5.586 C 18.789 5.961 19 6.47 19 7 L 19 10.04 C 20.7 10.28 22 11.74 22 13.5 M 17 15 L 18.5 15 C 18.898 15 19.279 14.842 19.561 14.561 C 19.842 14.279 20 13.898 20 13.5 C 20 13.102 19.842 12.721 19.561 12.439 C 19.279 12.158 18.898 12 18.5 12 L 17 12 L 17 7 L 12 7 L 12 5.5 C 12 5.102 11.842 4.721 11.561 4.439 C 11.279 4.158 10.898 4 10.5 4 C 10.102 4 9.721 4.158 9.439 4.439 C 9.158 4.721 9 5.102 9 5.5 L 9 7 L 4 7 L 4 9.12 C 5.76 9.8 7 11.5 7 13.5 C 7 15.5 5.75 17.2 4 17.88 L 4 20 L 6.12 20 C 6.8 18.25 8.5 17 10.5 17 C 12.5 17 14.2 18.25 14.88 20 L 17 20 L 17 15 Z" />
</vector>
</aapt:attr>
<target android:name="outlined">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="300"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="M 22 13.5 C 22 14.087 21.856 14.64 21.6 15.126 C 21.344 15.612 20.978 16.03 20.533 16.347 C 20.089 16.664 19.567 16.88 19 16.96 L 19 20 C 19 20.53 18.789 21.039 18.414 21.414 C 18.039 21.789 17.53 22 17 22 L 13.2 22 L 13.2 21.7 C 13.2 20.984 12.915 20.297 12.409 19.791 C 11.903 19.285 11.216 19 10.5 19 C 9 19 7.8 20.21 7.8 21.7 L 7.8 22 L 4 22 C 3.47 22 2.961 21.789 2.586 21.414 C 2.211 21.039 2 20.53 2 20 L 2 16.2 L 2.3 16.2 C 3.79 16.2 5 15 5 13.5 C 5 12 3.79 10.8 2.3 10.8 L 2 10.8 L 2 7 C 2 6.47 2.211 5.961 2.586 5.586 C 2.961 5.211 3.47 5 4 5 L 7.04 5 C 7.12 4.433 7.336 3.911 7.653 3.467 C 7.97 3.022 8.388 2.656 8.874 2.4 C 9.36 2.144 9.913 2 10.5 2 C 11.087 2 11.64 2.144 12.126 2.4 C 12.612 2.656 13.03 3.022 13.347 3.467 C 13.664 3.911 13.88 4.433 13.96 5 L 17 5 C 17.53 5 18.039 5.211 18.414 5.586 C 18.789 5.961 19 6.47 19 7 L 19 10.04 C 19.425 10.1 19.825 10.236 20.186 10.434 C 20.547 10.633 20.869 10.893 21.137 11.2 C 21.406 11.508 21.622 11.863 21.77 12.251 C 21.919 12.639 22 13.06 22 13.5 M 17 12 L 18.5 12 C 18.898 12 19.279 12.158 19.561 12.439 C 19.842 12.721 20 13.102 20 13.5 C 20 13.898 19.842 14.279 19.561 14.561 C 19.279 14.842 18.898 15 18.5 15 L 17 15 L 17 15 L 17 20 L 14.88 20 C 14.2 18.25 12.5 17 10.5 17 C 8.5 17 6.8 18.25 6.12 20 L 4 20 L 4 17.88 C 5.75 17.2 7 15.5 7 13.5 C 7 11.5 5.76 9.8 4 9.12 L 4 7 L 9 7 L 9 5.5 C 9 5.102 9.158 4.721 9.439 4.439 C 9.721 4.158 10.102 4 10.5 4 C 10.898 4 11.279 4.158 11.561 4.439 C 11.842 4.721 12 5.102 12 5.5 L 12 7 L 17 7 L 17 12"
android:valueTo="M 23 13.5 C 23 14.163 22.736 14.799 22.268 15.268 C 21.799 15.736 21.163 16 20.5 16 C 20 16 19.5 16 19 16 L 19 20 C 19 20.53 18.789 21.039 18.414 21.414 C 18.039 21.789 17.53 22 17 22 L 15.1 22 L 13.2 22 C 13.2 21.5 13.2 21 13.2 20.5 C 13.2 19 12 17.8 10.5 17.8 C 9 17.8 7.8 19 7.8 20.5 L 7.8 22 L 4 22 C 3.47 22 2.961 21.789 2.586 21.414 C 2.211 21.039 2 20.53 2 20 L 2 16.2 L 3.5 16.2 C 5 16.2 6.2 15 6.2 13.5 C 6.2 12 5 10.8 3.5 10.8 L 2 10.8 L 2 7 C 2 6.47 2.211 5.961 2.586 5.586 C 2.961 5.211 3.47 5 4 5 L 8 5 C 8 4.5 8 4 8 3.5 C 8 2.837 8.264 2.201 8.732 1.732 C 9.201 1.264 9.837 1 10.5 1 C 11.163 1 11.799 1.264 12.268 1.732 C 12.736 2.201 13 2.837 13 3.5 C 13 4 13 4.5 13 5 L 17 5 C 17.55 5 18.05 5.223 18.413 5.584 C 18.775 5.945 19 6.445 19 7 L 19 11 C 19.5 11 20 11 20.5 11 C 20.5 11 20.5 11 20.5 11 C 21.163 11 21.799 11.264 22.268 11.732 C 22.736 12.201 23 12.837 23 13.5 M 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12"
android:valueType="pathType" />
</aapt:attr>
</target>
</animated-vector>

View file

@ -0,0 +1,28 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path"
android:fillColor="#000000"
android:fillType="evenOdd"
android:pathData="M 12 15.5 C 11.072 15.5 10.181 15.131 9.525 14.475 C 8.869 13.819 8.5 12.928 8.5 12 C 8.5 11.072 8.869 10.181 9.525 9.525 C 10.181 8.869 11.072 8.5 12 8.5 C 12.614 8.5 13.218 8.662 13.75 8.969 C 14.282 9.276 14.724 9.718 15.031 10.25 C 15.338 10.782 15.5 11.386 15.5 12 C 15.5 12.614 15.338 13.218 15.031 13.75 C 14.724 14.282 14.282 14.724 13.75 15.031 C 13.218 15.338 12.614 15.5 12 15.5 M 19.43 12.97 C 19.47 12.65 19.5 12.33 19.5 12 C 19.5 11.67 19.47 11.34 19.43 11 L 21.54 9.37 C 21.73 9.22 21.78 8.95 21.66 8.73 L 19.66 5.27 C 19.54 5.05 19.27 4.96 19.05 5.05 L 16.56 6.05 C 16.04 5.66 15.5 5.32 14.87 5.07 L 14.5 2.42 C 14.46 2.18 14.25 2 14 2 L 10 2 C 9.75 2 9.54 2.18 9.5 2.42 L 9.13 5.07 C 8.5 5.32 7.96 5.66 7.44 6.05 L 4.95 5.05 C 4.73 4.96 4.46 5.05 4.34 5.27 L 2.34 8.73 C 2.21 8.95 2.27 9.22 2.46 9.37 L 4.57 11 C 4.53 11.34 4.5 11.67 4.5 12 C 4.5 12.33 4.53 12.65 4.57 12.97 L 2.46 14.63 C 2.27 14.78 2.21 15.05 2.34 15.27 L 4.34 18.73 C 4.46 18.95 4.73 19.03 4.95 18.95 L 7.44 17.94 C 7.96 18.34 8.5 18.68 9.13 18.93 L 9.5 21.58 C 9.54 21.82 9.75 22 10 22 L 14 22 C 14.25 22 14.46 21.82 14.5 21.58 L 14.87 18.93 C 15.5 18.67 16.04 18.34 16.56 17.94 L 19.05 18.95 C 19.27 19.03 19.54 18.95 19.66 18.73 L 21.66 15.27 C 21.78 15.05 21.73 14.78 21.54 14.63 L 19.43 12.97 Z" />
</vector>
</aapt:attr>
<target android:name="path">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="300"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="M 14.87 5.07 L 14.5 2.42 C 14.46 2.18 14.25 2 14 2 L 10 2 C 9.75 2 9.54 2.18 9.5 2.42 L 9.13 5.07 C 8.5 5.32 7.96 5.66 7.44 6.05 L 4.95 5.05 C 4.73 4.96 4.46 5.05 4.34 5.27 L 2.34 8.73 C 2.21 8.95 2.27 9.22 2.46 9.37 L 4.57 11 C 4.53 11.34 4.5 11.67 4.5 12 C 4.5 12.33 4.53 12.65 4.57 12.97 L 2.46 14.63 C 2.27 14.78 2.21 15.05 2.34 15.27 L 4.34 18.73 C 4.46 18.95 4.73 19.03 4.95 18.95 L 7.44 17.94 C 7.96 18.34 8.5 18.68 9.13 18.93 L 9.5 21.58 C 9.54 21.82 9.75 22 10 22 L 14 22 C 14.25 22 14.46 21.82 14.5 21.58 L 14.87 18.93 C 15.5 18.67 16.04 18.34 16.56 17.94 L 19.05 18.95 C 19.27 19.03 19.54 18.95 19.66 18.73 L 21.66 15.27 C 21.78 15.05 21.73 14.78 21.54 14.63 L 19.43 12.97 L 19.43 12.97 C 19.47 12.65 19.5 12.33 19.5 12 C 19.5 11.67 19.47 11.34 19.43 11 L 21.54 9.37 C 21.73 9.22 21.78 8.95 21.66 8.73 L 19.66 5.27 C 19.54 5.05 19.27 4.96 19.05 5.05 L 16.56 6.05 C 16.04 5.66 15.5 5.32 14.87 5.07 M 12 8.5 C 12.614 8.5 13.218 8.662 13.75 8.969 C 14.282 9.276 14.724 9.718 15.031 10.25 C 15.338 10.782 15.5 11.386 15.5 12 C 15.5 12.614 15.338 13.218 15.031 13.75 C 14.724 14.282 14.282 14.724 13.75 15.031 C 13.218 15.338 12.614 15.5 12 15.5 C 11.072 15.5 10.181 15.131 9.525 14.475 C 8.869 13.819 8.5 12.928 8.5 12 C 8.5 11.072 8.869 10.181 9.525 9.525 C 10.181 8.869 11.072 8.5 12 8.5 M 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 M 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12"
android:valueTo="M 14.87 5.07 L 14.5 2.42 C 14.46 2.18 14.25 2 14 2 L 10 2 C 9.75 2 9.54 2.18 9.5 2.42 L 9.13 5.07 C 8.5 5.32 7.96 5.66 7.44 6.05 L 4.95 5.05 C 4.73 4.96 4.46 5.05 4.34 5.27 L 2.34 8.73 C 2.21 8.95 2.27 9.22 2.46 9.37 L 4.57 11 C 4.547 11.333 4.523 11.667 4.5 12 C 4.523 12.323 4.547 12.647 4.57 12.97 L 2.46 14.63 C 2.27 14.78 2.21 15.05 2.34 15.27 L 4.34 18.73 C 4.46 18.95 4.73 19.03 4.95 18.95 L 7.44 17.94 C 7.96 18.34 8.5 18.68 9.13 18.93 L 9.5 21.58 C 9.54 21.82 9.75 22 10 22 L 14 22 C 14.25 22 14.46 21.82 14.5 21.58 L 14.87 18.93 C 15.5 18.68 16.04 18.34 16.56 17.95 L 19.05 18.95 C 19.27 19.04 19.54 18.95 19.66 18.73 L 21.66 15.27 C 21.79 15.05 21.73 14.78 21.54 14.63 L 19.43 13 L 19.465 12.499 C 19.477 12.333 19.488 12.166 19.5 12 C 19.477 11.667 19.453 11.333 19.43 11 L 21.54 9.37 C 21.73 9.22 21.79 8.95 21.66 8.73 L 19.66 5.27 C 19.54 5.05 19.27 4.96 19.05 5.05 L 16.56 6.05 C 16.04 5.66 15.5 5.32 14.87 5.07 M 12 8 C 12.53 8 13.05 8.105 13.531 8.305 C 14.011 8.504 14.454 8.797 14.828 9.172 C 15.578 9.921 16 10.94 16 12 C 16 12.53 15.895 13.05 15.695 13.531 C 15.496 14.011 15.203 14.454 14.828 14.828 C 14.079 15.578 13.06 16 12 16 C 10.94 16 9.921 15.578 9.172 14.828 C 8.422 14.079 8 13.06 8 12 C 8 10.94 8.422 9.921 9.172 9.172 C 9.921 8.422 10.94 8 12 8 M 12 10 C 11.912 10 11.824 10.006 11.737 10.017 C 11.651 10.029 11.565 10.046 11.481 10.069 C 11.397 10.091 11.315 10.119 11.235 10.152 C 11.155 10.186 11.077 10.224 11.001 10.267 C 10.926 10.311 10.854 10.359 10.784 10.412 C 10.715 10.466 10.649 10.524 10.586 10.586 C 10.524 10.649 10.466 10.715 10.412 10.784 C 10.359 10.854 10.311 10.926 10.267 11.001 C 10.224 11.077 10.186 11.155 10.152 11.235 C 10.119 11.315 10.091 11.397 10.069 11.481 C 10.046 11.565 10.029 11.651 10.017 11.737 C 10.006 11.824 10 11.912 10 12 C 10 12.088 10.006 12.176 10.017 12.263 C 10.029 12.349 10.046 12.435 10.069 12.519 C 10.091 12.603 10.119 12.685 10.152 12.765 C 10.186 12.845 10.224 12.923 10.267 12.999 C 10.311 13.074 10.359 13.146 10.412 13.216 C 10.466 13.285 10.524 13.351 10.586 13.414 C 10.649 13.476 10.715 13.534 10.784 13.588 C 10.854 13.641 10.926 13.689 11.001 13.733 C 11.077 13.776 11.155 13.814 11.235 13.848 C 11.315 13.881 11.397 13.909 11.481 13.931 C 11.565 13.954 11.651 13.971 11.737 13.983 C 11.824 13.994 11.912 14 12 14 C 12.53 14 13.039 13.789 13.414 13.414 C 13.468 13.36 13.518 13.304 13.565 13.245 C 13.611 13.187 13.655 13.126 13.694 13.062 C 13.734 12.999 13.77 12.934 13.802 12.867 C 13.834 12.8 13.863 12.731 13.887 12.661 C 13.912 12.591 13.933 12.519 13.949 12.447 C 13.966 12.374 13.979 12.3 13.987 12.226 C 13.996 12.151 14 12.076 14 12 C 14 11.912 13.994 11.824 13.983 11.737 C 13.971 11.651 13.954 11.565 13.931 11.481 C 13.909 11.397 13.881 11.315 13.848 11.235 C 13.814 11.155 13.776 11.077 13.733 11.001 C 13.689 10.926 13.641 10.854 13.588 10.784 C 13.534 10.715 13.476 10.649 13.414 10.586 C 13.039 10.211 12.53 10 12 10 M 11.25 4 L 11.25 4 L 12.75 4 L 13.12 6.62 C 14.32 6.86 15.38 7.5 16.15 8.39 L 18.56 7.35 L 19.31 8.65 L 17.2 10.2 C 17.6 11.37 17.6 12.64 17.2 13.81 L 19.32 15.36 L 18.57 16.66 L 16.14 15.62 C 15.37 16.5 14.32 17.14 13.13 17.39 L 12.76 20 L 11.24 20 L 10.87 17.38 C 9.68 17.14 8.63 16.5 7.86 15.62 L 5.43 16.66 L 4.68 15.36 L 6.8 13.8 C 6.4 12.64 6.4 11.37 6.8 10.2 L 4.69 8.65 L 5.44 7.35 L 7.85 8.39 C 8.62 7.5 9.68 6.86 10.88 6.61 L 11.25 4"
android:valueType="pathType" />
</aapt:attr>
</target>
</animated-vector>

View file

@ -0,0 +1,28 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path"
android:fillColor="#000000"
android:fillType="evenOdd"
android:pathData="M 14.87 5.07 L 14.5 2.42 C 14.46 2.18 14.25 2 14 2 L 10 2 C 9.75 2 9.54 2.18 9.5 2.42 L 9.13 5.07 C 8.5 5.32 7.96 5.66 7.44 6.05 L 4.95 5.05 C 4.73 4.96 4.46 5.05 4.34 5.27 L 2.34 8.73 C 2.21 8.95 2.27 9.22 2.46 9.37 L 4.57 11 C 4.547 11.333 4.523 11.667 4.5 12 C 4.523 12.323 4.547 12.647 4.57 12.97 L 2.46 14.63 C 2.27 14.78 2.21 15.05 2.34 15.27 L 4.34 18.73 C 4.46 18.95 4.73 19.03 4.95 18.95 L 7.44 17.94 C 7.96 18.34 8.5 18.68 9.13 18.93 L 9.5 21.58 C 9.54 21.82 9.75 22 10 22 L 14 22 C 14.25 22 14.46 21.82 14.5 21.58 L 14.87 18.93 C 15.5 18.68 16.04 18.34 16.56 17.95 L 19.05 18.95 C 19.27 19.04 19.54 18.95 19.66 18.73 L 21.66 15.27 C 21.79 15.05 21.73 14.78 21.54 14.63 L 19.43 13 L 19.465 12.499 C 19.477 12.333 19.488 12.166 19.5 12 C 19.477 11.667 19.453 11.333 19.43 11 L 21.54 9.37 C 21.73 9.22 21.79 8.95 21.66 8.73 L 19.66 5.27 C 19.54 5.05 19.27 4.96 19.05 5.05 L 16.56 6.05 C 16.04 5.66 15.5 5.32 14.87 5.07 M 12 8 C 12.53 8 13.05 8.105 13.531 8.305 C 14.011 8.504 14.454 8.797 14.828 9.172 C 15.578 9.921 16 10.94 16 12 C 16 12.53 15.895 13.05 15.695 13.531 C 15.496 14.011 15.203 14.454 14.828 14.828 C 14.079 15.578 13.06 16 12 16 C 10.94 16 9.921 15.578 9.172 14.828 C 8.422 14.079 8 13.06 8 12 C 8 10.94 8.422 9.921 9.172 9.172 C 9.921 8.422 10.94 8 12 8 M 12 10 C 11.912 10 11.824 10.006 11.737 10.017 C 11.651 10.029 11.565 10.046 11.481 10.069 C 11.397 10.091 11.315 10.119 11.235 10.152 C 11.155 10.186 11.077 10.224 11.001 10.267 C 10.926 10.311 10.854 10.359 10.784 10.412 C 10.715 10.466 10.649 10.524 10.586 10.586 C 10.524 10.649 10.466 10.715 10.412 10.784 C 10.359 10.854 10.311 10.926 10.267 11.001 C 10.224 11.077 10.186 11.155 10.152 11.235 C 10.119 11.315 10.091 11.397 10.069 11.481 C 10.046 11.565 10.029 11.651 10.017 11.737 C 10.006 11.824 10 11.912 10 12 C 10 12.088 10.006 12.176 10.017 12.263 C 10.029 12.349 10.046 12.435 10.069 12.519 C 10.091 12.603 10.119 12.685 10.152 12.765 C 10.186 12.845 10.224 12.923 10.267 12.999 C 10.311 13.074 10.359 13.146 10.412 13.216 C 10.466 13.285 10.524 13.351 10.586 13.414 C 10.649 13.476 10.715 13.534 10.784 13.588 C 10.854 13.641 10.926 13.689 11.001 13.733 C 11.077 13.776 11.155 13.814 11.235 13.848 C 11.315 13.881 11.397 13.909 11.481 13.931 C 11.565 13.954 11.651 13.971 11.737 13.983 C 11.824 13.994 11.912 14 12 14 C 12.53 14 13.039 13.789 13.414 13.414 C 13.468 13.36 13.518 13.304 13.565 13.245 C 13.611 13.187 13.655 13.126 13.694 13.062 C 13.734 12.999 13.77 12.934 13.802 12.867 C 13.834 12.8 13.863 12.731 13.887 12.661 C 13.912 12.591 13.933 12.519 13.949 12.447 C 13.966 12.374 13.979 12.3 13.987 12.226 C 13.996 12.151 14 12.076 14 12 C 14 11.912 13.994 11.824 13.983 11.737 C 13.971 11.651 13.954 11.565 13.931 11.481 C 13.909 11.397 13.881 11.315 13.848 11.235 C 13.814 11.155 13.776 11.077 13.733 11.001 C 13.689 10.926 13.641 10.854 13.588 10.784 C 13.534 10.715 13.476 10.649 13.414 10.586 C 13.039 10.211 12.53 10 12 10 M 11.25 4 L 11.25 4 L 12.75 4 L 13.12 6.62 C 14.32 6.86 15.38 7.5 16.15 8.39 L 18.56 7.35 L 19.31 8.65 L 17.2 10.2 C 17.6 11.37 17.6 12.64 17.2 13.81 L 19.32 15.36 L 18.57 16.66 L 16.14 15.62 C 15.37 16.5 14.32 17.14 13.13 17.39 L 12.76 20 L 11.24 20 L 10.87 17.38 C 9.68 17.14 8.63 16.5 7.86 15.62 L 5.43 16.66 L 4.68 15.36 L 6.8 13.8 C 6.4 12.64 6.4 11.37 6.8 10.2 L 4.69 8.65 L 5.44 7.35 L 7.85 8.39 C 8.62 7.5 9.68 6.86 10.88 6.61 L 11.25 4" />
</vector>
</aapt:attr>
<target android:name="path">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="300"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="M 14.87 5.07 L 14.5 2.42 C 14.46 2.18 14.25 2 14 2 L 10 2 C 9.75 2 9.54 2.18 9.5 2.42 L 9.13 5.07 C 8.5 5.32 7.96 5.66 7.44 6.05 L 4.95 5.05 C 4.73 4.96 4.46 5.05 4.34 5.27 L 2.34 8.73 C 2.21 8.95 2.27 9.22 2.46 9.37 L 4.57 11 C 4.547 11.333 4.523 11.667 4.5 12 C 4.523 12.323 4.547 12.647 4.57 12.97 L 2.46 14.63 C 2.27 14.78 2.21 15.05 2.34 15.27 L 4.34 18.73 C 4.46 18.95 4.73 19.03 4.95 18.95 L 7.44 17.94 C 7.96 18.34 8.5 18.68 9.13 18.93 L 9.5 21.58 C 9.54 21.82 9.75 22 10 22 L 14 22 C 14.25 22 14.46 21.82 14.5 21.58 L 14.87 18.93 C 15.5 18.68 16.04 18.34 16.56 17.95 L 19.05 18.95 C 19.27 19.04 19.54 18.95 19.66 18.73 L 21.66 15.27 C 21.79 15.05 21.73 14.78 21.54 14.63 L 19.43 13 L 19.465 12.499 C 19.477 12.333 19.488 12.166 19.5 12 C 19.477 11.667 19.453 11.333 19.43 11 L 21.54 9.37 C 21.73 9.22 21.79 8.95 21.66 8.73 L 19.66 5.27 C 19.54 5.05 19.27 4.96 19.05 5.05 L 16.56 6.05 C 16.04 5.66 15.5 5.32 14.87 5.07 M 12 8 C 12.53 8 13.05 8.105 13.531 8.305 C 14.011 8.504 14.454 8.797 14.828 9.172 C 15.578 9.921 16 10.94 16 12 C 16 12.53 15.895 13.05 15.695 13.531 C 15.496 14.011 15.203 14.454 14.828 14.828 C 14.079 15.578 13.06 16 12 16 C 10.94 16 9.921 15.578 9.172 14.828 C 8.422 14.079 8 13.06 8 12 C 8 10.94 8.422 9.921 9.172 9.172 C 9.921 8.422 10.94 8 12 8 M 12 10 C 11.912 10 11.824 10.006 11.737 10.017 C 11.651 10.029 11.565 10.046 11.481 10.069 C 11.397 10.091 11.315 10.119 11.235 10.152 C 11.155 10.186 11.077 10.224 11.001 10.267 C 10.926 10.311 10.854 10.359 10.784 10.412 C 10.715 10.466 10.649 10.524 10.586 10.586 C 10.524 10.649 10.466 10.715 10.412 10.784 C 10.359 10.854 10.311 10.926 10.267 11.001 C 10.224 11.077 10.186 11.155 10.152 11.235 C 10.119 11.315 10.091 11.397 10.069 11.481 C 10.046 11.565 10.029 11.651 10.017 11.737 C 10.006 11.824 10 11.912 10 12 C 10 12.088 10.006 12.176 10.017 12.263 C 10.029 12.349 10.046 12.435 10.069 12.519 C 10.091 12.603 10.119 12.685 10.152 12.765 C 10.186 12.845 10.224 12.923 10.267 12.999 C 10.311 13.074 10.359 13.146 10.412 13.216 C 10.466 13.285 10.524 13.351 10.586 13.414 C 10.649 13.476 10.715 13.534 10.784 13.588 C 10.854 13.641 10.926 13.689 11.001 13.733 C 11.077 13.776 11.155 13.814 11.235 13.848 C 11.315 13.881 11.397 13.909 11.481 13.931 C 11.565 13.954 11.651 13.971 11.737 13.983 C 11.824 13.994 11.912 14 12 14 C 12.53 14 13.039 13.789 13.414 13.414 C 13.468 13.36 13.518 13.304 13.565 13.245 C 13.611 13.187 13.655 13.126 13.694 13.062 C 13.734 12.999 13.77 12.934 13.802 12.867 C 13.834 12.8 13.863 12.731 13.887 12.661 C 13.912 12.591 13.933 12.519 13.949 12.447 C 13.966 12.374 13.979 12.3 13.987 12.226 C 13.996 12.151 14 12.076 14 12 C 14 11.912 13.994 11.824 13.983 11.737 C 13.971 11.651 13.954 11.565 13.931 11.481 C 13.909 11.397 13.881 11.315 13.848 11.235 C 13.814 11.155 13.776 11.077 13.733 11.001 C 13.689 10.926 13.641 10.854 13.588 10.784 C 13.534 10.715 13.476 10.649 13.414 10.586 C 13.039 10.211 12.53 10 12 10 M 11.25 4 L 11.25 4 L 12.75 4 L 13.12 6.62 C 14.32 6.86 15.38 7.5 16.15 8.39 L 18.56 7.35 L 19.31 8.65 L 17.2 10.2 C 17.6 11.37 17.6 12.64 17.2 13.81 L 19.32 15.36 L 18.57 16.66 L 16.14 15.62 C 15.37 16.5 14.32 17.14 13.13 17.39 L 12.76 20 L 11.24 20 L 10.87 17.38 C 9.68 17.14 8.63 16.5 7.86 15.62 L 5.43 16.66 L 4.68 15.36 L 6.8 13.8 C 6.4 12.64 6.4 11.37 6.8 10.2 L 4.69 8.65 L 5.44 7.35 L 7.85 8.39 C 8.62 7.5 9.68 6.86 10.88 6.61 L 11.25 4"
android:valueTo="M 14.87 5.07 L 14.5 2.42 C 14.46 2.18 14.25 2 14 2 L 10 2 C 9.75 2 9.54 2.18 9.5 2.42 L 9.13 5.07 C 8.5 5.32 7.96 5.66 7.44 6.05 L 4.95 5.05 C 4.73 4.96 4.46 5.05 4.34 5.27 L 2.34 8.73 C 2.21 8.95 2.27 9.22 2.46 9.37 L 4.57 11 C 4.53 11.34 4.5 11.67 4.5 12 C 4.5 12.33 4.53 12.65 4.57 12.97 L 2.46 14.63 C 2.27 14.78 2.21 15.05 2.34 15.27 L 4.34 18.73 C 4.46 18.95 4.73 19.03 4.95 18.95 L 7.44 17.94 C 7.96 18.34 8.5 18.68 9.13 18.93 L 9.5 21.58 C 9.54 21.82 9.75 22 10 22 L 14 22 C 14.25 22 14.46 21.82 14.5 21.58 L 14.87 18.93 C 15.5 18.67 16.04 18.34 16.56 17.94 L 19.05 18.95 C 19.27 19.03 19.54 18.95 19.66 18.73 L 21.66 15.27 C 21.78 15.05 21.73 14.78 21.54 14.63 L 19.43 12.97 L 19.43 12.97 C 19.47 12.65 19.5 12.33 19.5 12 C 19.5 11.67 19.47 11.34 19.43 11 L 21.54 9.37 C 21.73 9.22 21.78 8.95 21.66 8.73 L 19.66 5.27 C 19.54 5.05 19.27 4.96 19.05 5.05 L 16.56 6.05 C 16.04 5.66 15.5 5.32 14.87 5.07 M 12 8.5 C 12.614 8.5 13.218 8.662 13.75 8.969 C 14.282 9.276 14.724 9.718 15.031 10.25 C 15.338 10.782 15.5 11.386 15.5 12 C 15.5 12.614 15.338 13.218 15.031 13.75 C 14.724 14.282 14.282 14.724 13.75 15.031 C 13.218 15.338 12.614 15.5 12 15.5 C 11.072 15.5 10.181 15.131 9.525 14.475 C 8.869 13.819 8.5 12.928 8.5 12 C 8.5 11.072 8.869 10.181 9.525 9.525 C 10.181 8.869 11.072 8.5 12 8.5 M 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 M 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12"
android:valueType="pathType" />
</aapt:attr>
</target>
</animated-vector>

View file

@ -0,0 +1,28 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path"
android:fillColor="#000000"
android:fillType="evenOdd"
android:pathData="M 12 1 L 3 5 L 3 11 C 3 16.55 6.84 21.74 12 23 C 17.16 21.74 21 16.55 21 11 L 21 5 L 12 1 Z" />
</vector>
</aapt:attr>
<target android:name="path">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="300"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="M 21 11 C 21 16.55 17.16 21.74 12 23 C 6.84 21.74 3 16.55 3 11 L 3 5 L 12 1 L 12 1 L 21 5 L 21 11 M 12 10.18 L 12 10.18 C 12 10.18 12 10.18 12 10.18 L 12 10.18 L 12 10.18 L 12 10.18 L 12 10.18 C 12 10.18 12 10.18 12 10.18"
android:valueTo="M 21 11 C 21 16.55 17.16 21.74 12 23 C 6.84 21.74 3 16.55 3 11 L 3 5 L 7.5 3 L 12 1 L 21 5 L 21 11 M 12 21 L 12 21 C 8.25 20 5 15.54 5 11.22 L 5 6.3 L 12 3.18 L 19 6.3 L 19 11.22 C 19 15.54 15.75 20 12 21"
android:valueType="pathType" />
</aapt:attr>
</target>
</animated-vector>

View file

@ -0,0 +1,28 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="path"
android:fillColor="#000000"
android:fillType="evenOdd"
android:pathData="M 21 11 C 21 16.55 17.16 21.74 12 23 C 6.84 21.74 3 16.55 3 11 L 3 5 L 7.5 3 L 12 1 L 21 5 L 21 11 M 12 21 L 12 21 C 8.25 20 5 15.54 5 11.22 L 5 6.3 L 12 3.18 L 19 6.3 L 19 11.22 C 19 15.54 15.75 20 12 21" />
</vector>
</aapt:attr>
<target android:name="path">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="300"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="M 21 11 C 21 16.55 17.16 21.74 12 23 C 6.84 21.74 3 16.55 3 11 L 3 5 L 7.5 3 L 12 1 L 21 5 L 21 11 M 12 21 L 12 21 C 8.25 20 5 15.54 5 11.22 L 5 6.3 L 12 3.18 L 19 6.3 L 19 11.22 C 19 15.54 15.75 20 12 21"
android:valueTo="M 21 11 C 21 16.55 17.16 21.74 12 23 C 6.84 21.74 3 16.55 3 11 L 3 5 L 12 1 L 12 1 L 21 5 L 21 11 M 12 10.18 L 12 10.18 C 12 10.18 12 10.18 12 10.18 L 12 10.18 L 12 10.18 L 12 10.18 L 12 10.18 C 12 10.18 12 10.18 12 10.18"
android:valueType="pathType" />
</aapt:attr>
</target>
</animated-vector>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@android:color/black" />
<corners android:bottomLeftRadius="2dp" android:bottomRightRadius="2dp" />
</shape>
</item>
</selector>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@android:color/black" />
<corners android:topLeftRadius="2dp" android:topRightRadius="2dp" />
</shape>
</item>
</selector>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="oval">
<solid android:color="#43A047" />
</shape>
</item>
</selector>

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M8,5v14l11,-7z"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?colorOnSurface"
android:pathData="M20,11H6.83l2.88,-2.88c0.39,-0.39 0.39,-1.02 0,-1.41 -0.39,-0.39 -1.02,-0.39 -1.41,0L3.71,11.3c-0.39,0.39 -0.39,1.02 0,1.41L8.3,17.3c0.39,0.39 1.02,0.39 1.41,0 0.39,-0.39 0.39,-1.02 0,-1.41L6.83,13H20c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1z" />
</vector>

Some files were not shown because too many files have changed in this diff Show more