Repo created
This commit is contained in:
parent
d327c31227
commit
0b2aca0925
638 changed files with 76461 additions and 0 deletions
674
LICENSE
Normal file
674
LICENSE
Normal 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
58
README.MD
Normal file
|
|
@ -0,0 +1,58 @@
|
|||

|
||||
|
||||
[](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
7
app/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
/dict.txt
|
||||
|
||||
# Gradle
|
||||
.gradle
|
||||
.kotlin
|
||||
/local.properties
|
||||
/build
|
||||
1
app/apk/.gitignore
vendored
Normal file
1
app/apk/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
59
app/apk/build.gradle.kts
Normal file
59
app/apk/build.gradle.kts
Normal 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"))
|
||||
}
|
||||
33
app/apk/src/main/AndroidManifest.xml
Normal file
33
app/apk/src/main/AndroidManifest.xml
Normal 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>
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
141
app/apk/src/main/java/com/topjohnwu/magisk/arch/UIActivity.kt
Normal file
141
app/apk/src/main/java/com/topjohnwu/magisk/arch/UIActivity.kt
Normal 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
|
||||
)
|
||||
}
|
||||
21
app/apk/src/main/java/com/topjohnwu/magisk/arch/ViewEvent.kt
Normal file
21
app/apk/src/main/java/com/topjohnwu/magisk/arch/ViewEvent.kt
Normal 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<*>)
|
||||
}
|
||||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
|
||||
}
|
||||
124
app/apk/src/main/java/com/topjohnwu/magisk/events/ViewEvents.kt
Normal file
124
app/apk/src/main/java/com/topjohnwu/magisk/events/ViewEvents.kt
Normal 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)
|
||||
}
|
||||
278
app/apk/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt
Normal file
278
app/apk/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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?>()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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?>()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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() }
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
@ -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) {}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
50
app/apk/src/main/java/com/topjohnwu/magisk/ui/theme/Theme.kt
Normal file
50
app/apk/src/main/java/com/topjohnwu/magisk/ui/theme/Theme.kt
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
232
app/apk/src/main/java/com/topjohnwu/magisk/view/MagiskDialog.kt
Normal file
232
app/apk/src/main/java/com/topjohnwu/magisk/view/MagiskDialog.kt
Normal 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?) {}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
10
app/apk/src/main/java/com/topjohnwu/magisk/view/TextItem.kt
Normal file
10
app/apk/src/main/java/com/topjohnwu/magisk/view/TextItem.kt
Normal 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
|
||||
}
|
||||
|
|
@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
15
app/apk/src/main/res/anim/fragment_enter.xml
Normal file
15
app/apk/src/main/res/anim/fragment_enter.xml
Normal 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>
|
||||
15
app/apk/src/main/res/anim/fragment_enter_pop.xml
Normal file
15
app/apk/src/main/res/anim/fragment_enter_pop.xml
Normal 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>
|
||||
15
app/apk/src/main/res/anim/fragment_exit.xml
Normal file
15
app/apk/src/main/res/anim/fragment_exit.xml
Normal 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>
|
||||
15
app/apk/src/main/res/anim/fragment_exit_pop.xml
Normal file
15
app/apk/src/main/res/anim/fragment_exit_pop.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
5
app/apk/src/main/res/color/color_error_transient.xml
Normal file
5
app/apk/src/main/res/color/color_error_transient.xml
Normal 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>
|
||||
6
app/apk/src/main/res/color/color_menu_tint.xml
Normal file
6
app/apk/src/main/res/color/color_menu_tint.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
5
app/apk/src/main/res/color/color_primary_transient.xml
Normal file
5
app/apk/src/main/res/color/color_primary_transient.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
5
app/apk/src/main/res/color/color_text_transient.xml
Normal file
5
app/apk/src/main/res/color/color_text_transient.xml
Normal 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>
|
||||
28
app/apk/src/main/res/drawable/avd_bug_from_filled.xml
Normal file
28
app/apk/src/main/res/drawable/avd_bug_from_filled.xml
Normal 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>
|
||||
28
app/apk/src/main/res/drawable/avd_bug_to_filled.xml
Normal file
28
app/apk/src/main/res/drawable/avd_bug_to_filled.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
27
app/apk/src/main/res/drawable/avd_circle_check_to_filled.xml
Normal file
27
app/apk/src/main/res/drawable/avd_circle_check_to_filled.xml
Normal 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>
|
||||
29
app/apk/src/main/res/drawable/avd_home_from_filled.xml
Normal file
29
app/apk/src/main/res/drawable/avd_home_from_filled.xml
Normal 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>
|
||||
29
app/apk/src/main/res/drawable/avd_home_to_filled.xml
Normal file
29
app/apk/src/main/res/drawable/avd_home_to_filled.xml
Normal 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>
|
||||
28
app/apk/src/main/res/drawable/avd_module_from_filled.xml
Normal file
28
app/apk/src/main/res/drawable/avd_module_from_filled.xml
Normal 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>
|
||||
28
app/apk/src/main/res/drawable/avd_module_to_filled.xml
Normal file
28
app/apk/src/main/res/drawable/avd_module_to_filled.xml
Normal 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>
|
||||
28
app/apk/src/main/res/drawable/avd_settings_from_filled.xml
Normal file
28
app/apk/src/main/res/drawable/avd_settings_from_filled.xml
Normal 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>
|
||||
28
app/apk/src/main/res/drawable/avd_settings_to_filled.xml
Normal file
28
app/apk/src/main/res/drawable/avd_settings_to_filled.xml
Normal 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>
|
||||
28
app/apk/src/main/res/drawable/avd_superuser_from_filled.xml
Normal file
28
app/apk/src/main/res/drawable/avd_superuser_from_filled.xml
Normal 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>
|
||||
28
app/apk/src/main/res/drawable/avd_superuser_to_filled.xml
Normal file
28
app/apk/src/main/res/drawable/avd_superuser_to_filled.xml
Normal 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>
|
||||
11
app/apk/src/main/res/drawable/bg_line_bottom_rounded.xml
Normal file
11
app/apk/src/main/res/drawable/bg_line_bottom_rounded.xml
Normal 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>
|
||||
11
app/apk/src/main/res/drawable/bg_line_top_rounded.xml
Normal file
11
app/apk/src/main/res/drawable/bg_line_top_rounded.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
5
app/apk/src/main/res/drawable/ic_action_md2.xml
Normal file
5
app/apk/src/main/res/drawable/ic_action_md2.xml
Normal 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>
|
||||
10
app/apk/src/main/res/drawable/ic_back_md2.xml
Normal file
10
app/apk/src/main/res/drawable/ic_back_md2.xml
Normal 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
Loading…
Add table
Add a link
Reference in a new issue