Repo cloned
This commit is contained in:
commit
11ea8025b0
214 changed files with 33943 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. <https://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 <https://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:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
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
|
||||
<https://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
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
88
README.md
Normal file
88
README.md
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
<div align="center">
|
||||
<a href="https://github.com/bmax121/APatch/releases/latest"><img src="https://images.weserv.nl/?url=https://raw.githubusercontent.com/bmax121/APatch/main/app/src/main/ic_launcher-playstore.png&mask=circle" style="width: 128px;" alt="logo"></a>
|
||||
|
||||
<h1 align="center">APatch</h1>
|
||||
|
||||
[](https://github.com/bmax121/APatch/releases/latest)
|
||||
[](https://nightly.link/bmax121/APatch/workflows/build/main/APatch)
|
||||
[](https://hosted.weblate.org/engage/APatch)
|
||||
[](https://t.me/APatchGroup)
|
||||
[](/LICENSE)
|
||||
|
||||
</div>
|
||||
|
||||
The patching of Android kernel and Android system.
|
||||
|
||||
- A new kernel-based root solution for Android devices.
|
||||
- APM: Support for modules similar to Magisk.
|
||||
- KPM: Support for modules that allow you to inject any code into the kernel (Provides kernel function `inline-hook` and `syscall-table-hook`).
|
||||
- APatch relies on [KernelPatch](https://github.com/bmax121/KernelPatch/).
|
||||
- The APatch UI and the APModule source code have been derived and modified from [KernelSU](https://github.com/tiann/KernelSU).
|
||||
|
||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
||||
alt="Get it on F-Droid"
|
||||
height="80">](https://f-droid.org/packages/me.bmax.apatch/)
|
||||
|
||||
Or download the latest APK from the [Releases Section](https://github.com/bmax121/APatch/releases/latest).
|
||||
|
||||
## Supported Versions
|
||||
|
||||
- Only supports the ARM64 architecture.
|
||||
- Only supports Android kernel versions 3.18 - 6.12
|
||||
|
||||
Support for Samsung devices with security protection: Planned
|
||||
|
||||
## Requirement
|
||||
|
||||
Kernel configs:
|
||||
|
||||
- `CONFIG_KALLSYMS=y` and `CONFIG_KALLSYMS_ALL=y`
|
||||
|
||||
- `CONFIG_KALLSYMS=y` and `CONFIG_KALLSYMS_ALL=n`: Initial support
|
||||
|
||||
## Security Alert
|
||||
|
||||
The **SuperKey** has higher privileges than root access.
|
||||
Weak or compromised keys can lead to unauthorized control of your device.
|
||||
It is critical to use robust keys and safeguard them from exposure to maintain the security of your device.
|
||||
|
||||
## Translation
|
||||
|
||||
To help translate APatch or improve existing translations, please use [Weblate](https://hosted.weblate.org/engage/apatch/). PR of APatch translation is no longer accepted, because it will conflict with Weblate.
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://hosted.weblate.org/engage/APatch/)
|
||||
|
||||
</div>
|
||||
|
||||
## Get Help
|
||||
|
||||
### Usage
|
||||
|
||||
For usage, please refer to [our official documentation](https://apatch.dev).
|
||||
It's worth noting that the documentation is currently not quite complete, and the content may change at any time.
|
||||
Furthermore, we need more volunteers to [contribute to the documentation](https://github.com/AndroidPatch/APatchDocs) in other languages.
|
||||
|
||||
### Updates
|
||||
|
||||
- Telegram Channel: [@APatchUpdates](https://t.me/APatchChannel)
|
||||
|
||||
### Discussions
|
||||
|
||||
- Telegram Group: [@APatchDiscussions(EN/CN)](https://t.me/Apatch_discuss)
|
||||
- Telegram Group: [中文](https://t.me/APatch_CN_Group)
|
||||
|
||||
### More Information
|
||||
|
||||
- [Documents](docs/)
|
||||
|
||||
## Credits
|
||||
|
||||
- [KernelPatch](https://github.com/bmax121/KernelPatch/): The core.
|
||||
- [Magisk](https://github.com/topjohnwu/Magisk): magiskboot and magiskpolicy.
|
||||
- [KernelSU](https://github.com/tiann/KernelSU): App UI, and Magisk module like support.
|
||||
|
||||
## License
|
||||
|
||||
APatch is licensed under the GNU General Public License v3 [GPL-3](http://www.gnu.org/copyleft/gpl.html).
|
||||
2
apd/.gitignore
vendored
Normal file
2
apd/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
.cargo/
|
||||
1766
apd/Cargo.lock
generated
Normal file
1766
apd/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
61
apd/Cargo.toml
Normal file
61
apd/Cargo.toml
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
[package]
|
||||
name = "apd"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
csv = "1.3.1"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
const_format = "0.2"
|
||||
zip = { version = "5.1.1",features = [
|
||||
"deflate",
|
||||
"deflate64",
|
||||
"time",
|
||||
"lzma",
|
||||
"xz",
|
||||
], default-features = false }
|
||||
zip-extensions = { git = "https://github.com/AndroidPatch/zip-extensions-rs.git", branch = "master", features = [
|
||||
"deflate",
|
||||
"lzma",
|
||||
"xz",
|
||||
], default-features = false }
|
||||
java-properties = { git = "https://github.com/AndroidPatch/java-properties.git", branch = "master", default-features = false }
|
||||
log = "0.4"
|
||||
env_logger = "0.11"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
encoding_rs = "0.8"
|
||||
walkdir="2.4"
|
||||
retry = "2"
|
||||
libc = "0.2"
|
||||
extattr = "1"
|
||||
jwalk = "0.8"
|
||||
is_executable = "1"
|
||||
nom = "8"
|
||||
derive-new = "0.7.0"
|
||||
which = "8"
|
||||
getopts = "0.2"
|
||||
errno = "0.3.14"
|
||||
notify = "8.2"
|
||||
signal-hook = "0.3"
|
||||
|
||||
[target.'cfg(any(target_os = "android", target_os = "linux"))'.dependencies]
|
||||
rustix = { git = "https://github.com/AndroidPatch/rustix", branch = "main", features = ["all-apis"] }
|
||||
# some android specific dependencies which compiles under unix are also listed here for convenience of coding
|
||||
android-properties = { version = "0.2.2", features = ["bionic-deprecated"] }
|
||||
procfs = "0.17"
|
||||
loopdev = { git = "https://github.com/AndroidPatch/loopdev" }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
android_logger = { version = "0.15", default-features = false }
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
overflow-checks = false
|
||||
opt-level = 3
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
lto = "fat"
|
||||
62
apd/build.rs
Normal file
62
apd/build.rs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
fn get_git_version() -> Result<(u32, String), std::io::Error> {
|
||||
let output = Command::new("git")
|
||||
.args(["rev-list", "--count", "HEAD"])
|
||||
.output()?;
|
||||
|
||||
let output = output.stdout;
|
||||
let version_code = String::from_utf8(output).expect("Failed to read git count stdout");
|
||||
let version_code: u32 = version_code
|
||||
.trim()
|
||||
.parse()
|
||||
.map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, "Failed to parse git count"))?;
|
||||
let version_code = 10000 + 200 + version_code; // For historical reasons
|
||||
|
||||
let version_name = String::from_utf8(
|
||||
Command::new("git")
|
||||
.args(["describe", "--tags", "--always"])
|
||||
.output()?
|
||||
.stdout,
|
||||
)
|
||||
.map_err(|_| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"Failed to read git describe stdout",
|
||||
)
|
||||
})?;
|
||||
let version_name = version_name.trim_start_matches('v').to_string();
|
||||
Ok((version_code, version_name))
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// update VersionCode when git repository change
|
||||
println!("cargo:rerun-if-changed=../.git/HEAD");
|
||||
println!("cargo:rerun-if-changed=../.git/refs/");
|
||||
|
||||
let (code, name) = match get_git_version() {
|
||||
Ok((code, name)) => (code, name),
|
||||
Err(_) => {
|
||||
// show warning if git is not installed
|
||||
println!("cargo:warning=Failed to get git version, using 0.0.0");
|
||||
(0, "0.0.0".to_string())
|
||||
}
|
||||
};
|
||||
let out_dir = env::var("OUT_DIR").expect("Failed to get $OUT_DIR");
|
||||
println!("out_dir: ${out_dir}");
|
||||
println!("code: ${code}");
|
||||
let out_dir = Path::new(&out_dir);
|
||||
File::create(Path::new(out_dir).join("VERSION_CODE"))
|
||||
.expect("Failed to create VERSION_CODE")
|
||||
.write_all(code.to_string().as_bytes())
|
||||
.expect("Failed to write VERSION_CODE");
|
||||
|
||||
File::create(Path::new(out_dir).join("VERSION_NAME"))
|
||||
.expect("Failed to create VERSION_NAME")
|
||||
.write_all(name.trim().as_bytes())
|
||||
.expect("Failed to write VERSION_NAME");
|
||||
}
|
||||
231
apd/src/apd.rs
Normal file
231
apd/src/apd.rs
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
use anyhow::{Ok, Result};
|
||||
|
||||
#[cfg(unix)]
|
||||
use getopts::Options;
|
||||
use std::env;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::path::PathBuf;
|
||||
use std::{ffi::CStr, process::Command};
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use crate::pty::prepare_pty;
|
||||
use crate::{
|
||||
defs,
|
||||
utils::{self, umask},
|
||||
};
|
||||
use rustix::thread::{Gid, Uid, set_thread_res_gid, set_thread_res_uid};
|
||||
|
||||
fn print_usage(opts: Options) {
|
||||
let brief = "APatch\n\nUsage: <command> [options] [-] [user [argument...]]".to_string();
|
||||
print!("{}", opts.usage(&brief));
|
||||
}
|
||||
|
||||
fn set_identity(uid: u32, gid: u32) {
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
let gid = unsafe { Gid::from_raw(gid) };
|
||||
let uid = unsafe { Uid::from_raw(uid) };
|
||||
set_thread_res_gid(gid, gid, gid).ok();
|
||||
set_thread_res_uid(uid, uid, uid).ok();
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
pub fn root_shell() -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub fn root_shell() -> Result<()> {
|
||||
// we are root now, this was set in kernel!
|
||||
let env_args: Vec<String> = env::args().collect();
|
||||
let args = env_args
|
||||
.iter()
|
||||
.position(|arg| arg == "-c")
|
||||
.map(|i| {
|
||||
let rest = env_args[i + 1..].to_vec();
|
||||
let mut new_args = env_args[..i].to_vec();
|
||||
new_args.push("-c".to_string());
|
||||
if !rest.is_empty() {
|
||||
new_args.push(rest.join(" "));
|
||||
}
|
||||
new_args
|
||||
})
|
||||
.unwrap_or_else(|| env_args.clone());
|
||||
|
||||
let mut opts = Options::new();
|
||||
opts.optopt(
|
||||
"c",
|
||||
"command",
|
||||
"pass COMMAND to the invoked shell",
|
||||
"COMMAND",
|
||||
);
|
||||
opts.optflag("h", "help", "display this help message and exit");
|
||||
opts.optflag("l", "login", "pretend the shell to be a login shell");
|
||||
opts.optflag(
|
||||
"p",
|
||||
"preserve-environment",
|
||||
"preserve the entire environment",
|
||||
);
|
||||
opts.optopt(
|
||||
"s",
|
||||
"shell",
|
||||
"use SHELL instead of the default /system/bin/sh",
|
||||
"SHELL",
|
||||
);
|
||||
opts.optflag("v", "version", "display version number and exit");
|
||||
opts.optflag("V", "", "display version code and exit");
|
||||
opts.optflag(
|
||||
"M",
|
||||
"mount-master",
|
||||
"force run in the global mount namespace",
|
||||
);
|
||||
opts.optflag("", "no-pty", "Do not allocate a new pseudo terminal.");
|
||||
|
||||
// Replace -cn with -z, -mm with -M for supporting getopt_long
|
||||
let args = args
|
||||
.into_iter()
|
||||
.map(|e| {
|
||||
if e == "-mm" {
|
||||
"-M".to_string()
|
||||
} else if e == "-cn" {
|
||||
"-z".to_string()
|
||||
} else {
|
||||
e
|
||||
}
|
||||
})
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
let matches = match opts.parse(&args[1..]) {
|
||||
Result::Ok(m) => m,
|
||||
Err(f) => {
|
||||
println!("{f}");
|
||||
print_usage(opts);
|
||||
std::process::exit(-1);
|
||||
}
|
||||
};
|
||||
|
||||
if matches.opt_present("h") {
|
||||
print_usage(opts);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if matches.opt_present("v") {
|
||||
println!("{}:APatch", defs::VERSION_NAME);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if matches.opt_present("V") {
|
||||
println!("{}", defs::VERSION_CODE);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let shell = matches.opt_str("s").unwrap_or("/system/bin/sh".to_string());
|
||||
let mut is_login = matches.opt_present("l");
|
||||
let preserve_env = matches.opt_present("p");
|
||||
let mount_master = matches.opt_present("M");
|
||||
|
||||
// we've made sure that -c is the last option and it already contains the whole command, no need to construct it again
|
||||
let args = matches
|
||||
.opt_str("c")
|
||||
.map(|cmd| vec!["-c".to_string(), cmd])
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut free_idx = 0;
|
||||
if !matches.free.is_empty() && matches.free[free_idx] == "-" {
|
||||
is_login = true;
|
||||
free_idx += 1;
|
||||
}
|
||||
|
||||
// use current uid if no user specified, these has been done in kernel!
|
||||
let mut uid = unsafe { libc::getuid() };
|
||||
let gid = unsafe { libc::getgid() };
|
||||
if free_idx < matches.free.len() {
|
||||
let name = &matches.free[free_idx];
|
||||
uid = unsafe {
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
let pw = libc::getpwnam(name.as_ptr()).as_ref();
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
let pw = libc::getpwnam(name.as_ptr() as *const i8).as_ref();
|
||||
|
||||
match pw {
|
||||
Some(pw) => pw.pw_uid,
|
||||
None => name.parse::<u32>().unwrap_or(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/topjohnwu/Magisk/blob/master/native/src/core/su/su_daemon.cpp#L408
|
||||
let arg0 = if is_login { "-" } else { &shell };
|
||||
|
||||
let mut command = &mut Command::new(&shell);
|
||||
|
||||
if !preserve_env {
|
||||
// This is actually incorrect, I don't know why.
|
||||
// command = command.env_clear();
|
||||
|
||||
let pw = unsafe { libc::getpwuid(uid).as_ref() };
|
||||
|
||||
if let Some(pw) = pw {
|
||||
let home = unsafe { CStr::from_ptr(pw.pw_dir) };
|
||||
let pw_name = unsafe { CStr::from_ptr(pw.pw_name) };
|
||||
|
||||
let home = home.to_string_lossy();
|
||||
let pw_name = pw_name.to_string_lossy();
|
||||
|
||||
command = command
|
||||
.env("HOME", home.as_ref())
|
||||
.env("USER", pw_name.as_ref())
|
||||
.env("LOGNAME", pw_name.as_ref())
|
||||
.env("SHELL", &shell);
|
||||
}
|
||||
}
|
||||
|
||||
// add /data/adb/ap/bin to PATH
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
add_path_to_env(defs::BINARY_DIR)?;
|
||||
|
||||
// when AP_RC_PATH exists and ENV is not set, set ENV to AP_RC_PATH
|
||||
if PathBuf::from(defs::AP_RC_PATH).exists() && env::var("ENV").is_err() {
|
||||
command = command.env("ENV", defs::AP_RC_PATH);
|
||||
}
|
||||
#[cfg(target_os = "android")]
|
||||
if !matches.opt_present("no-pty") {
|
||||
if let Err(e) = prepare_pty() {
|
||||
log::error!("failed to prepare pty: {:?}", e);
|
||||
}
|
||||
}
|
||||
// escape from the current cgroup and become session leader
|
||||
// WARNING!!! This cause some root shell hang forever!
|
||||
// command = command.process_group(0);
|
||||
command = unsafe {
|
||||
command.pre_exec(move || {
|
||||
umask(0o22);
|
||||
utils::switch_cgroups();
|
||||
|
||||
// switch to global mount namespace
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
let global_namespace_enable =
|
||||
std::fs::read_to_string(defs::GLOBAL_NAMESPACE_FILE).unwrap_or("0".to_string());
|
||||
if global_namespace_enable.trim() == "1" || mount_master {
|
||||
let _ = utils::switch_mnt_ns(1);
|
||||
}
|
||||
|
||||
set_identity(uid, gid);
|
||||
|
||||
Result::Ok(())
|
||||
})
|
||||
};
|
||||
|
||||
command = command.args(args).arg0(arg0);
|
||||
Err(command.exec().into())
|
||||
}
|
||||
|
||||
fn add_path_to_env(path: &str) -> Result<()> {
|
||||
let mut paths =
|
||||
env::var_os("PATH").map_or(Vec::new(), |val| env::split_paths(&val).collect::<Vec<_>>());
|
||||
let new_path = PathBuf::from(path.trim_end_matches('/'));
|
||||
paths.push(new_path);
|
||||
let new_path_env = env::join_paths(paths)?;
|
||||
unsafe { env::set_var("PATH", new_path_env) };
|
||||
Ok(())
|
||||
}
|
||||
15
apd/src/assets.rs
Normal file
15
apd/src/assets.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
use anyhow::Result;
|
||||
use const_format::concatcp;
|
||||
|
||||
use crate::{defs::BINARY_DIR, utils};
|
||||
|
||||
pub const RESETPROP_PATH: &str = concatcp!(BINARY_DIR, "resetprop");
|
||||
pub const BUSYBOX_PATH: &str = concatcp!(BINARY_DIR, "busybox");
|
||||
pub const MAGISKPOLICY_PATH: &str = concatcp!(BINARY_DIR, "magiskpolicy");
|
||||
|
||||
pub fn ensure_binaries() -> Result<()> {
|
||||
utils::ensure_binary(RESETPROP_PATH)?;
|
||||
utils::ensure_binary(BUSYBOX_PATH)?;
|
||||
utils::ensure_binary(MAGISKPOLICY_PATH)?;
|
||||
Ok(())
|
||||
}
|
||||
5
apd/src/banner
Normal file
5
apd/src/banner
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
_ ____ _ _
|
||||
/ \ | _ \ __ _| |_ ___| |__
|
||||
/ _ \ | |_) / _` | __/ __| '_ \
|
||||
/ ___ \| __/ (_| | || (__| | | |
|
||||
/_/ \_\_| \__,_|\__\___|_| |_|
|
||||
162
apd/src/cli.rs
Normal file
162
apd/src/cli.rs
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
use android_logger::Config;
|
||||
#[cfg(target_os = "android")]
|
||||
use log::LevelFilter;
|
||||
|
||||
use crate::{defs, event, module, supercall, utils};
|
||||
|
||||
/// APatch cli
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version = defs::VERSION_CODE, about, long_about = None)]
|
||||
struct Args {
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
value_name = "KEY",
|
||||
help = "Super key for authentication root"
|
||||
)]
|
||||
superkey: Option<String>,
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand, Debug)]
|
||||
enum Commands {
|
||||
/// Manage APatch modules
|
||||
Module {
|
||||
#[command(subcommand)]
|
||||
command: Module,
|
||||
},
|
||||
|
||||
/// Trigger `post-fs-data` event
|
||||
PostFsData,
|
||||
|
||||
/// Trigger `service` event
|
||||
Services,
|
||||
|
||||
/// Trigger `boot-complete` event
|
||||
BootCompleted,
|
||||
|
||||
/// Start uid listener for synchronizing root list
|
||||
UidListener,
|
||||
|
||||
/// SELinux policy Patch tool
|
||||
Sepolicy {
|
||||
#[command(subcommand)]
|
||||
command: Sepolicy,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand, Debug)]
|
||||
enum Module {
|
||||
/// Install module <ZIP>
|
||||
Install {
|
||||
/// module zip file path
|
||||
zip: String,
|
||||
},
|
||||
|
||||
/// Uninstall module <id>
|
||||
Uninstall {
|
||||
/// module id
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// enable module <id>
|
||||
Enable {
|
||||
/// module id
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// disable module <id>
|
||||
Disable {
|
||||
// module id
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// run action for module <id>
|
||||
Action {
|
||||
// module id
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// list all modules
|
||||
List,
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand, Debug)]
|
||||
enum Sepolicy {
|
||||
/// Check if sepolicy statement is supported/valid
|
||||
Check {
|
||||
/// sepolicy statements
|
||||
sepolicy: String,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn run() -> Result<()> {
|
||||
#[cfg(target_os = "android")]
|
||||
android_logger::init_once(
|
||||
Config::default()
|
||||
.with_max_level(LevelFilter::Trace) // limit log level
|
||||
.with_tag("APatchD")
|
||||
.with_filter(
|
||||
android_logger::FilterBuilder::new()
|
||||
.filter_level(LevelFilter::Trace)
|
||||
.filter_module("notify", LevelFilter::Warn)
|
||||
.build(),
|
||||
),
|
||||
);
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
env_logger::init();
|
||||
|
||||
// the kernel executes su with argv[0] = "/system/bin/kp" or "/system/bin/su" or "su" or "kp" and replace it with us
|
||||
let arg0 = std::env::args().next().unwrap_or_default();
|
||||
if arg0.ends_with("kp") || arg0.ends_with("su") {
|
||||
return crate::apd::root_shell();
|
||||
}
|
||||
|
||||
let cli = Args::parse();
|
||||
|
||||
log::info!("command: {:?}", cli.command);
|
||||
|
||||
if let Some(ref _superkey) = cli.superkey {
|
||||
supercall::privilege_apd_profile(&cli.superkey);
|
||||
}
|
||||
|
||||
let result = match cli.command {
|
||||
Commands::PostFsData => event::on_post_data_fs(cli.superkey),
|
||||
|
||||
Commands::BootCompleted => event::on_boot_completed(cli.superkey),
|
||||
|
||||
Commands::UidListener => event::start_uid_listener(),
|
||||
|
||||
Commands::Module { command } => {
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
{
|
||||
utils::switch_mnt_ns(1)?;
|
||||
}
|
||||
match command {
|
||||
Module::Install { zip } => module::install_module(&zip),
|
||||
Module::Uninstall { id } => module::uninstall_module(&id),
|
||||
Module::Action { id } => module::run_action(&id),
|
||||
Module::Enable { id } => module::enable_module(&id),
|
||||
Module::Disable { id } => module::disable_module(&id),
|
||||
Module::List => module::list_modules(),
|
||||
}
|
||||
}
|
||||
|
||||
Commands::Sepolicy { command } => match command {
|
||||
Sepolicy::Check { sepolicy } => crate::sepolicy::check_rule(&sepolicy),
|
||||
},
|
||||
|
||||
Commands::Services => event::on_services(cli.superkey),
|
||||
};
|
||||
|
||||
if let Err(e) = &result {
|
||||
log::error!("Error: {:?}", e);
|
||||
}
|
||||
result
|
||||
}
|
||||
36
apd/src/defs.rs
Normal file
36
apd/src/defs.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
use const_format::concatcp;
|
||||
|
||||
pub const ADB_DIR: &str = "/data/adb/";
|
||||
pub const WORKING_DIR: &str = concatcp!(ADB_DIR, "ap/");
|
||||
pub const BINARY_DIR: &str = concatcp!(WORKING_DIR, "bin/");
|
||||
pub const APATCH_LOG_FOLDER: &str = concatcp!(WORKING_DIR, "log/");
|
||||
|
||||
pub const AP_RC_PATH: &str = concatcp!(WORKING_DIR, ".aprc");
|
||||
pub const GLOBAL_NAMESPACE_FILE: &str = concatcp!(ADB_DIR, ".global_namespace_enable");
|
||||
pub const LITEMODE_FILE: &str = concatcp!(ADB_DIR, ".litemode_enable");
|
||||
pub const FORCE_OVERLAYFS_FILE: &str = concatcp!(ADB_DIR, ".overlayfs_enable");
|
||||
pub const AP_OVERLAY_SOURCE: &str = "APatch";
|
||||
pub const DAEMON_PATH: &str = concatcp!(ADB_DIR, "apd");
|
||||
|
||||
pub const MODULE_DIR: &str = concatcp!(ADB_DIR, "modules/");
|
||||
pub const MODULE_UPDATE_TMP_IMG: &str = concatcp!(WORKING_DIR, "update_tmp.img");
|
||||
|
||||
// warning: this directory should not change, or you need to change the code in module_installer.sh!!!
|
||||
pub const MODULE_UPDATE_TMP_DIR: &str = concatcp!(ADB_DIR, "modules_update/");
|
||||
pub const MODULE_MOUNT_DIR: &str = concatcp!(ADB_DIR, "modules_mount/");
|
||||
|
||||
pub const SYSTEM_RW_DIR: &str = concatcp!(MODULE_DIR, ".rw/");
|
||||
|
||||
pub const TEMP_DIR: &str = "/debug_ramdisk";
|
||||
pub const TEMP_DIR_LEGACY: &str = "/sbin";
|
||||
|
||||
pub const MODULE_WEB_DIR: &str = "webroot";
|
||||
pub const MODULE_ACTION_SH: &str = "action.sh";
|
||||
pub const DISABLE_FILE_NAME: &str = "disable";
|
||||
pub const UPDATE_FILE_NAME: &str = "update";
|
||||
pub const REMOVE_FILE_NAME: &str = "remove";
|
||||
pub const SKIP_MOUNT_FILE_NAME: &str = "skip_mount";
|
||||
pub const PTS_NAME: &str = "pts";
|
||||
|
||||
pub const VERSION_CODE: &str = include_str!(concat!(env!("OUT_DIR"), "/VERSION_CODE"));
|
||||
pub const VERSION_NAME: &str = include_str!(concat!(env!("OUT_DIR"), "/VERSION_NAME"));
|
||||
606
apd/src/event.rs
Normal file
606
apd/src/event.rs
Normal file
|
|
@ -0,0 +1,606 @@
|
|||
use crate::magic_mount;
|
||||
use crate::module;
|
||||
use crate::supercall::fork_for_result;
|
||||
use crate::utils::{ensure_dir_exists, ensure_file_exists, get_work_dir, switch_cgroups};
|
||||
use crate::{
|
||||
assets, defs, mount, restorecon, supercall,
|
||||
supercall::{init_load_package_uid_config, init_load_su_path, refresh_ap_package_list},
|
||||
utils::{self, ensure_clean_dir},
|
||||
};
|
||||
use anyhow::{Context, Result, bail, ensure};
|
||||
use extattr::{Flags as XattrFlags, lgetxattr, lsetxattr};
|
||||
use libc::SIGPWR;
|
||||
use log::{info, warn};
|
||||
use notify::event::{ModifyKind, RenameMode};
|
||||
use notify::{Config, Event, EventKind, INotifyWatcher, RecursiveMode, Watcher};
|
||||
use rustix::mount::*;
|
||||
use signal_hook::consts::signal::*;
|
||||
use signal_hook::iterator::Signals;
|
||||
use std::ffi::CStr;
|
||||
use std::fs::{remove_dir_all, rename};
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use std::{collections::HashMap, thread};
|
||||
use std::{env, fs, io};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
fn copy_with_xattr(src: &Path, dest: &Path) -> io::Result<()> {
|
||||
fs::copy(src, dest)?;
|
||||
|
||||
if let Ok(xattr_value) = lgetxattr(src, "security.selinux") {
|
||||
lsetxattr(dest, "security.selinux", &xattr_value, XattrFlags::empty())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn copy_dir_with_xattr(src: &Path, dest: &Path) -> io::Result<()> {
|
||||
for entry in WalkDir::new(src) {
|
||||
let entry = entry?;
|
||||
let rel_path = entry
|
||||
.path()
|
||||
.strip_prefix(src)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
|
||||
let target_path = dest.join(rel_path);
|
||||
if entry.file_type().is_dir() {
|
||||
fs::create_dir_all(&target_path)?;
|
||||
} else if entry.file_type().is_file() {
|
||||
copy_with_xattr(entry.path(), &target_path)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mount_partition(partition_name: &str, lowerdir: &Vec<String>) -> Result<()> {
|
||||
if lowerdir.is_empty() {
|
||||
warn!("partition: {partition_name} lowerdir is empty");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let partition = format!("/{partition_name}");
|
||||
|
||||
// if /partition is a symlink and linked to /system/partition, then we don't need to overlay it separately
|
||||
if Path::new(&partition).read_link().is_ok() {
|
||||
warn!("partition: {partition} is a symlink");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut workdir = None;
|
||||
let mut upperdir = None;
|
||||
let system_rw_dir = Path::new(defs::SYSTEM_RW_DIR);
|
||||
if system_rw_dir.exists() {
|
||||
workdir = Some(system_rw_dir.join(partition_name).join("workdir"));
|
||||
upperdir = Some(system_rw_dir.join(partition_name).join("upperdir"));
|
||||
}
|
||||
|
||||
mount::mount_overlay(&partition, lowerdir, workdir, upperdir)
|
||||
}
|
||||
|
||||
pub fn mount_systemlessly(module_dir: &str, is_img: bool) -> Result<()> {
|
||||
// construct overlay mount params
|
||||
if !is_img {
|
||||
info!("fallback to modules.img");
|
||||
let module_update_dir = defs::MODULE_DIR;
|
||||
let module_dir = defs::MODULE_MOUNT_DIR;
|
||||
let tmp_module_img = defs::MODULE_UPDATE_TMP_IMG;
|
||||
let tmp_module_path = Path::new(tmp_module_img);
|
||||
|
||||
ensure_clean_dir(module_dir)?;
|
||||
info!("- Preparing image");
|
||||
|
||||
let module_update_flag = Path::new(defs::WORKING_DIR).join(defs::UPDATE_FILE_NAME);
|
||||
|
||||
if !tmp_module_path.exists() {
|
||||
ensure_file_exists(&module_update_flag)?;
|
||||
}
|
||||
|
||||
if module_update_flag.exists() {
|
||||
if tmp_module_path.exists() {
|
||||
//if it has update, remove tmp file
|
||||
fs::remove_file(tmp_module_path)?;
|
||||
}
|
||||
let total_size = calculate_total_size(Path::new(module_update_dir))?; //create modules adapt size
|
||||
info!(
|
||||
"Total size of files in '{}': {} bytes",
|
||||
tmp_module_path.display(),
|
||||
total_size
|
||||
);
|
||||
let grow_size = 128 * 1024 * 1024 + total_size;
|
||||
fs::File::create(tmp_module_img)
|
||||
.context("Failed to create ext4 image file")?
|
||||
.set_len(grow_size)
|
||||
.context("Failed to extend ext4 image")?;
|
||||
let result = Command::new("mkfs.ext4")
|
||||
.arg("-b")
|
||||
.arg("1024")
|
||||
.arg(tmp_module_img)
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.output()?;
|
||||
ensure!(
|
||||
result.status.success(),
|
||||
"Failed to format ext4 image: {}",
|
||||
String::from_utf8(result.stderr)?
|
||||
);
|
||||
info!("Checking Image");
|
||||
module::check_image(tmp_module_img)?;
|
||||
}
|
||||
info!("- Mounting image");
|
||||
mount::AutoMountExt4::try_new(tmp_module_img, module_dir, false)
|
||||
.with_context(|| "mount module image failed".to_string())?;
|
||||
info!("mounted {} to {}", tmp_module_img, module_dir);
|
||||
let _ = restorecon::setsyscon(module_dir);
|
||||
if module_update_flag.exists() {
|
||||
let command_string = format!(
|
||||
"cp --preserve=context -RP {}* {};",
|
||||
module_update_dir, module_dir
|
||||
);
|
||||
let args = vec!["-c", &command_string];
|
||||
let _ = utils::run_command("sh", &args, None)?.wait()?;
|
||||
}
|
||||
mount_systemlessly(module_dir, true)?;
|
||||
return Ok(());
|
||||
}
|
||||
let module_dir_origin = Path::new(defs::MODULE_DIR);
|
||||
let dir = fs::read_dir(module_dir);
|
||||
let Ok(dir) = dir else {
|
||||
bail!("open {} failed", defs::MODULE_DIR);
|
||||
};
|
||||
|
||||
let mut system_lowerdir: Vec<String> = Vec::new();
|
||||
|
||||
let partition = vec!["vendor", "product", "system_ext", "odm", "oem"];
|
||||
let mut partition_lowerdir: HashMap<String, Vec<String>> = HashMap::new();
|
||||
for ele in &partition {
|
||||
partition_lowerdir.insert((*ele).to_string(), Vec::new());
|
||||
}
|
||||
|
||||
for entry in dir.flatten() {
|
||||
let module = entry.path();
|
||||
if !module.is_dir() {
|
||||
continue;
|
||||
}
|
||||
if let Some(module_name) = module.file_name() {
|
||||
let real_module_path = module_dir_origin.join(module_name);
|
||||
|
||||
let disabled = real_module_path.join(defs::DISABLE_FILE_NAME).exists();
|
||||
|
||||
if disabled {
|
||||
info!("module: {} is disabled, ignore!", module.display());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let skip_mount = module.join(defs::SKIP_MOUNT_FILE_NAME).exists();
|
||||
if skip_mount {
|
||||
info!("module: {} skip_mount exist, skip!", module.display());
|
||||
continue;
|
||||
}
|
||||
|
||||
let module_system = Path::new(&module).join("system");
|
||||
if module_system.is_dir() {
|
||||
system_lowerdir.push(format!("{}", module_system.display()));
|
||||
}
|
||||
|
||||
for part in &partition {
|
||||
// if /partition is a mountpoint, we would move it to $MODPATH/$partition when install
|
||||
// otherwise it must be a symlink and we don't need to overlay!
|
||||
let part_path = Path::new(&module).join(part);
|
||||
if part_path.is_dir() {
|
||||
if let Some(v) = partition_lowerdir.get_mut(*part) {
|
||||
v.push(format!("{}", part_path.display()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mount /system first
|
||||
if let Err(e) = mount_partition("system", &system_lowerdir) {
|
||||
warn!("mount system failed: {:#}", e);
|
||||
}
|
||||
|
||||
// mount other partitions
|
||||
for (k, v) in partition_lowerdir {
|
||||
if let Err(e) = mount_partition(&k, &v) {
|
||||
warn!("mount {k} failed: {:#}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn systemless_bind_mount(_module_dir: &str) -> Result<()> {
|
||||
// call magisk mount
|
||||
magic_mount::magic_mount()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn calculate_total_size(path: &Path) -> io::Result<u64> {
|
||||
let mut total_size = 0;
|
||||
if path.is_dir() {
|
||||
for entry in fs::read_dir(path)? {
|
||||
let entry = entry?;
|
||||
let file_type = entry.file_type()?;
|
||||
if file_type.is_file() {
|
||||
total_size += entry.metadata()?.len();
|
||||
} else if file_type.is_dir() {
|
||||
total_size += calculate_total_size(&entry.path())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(total_size)
|
||||
}
|
||||
pub fn move_file(module_update_dir: &str, module_dir: &str) -> Result<()> {
|
||||
for entry in fs::read_dir(module_update_dir)? {
|
||||
let entry = entry?;
|
||||
let file_name = entry.file_name();
|
||||
let file_name_str = file_name.to_string_lossy();
|
||||
|
||||
if entry.path().is_dir() {
|
||||
let source_path = Path::new(module_update_dir).join(file_name_str.as_ref());
|
||||
let target_path = Path::new(module_dir).join(file_name_str.as_ref());
|
||||
let update = target_path.join(defs::UPDATE_FILE_NAME).exists();
|
||||
if update {
|
||||
if target_path.exists() {
|
||||
info!(
|
||||
"Removing existing folder in target directory: {}",
|
||||
file_name_str
|
||||
);
|
||||
remove_dir_all(&target_path)?;
|
||||
}
|
||||
|
||||
info!("Moving {} to target directory", file_name_str);
|
||||
rename(&source_path, &target_path)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn on_post_data_fs(superkey: Option<String>) -> Result<()> {
|
||||
utils::umask(0);
|
||||
use std::process::Stdio;
|
||||
#[cfg(unix)]
|
||||
init_load_package_uid_config(&superkey);
|
||||
|
||||
init_load_su_path(&superkey);
|
||||
|
||||
let args = ["/data/adb/ap/bin/magiskpolicy", "--magisk", "--live"];
|
||||
fork_for_result("/data/adb/ap/bin/magiskpolicy", &args, &superkey);
|
||||
|
||||
info!("Re-privilege apd profile after injecting sepolicy");
|
||||
supercall::privilege_apd_profile(&superkey);
|
||||
|
||||
if utils::has_magisk() {
|
||||
warn!("Magisk detected, skip post-fs-data!");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Create log environment
|
||||
if !Path::new(defs::APATCH_LOG_FOLDER).exists() {
|
||||
fs::create_dir(defs::APATCH_LOG_FOLDER).expect("Failed to create log folder");
|
||||
let permissions = fs::Permissions::from_mode(0o700);
|
||||
fs::set_permissions(defs::APATCH_LOG_FOLDER, permissions)
|
||||
.expect("Failed to set permissions");
|
||||
}
|
||||
let command_string = format!(
|
||||
"rm -rf {}*.old.log; for file in {}*; do mv \"$file\" \"$file.old.log\"; done",
|
||||
defs::APATCH_LOG_FOLDER,
|
||||
defs::APATCH_LOG_FOLDER
|
||||
);
|
||||
let mut args = vec!["-c", &command_string];
|
||||
// for all file to .old
|
||||
let result = utils::run_command("sh", &args, None)?.wait()?;
|
||||
if result.success() {
|
||||
info!("Successfully deleted .old files.");
|
||||
} else {
|
||||
info!("Failed to delete .old files.");
|
||||
}
|
||||
let logcat_path = format!("{}locat.log", defs::APATCH_LOG_FOLDER);
|
||||
let dmesg_path = format!("{}dmesg.log", defs::APATCH_LOG_FOLDER);
|
||||
let bootlog = fs::File::create(dmesg_path)?;
|
||||
args = vec![
|
||||
"-s",
|
||||
"9",
|
||||
"120s",
|
||||
"logcat",
|
||||
"-b",
|
||||
"main,system,crash",
|
||||
"-f",
|
||||
&logcat_path,
|
||||
"logcatcher-bootlog:S",
|
||||
"&",
|
||||
];
|
||||
let _ = unsafe {
|
||||
Command::new("timeout")
|
||||
.process_group(0)
|
||||
.pre_exec(|| {
|
||||
switch_cgroups();
|
||||
Ok(())
|
||||
})
|
||||
.args(args)
|
||||
.spawn()
|
||||
};
|
||||
args = vec!["-s", "9", "120s", "dmesg", "-w"];
|
||||
let _result = unsafe {
|
||||
Command::new("timeout")
|
||||
.process_group(0)
|
||||
.pre_exec(|| {
|
||||
switch_cgroups();
|
||||
Ok(())
|
||||
})
|
||||
.args(args)
|
||||
.stdout(Stdio::from(bootlog))
|
||||
.spawn()
|
||||
};
|
||||
|
||||
let key = "KERNELPATCH_VERSION";
|
||||
match env::var(key) {
|
||||
Ok(value) => println!("{}: {}", key, value),
|
||||
Err(_) => println!("{} not found", key),
|
||||
}
|
||||
|
||||
let key = "KERNEL_VERSION";
|
||||
match env::var(key) {
|
||||
Ok(value) => println!("{}: {}", key, value),
|
||||
Err(_) => println!("{} not found", key),
|
||||
}
|
||||
|
||||
let safe_mode = utils::is_safe_mode(superkey.clone());
|
||||
|
||||
if safe_mode {
|
||||
// we should still mount modules.img to `/data/adb/modules` in safe mode
|
||||
// becuase we may need to operate the module dir in safe mode
|
||||
warn!("safe mode, skip common post-fs-data.d scripts");
|
||||
if let Err(e) = module::disable_all_modules() {
|
||||
warn!("disable all modules failed: {}", e);
|
||||
}
|
||||
} else {
|
||||
// Then exec common post-fs-data scripts
|
||||
if let Err(e) = module::exec_common_scripts("post-fs-data.d", true) {
|
||||
warn!("exec common post-fs-data scripts failed: {}", e);
|
||||
}
|
||||
}
|
||||
let module_update_dir = defs::MODULE_UPDATE_TMP_DIR; //save module place
|
||||
let module_dir = defs::MODULE_DIR; // run modules place
|
||||
let module_update_flag = Path::new(defs::WORKING_DIR).join(defs::UPDATE_FILE_NAME); // if update ,there will be renewed modules file
|
||||
assets::ensure_binaries().with_context(|| "binary missing")?;
|
||||
|
||||
if Path::new(defs::MODULE_UPDATE_TMP_DIR).exists() {
|
||||
move_file(module_update_dir, module_dir)?;
|
||||
fs::remove_dir_all(module_update_dir)?;
|
||||
}
|
||||
let is_lite_mode_enabled = Path::new(defs::LITEMODE_FILE).exists();
|
||||
|
||||
if safe_mode {
|
||||
warn!("safe mode, skip post-fs-data scripts and disable all modules!");
|
||||
if let Err(e) = module::disable_all_modules() {
|
||||
warn!("disable all modules failed: {}", e);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Err(e) = module::prune_modules() {
|
||||
warn!("prune modules failed: {}", e);
|
||||
}
|
||||
|
||||
if let Err(e) = restorecon::restorecon() {
|
||||
warn!("restorecon failed: {}", e);
|
||||
}
|
||||
|
||||
// load sepolicy.rule
|
||||
if module::load_sepolicy_rule().is_err() {
|
||||
warn!("load sepolicy.rule failed");
|
||||
}
|
||||
if is_lite_mode_enabled {
|
||||
info!("litemode runing skip mount tempfs")
|
||||
} else {
|
||||
if let Err(e) = mount::mount_tmpfs(utils::get_tmp_path()) {
|
||||
warn!("do temp dir mount failed: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// exec modules post-fs-data scripts
|
||||
// TODO: Add timeout
|
||||
if let Err(e) = module::exec_stage_script("post-fs-data", true) {
|
||||
warn!("exec post-fs-data scripts failed: {}", e);
|
||||
}
|
||||
|
||||
// load system.prop
|
||||
if let Err(e) = module::load_system_prop() {
|
||||
warn!("load system.prop failed: {}", e);
|
||||
}
|
||||
|
||||
if utils::should_use_overlayfs()? {
|
||||
// mount module systemlessly by overlay
|
||||
let work_dir = get_work_dir();
|
||||
let tmp_dir = PathBuf::from(work_dir.clone());
|
||||
ensure_dir_exists(&tmp_dir)?;
|
||||
mount(
|
||||
defs::AP_OVERLAY_SOURCE,
|
||||
&tmp_dir,
|
||||
"tmpfs",
|
||||
MountFlags::empty(),
|
||||
"",
|
||||
)
|
||||
.context("mount tmp")?;
|
||||
mount_change(&tmp_dir, MountPropagationFlags::PRIVATE).context("make tmp private")?;
|
||||
let dir_names = vec!["vendor", "product", "system_ext", "odm", "oem", "system"];
|
||||
let dir = fs::read_dir(module_dir)?;
|
||||
for entry in dir.flatten() {
|
||||
let module_path = entry.path();
|
||||
let disabled = module_path.join(defs::DISABLE_FILE_NAME).exists();
|
||||
if disabled {
|
||||
info!("module: {} is disabled, ignore!", module_path.display());
|
||||
continue;
|
||||
}
|
||||
if module_path.is_dir() {
|
||||
let module_name = module_path.file_name().unwrap().to_string_lossy();
|
||||
let module_dest = Path::new(&work_dir).join(module_name.as_ref());
|
||||
|
||||
for sub_dir in dir_names.iter() {
|
||||
let sub_dir_path = module_path.join(sub_dir);
|
||||
if sub_dir_path.exists() && sub_dir_path.is_dir() {
|
||||
let sub_dir_dest = module_dest.join(sub_dir);
|
||||
fs::create_dir_all(&sub_dir_dest)?;
|
||||
|
||||
copy_dir_with_xattr(&sub_dir_path, &sub_dir_dest)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Err(e) = mount_systemlessly(&get_work_dir(), false) {
|
||||
warn!("do systemless mount failed: {}", e);
|
||||
}
|
||||
if let Err(e) = unmount(&tmp_dir, UnmountFlags::DETACH) {
|
||||
log::error!("failed to unmount tmp {}", e);
|
||||
}
|
||||
} else {
|
||||
if !is_lite_mode_enabled {
|
||||
if let Err(e) = systemless_bind_mount(module_dir) {
|
||||
warn!("do systemless bind_mount failed: {}", e);
|
||||
}
|
||||
} else {
|
||||
info!("litemode runing skip magic mount");
|
||||
}
|
||||
}
|
||||
|
||||
info!("remove update flag");
|
||||
let _ = fs::remove_file(module_update_flag);
|
||||
|
||||
|
||||
|
||||
run_stage("post-mount", superkey, true);
|
||||
|
||||
env::set_current_dir("/").with_context(|| "failed to chdir to /")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_stage(stage: &str, superkey: Option<String>, block: bool) {
|
||||
utils::umask(0);
|
||||
|
||||
if utils::has_magisk() {
|
||||
warn!("Magisk detected, skip {stage}");
|
||||
return;
|
||||
}
|
||||
|
||||
if utils::is_safe_mode(superkey) {
|
||||
warn!("safe mode, skip {stage} scripts");
|
||||
if let Err(e) = module::disable_all_modules() {
|
||||
warn!("disable all modules failed: {}", e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(e) = module::exec_common_scripts(&format!("{stage}.d"), block) {
|
||||
warn!("Failed to exec common {stage} scripts: {e}");
|
||||
}
|
||||
if let Err(e) = module::exec_stage_script(stage, block) {
|
||||
warn!("Failed to exec {stage} scripts: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_services(superkey: Option<String>) -> Result<()> {
|
||||
info!("on_services triggered!");
|
||||
run_stage("service", superkey, false);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_uid_monitor() {
|
||||
info!("Trigger run_uid_monitor!");
|
||||
|
||||
let mut command = &mut Command::new("/data/adb/apd");
|
||||
{
|
||||
command = command.process_group(0);
|
||||
command = unsafe {
|
||||
command.pre_exec(|| {
|
||||
// ignore the error?
|
||||
switch_cgroups();
|
||||
Ok(())
|
||||
})
|
||||
};
|
||||
}
|
||||
command = command.arg("uid-listener");
|
||||
|
||||
command
|
||||
.spawn()
|
||||
.map(|_| ())
|
||||
.expect("[run_uid_monitor] Failed to run uid monitor");
|
||||
}
|
||||
|
||||
pub fn on_boot_completed(superkey: Option<String>) -> Result<()> {
|
||||
info!("on_boot_completed triggered!");
|
||||
|
||||
run_stage("boot-completed", superkey, false);
|
||||
|
||||
run_uid_monitor();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn start_uid_listener() -> Result<()> {
|
||||
info!("start_uid_listener triggered!");
|
||||
println!("[start_uid_listener] Registering...");
|
||||
|
||||
// create inotify instance
|
||||
const SYS_PACKAGES_LIST_TMP: &str = "/data/system/packages.list.tmp";
|
||||
let sys_packages_list_tmp = PathBuf::from(&SYS_PACKAGES_LIST_TMP);
|
||||
let dir: PathBuf = sys_packages_list_tmp.parent().unwrap().into();
|
||||
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let tx_clone = tx.clone();
|
||||
let mutex = Arc::new(Mutex::new(()));
|
||||
|
||||
{
|
||||
let mutex_clone = mutex.clone();
|
||||
thread::spawn(move || {
|
||||
let mut signals = Signals::new(&[SIGTERM, SIGINT, SIGPWR]).unwrap();
|
||||
for sig in signals.forever() {
|
||||
log::warn!("[shutdown] Caught signal {sig}, refreshing package list...");
|
||||
let skey = CStr::from_bytes_with_nul(b"su\0")
|
||||
.expect("[shutdown_listener] CStr::from_bytes_with_nul failed");
|
||||
refresh_ap_package_list(&skey, &mutex_clone);
|
||||
break; // 执行一次后退出线程
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let mut watcher = INotifyWatcher::new(
|
||||
move |ev: notify::Result<Event>| match ev {
|
||||
Ok(Event {
|
||||
kind: EventKind::Modify(ModifyKind::Name(RenameMode::Both)),
|
||||
paths,
|
||||
..
|
||||
}) => {
|
||||
if paths.contains(&sys_packages_list_tmp) {
|
||||
info!("[uid_monitor] System packages list changed, sending to tx...");
|
||||
tx_clone.send(false).unwrap()
|
||||
}
|
||||
}
|
||||
Err(err) => warn!("inotify error: {err}"),
|
||||
_ => (),
|
||||
},
|
||||
Config::default(),
|
||||
)?;
|
||||
|
||||
watcher.watch(dir.as_ref(), RecursiveMode::NonRecursive)?;
|
||||
|
||||
let mut debounce = false;
|
||||
while let Ok(delayed) = rx.recv() {
|
||||
if delayed {
|
||||
debounce = false;
|
||||
let skey = CStr::from_bytes_with_nul(b"su\0")
|
||||
.expect("[start_uid_listener] CStr::from_bytes_with_nul failed");
|
||||
refresh_ap_package_list(&skey, &mutex);
|
||||
} else if !debounce {
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
debounce = true;
|
||||
tx.send(true)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
441
apd/src/installer.sh
Normal file
441
apd/src/installer.sh
Normal file
|
|
@ -0,0 +1,441 @@
|
|||
#!/system/bin/sh
|
||||
############################################
|
||||
# APatch Module installer script
|
||||
# mostly from module_installer.sh
|
||||
# and util_functions.sh in Magisk
|
||||
############################################
|
||||
|
||||
umask 022
|
||||
|
||||
ui_print() {
|
||||
if $BOOTMODE; then
|
||||
echo "$1"
|
||||
else
|
||||
echo -e "ui_print $1\nui_print" >> /proc/self/fd/$OUTFD
|
||||
fi
|
||||
}
|
||||
|
||||
toupper() {
|
||||
echo "$@" | tr '[:lower:]' '[:upper:]'
|
||||
}
|
||||
|
||||
grep_cmdline() {
|
||||
local REGEX="s/^$1=//p"
|
||||
{ echo $(cat /proc/cmdline)$(sed -e 's/[^"]//g' -e 's/""//g' /proc/cmdline) | xargs -n 1; \
|
||||
sed -e 's/ = /=/g' -e 's/, /,/g' -e 's/"//g' /proc/bootconfig; \
|
||||
} 2>/dev/null | sed -n "$REGEX"
|
||||
}
|
||||
|
||||
grep_prop() {
|
||||
local REGEX="s/^$1=//p"
|
||||
shift
|
||||
local FILES=$@
|
||||
[ -z "$FILES" ] && FILES='/system/build.prop'
|
||||
cat $FILES 2>/dev/null | dos2unix | sed -n "$REGEX" | head -n 1
|
||||
}
|
||||
|
||||
grep_get_prop() {
|
||||
local result=$(grep_prop $@)
|
||||
if [ -z "$result" ]; then
|
||||
# Fallback to getprop
|
||||
getprop "$1"
|
||||
else
|
||||
echo $result
|
||||
fi
|
||||
}
|
||||
|
||||
is_mounted() {
|
||||
grep -q " $(readlink -f $1) " /proc/mounts 2>/dev/null
|
||||
return $?
|
||||
}
|
||||
|
||||
abort() {
|
||||
ui_print "$1"
|
||||
$BOOTMODE || recovery_cleanup
|
||||
[ ! -z $MODPATH ] && rm -rf $MODPATH
|
||||
rm -rf $TMPDIR
|
||||
exit 1
|
||||
}
|
||||
|
||||
print_title() {
|
||||
local len line1len line2len bar
|
||||
line1len=$(echo -n $1 | wc -c)
|
||||
line2len=$(echo -n $2 | wc -c)
|
||||
len=$line2len
|
||||
[ $line1len -gt $line2len ] && len=$line1len
|
||||
len=$((len + 2))
|
||||
bar=$(printf "%${len}s" | tr ' ' '*')
|
||||
ui_print "$bar"
|
||||
ui_print " $1 "
|
||||
[ "$2" ] && ui_print " $2 "
|
||||
ui_print "$bar"
|
||||
}
|
||||
|
||||
######################
|
||||
# Environment Related
|
||||
######################
|
||||
|
||||
setup_flashable() {
|
||||
ensure_bb
|
||||
$BOOTMODE && return
|
||||
if [ -z $OUTFD ] || readlink /proc/$$/fd/$OUTFD | grep -q /tmp; then
|
||||
# We will have to manually find out OUTFD
|
||||
for FD in `ls /proc/$$/fd`; do
|
||||
if readlink /proc/$$/fd/$FD | grep -q pipe; then
|
||||
if ps | grep -v grep | grep -qE " 3 $FD |status_fd=$FD"; then
|
||||
OUTFD=$FD
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
recovery_actions
|
||||
}
|
||||
|
||||
ensure_bb() {
|
||||
:
|
||||
}
|
||||
|
||||
recovery_actions() {
|
||||
:
|
||||
}
|
||||
|
||||
recovery_cleanup() {
|
||||
:
|
||||
}
|
||||
|
||||
#######################
|
||||
# Installation Related
|
||||
#######################
|
||||
|
||||
# find_block [partname...]
|
||||
find_block() {
|
||||
local BLOCK DEV DEVICE DEVNAME PARTNAME UEVENT
|
||||
for BLOCK in "$@"; do
|
||||
DEVICE=`find /dev/block \( -type b -o -type c -o -type l \) -iname $BLOCK | head -n 1` 2>/dev/null
|
||||
if [ ! -z $DEVICE ]; then
|
||||
readlink -f $DEVICE
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
# Fallback by parsing sysfs uevents
|
||||
for UEVENT in /sys/dev/block/*/uevent; do
|
||||
DEVNAME=`grep_prop DEVNAME $UEVENT`
|
||||
PARTNAME=`grep_prop PARTNAME $UEVENT`
|
||||
for BLOCK in "$@"; do
|
||||
if [ "$(toupper $BLOCK)" = "$(toupper $PARTNAME)" ]; then
|
||||
echo /dev/block/$DEVNAME
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
done
|
||||
# Look just in /dev in case we're dealing with MTD/NAND without /dev/block devices/links
|
||||
for DEV in "$@"; do
|
||||
DEVICE=`find /dev \( -type b -o -type c -o -type l \) -maxdepth 1 -iname $DEV | head -n 1` 2>/dev/null
|
||||
if [ ! -z $DEVICE ]; then
|
||||
readlink -f $DEVICE
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# setup_mntpoint <mountpoint>
|
||||
setup_mntpoint() {
|
||||
local POINT=$1
|
||||
[ -L $POINT ] && mv -f $POINT ${POINT}_link
|
||||
if [ ! -d $POINT ]; then
|
||||
rm -f $POINT
|
||||
mkdir -p $POINT
|
||||
fi
|
||||
}
|
||||
|
||||
# mount_name <partname(s)> <mountpoint> <flag>
|
||||
mount_name() {
|
||||
local PART=$1
|
||||
local POINT=$2
|
||||
local FLAG=$3
|
||||
setup_mntpoint $POINT
|
||||
is_mounted $POINT && return
|
||||
# First try mounting with fstab
|
||||
mount $FLAG $POINT 2>/dev/null
|
||||
if ! is_mounted $POINT; then
|
||||
local BLOCK=$(find_block $PART)
|
||||
mount $FLAG $BLOCK $POINT || return
|
||||
fi
|
||||
ui_print "- Mounting $POINT"
|
||||
}
|
||||
|
||||
# mount_ro_ensure <partname(s)> <mountpoint>
|
||||
mount_ro_ensure() {
|
||||
# We handle ro partitions only in recovery
|
||||
$BOOTMODE && return
|
||||
local PART=$1
|
||||
local POINT=$2
|
||||
mount_name "$PART" $POINT '-o ro'
|
||||
is_mounted $POINT || abort "! Cannot mount $POINT"
|
||||
}
|
||||
|
||||
mount_partitions() {
|
||||
# Check A/B slot
|
||||
SLOT=`grep_cmdline androidboot.slot_suffix`
|
||||
if [ -z $SLOT ]; then
|
||||
SLOT=`grep_cmdline androidboot.slot`
|
||||
[ -z $SLOT ] || SLOT=_${SLOT}
|
||||
fi
|
||||
[ -z $SLOT ] || ui_print "- Current boot slot: $SLOT"
|
||||
|
||||
# Mount ro partitions
|
||||
if is_mounted /system_root; then
|
||||
umount /system 2&>/dev/null
|
||||
umount /system_root 2&>/dev/null
|
||||
fi
|
||||
mount_ro_ensure "system$SLOT app$SLOT" /system
|
||||
if [ -f /system/init -o -L /system/init ]; then
|
||||
SYSTEM_ROOT=true
|
||||
setup_mntpoint /system_root
|
||||
if ! mount --move /system /system_root; then
|
||||
umount /system
|
||||
umount -l /system 2>/dev/null
|
||||
mount_ro_ensure "system$SLOT app$SLOT" /system_root
|
||||
fi
|
||||
mount -o bind /system_root/system /system
|
||||
else
|
||||
SYSTEM_ROOT=false
|
||||
grep ' / ' /proc/mounts | grep -qv 'rootfs' || grep -q ' /system_root ' /proc/mounts && SYSTEM_ROOT=true
|
||||
fi
|
||||
# /vendor is used only on some older devices for recovery AVBv1 signing so is not critical if fails
|
||||
[ -L /system/vendor ] && mount_name vendor$SLOT /vendor '-o ro'
|
||||
$SYSTEM_ROOT && ui_print "- Device is system-as-root"
|
||||
|
||||
# Mount sepolicy rules dir locations in recovery (best effort)
|
||||
if ! $BOOTMODE; then
|
||||
mount_name "cache cac" /cache
|
||||
mount_name metadata /metadata
|
||||
mount_name persist /persist
|
||||
fi
|
||||
}
|
||||
|
||||
api_level_arch_detect() {
|
||||
API=$(grep_get_prop ro.build.version.sdk)
|
||||
ABI=$(grep_get_prop ro.product.cpu.abi)
|
||||
if [ "$ABI" = "x86" ]; then
|
||||
ARCH=x86
|
||||
ABI32=x86
|
||||
IS64BIT=false
|
||||
elif [ "$ABI" = "arm64-v8a" ]; then
|
||||
ARCH=arm64
|
||||
ABI32=armeabi-v7a
|
||||
IS64BIT=true
|
||||
elif [ "$ABI" = "x86_64" ]; then
|
||||
ARCH=x64
|
||||
ABI32=x86
|
||||
IS64BIT=true
|
||||
else
|
||||
ARCH=arm
|
||||
ABI=armeabi-v7a
|
||||
ABI32=armeabi-v7a
|
||||
IS64BIT=false
|
||||
fi
|
||||
}
|
||||
|
||||
#################
|
||||
# Module Related
|
||||
#################
|
||||
|
||||
set_perm() {
|
||||
chown $2:$3 $1 || return 1
|
||||
chmod $4 $1 || return 1
|
||||
local CON=$5
|
||||
[ -z $CON ] && CON=u:object_r:system_file:s0
|
||||
chcon $CON $1 || return 1
|
||||
}
|
||||
|
||||
set_perm_recursive() {
|
||||
find $1 -type d 2>/dev/null | while read dir; do
|
||||
set_perm $dir $2 $3 $4 $6
|
||||
done
|
||||
find $1 -type f -o -type l 2>/dev/null | while read file; do
|
||||
set_perm $file $2 $3 $5 $6
|
||||
done
|
||||
}
|
||||
|
||||
mktouch() {
|
||||
mkdir -p ${1%/*} 2>/dev/null
|
||||
[ -z $2 ] && touch $1 || echo $2 > $1
|
||||
chmod 644 $1
|
||||
}
|
||||
|
||||
mark_remove() {
|
||||
mkdir -p ${1%/*} 2>/dev/null
|
||||
mknod $1 c 0 0
|
||||
chmod 644 $1
|
||||
}
|
||||
|
||||
mark_replace() {
|
||||
# REPLACE must be directory!!!
|
||||
# https://docs.kernel.org/filesystems/overlayfs.html#whiteouts-and-opaque-directories
|
||||
mkdir -p $1 2>/dev/null
|
||||
setfattr -n trusted.overlay.opaque -v y $1
|
||||
chmod 644 $1
|
||||
}
|
||||
|
||||
request_size_check() {
|
||||
reqSizeM=`du -ms "$1" | cut -f1`
|
||||
}
|
||||
|
||||
request_zip_size_check() {
|
||||
reqSizeM=`unzip -l "$1" | tail -n 1 | awk '{ print int(($1 - 1) / 1048576 + 1) }'`
|
||||
}
|
||||
|
||||
boot_actions() { return; }
|
||||
|
||||
# Require ZIPFILE to be set
|
||||
is_legacy_script() {
|
||||
unzip -l "$ZIPFILE" install.sh | grep -q install.sh
|
||||
return $?
|
||||
}
|
||||
|
||||
handle_partition() {
|
||||
# if /system/vendor is a symlink, we need to move it out of $MODPATH/system, otherwise it will be overlayed
|
||||
# if /system/vendor is a normal directory, it is ok to overlay it and we don't need to overlay it separately.
|
||||
if [ ! -e $MODPATH/system/$1 ]; then
|
||||
# no partition found
|
||||
return;
|
||||
fi
|
||||
|
||||
if [ -L "/system/$1" ] && [ "$(readlink -f /system/$1)" = "/$1" ]; then
|
||||
ui_print "- Handle partition /$1"
|
||||
# we create a symlink if module want to access $MODPATH/system/$1
|
||||
# but it doesn't always work(ie. write it in post-fs-data.sh would fail because it is readonly)
|
||||
mv -f $MODPATH/system/$1 $MODPATH/$1 && ln -sf ../$1 $MODPATH/system/$1
|
||||
fi
|
||||
}
|
||||
|
||||
# Require OUTFD, ZIPFILE to be set
|
||||
install_module() {
|
||||
rm -rf $TMPDIR
|
||||
mkdir -p $TMPDIR
|
||||
chcon u:object_r:system_file:s0 $TMPDIR
|
||||
cd $TMPDIR
|
||||
|
||||
mount_partitions
|
||||
api_level_arch_detect
|
||||
|
||||
# Setup busybox and binaries
|
||||
if $BOOTMODE; then
|
||||
boot_actions
|
||||
else
|
||||
recovery_actions
|
||||
fi
|
||||
|
||||
# Extract prop file
|
||||
unzip -o "$ZIPFILE" module.prop -d $TMPDIR >&2
|
||||
[ ! -f $TMPDIR/module.prop ] && abort "! Unable to extract zip file!"
|
||||
|
||||
local MODDIRNAME=modules
|
||||
$BOOTMODE && MODDIRNAME=modules_update
|
||||
local MODULEROOT=$NVBASE/$MODDIRNAME
|
||||
MODID=`grep_prop id $TMPDIR/module.prop`
|
||||
MODNAME=`grep_prop name $TMPDIR/module.prop`
|
||||
MODAUTH=`grep_prop author $TMPDIR/module.prop`
|
||||
MODPATH=$MODULEROOT/$MODID
|
||||
|
||||
# Create mod paths
|
||||
rm -rf $MODPATH
|
||||
mkdir -p $MODPATH
|
||||
|
||||
if is_legacy_script; then
|
||||
unzip -oj "$ZIPFILE" module.prop install.sh uninstall.sh 'common/*' -d $TMPDIR >&2
|
||||
|
||||
# Load install script
|
||||
. $TMPDIR/install.sh
|
||||
|
||||
# Callbacks
|
||||
print_modname
|
||||
on_install
|
||||
|
||||
[ -f $TMPDIR/uninstall.sh ] && cp -af $TMPDIR/uninstall.sh $MODPATH/uninstall.sh
|
||||
$SKIPMOUNT && touch $MODPATH/skip_mount
|
||||
$PROPFILE && cp -af $TMPDIR/system.prop $MODPATH/system.prop
|
||||
cp -af $TMPDIR/module.prop $MODPATH/module.prop
|
||||
$POSTFSDATA && cp -af $TMPDIR/post-fs-data.sh $MODPATH/post-fs-data.sh
|
||||
$LATESTARTSERVICE && cp -af $TMPDIR/service.sh $MODPATH/service.sh
|
||||
|
||||
ui_print "- Setting permissions"
|
||||
set_permissions
|
||||
else
|
||||
print_title "$MODNAME" "by $MODAUTH"
|
||||
print_title "Powered by APatch"
|
||||
|
||||
unzip -o "$ZIPFILE" customize.sh -d $MODPATH >&2
|
||||
|
||||
if ! grep -q '^SKIPUNZIP=1$' $MODPATH/customize.sh 2>/dev/null; then
|
||||
ui_print "- Extracting module files"
|
||||
unzip -o "$ZIPFILE" -x 'META-INF/*' -d $MODPATH >&2
|
||||
|
||||
# Default permissions
|
||||
set_perm_recursive $MODPATH 0 0 0755 0644
|
||||
set_perm_recursive $MODPATH/system/bin 0 2000 0755 0755
|
||||
set_perm_recursive $MODPATH/system/xbin 0 2000 0755 0755
|
||||
set_perm_recursive $MODPATH/system/system_ext/bin 0 2000 0755 0755
|
||||
set_perm_recursive $MODPATH/system/vendor 0 2000 0755 0755 u:object_r:vendor_file:s0
|
||||
fi
|
||||
|
||||
# Load customization script
|
||||
[ -f $MODPATH/customize.sh ] && . $MODPATH/customize.sh
|
||||
fi
|
||||
|
||||
# Handle replace folders
|
||||
for TARGET in $REPLACE; do
|
||||
ui_print "- Replace target: $TARGET"
|
||||
mark_replace $MODPATH$TARGET
|
||||
done
|
||||
|
||||
# Handle remove files
|
||||
for TARGET in $REMOVE; do
|
||||
ui_print "- Remove target: $TARGET"
|
||||
mark_remove $MODPATH$TARGET
|
||||
done
|
||||
|
||||
handle_partition vendor
|
||||
handle_partition system_ext
|
||||
handle_partition product
|
||||
|
||||
if $BOOTMODE; then
|
||||
mktouch $NVBASE/modules/$MODID/update
|
||||
rm -rf $NVBASE/modules/$MODID/remove 2>/dev/null
|
||||
rm -rf $NVBASE/modules/$MODID/disable 2>/dev/null
|
||||
cp -af $MODPATH/module.prop $NVBASE/modules/$MODID/module.prop
|
||||
fi
|
||||
|
||||
# Remove stuff that doesn't belong to modules and clean up any empty directories
|
||||
rm -rf \
|
||||
$MODPATH/system/placeholder $MODPATH/customize.sh \
|
||||
$MODPATH/README.md $MODPATH/.git*
|
||||
rmdir -p $MODPATH 2>/dev/null
|
||||
|
||||
cd /
|
||||
$BOOTMODE || recovery_cleanup
|
||||
rm -rf $TMPDIR
|
||||
|
||||
ui_print "- Done"
|
||||
}
|
||||
|
||||
##########
|
||||
# Presets
|
||||
##########
|
||||
|
||||
# Detect whether in boot mode
|
||||
[ -z $BOOTMODE ] && ps | grep zygote | grep -qv grep && BOOTMODE=true
|
||||
[ -z $BOOTMODE ] && ps -A 2>/dev/null | grep zygote | grep -qv grep && BOOTMODE=true
|
||||
[ -z $BOOTMODE ] && BOOTMODE=false
|
||||
|
||||
NVBASE=/data/adb
|
||||
TMPDIR=/dev/tmp
|
||||
POSTFSDATAD=$NVBASE/post-fs-data.d
|
||||
SERVICED=$NVBASE/service.d
|
||||
|
||||
# Some modules dependents on this
|
||||
export MAGISK_VER=27.0
|
||||
export MAGISK_VER_CODE=27000
|
||||
445
apd/src/installer_bind.sh
Normal file
445
apd/src/installer_bind.sh
Normal file
|
|
@ -0,0 +1,445 @@
|
|||
#!/system/bin/sh
|
||||
############################################
|
||||
# APatch Module installer script
|
||||
# mostly from module_installer.sh
|
||||
# and util_functions.sh in Magisk
|
||||
############################################
|
||||
|
||||
umask 022
|
||||
|
||||
ui_print() {
|
||||
if $BOOTMODE; then
|
||||
echo "$1"
|
||||
else
|
||||
echo -e "ui_print $1\nui_print" >> /proc/self/fd/$OUTFD
|
||||
fi
|
||||
}
|
||||
|
||||
toupper() {
|
||||
echo "$@" | tr '[:lower:]' '[:upper:]'
|
||||
}
|
||||
|
||||
grep_cmdline() {
|
||||
local REGEX="s/^$1=//p"
|
||||
{ echo $(cat /proc/cmdline)$(sed -e 's/[^"]//g' -e 's/""//g' /proc/cmdline) | xargs -n 1; \
|
||||
sed -e 's/ = /=/g' -e 's/, /,/g' -e 's/"//g' /proc/bootconfig; \
|
||||
} 2>/dev/null | sed -n "$REGEX"
|
||||
}
|
||||
|
||||
grep_prop() {
|
||||
local REGEX="s/$1=//p"
|
||||
shift
|
||||
local FILES=$@
|
||||
[ -z "$FILES" ] && FILES='/system/build.prop'
|
||||
cat $FILES 2>/dev/null | dos2unix | sed -n "$REGEX" | head -n 1 | xargs
|
||||
}
|
||||
|
||||
grep_get_prop() {
|
||||
local result=$(grep_prop $@)
|
||||
if [ -z "$result" ]; then
|
||||
# Fallback to getprop
|
||||
getprop "$1"
|
||||
else
|
||||
echo $result
|
||||
fi
|
||||
}
|
||||
|
||||
is_mounted() {
|
||||
grep -q " $(readlink -f $1) " /proc/mounts 2>/dev/null
|
||||
return $?
|
||||
}
|
||||
|
||||
abort() {
|
||||
ui_print "$1"
|
||||
$BOOTMODE || recovery_cleanup
|
||||
[ ! -z $MODPATH ] && rm -rf $MODPATH
|
||||
rm -rf $TMPDIR
|
||||
exit 1
|
||||
}
|
||||
|
||||
print_title() {
|
||||
local len line1len line2len bar
|
||||
line1len=$(echo -n $1 | wc -c)
|
||||
line2len=$(echo -n $2 | wc -c)
|
||||
len=$line2len
|
||||
[ $line1len -gt $line2len ] && len=$line1len
|
||||
len=$((len + 2))
|
||||
bar=$(printf "%${len}s" | tr ' ' '*')
|
||||
ui_print "$bar"
|
||||
ui_print " $1 "
|
||||
[ "$2" ] && ui_print " $2 "
|
||||
ui_print "$bar"
|
||||
}
|
||||
|
||||
check_sepolicy() {
|
||||
/data/adb/apd sepolicy check "$1"
|
||||
return $?
|
||||
}
|
||||
|
||||
######################
|
||||
# Environment Related
|
||||
######################
|
||||
|
||||
setup_flashable() {
|
||||
ensure_bb
|
||||
$BOOTMODE && return
|
||||
if [ -z $OUTFD ] || readlink /proc/$$/fd/$OUTFD | grep -q /tmp; then
|
||||
# We will have to manually find out OUTFD
|
||||
for FD in /proc/$$/fd/*; do
|
||||
if readlink /proc/$$/fd/$FD | grep -q pipe; then
|
||||
if ps | grep -v grep | grep -qE " 3 $FD |status_fd=$FD"; then
|
||||
OUTFD=$FD
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
recovery_actions
|
||||
}
|
||||
|
||||
ensure_bb() {
|
||||
:
|
||||
}
|
||||
|
||||
recovery_actions() {
|
||||
:
|
||||
}
|
||||
|
||||
recovery_cleanup() {
|
||||
:
|
||||
}
|
||||
|
||||
#######################
|
||||
# Installation Related
|
||||
#######################
|
||||
|
||||
# find_block [partname...]
|
||||
find_block() {
|
||||
local BLOCK DEV DEVICE DEVNAME PARTNAME UEVENT
|
||||
for BLOCK in "$@"; do
|
||||
DEVICE=`find /dev/block \( -type b -o -type c -o -type l \) -iname $BLOCK | head -n 1` 2>/dev/null
|
||||
if [ ! -z $DEVICE ]; then
|
||||
readlink -f $DEVICE
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
# Fallback by parsing sysfs uevents
|
||||
for UEVENT in /sys/dev/block/*/uevent; do
|
||||
DEVNAME=`grep_prop DEVNAME $UEVENT`
|
||||
PARTNAME=`grep_prop PARTNAME $UEVENT`
|
||||
for BLOCK in "$@"; do
|
||||
if [ "$(toupper $BLOCK)" = "$(toupper $PARTNAME)" ]; then
|
||||
echo /dev/block/$DEVNAME
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
done
|
||||
# Look just in /dev in case we're dealing with MTD/NAND without /dev/block devices/links
|
||||
for DEV in "$@"; do
|
||||
DEVICE=`find /dev \( -type b -o -type c -o -type l \) -maxdepth 1 -iname $DEV | head -n 1` 2>/dev/null
|
||||
if [ ! -z $DEVICE ]; then
|
||||
readlink -f $DEVICE
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# setup_mntpoint <mountpoint>
|
||||
setup_mntpoint() {
|
||||
local POINT=$1
|
||||
[ -L $POINT ] && mv -f $POINT ${POINT}_link
|
||||
if [ ! -d $POINT ]; then
|
||||
rm -f $POINT
|
||||
mkdir -p $POINT
|
||||
fi
|
||||
}
|
||||
|
||||
# mount_name <partname(s)> <mountpoint> <flag>
|
||||
mount_name() {
|
||||
local PART=$1
|
||||
local POINT=$2
|
||||
local FLAG=$3
|
||||
setup_mntpoint $POINT
|
||||
is_mounted $POINT && return
|
||||
# First try mounting with fstab
|
||||
mount $FLAG $POINT 2>/dev/null
|
||||
if ! is_mounted $POINT; then
|
||||
local BLOCK=$(find_block $PART)
|
||||
mount $FLAG $BLOCK $POINT || return
|
||||
fi
|
||||
ui_print "- Mounting $POINT"
|
||||
}
|
||||
|
||||
# mount_ro_ensure <partname(s)> <mountpoint>
|
||||
mount_ro_ensure() {
|
||||
# We handle ro partitions only in recovery
|
||||
$BOOTMODE && return
|
||||
local PART=$1
|
||||
local POINT=$2
|
||||
mount_name "$PART" $POINT '-o ro'
|
||||
is_mounted $POINT || abort "! Cannot mount $POINT"
|
||||
}
|
||||
|
||||
mount_partitions() {
|
||||
# Check A/B slot
|
||||
SLOT=`grep_cmdline androidboot.slot_suffix`
|
||||
if [ -z $SLOT ]; then
|
||||
SLOT=`grep_cmdline androidboot.slot`
|
||||
[ -z $SLOT ] || SLOT=_${SLOT}
|
||||
fi
|
||||
[ -z $SLOT ] || ui_print "- Current boot slot: $SLOT"
|
||||
|
||||
# Mount ro partitions
|
||||
if is_mounted /system_root; then
|
||||
umount /system 2&>/dev/null
|
||||
umount /system_root 2&>/dev/null
|
||||
fi
|
||||
mount_ro_ensure "system$SLOT app$SLOT" /system
|
||||
if [ -f /system/init -o -L /system/init ]; then
|
||||
SYSTEM_ROOT=true
|
||||
setup_mntpoint /system_root
|
||||
if ! mount --move /system /system_root; then
|
||||
umount /system
|
||||
umount -l /system 2>/dev/null
|
||||
mount_ro_ensure "system$SLOT app$SLOT" /system_root
|
||||
fi
|
||||
mount -o bind /system_root/system /system
|
||||
else
|
||||
SYSTEM_ROOT=false
|
||||
grep ' / ' /proc/mounts | grep -qv 'rootfs' || grep -q ' /system_root ' /proc/mounts && SYSTEM_ROOT=true
|
||||
fi
|
||||
# /vendor is used only on some older devices for recovery AVBv1 signing so is not critical if fails
|
||||
[ -L /system/vendor ] && mount_name vendor$SLOT /vendor '-o ro'
|
||||
$SYSTEM_ROOT && ui_print "- Device is system-as-root"
|
||||
|
||||
# Mount sepolicy rules dir locations in recovery (best effort)
|
||||
if ! $BOOTMODE; then
|
||||
mount_name "cache cac" /cache
|
||||
mount_name metadata /metadata
|
||||
mount_name persist /persist
|
||||
fi
|
||||
}
|
||||
|
||||
api_level_arch_detect() {
|
||||
API=$(grep_get_prop ro.build.version.sdk)
|
||||
ABI=$(grep_get_prop ro.product.cpu.abi)
|
||||
if [ "$ABI" = "x86" ]; then
|
||||
ARCH=x86
|
||||
ABI32=x86
|
||||
IS64BIT=false
|
||||
elif [ "$ABI" = "arm64-v8a" ]; then
|
||||
ARCH=arm64
|
||||
ABI32=armeabi-v7a
|
||||
IS64BIT=true
|
||||
elif [ "$ABI" = "x86_64" ]; then
|
||||
ARCH=x64
|
||||
ABI32=x86
|
||||
IS64BIT=true
|
||||
else
|
||||
ARCH=arm
|
||||
ABI=armeabi-v7a
|
||||
ABI32=armeabi-v7a
|
||||
IS64BIT=false
|
||||
fi
|
||||
}
|
||||
|
||||
#################
|
||||
# Module Related
|
||||
#################
|
||||
|
||||
set_perm() {
|
||||
chown $2:$3 $1 || return 1
|
||||
chmod $4 $1 || return 1
|
||||
local CON=$5
|
||||
[ -z $CON ] && CON=u:object_r:system_file:s0
|
||||
chcon $CON $1 || return 1
|
||||
}
|
||||
|
||||
set_perm_recursive() {
|
||||
find $1 -type d 2>/dev/null | while read dir; do
|
||||
set_perm $dir $2 $3 $4 $6
|
||||
done
|
||||
find $1 -type f -o -type l 2>/dev/null | while read file; do
|
||||
set_perm $file $2 $3 $5 $6
|
||||
done
|
||||
}
|
||||
|
||||
mktouch() {
|
||||
mkdir -p ${1%/*} 2>/dev/null
|
||||
[ -z $2 ] && touch $1 || echo $2 > $1
|
||||
chmod 644 $1
|
||||
}
|
||||
|
||||
mark_remove() {
|
||||
mkdir -p ${1%/*} 2>/dev/null
|
||||
mknod $1 c 0 0
|
||||
chmod 644 $1
|
||||
}
|
||||
|
||||
mark_replace() {
|
||||
# REPLACE must be directory!!!
|
||||
# https://docs.kernel.org/filesystems/overlayfs.html#whiteouts-and-opaque-directories
|
||||
mkdir -p $1 2>/dev/null
|
||||
setfattr -n trusted.overlay.opaque -v y $1
|
||||
chmod 644 $1
|
||||
}
|
||||
|
||||
request_size_check() {
|
||||
reqSizeM=`du -ms "$1" | cut -f1`
|
||||
}
|
||||
|
||||
request_zip_size_check() {
|
||||
reqSizeM=`unzip -l "$1" | tail -n 1 | awk '{ print int(($1 - 1) / 1048576 + 1) }'`
|
||||
}
|
||||
|
||||
boot_actions() { return; }
|
||||
|
||||
# Require ZIPFILE to be set
|
||||
is_legacy_script() {
|
||||
unzip -l "$ZIPFILE" install.sh | grep -q install.sh
|
||||
return $?
|
||||
}
|
||||
|
||||
handle_partition() {
|
||||
PARTITION="$1"
|
||||
REQUIRE_SYMLINK="$2"
|
||||
if [ ! -e "$MODPATH/system/$PARTITION" ]; then
|
||||
# no partition found
|
||||
return;
|
||||
fi
|
||||
|
||||
if [ "$REQUIRE_SYMLINK" = "false" ] || [ -L "/system/$PARTITION" ] && [ "$(readlink -f "/system/$PARTITION")" = "/$PARTITION" ]; then
|
||||
ui_print "- Handle partition /$PARTITION"
|
||||
ln -sf "./system/$PARTITION" "$MODPATH/$PARTITION"
|
||||
fi
|
||||
}
|
||||
|
||||
# Require OUTFD, ZIPFILE to be set
|
||||
install_module() {
|
||||
rm -rf $TMPDIR
|
||||
mkdir -p $TMPDIR
|
||||
chcon u:object_r:system_file:s0 $TMPDIR
|
||||
cd $TMPDIR
|
||||
|
||||
mount_partitions
|
||||
api_level_arch_detect
|
||||
|
||||
# Setup busybox and binaries
|
||||
if $BOOTMODE; then
|
||||
boot_actions
|
||||
else
|
||||
recovery_actions
|
||||
fi
|
||||
|
||||
# Extract prop file
|
||||
unzip -o "$ZIPFILE" module.prop -d $TMPDIR >&2
|
||||
[ ! -f $TMPDIR/module.prop ] && abort "! Unable to extract zip file!"
|
||||
|
||||
local MODDIRNAME=modules
|
||||
$BOOTMODE && MODDIRNAME=modules_update
|
||||
local MODULEROOT=$NVBASE/$MODDIRNAME
|
||||
MODID=`grep_prop id $TMPDIR/module.prop`
|
||||
MODNAME=`grep_prop name $TMPDIR/module.prop`
|
||||
MODAUTH=`grep_prop author $TMPDIR/module.prop`
|
||||
MODPATH=$MODULEROOT/$MODID
|
||||
|
||||
# Create mod paths
|
||||
rm -rf $MODPATH
|
||||
mkdir -p $MODPATH
|
||||
|
||||
if is_legacy_script; then
|
||||
unzip -oj "$ZIPFILE" module.prop install.sh uninstall.sh 'common/*' -d $TMPDIR >&2
|
||||
|
||||
# Load install script
|
||||
. $TMPDIR/install.sh
|
||||
|
||||
# Callbacks
|
||||
print_modname
|
||||
on_install
|
||||
|
||||
[ -f $TMPDIR/uninstall.sh ] && cp -af $TMPDIR/uninstall.sh $MODPATH/uninstall.sh
|
||||
$SKIPMOUNT && touch $MODPATH/skip_mount
|
||||
$PROPFILE && cp -af $TMPDIR/system.prop $MODPATH/system.prop
|
||||
cp -af $TMPDIR/module.prop $MODPATH/module.prop
|
||||
$POSTFSDATA && cp -af $TMPDIR/post-fs-data.sh $MODPATH/post-fs-data.sh
|
||||
$LATESTARTSERVICE && cp -af $TMPDIR/service.sh $MODPATH/service.sh
|
||||
|
||||
ui_print "- Setting permissions"
|
||||
set_permissions
|
||||
else
|
||||
print_title "$MODNAME" "by $MODAUTH"
|
||||
print_title "Powered by APatch"
|
||||
|
||||
unzip -o "$ZIPFILE" customize.sh -d $MODPATH >&2
|
||||
|
||||
if ! grep -q '^SKIPUNZIP=1$' $MODPATH/customize.sh 2>/dev/null; then
|
||||
ui_print "- Extracting module files"
|
||||
unzip -o "$ZIPFILE" -x 'META-INF/*' -d $MODPATH >&2
|
||||
|
||||
# Default permissions
|
||||
set_perm_recursive $MODPATH 0 0 0755 0644
|
||||
set_perm_recursive $MODPATH/system/bin 0 2000 0755 0755
|
||||
set_perm_recursive $MODPATH/system/xbin 0 2000 0755 0755
|
||||
set_perm_recursive $MODPATH/system/system_ext/bin 0 2000 0755 0755
|
||||
set_perm_recursive $MODPATH/system/vendor 0 2000 0755 0755 u:object_r:vendor_file:s0
|
||||
fi
|
||||
|
||||
# Load customization script
|
||||
[ -f $MODPATH/customize.sh ] && . $MODPATH/customize.sh
|
||||
fi
|
||||
|
||||
handle_partition vendor true
|
||||
handle_partition system_ext true
|
||||
handle_partition product true
|
||||
handle_partition odm false
|
||||
|
||||
# Handle replace folders
|
||||
for TARGET in $REPLACE; do
|
||||
ui_print "- Replace target: $TARGET"
|
||||
mark_replace "$MODPATH$TARGET"
|
||||
done
|
||||
|
||||
# Handle remove files
|
||||
for TARGET in $REMOVE; do
|
||||
ui_print "- Remove target: $TARGET"
|
||||
mark_remove "$MODPATH$TARGET"
|
||||
done
|
||||
|
||||
if $BOOTMODE; then
|
||||
mktouch $NVBASE/modules/$MODID/update
|
||||
rm -rf $NVBASE/modules/$MODID/remove 2>/dev/null
|
||||
rm -rf $NVBASE/modules/$MODID/disable 2>/dev/null
|
||||
cp -af $MODPATH/module.prop $NVBASE/modules/$MODID/module.prop
|
||||
fi
|
||||
|
||||
# Remove stuff that doesn't belong to modules and clean up any empty directories
|
||||
rm -rf \
|
||||
$MODPATH/system/placeholder $MODPATH/customize.sh \
|
||||
$MODPATH/README.md $MODPATH/.git*
|
||||
rmdir -p $MODPATH 2>/dev/null
|
||||
|
||||
cd /
|
||||
$BOOTMODE || recovery_cleanup
|
||||
rm -rf $TMPDIR
|
||||
|
||||
ui_print "- Done"
|
||||
}
|
||||
|
||||
##########
|
||||
# Presets
|
||||
##########
|
||||
|
||||
# Detect whether in boot mode
|
||||
[ -z $BOOTMODE ] && ps | grep zygote | grep -qv grep && BOOTMODE=true
|
||||
[ -z $BOOTMODE ] && ps -A 2>/dev/null | grep zygote | grep -qv grep && BOOTMODE=true
|
||||
[ -z $BOOTMODE ] && BOOTMODE=false
|
||||
|
||||
NVBASE=/data/adb
|
||||
TMPDIR=/dev/tmp
|
||||
POSTFSDATAD=$NVBASE/post-fs-data.d
|
||||
SERVICED=$NVBASE/service.d
|
||||
|
||||
# Some modules dependents on this
|
||||
export MAGISK_VER=27.0
|
||||
export MAGISK_VER_CODE=27000
|
||||
444
apd/src/magic_mount.rs
Normal file
444
apd/src/magic_mount.rs
Normal file
|
|
@ -0,0 +1,444 @@
|
|||
use crate::defs::{AP_OVERLAY_SOURCE, DISABLE_FILE_NAME, MODULE_DIR, SKIP_MOUNT_FILE_NAME};
|
||||
use crate::magic_mount::NodeFileType::{Directory, RegularFile, Symlink, Whiteout};
|
||||
use crate::restorecon::{lgetfilecon, lsetfilecon};
|
||||
use crate::utils::ensure_dir_exists;
|
||||
use crate::utils::get_work_dir;
|
||||
use anyhow::{Context, Result, bail};
|
||||
use extattr::lgetxattr;
|
||||
use rustix::fs::{
|
||||
Gid, MetadataExt, Mode, MountFlags, MountPropagationFlags, Uid, UnmountFlags, bind_mount,
|
||||
chmod, chown, mount, move_mount, unmount,
|
||||
};
|
||||
use rustix::mount::mount_change;
|
||||
use rustix::path::Arg;
|
||||
use std::cmp::PartialEq;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::fs;
|
||||
use std::fs::{DirEntry, FileType, create_dir, create_dir_all, read_dir, read_link};
|
||||
use std::os::unix::fs::{FileTypeExt, symlink};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const REPLACE_DIR_XATTR: &str = "trusted.overlay.opaque";
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone, Debug)]
|
||||
enum NodeFileType {
|
||||
RegularFile,
|
||||
Directory,
|
||||
Symlink,
|
||||
Whiteout,
|
||||
}
|
||||
|
||||
impl NodeFileType {
|
||||
fn from_file_type(file_type: FileType) -> Option<Self> {
|
||||
if file_type.is_file() {
|
||||
Some(RegularFile)
|
||||
} else if file_type.is_dir() {
|
||||
Some(Directory)
|
||||
} else if file_type.is_symlink() {
|
||||
Some(Symlink)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Node {
|
||||
name: String,
|
||||
file_type: NodeFileType,
|
||||
children: HashMap<String, Node>,
|
||||
// the module that owned this node
|
||||
module_path: Option<PathBuf>,
|
||||
replace: bool,
|
||||
skip: bool,
|
||||
}
|
||||
|
||||
impl Node {
|
||||
fn collect_module_files<T: AsRef<Path>>(&mut self, module_dir: T) -> Result<bool> {
|
||||
let dir = module_dir.as_ref();
|
||||
let mut has_file = false;
|
||||
for entry in dir.read_dir()?.flatten() {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
|
||||
let node = match self.children.entry(name.clone()) {
|
||||
Entry::Occupied(o) => Some(o.into_mut()),
|
||||
Entry::Vacant(v) => Self::new_module(&name, &entry).map(|it| v.insert(it)),
|
||||
};
|
||||
|
||||
if let Some(node) = node {
|
||||
has_file |= if node.file_type == Directory {
|
||||
node.collect_module_files(dir.join(&node.name))? || node.replace
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(has_file)
|
||||
}
|
||||
|
||||
fn new_root<T: ToString>(name: T) -> Self {
|
||||
Node {
|
||||
name: name.to_string(),
|
||||
file_type: Directory,
|
||||
children: Default::default(),
|
||||
module_path: None,
|
||||
replace: false,
|
||||
skip: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn new_module<T: ToString>(name: T, entry: &DirEntry) -> Option<Self> {
|
||||
if let Ok(metadata) = entry.metadata() {
|
||||
let path = entry.path();
|
||||
let file_type = if metadata.file_type().is_char_device() && metadata.rdev() == 0 {
|
||||
Some(Whiteout)
|
||||
} else {
|
||||
NodeFileType::from_file_type(metadata.file_type())
|
||||
};
|
||||
if let Some(file_type) = file_type {
|
||||
let mut replace = false;
|
||||
if file_type == Directory {
|
||||
if let Ok(v) = lgetxattr(&path, REPLACE_DIR_XATTR) {
|
||||
if String::from_utf8_lossy(&v) == "y" {
|
||||
replace = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Some(Node {
|
||||
name: name.to_string(),
|
||||
file_type,
|
||||
children: Default::default(),
|
||||
module_path: Some(path),
|
||||
replace,
|
||||
skip: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_module_files() -> Result<Option<Node>> {
|
||||
let mut root = Node::new_root("");
|
||||
let mut system = Node::new_root("system");
|
||||
let module_root = Path::new(MODULE_DIR);
|
||||
let mut has_file = false;
|
||||
for entry in module_root.read_dir()?.flatten() {
|
||||
if !entry.file_type()?.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if entry.path().join(DISABLE_FILE_NAME).exists()
|
||||
|| entry.path().join(SKIP_MOUNT_FILE_NAME).exists()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let mod_system = entry.path().join("system");
|
||||
if !mod_system.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
log::debug!("collecting {}", entry.path().display());
|
||||
|
||||
has_file |= system.collect_module_files(&mod_system)?;
|
||||
}
|
||||
|
||||
if has_file {
|
||||
for (partition, require_symlink) in [
|
||||
("vendor", true),
|
||||
("system_ext", true),
|
||||
("product", true),
|
||||
("odm", false),
|
||||
("oem", false),
|
||||
] {
|
||||
let path_of_root = Path::new("/").join(partition);
|
||||
let path_of_system = Path::new("/system").join(partition);
|
||||
if path_of_root.is_dir() && (!require_symlink || path_of_system.is_symlink()) {
|
||||
let name = partition.to_string();
|
||||
if let Some(node) = system.children.remove(&name) {
|
||||
root.children.insert(name, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
root.children.insert("system".to_string(), system);
|
||||
Ok(Some(root))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn clone_symlink<Src: AsRef<Path>, Dst: AsRef<Path>>(src: Src, dst: Dst) -> Result<()> {
|
||||
let src_symlink = read_link(src.as_ref())?;
|
||||
symlink(&src_symlink, dst.as_ref())?;
|
||||
lsetfilecon(dst.as_ref(), lgetfilecon(src.as_ref())?.as_str())?;
|
||||
log::debug!(
|
||||
"clone symlink {} -> {}({})",
|
||||
dst.as_ref().display(),
|
||||
dst.as_ref().display(),
|
||||
src_symlink.display()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mount_mirror<P: AsRef<Path>, WP: AsRef<Path>>(
|
||||
path: P,
|
||||
work_dir_path: WP,
|
||||
entry: &DirEntry,
|
||||
) -> Result<()> {
|
||||
let path = path.as_ref().join(entry.file_name());
|
||||
let work_dir_path = work_dir_path.as_ref().join(entry.file_name());
|
||||
let file_type = entry.file_type()?;
|
||||
|
||||
if file_type.is_file() {
|
||||
log::debug!(
|
||||
"mount mirror file {} -> {}",
|
||||
path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
fs::File::create(&work_dir_path)?;
|
||||
bind_mount(&path, &work_dir_path)?;
|
||||
} else if file_type.is_dir() {
|
||||
log::debug!(
|
||||
"mount mirror dir {} -> {}",
|
||||
path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
create_dir(&work_dir_path)?;
|
||||
let metadata = entry.metadata()?;
|
||||
chmod(&work_dir_path, Mode::from_raw_mode(metadata.mode()))?;
|
||||
unsafe {
|
||||
chown(
|
||||
&work_dir_path,
|
||||
Some(Uid::from_raw(metadata.uid())),
|
||||
Some(Gid::from_raw(metadata.gid())),
|
||||
)?;
|
||||
}
|
||||
lsetfilecon(&work_dir_path, lgetfilecon(&path)?.as_str())?;
|
||||
for entry in read_dir(&path)?.flatten() {
|
||||
mount_mirror(&path, &work_dir_path, &entry)?;
|
||||
}
|
||||
} else if file_type.is_symlink() {
|
||||
log::debug!(
|
||||
"create mirror symlink {} -> {}",
|
||||
path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
clone_symlink(&path, &work_dir_path)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn do_magic_mount<P: AsRef<Path>, WP: AsRef<Path>>(
|
||||
path: P,
|
||||
work_dir_path: WP,
|
||||
current: Node,
|
||||
has_tmpfs: bool,
|
||||
) -> Result<()> {
|
||||
let mut current = current;
|
||||
let path = path.as_ref().join(¤t.name);
|
||||
let work_dir_path = work_dir_path.as_ref().join(¤t.name);
|
||||
match current.file_type {
|
||||
RegularFile => {
|
||||
let target_path = if has_tmpfs {
|
||||
fs::File::create(&work_dir_path)?;
|
||||
&work_dir_path
|
||||
} else {
|
||||
&path
|
||||
};
|
||||
if let Some(module_path) = ¤t.module_path {
|
||||
log::debug!(
|
||||
"mount module file {} -> {}",
|
||||
module_path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
bind_mount(module_path, target_path)?;
|
||||
} else {
|
||||
bail!("cannot mount root file {}!", path.display());
|
||||
}
|
||||
}
|
||||
Symlink => {
|
||||
if let Some(module_path) = ¤t.module_path {
|
||||
log::debug!(
|
||||
"create module symlink {} -> {}",
|
||||
module_path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
clone_symlink(module_path, &work_dir_path)?;
|
||||
} else {
|
||||
bail!("cannot mount root symlink {}!", path.display());
|
||||
}
|
||||
}
|
||||
Directory => {
|
||||
let mut create_tmpfs = !has_tmpfs && current.replace && current.module_path.is_some();
|
||||
if !has_tmpfs && !create_tmpfs {
|
||||
for it in &mut current.children {
|
||||
let (name, node) = it;
|
||||
let real_path = path.join(name);
|
||||
let need = match node.file_type {
|
||||
Symlink => true,
|
||||
Whiteout => real_path.exists(),
|
||||
_ => {
|
||||
if let Ok(metadata) = real_path.symlink_metadata() {
|
||||
let file_type = NodeFileType::from_file_type(metadata.file_type())
|
||||
.unwrap_or(Whiteout);
|
||||
file_type != node.file_type || file_type == Symlink
|
||||
} else {
|
||||
// real path not exists
|
||||
true
|
||||
}
|
||||
}
|
||||
};
|
||||
if need {
|
||||
if current.module_path.is_none() {
|
||||
log::error!(
|
||||
"cannot create tmpfs on {}, ignore: {name}",
|
||||
path.display()
|
||||
);
|
||||
node.skip = true;
|
||||
continue;
|
||||
}
|
||||
create_tmpfs = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let has_tmpfs = has_tmpfs || create_tmpfs;
|
||||
|
||||
if has_tmpfs {
|
||||
log::debug!(
|
||||
"creating tmpfs skeleton for {} at {}",
|
||||
path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
create_dir_all(&work_dir_path)?;
|
||||
let (metadata, path) = if path.exists() {
|
||||
(path.metadata()?, &path)
|
||||
} else if let Some(module_path) = ¤t.module_path {
|
||||
(module_path.metadata()?, module_path)
|
||||
} else {
|
||||
bail!("cannot mount root dir {}!", path.display());
|
||||
};
|
||||
chmod(&work_dir_path, Mode::from_raw_mode(metadata.mode()))?;
|
||||
unsafe {
|
||||
chown(
|
||||
&work_dir_path,
|
||||
Some(Uid::from_raw(metadata.uid())),
|
||||
Some(Gid::from_raw(metadata.gid())),
|
||||
)?;
|
||||
}
|
||||
lsetfilecon(&work_dir_path, lgetfilecon(path)?.as_str())?;
|
||||
}
|
||||
|
||||
if create_tmpfs {
|
||||
log::debug!(
|
||||
"creating tmpfs for {} at {}",
|
||||
path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
bind_mount(&work_dir_path, &work_dir_path).context("bind self")?;
|
||||
}
|
||||
|
||||
if path.exists() && !current.replace {
|
||||
for entry in path.read_dir()?.flatten() {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
let result = if let Some(node) = current.children.remove(&name) {
|
||||
if node.skip {
|
||||
continue;
|
||||
}
|
||||
do_magic_mount(&path, &work_dir_path, node, has_tmpfs)
|
||||
.with_context(|| format!("magic mount {}/{name}", path.display()))
|
||||
} else if has_tmpfs {
|
||||
mount_mirror(&path, &work_dir_path, &entry)
|
||||
.with_context(|| format!("mount mirror {}/{name}", path.display()))
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
|
||||
if let Err(e) = result {
|
||||
if has_tmpfs {
|
||||
return Err(e);
|
||||
} else {
|
||||
log::error!("mount child {}/{name} failed: {}", path.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if current.replace {
|
||||
if current.module_path.is_none() {
|
||||
bail!(
|
||||
"dir {} is declared as replaced but it is root!",
|
||||
path.display()
|
||||
);
|
||||
} else {
|
||||
log::debug!("dir {} is replaced", path.display());
|
||||
}
|
||||
}
|
||||
|
||||
for (name, node) in current.children.into_iter() {
|
||||
if node.skip {
|
||||
continue;
|
||||
}
|
||||
if let Err(e) = do_magic_mount(&path, &work_dir_path, node, has_tmpfs)
|
||||
.with_context(|| format!("magic mount {}/{name}", path.display()))
|
||||
{
|
||||
if has_tmpfs {
|
||||
return Err(e);
|
||||
} else {
|
||||
log::error!("mount child {}/{name} failed: {}", path.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if create_tmpfs {
|
||||
log::debug!(
|
||||
"moving tmpfs {} -> {}",
|
||||
work_dir_path.display(),
|
||||
path.display()
|
||||
);
|
||||
move_mount(&work_dir_path, &path).context("move self")?;
|
||||
mount_change(&path, MountPropagationFlags::PRIVATE).context("make self private")?;
|
||||
}
|
||||
}
|
||||
Whiteout => {
|
||||
log::debug!("file {} is removed", path.display());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn magic_mount() -> Result<()> {
|
||||
match collect_module_files()? {
|
||||
Some(root) => {
|
||||
log::debug!("collected: {:#?}", root);
|
||||
let tmp_dir = PathBuf::from(get_work_dir());
|
||||
ensure_dir_exists(&tmp_dir)?;
|
||||
mount(
|
||||
AP_OVERLAY_SOURCE,
|
||||
&tmp_dir,
|
||||
"tmpfs",
|
||||
MountFlags::empty(),
|
||||
"",
|
||||
)
|
||||
.context("mount tmp")?;
|
||||
mount_change(&tmp_dir, MountPropagationFlags::PRIVATE).context("make tmp private")?;
|
||||
let result = do_magic_mount("/", &tmp_dir, root, false);
|
||||
if let Err(e) = unmount(&tmp_dir, UnmountFlags::DETACH) {
|
||||
log::error!("failed to unmount tmp {}", e);
|
||||
}
|
||||
fs::remove_dir(tmp_dir).ok();
|
||||
result
|
||||
}
|
||||
_ => {
|
||||
log::info!("no modules to mount, skipping!");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
18
apd/src/main.rs
Normal file
18
apd/src/main.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
mod apd;
|
||||
mod assets;
|
||||
mod cli;
|
||||
mod defs;
|
||||
mod event;
|
||||
mod magic_mount;
|
||||
mod module;
|
||||
mod mount;
|
||||
mod package;
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
mod pty;
|
||||
mod restorecon;
|
||||
mod sepolicy;
|
||||
mod supercall;
|
||||
mod utils;
|
||||
fn main() -> anyhow::Result<()> {
|
||||
cli::run()
|
||||
}
|
||||
565
apd/src/module.rs
Normal file
565
apd/src/module.rs
Normal file
|
|
@ -0,0 +1,565 @@
|
|||
#[allow(clippy::wildcard_imports)]
|
||||
use crate::utils::*;
|
||||
use crate::{assets, defs, restorecon};
|
||||
use anyhow::{Context, Result, anyhow, bail, ensure};
|
||||
use const_format::concatcp;
|
||||
use is_executable::is_executable;
|
||||
use java_properties::PropertiesIter;
|
||||
use log::{info, warn};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env::var as env_var,
|
||||
fs,
|
||||
io::Cursor,
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, Stdio},
|
||||
str::FromStr,
|
||||
};
|
||||
use zip_extensions::zip_extract_file_to_memory;
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::{prelude::PermissionsExt, process::CommandExt};
|
||||
|
||||
const INSTALLER_CONTENT: &str = include_str!("./installer.sh");
|
||||
const INSTALLER_CONTENT_: &str = include_str!("./installer_bind.sh");
|
||||
const INSTALL_MODULE_SCRIPT: &str = concatcp!(
|
||||
INSTALLER_CONTENT,
|
||||
"\n",
|
||||
"install_module",
|
||||
"\n",
|
||||
"exit 0",
|
||||
"\n"
|
||||
);
|
||||
const INSTALL_MODULE_SCRIPT_: &str = concatcp!(
|
||||
INSTALLER_CONTENT_,
|
||||
"\n",
|
||||
"install_module",
|
||||
"\n",
|
||||
"exit 0",
|
||||
"\n"
|
||||
);
|
||||
|
||||
fn exec_install_script(module_file: &str) -> Result<()> {
|
||||
let realpath =
|
||||
fs::canonicalize(module_file).with_context(|| format!("realpath: {module_file} failed"))?;
|
||||
|
||||
let content;
|
||||
|
||||
if !should_use_overlayfs()? {
|
||||
content = INSTALL_MODULE_SCRIPT_.to_string();
|
||||
} else {
|
||||
content = INSTALL_MODULE_SCRIPT.to_string();
|
||||
}
|
||||
let result = Command::new(assets::BUSYBOX_PATH)
|
||||
.args(["sh", "-c", &content])
|
||||
.env("ASH_STANDALONE", "1")
|
||||
.env(
|
||||
"PATH",
|
||||
format!(
|
||||
"{}:{}",
|
||||
env_var("PATH").unwrap(),
|
||||
defs::BINARY_DIR.trim_end_matches('/')
|
||||
),
|
||||
)
|
||||
.env("APATCH", "true")
|
||||
.env("APATCH_VER", defs::VERSION_NAME)
|
||||
.env("APATCH_VER_CODE", defs::VERSION_CODE)
|
||||
.env("APATCH_BIND_MOUNT", format!("{}", !should_use_overlayfs()?))
|
||||
.env("OUTFD", "1")
|
||||
.env("ZIPFILE", realpath)
|
||||
.status()?;
|
||||
ensure!(result.success(), "Failed to install module script");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// becuase we use something like A-B update
|
||||
// we need to update the module state after the boot_completed
|
||||
// if someone(such as the module) install a module before the boot_completed
|
||||
// then it may cause some problems, just forbid it
|
||||
fn ensure_boot_completed() -> Result<()> {
|
||||
// ensure getprop sys.boot_completed == 1
|
||||
if getprop("sys.boot_completed").as_deref() != Some("1") {
|
||||
bail!("Android is Booting!");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mark_update() -> Result<()> {
|
||||
ensure_file_exists(concatcp!(defs::WORKING_DIR, defs::UPDATE_FILE_NAME))
|
||||
}
|
||||
|
||||
fn mark_module_state(module: &str, flag_file: &str, create_or_delete: bool) -> Result<()> {
|
||||
let module_state_file = Path::new(defs::MODULE_DIR).join(module).join(flag_file);
|
||||
if create_or_delete {
|
||||
ensure_file_exists(module_state_file)
|
||||
} else {
|
||||
if module_state_file.exists() {
|
||||
fs::remove_file(module_state_file)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn foreach_module(active_only: bool, mut f: impl FnMut(&Path) -> Result<()>) -> Result<()> {
|
||||
let modules_dir = Path::new(defs::MODULE_DIR);
|
||||
let dir = fs::read_dir(modules_dir)?;
|
||||
for entry in dir.flatten() {
|
||||
let path = entry.path();
|
||||
if !path.is_dir() {
|
||||
warn!("{} is not a directory, skip", path.display());
|
||||
continue;
|
||||
}
|
||||
|
||||
if active_only && path.join(defs::DISABLE_FILE_NAME).exists() {
|
||||
info!("{} is disabled, skip", path.display());
|
||||
continue;
|
||||
}
|
||||
if active_only && path.join(defs::REMOVE_FILE_NAME).exists() {
|
||||
warn!("{} is removed, skip", path.display());
|
||||
continue;
|
||||
}
|
||||
|
||||
f(&path)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn foreach_active_module(f: impl FnMut(&Path) -> Result<()>) -> Result<()> {
|
||||
foreach_module(true, f)
|
||||
}
|
||||
|
||||
pub fn check_image(img: &str) -> Result<()> {
|
||||
let result = Command::new("e2fsck")
|
||||
.args(["-yf", img])
|
||||
.stdout(Stdio::piped())
|
||||
.status()
|
||||
.with_context(|| format!("Failed to exec e2fsck {img}"))?;
|
||||
let code = result.code();
|
||||
// 0 or 1 is ok
|
||||
// 0: no error
|
||||
// 1: file system errors corrected
|
||||
// https://man7.org/linux/man-pages/man8/e2fsck.8.html
|
||||
// ensure!(
|
||||
// code == Some(0) || code == Some(1),
|
||||
// "Failed to check image, e2fsck exit code: {}",
|
||||
// code.unwrap_or(-1)
|
||||
// );
|
||||
info!("e2fsck exit code: {}", code.unwrap_or(-1));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_sepolicy_rule() -> Result<()> {
|
||||
foreach_active_module(|path| {
|
||||
let rule_file = path.join("sepolicy.rule");
|
||||
if !rule_file.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!("load policy: {}", &rule_file.display());
|
||||
Command::new(assets::MAGISKPOLICY_PATH)
|
||||
.arg("--live")
|
||||
.arg("--apply")
|
||||
.arg(&rule_file)
|
||||
.status()
|
||||
.with_context(|| format!("Failed to exec {}", rule_file.display()))?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn exec_script<T: AsRef<Path>>(path: T, wait: bool) -> Result<()> {
|
||||
info!("exec {}", path.as_ref().display());
|
||||
|
||||
let mut command = &mut Command::new(assets::BUSYBOX_PATH);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
command = command.process_group(0);
|
||||
command = unsafe {
|
||||
command.pre_exec(|| {
|
||||
// ignore the error?
|
||||
switch_cgroups();
|
||||
Ok(())
|
||||
})
|
||||
};
|
||||
}
|
||||
command = command
|
||||
.current_dir(path.as_ref().parent().unwrap())
|
||||
.arg("sh")
|
||||
.arg(path.as_ref())
|
||||
.env("ASH_STANDALONE", "1")
|
||||
.env("APATCH", "true")
|
||||
.env("APATCH_VER", defs::VERSION_NAME)
|
||||
.env("APATCH_VER_CODE", defs::VERSION_CODE)
|
||||
.env("APATCH_BIND_MOUNT", format!("{}", !should_use_overlayfs()?))
|
||||
.env(
|
||||
"PATH",
|
||||
format!(
|
||||
"{}:{}",
|
||||
env_var("PATH")?,
|
||||
defs::BINARY_DIR.trim_end_matches('/')
|
||||
),
|
||||
);
|
||||
|
||||
let result = if wait {
|
||||
command.status().map(|_| ())
|
||||
} else {
|
||||
command.spawn().map(|_| ())
|
||||
};
|
||||
result.map_err(|err| anyhow!("Failed to exec {}: {}", path.as_ref().display(), err))
|
||||
}
|
||||
|
||||
pub fn exec_stage_script(stage: &str, block: bool) -> Result<()> {
|
||||
foreach_active_module(|module| {
|
||||
let script_path = module.join(format!("{stage}.sh"));
|
||||
if !script_path.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
exec_script(&script_path, block)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn exec_common_scripts(dir: &str, wait: bool) -> Result<()> {
|
||||
let script_dir = Path::new(defs::ADB_DIR).join(dir);
|
||||
if !script_dir.exists() {
|
||||
info!("{} not exists, skip", script_dir.display());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let dir = fs::read_dir(&script_dir)?;
|
||||
for entry in dir.flatten() {
|
||||
let path = entry.path();
|
||||
|
||||
if !is_executable(&path) {
|
||||
warn!("{} is not executable, skip", path.display());
|
||||
continue;
|
||||
}
|
||||
|
||||
exec_script(path, wait)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_system_prop() -> Result<()> {
|
||||
foreach_active_module(|module| {
|
||||
let system_prop = module.join("system.prop");
|
||||
if !system_prop.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
info!("load {} system.prop", module.display());
|
||||
|
||||
// resetprop -n --file system.prop
|
||||
Command::new(assets::RESETPROP_PATH)
|
||||
.arg("-n")
|
||||
.arg("--file")
|
||||
.arg(&system_prop)
|
||||
.status()
|
||||
.with_context(|| format!("Failed to exec {}", system_prop.display()))?;
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn prune_modules() -> Result<()> {
|
||||
foreach_module(false, |module| {
|
||||
fs::remove_file(module.join(defs::UPDATE_FILE_NAME)).ok();
|
||||
if !module.join(defs::REMOVE_FILE_NAME).exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!("remove module: {}", module.display());
|
||||
|
||||
let uninstaller = module.join("uninstall.sh");
|
||||
if uninstaller.exists() {
|
||||
if let Err(e) = exec_script(uninstaller, true) {
|
||||
warn!("Failed to exec uninstaller: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = fs::remove_dir_all(module) {
|
||||
warn!("Failed to remove {}: {}", module.display(), e);
|
||||
}
|
||||
let module_path = module.display().to_string();
|
||||
let updated_path = module_path.replace(defs::MODULE_DIR, defs::MODULE_UPDATE_TMP_DIR);
|
||||
|
||||
if let Err(e) = fs::remove_dir_all(&updated_path) {
|
||||
warn!("Failed to remove {}: {}", updated_path, e);
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn _install_module(zip: &str) -> Result<()> {
|
||||
ensure_boot_completed()?;
|
||||
|
||||
// print banner
|
||||
println!(include_str!("banner"));
|
||||
|
||||
assets::ensure_binaries().with_context(|| "binary missing")?;
|
||||
|
||||
// first check if workding dir is usable
|
||||
ensure_dir_exists(defs::WORKING_DIR).with_context(|| "Failed to create working dir")?;
|
||||
ensure_dir_exists(defs::BINARY_DIR).with_context(|| "Failed to create bin dir")?;
|
||||
|
||||
// read the module_id from zip
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
let entry_path = PathBuf::from_str("module.prop")?;
|
||||
let zip_path = PathBuf::from_str(zip)?;
|
||||
let zip_path = zip_path.canonicalize()?;
|
||||
zip_extract_file_to_memory(&zip_path, &entry_path, &mut buffer)?;
|
||||
let mut module_prop = HashMap::new();
|
||||
PropertiesIter::new_with_encoding(Cursor::new(buffer), encoding_rs::UTF_8).read_into(
|
||||
|k, v| {
|
||||
module_prop.insert(k, v);
|
||||
},
|
||||
)?;
|
||||
info!("module prop: {:?}", module_prop);
|
||||
|
||||
let Some(module_id) = module_prop.get("id") else {
|
||||
bail!("module id not found in module.prop!");
|
||||
};
|
||||
|
||||
let modules_dir = Path::new(defs::MODULE_DIR);
|
||||
let modules_update_dir = Path::new(defs::MODULE_UPDATE_TMP_DIR);
|
||||
if !Path::new(modules_dir).exists() {
|
||||
fs::create_dir(modules_dir).expect("Failed to create modules folder");
|
||||
let permissions = fs::Permissions::from_mode(0o700);
|
||||
fs::set_permissions(modules_dir, permissions).expect("Failed to set permissions");
|
||||
}
|
||||
|
||||
let module_dir = format!("{}{}", modules_dir.display(), module_id.clone());
|
||||
let _module_update_dir = format!("{}{}", modules_update_dir.display(), module_id.clone());
|
||||
info!("module dir: {}", module_dir);
|
||||
if !Path::new(&module_dir.clone()).exists() {
|
||||
fs::create_dir(&module_dir.clone()).expect("Failed to create module folder");
|
||||
let permissions = fs::Permissions::from_mode(0o700);
|
||||
fs::set_permissions(module_dir.clone(), permissions).expect("Failed to set permissions");
|
||||
}
|
||||
// unzip the image and move it to modules_update/<id> dir
|
||||
let file = fs::File::open(zip)?;
|
||||
let mut archive = zip::ZipArchive::new(file)?;
|
||||
archive.extract(&_module_update_dir)?;
|
||||
|
||||
// set permission and selinux context for $MOD/system
|
||||
let module_system_dir = PathBuf::from(module_dir.clone()).join("system");
|
||||
if module_system_dir.exists() {
|
||||
#[cfg(unix)]
|
||||
fs::set_permissions(&module_system_dir, fs::Permissions::from_mode(0o755))?;
|
||||
restorecon::restore_syscon(&module_system_dir)?;
|
||||
}
|
||||
exec_install_script(zip)?;
|
||||
mark_update()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn install_module(zip: &str) -> Result<()> {
|
||||
let result = _install_module(zip);
|
||||
result
|
||||
}
|
||||
|
||||
pub fn _uninstall_module(id: &str, update_dir: &str) -> Result<()> {
|
||||
let dir = Path::new(update_dir);
|
||||
ensure!(dir.exists(), "No module installed");
|
||||
|
||||
// iterate the modules_update dir, find the module to be removed
|
||||
let dir = fs::read_dir(dir)?;
|
||||
for entry in dir.flatten() {
|
||||
let path = entry.path();
|
||||
let module_prop = path.join("module.prop");
|
||||
if !module_prop.exists() {
|
||||
continue;
|
||||
}
|
||||
let content = fs::read(module_prop)?;
|
||||
let mut module_id: String = String::new();
|
||||
PropertiesIter::new_with_encoding(Cursor::new(content), encoding_rs::UTF_8).read_into(
|
||||
|k, v| {
|
||||
if k.eq("id") {
|
||||
module_id = v;
|
||||
}
|
||||
},
|
||||
)?;
|
||||
if module_id.eq(id) {
|
||||
let remove_file = path.join(defs::REMOVE_FILE_NAME);
|
||||
fs::File::create(remove_file).with_context(|| "Failed to create remove file.")?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// santity check
|
||||
let target_module_path = format!("{update_dir}/{id}");
|
||||
let target_module = Path::new(&target_module_path);
|
||||
if target_module.exists() {
|
||||
let remove_file = target_module.join(defs::REMOVE_FILE_NAME);
|
||||
if !remove_file.exists() {
|
||||
fs::File::create(remove_file).with_context(|| "Failed to create remove file.")?;
|
||||
}
|
||||
}
|
||||
|
||||
let _ = mark_module_state(id, defs::REMOVE_FILE_NAME, true);
|
||||
Ok(())
|
||||
}
|
||||
pub fn uninstall_module(id: &str) -> Result<()> {
|
||||
_uninstall_module(id, defs::MODULE_DIR)?;
|
||||
mark_update()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run_action(id: &str) -> Result<()> {
|
||||
let action_script_path = format!("/data/adb/modules/{}/action.sh", id);
|
||||
let _ = exec_script(&action_script_path, true);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn _change_module_state(module_dir: &str, mid: &str, enable: bool) -> Result<()> {
|
||||
let src_module_path = format!("{module_dir}/{mid}");
|
||||
let src_module = Path::new(&src_module_path);
|
||||
ensure!(src_module.exists(), "module: {} not found!", mid);
|
||||
|
||||
let disable_path = src_module.join(defs::DISABLE_FILE_NAME);
|
||||
if enable {
|
||||
if disable_path.exists() {
|
||||
fs::remove_file(&disable_path).with_context(|| {
|
||||
format!("Failed to remove disable file: {}", &disable_path.display())
|
||||
})?;
|
||||
}
|
||||
} else {
|
||||
ensure_file_exists(disable_path)?;
|
||||
}
|
||||
|
||||
let _ = mark_module_state(mid, defs::DISABLE_FILE_NAME, !enable);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn _enable_module(id: &str, update_dir: &Path) -> Result<()> {
|
||||
if let Some(module_dir_str) = update_dir.to_str() {
|
||||
_change_module_state(module_dir_str, id, true)
|
||||
} else {
|
||||
info!("Enable module failed: Invalid path");
|
||||
Err(anyhow::anyhow!("Invalid module directory"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enable_module(id: &str) -> Result<()> {
|
||||
let update_dir = Path::new(defs::MODULE_DIR);
|
||||
_enable_module(id, update_dir)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn _disable_module(id: &str, update_dir: &Path) -> Result<()> {
|
||||
if let Some(module_dir_str) = update_dir.to_str() {
|
||||
_change_module_state(module_dir_str, id, false)
|
||||
} else {
|
||||
info!("Disable module failed: Invalid path");
|
||||
Err(anyhow::anyhow!("Invalid module directory"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disable_module(id: &str) -> Result<()> {
|
||||
let module_dir = Path::new(defs::MODULE_DIR);
|
||||
_disable_module(id, module_dir)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn _disable_all_modules(dir: &str) -> Result<()> {
|
||||
let dir = fs::read_dir(dir)?;
|
||||
for entry in dir.flatten() {
|
||||
let path = entry.path();
|
||||
let disable_flag = path.join(defs::DISABLE_FILE_NAME);
|
||||
if let Err(e) = ensure_file_exists(disable_flag) {
|
||||
warn!("Failed to disable module: {}: {}", path.display(), e);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn disable_all_modules() -> Result<()> {
|
||||
// Skip disabling modules since boot completed
|
||||
if getprop("sys.boot_completed").as_deref() == Some("1") {
|
||||
info!("System boot completed, no need to disable all modules");
|
||||
return Ok(());
|
||||
}
|
||||
mark_update()?;
|
||||
_disable_all_modules(defs::MODULE_DIR)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn _list_modules(path: &str) -> Vec<HashMap<String, String>> {
|
||||
// first check enabled modules
|
||||
let dir = fs::read_dir(path);
|
||||
let Ok(dir) = dir else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let mut modules: Vec<HashMap<String, String>> = Vec::new();
|
||||
|
||||
for entry in dir.flatten() {
|
||||
let path = entry.path();
|
||||
info!("path: {}", path.display());
|
||||
let module_prop = path.join("module.prop");
|
||||
if !module_prop.exists() {
|
||||
continue;
|
||||
}
|
||||
let content = fs::read(&module_prop);
|
||||
let Ok(content) = content else {
|
||||
warn!("Failed to read file: {}", module_prop.display());
|
||||
continue;
|
||||
};
|
||||
let mut module_prop_map: HashMap<String, String> = HashMap::new();
|
||||
let encoding = encoding_rs::UTF_8;
|
||||
let result =
|
||||
PropertiesIter::new_with_encoding(Cursor::new(content), encoding).read_into(|k, v| {
|
||||
module_prop_map.insert(k, v);
|
||||
});
|
||||
|
||||
if !module_prop_map.contains_key("id") || module_prop_map["id"].is_empty() {
|
||||
match entry.file_name().to_str() {
|
||||
Some(id) => {
|
||||
info!("Use dir name as module id: {}", id);
|
||||
module_prop_map.insert("id".to_owned(), id.to_owned());
|
||||
}
|
||||
_ => {
|
||||
info!("Failed to get module id: {:?}", module_prop);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add enabled, update, remove flags
|
||||
let enabled = !path.join(defs::DISABLE_FILE_NAME).exists();
|
||||
let update = path.join(defs::UPDATE_FILE_NAME).exists();
|
||||
let remove = path.join(defs::REMOVE_FILE_NAME).exists();
|
||||
let web = path.join(defs::MODULE_WEB_DIR).exists();
|
||||
let action = path.join(defs::MODULE_ACTION_SH).exists();
|
||||
|
||||
module_prop_map.insert("enabled".to_owned(), enabled.to_string());
|
||||
module_prop_map.insert("update".to_owned(), update.to_string());
|
||||
module_prop_map.insert("remove".to_owned(), remove.to_string());
|
||||
module_prop_map.insert("web".to_owned(), web.to_string());
|
||||
module_prop_map.insert("action".to_owned(), action.to_string());
|
||||
|
||||
if result.is_err() {
|
||||
warn!("Failed to parse module.prop: {}", module_prop.display());
|
||||
continue;
|
||||
}
|
||||
modules.push(module_prop_map);
|
||||
}
|
||||
|
||||
modules
|
||||
}
|
||||
|
||||
pub fn list_modules() -> Result<()> {
|
||||
let modules = _list_modules(defs::MODULE_DIR);
|
||||
println!("{}", serde_json::to_string_pretty(&modules)?);
|
||||
Ok(())
|
||||
}
|
||||
371
apd/src/mount.rs
Normal file
371
apd/src/mount.rs
Normal file
|
|
@ -0,0 +1,371 @@
|
|||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use anyhow::Context;
|
||||
use anyhow::{Ok, Result, anyhow, bail};
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
#[allow(unused_imports)]
|
||||
use retry::delay::NoDelay;
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
//use sys_mount::{unmount, FilesystemType, Mount, MountFlags, Unmount, UnmountFlags};
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use rustix::{fd::AsFd, fs::CWD, mount::*};
|
||||
use std::fs::create_dir;
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
use crate::defs::AP_OVERLAY_SOURCE;
|
||||
use crate::defs::PTS_NAME;
|
||||
use log::{info, warn};
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use procfs::process::Process;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub struct AutoMountExt4 {
|
||||
target: String,
|
||||
auto_umount: bool,
|
||||
}
|
||||
|
||||
impl AutoMountExt4 {
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
|
||||
pub fn try_new(source: &str, target: &str, auto_umount: bool) -> Result<Self> {
|
||||
let path = Path::new(source);
|
||||
if !path.exists() {
|
||||
println!("Source path does not exist");
|
||||
} else {
|
||||
let metadata = fs::metadata(path)?;
|
||||
let permissions = metadata.permissions();
|
||||
let mode = permissions.mode();
|
||||
|
||||
if permissions.readonly() {
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
println!("File permissions: {:o} (octal)", mode & 0o777);
|
||||
}
|
||||
}
|
||||
|
||||
mount_ext4(source, target)?;
|
||||
Ok(Self {
|
||||
target: target.to_string(),
|
||||
auto_umount,
|
||||
})
|
||||
}
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn try_new(_src: &str, _mnt: &str, _auto_umount: bool) -> Result<Self> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn umount(&self) -> Result<()> {
|
||||
unmount(self.target.as_str(), UnmountFlags::DETACH)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
impl Drop for AutoMountExt4 {
|
||||
fn drop(&mut self) {
|
||||
info!(
|
||||
"AutoMountExt4 drop: {}, auto_umount: {}",
|
||||
self.target, self.auto_umount
|
||||
);
|
||||
if self.auto_umount {
|
||||
let _ = self.umount();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn mount_image(src: &str, target: &str, _autodrop: bool) -> Result<()> {
|
||||
mount_ext4(src, target)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn mount_ext4(source: impl AsRef<Path>, target: impl AsRef<Path>) -> Result<()> {
|
||||
let new_loopback = loopdev::LoopControl::open()?.next_free()?;
|
||||
new_loopback.with().attach(source)?;
|
||||
let lo = new_loopback.path().ok_or(anyhow!("no loop"))?;
|
||||
match fsopen("ext4", FsOpenFlags::FSOPEN_CLOEXEC) {
|
||||
Result::Ok(fs) => {
|
||||
let fs = fs.as_fd();
|
||||
fsconfig_set_string(fs, "source", lo)?;
|
||||
fsconfig_create(fs)?;
|
||||
let mount = fsmount(fs, FsMountFlags::FSMOUNT_CLOEXEC, MountAttrFlags::empty())?;
|
||||
move_mount(
|
||||
mount.as_fd(),
|
||||
"",
|
||||
CWD,
|
||||
target.as_ref(),
|
||||
MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH,
|
||||
)?;
|
||||
}
|
||||
_ => {
|
||||
mount(lo, target.as_ref(), "ext4", MountFlags::empty(), "")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn umount_dir(src: impl AsRef<Path>) -> Result<()> {
|
||||
unmount(src.as_ref(), UnmountFlags::empty())
|
||||
.with_context(|| format!("Failed to umount {}", src.as_ref().display()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn mount_overlayfs(
|
||||
lower_dirs: &[String],
|
||||
lowest: &str,
|
||||
upperdir: Option<PathBuf>,
|
||||
workdir: Option<PathBuf>,
|
||||
dest: impl AsRef<Path>,
|
||||
) -> Result<()> {
|
||||
let lowerdir_config = lower_dirs
|
||||
.iter()
|
||||
.map(|s| s.as_ref())
|
||||
.chain(std::iter::once(lowest))
|
||||
.collect::<Vec<_>>()
|
||||
.join(":");
|
||||
info!(
|
||||
"mount overlayfs on {:?}, lowerdir={}, upperdir={:?}, workdir={:?}",
|
||||
dest.as_ref(),
|
||||
lowerdir_config,
|
||||
upperdir,
|
||||
workdir
|
||||
);
|
||||
|
||||
let upperdir = upperdir
|
||||
.filter(|up| up.exists())
|
||||
.map(|e| e.display().to_string());
|
||||
let workdir = workdir
|
||||
.filter(|wd| wd.exists())
|
||||
.map(|e| e.display().to_string());
|
||||
|
||||
let result = (|| {
|
||||
let fs = fsopen("overlay", FsOpenFlags::FSOPEN_CLOEXEC)?;
|
||||
let fs = fs.as_fd();
|
||||
fsconfig_set_string(fs, "lowerdir", &lowerdir_config)?;
|
||||
if let (Some(upperdir), Some(workdir)) = (&upperdir, &workdir) {
|
||||
fsconfig_set_string(fs, "upperdir", upperdir)?;
|
||||
fsconfig_set_string(fs, "workdir", workdir)?;
|
||||
}
|
||||
fsconfig_set_string(fs, "source", AP_OVERLAY_SOURCE)?;
|
||||
fsconfig_create(fs)?;
|
||||
let mount = fsmount(fs, FsMountFlags::FSMOUNT_CLOEXEC, MountAttrFlags::empty())?;
|
||||
move_mount(
|
||||
mount.as_fd(),
|
||||
"",
|
||||
CWD,
|
||||
dest.as_ref(),
|
||||
MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH,
|
||||
)
|
||||
})();
|
||||
|
||||
if let Err(e) = result {
|
||||
warn!("fsopen mount failed: {:#}, fallback to mount", e);
|
||||
let mut data = format!("lowerdir={lowerdir_config}");
|
||||
if let (Some(upperdir), Some(workdir)) = (upperdir, workdir) {
|
||||
data = format!("{data},upperdir={upperdir},workdir={workdir}");
|
||||
}
|
||||
mount(
|
||||
AP_OVERLAY_SOURCE,
|
||||
dest.as_ref(),
|
||||
"overlay",
|
||||
MountFlags::empty(),
|
||||
data,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn mount_devpts(dest: impl AsRef<Path>) -> Result<()> {
|
||||
create_dir(dest.as_ref())?;
|
||||
mount(
|
||||
AP_OVERLAY_SOURCE,
|
||||
dest.as_ref(),
|
||||
"devpts",
|
||||
MountFlags::empty(),
|
||||
"newinstance",
|
||||
)?;
|
||||
mount_change(dest.as_ref(), MountPropagationFlags::PRIVATE).context("make devpts private")?;
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn mount_devpts(_dest: impl AsRef<Path>) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn mount_tmpfs(dest: impl AsRef<Path>) -> Result<()> {
|
||||
info!("mount tmpfs on {}", dest.as_ref().display());
|
||||
match fsopen("tmpfs", FsOpenFlags::FSOPEN_CLOEXEC) {
|
||||
Result::Ok(fs) => {
|
||||
let fs = fs.as_fd();
|
||||
fsconfig_set_string(fs, "source", AP_OVERLAY_SOURCE)?;
|
||||
fsconfig_create(fs)?;
|
||||
let mount = fsmount(fs, FsMountFlags::FSMOUNT_CLOEXEC, MountAttrFlags::empty())?;
|
||||
move_mount(
|
||||
mount.as_fd(),
|
||||
"",
|
||||
CWD,
|
||||
dest.as_ref(),
|
||||
MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH,
|
||||
)?;
|
||||
}
|
||||
_ => {
|
||||
mount(
|
||||
AP_OVERLAY_SOURCE,
|
||||
dest.as_ref(),
|
||||
"tmpfs",
|
||||
MountFlags::empty(),
|
||||
"",
|
||||
)?;
|
||||
}
|
||||
}
|
||||
mount_change(dest.as_ref(), MountPropagationFlags::PRIVATE).context("make tmpfs private")?;
|
||||
let pts_dir = format!("{}/{PTS_NAME}", dest.as_ref().display());
|
||||
if let Err(e) = mount_devpts(pts_dir) {
|
||||
warn!("do devpts mount failed: {}", e);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn bind_mount(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result<()> {
|
||||
info!(
|
||||
"bind mount {} -> {}",
|
||||
from.as_ref().display(),
|
||||
to.as_ref().display()
|
||||
);
|
||||
match open_tree(
|
||||
CWD,
|
||||
from.as_ref(),
|
||||
OpenTreeFlags::OPEN_TREE_CLOEXEC
|
||||
| OpenTreeFlags::OPEN_TREE_CLONE
|
||||
| OpenTreeFlags::AT_RECURSIVE,
|
||||
) {
|
||||
Result::Ok(tree) => {
|
||||
move_mount(
|
||||
tree.as_fd(),
|
||||
"",
|
||||
CWD,
|
||||
to.as_ref(),
|
||||
MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH,
|
||||
)?;
|
||||
}
|
||||
_ => {
|
||||
mount(
|
||||
from.as_ref(),
|
||||
to.as_ref(),
|
||||
"",
|
||||
MountFlags::BIND | MountFlags::REC,
|
||||
"",
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
fn mount_overlay_child(
|
||||
mount_point: &str,
|
||||
relative: &String,
|
||||
module_roots: &Vec<String>,
|
||||
stock_root: &String,
|
||||
) -> Result<()> {
|
||||
if !module_roots
|
||||
.iter()
|
||||
.any(|lower| Path::new(&format!("{lower}{relative}")).exists())
|
||||
{
|
||||
return bind_mount(stock_root, mount_point);
|
||||
}
|
||||
if !Path::new(&stock_root).is_dir() {
|
||||
return Ok(());
|
||||
}
|
||||
let mut lower_dirs: Vec<String> = vec![];
|
||||
for lower in module_roots {
|
||||
let lower_dir = format!("{lower}{relative}");
|
||||
let path = Path::new(&lower_dir);
|
||||
if path.is_dir() {
|
||||
lower_dirs.push(lower_dir);
|
||||
} else if path.exists() {
|
||||
// stock root has been blocked by this file
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
if lower_dirs.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
// merge modules and stock
|
||||
if let Err(e) = mount_overlayfs(&lower_dirs, stock_root, None, None, mount_point) {
|
||||
warn!("failed: {:#}, fallback to bind mount", e);
|
||||
bind_mount(stock_root, mount_point)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn mount_overlay(
|
||||
root: &String,
|
||||
module_roots: &Vec<String>,
|
||||
workdir: Option<PathBuf>,
|
||||
upperdir: Option<PathBuf>,
|
||||
) -> Result<()> {
|
||||
info!("mount overlay for {}", root);
|
||||
std::env::set_current_dir(root).with_context(|| format!("failed to chdir to {root}"))?;
|
||||
let stock_root = ".";
|
||||
|
||||
// collect child mounts before mounting the root
|
||||
let mounts = Process::myself()?
|
||||
.mountinfo()
|
||||
.with_context(|| "get mountinfo")?;
|
||||
let mut mount_seq = mounts
|
||||
.0
|
||||
.iter()
|
||||
.filter(|m| {
|
||||
m.mount_point.starts_with(root) && !Path::new(&root).starts_with(&m.mount_point)
|
||||
})
|
||||
.map(|m| m.mount_point.to_str())
|
||||
.collect::<Vec<_>>();
|
||||
mount_seq.sort();
|
||||
mount_seq.dedup();
|
||||
|
||||
mount_overlayfs(module_roots, root, upperdir, workdir, root)
|
||||
.with_context(|| "mount overlayfs for root failed")?;
|
||||
for mount_point in mount_seq.iter() {
|
||||
let Some(mount_point) = mount_point else {
|
||||
continue;
|
||||
};
|
||||
let relative = mount_point.replacen(root, "", 1);
|
||||
let stock_root: String = format!("{stock_root}{relative}");
|
||||
if !Path::new(&stock_root).exists() {
|
||||
continue;
|
||||
}
|
||||
if let Err(e) = mount_overlay_child(mount_point, &relative, module_roots, &stock_root) {
|
||||
warn!(
|
||||
"failed to mount overlay for child {}: {:#}, revert",
|
||||
mount_point, e
|
||||
);
|
||||
umount_dir(root).with_context(|| format!("failed to revert {root}"))?;
|
||||
bail!(e);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn mount_ext4(_src: &str, _target: &str, _autodrop: bool) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn umount_dir(_src: &str) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn mount_overlay(_dest: &String, _lower_dirs: &Vec<String>) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
178
apd/src/package.rs
Normal file
178
apd/src/package.rs
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
use log::{info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::File;
|
||||
use std::io::{self, BufRead};
|
||||
use std::path::Path;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
pub struct PackageConfig {
|
||||
pub pkg: String,
|
||||
pub exclude: i32,
|
||||
pub allow: i32,
|
||||
pub uid: i32,
|
||||
pub to_uid: i32,
|
||||
pub sctx: String,
|
||||
}
|
||||
|
||||
pub fn read_ap_package_config() -> Vec<PackageConfig> {
|
||||
let max_retry = 5;
|
||||
for _ in 0..max_retry {
|
||||
let file = match File::open("/data/adb/ap/package_config") {
|
||||
Ok(file) => file,
|
||||
Err(e) => {
|
||||
warn!("Error opening file: {}", e);
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let mut reader = csv::Reader::from_reader(file);
|
||||
let mut package_configs = Vec::new();
|
||||
let mut success = true;
|
||||
|
||||
for record in reader.deserialize() {
|
||||
match record {
|
||||
Ok(config) => package_configs.push(config),
|
||||
Err(e) => {
|
||||
warn!("Error deserializing record: {}", e);
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if success {
|
||||
return package_configs;
|
||||
}
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
pub fn write_ap_package_config(package_configs: &[PackageConfig]) -> io::Result<()> {
|
||||
let max_retry = 5;
|
||||
for _ in 0..max_retry {
|
||||
let temp_path = "/data/adb/ap/package_config.tmp";
|
||||
let file = match File::create(temp_path) {
|
||||
Ok(file) => file,
|
||||
Err(e) => {
|
||||
warn!("Error creating temp file: {}", e);
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let mut writer = csv::Writer::from_writer(file);
|
||||
let mut success = true;
|
||||
|
||||
for config in package_configs {
|
||||
if let Err(e) = writer.serialize(config) {
|
||||
warn!("Error serializing record: {}", e);
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !success {
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(e) = writer.flush() {
|
||||
warn!("Error flushing writer: {}", e);
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(e) = std::fs::rename(temp_path, "/data/adb/ap/package_config") {
|
||||
warn!("Error renaming temp file: {}", e);
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
continue;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Failed after max retries",
|
||||
))
|
||||
}
|
||||
|
||||
fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
File::open(filename).map(|file| io::BufReader::new(file).lines())
|
||||
}
|
||||
|
||||
pub fn synchronize_package_uid() -> io::Result<()> {
|
||||
info!("[synchronize_package_uid] Start synchronizing root list with system packages...");
|
||||
|
||||
let max_retry = 5;
|
||||
for _ in 0..max_retry {
|
||||
match read_lines("/data/system/packages.list") {
|
||||
Ok(lines) => {
|
||||
let lines: Vec<_> = lines.filter_map(|line| line.ok()).collect();
|
||||
|
||||
let mut package_configs = read_ap_package_config();
|
||||
|
||||
let system_packages: Vec<String> = lines
|
||||
.iter()
|
||||
.filter_map(|line| line.split_whitespace().next())
|
||||
.map(|pkg| pkg.to_string())
|
||||
.collect();
|
||||
|
||||
let original_len = package_configs.len();
|
||||
package_configs.retain(|config| system_packages.contains(&config.pkg));
|
||||
let removed_count = original_len - package_configs.len();
|
||||
|
||||
if removed_count > 0 {
|
||||
info!(
|
||||
"Removed {} uninstalled package configurations",
|
||||
removed_count
|
||||
);
|
||||
}
|
||||
|
||||
let mut updated = false;
|
||||
|
||||
for line in &lines {
|
||||
let words: Vec<&str> = line.split_whitespace().collect();
|
||||
if words.len() >= 2 {
|
||||
let pkg_name = words[0];
|
||||
if let Ok(uid) = words[1].parse::<i32>() {
|
||||
if let Some(config) = package_configs
|
||||
.iter_mut()
|
||||
.find(|config| config.pkg == pkg_name)
|
||||
{
|
||||
if config.uid != uid {
|
||||
info!(
|
||||
"Updating uid for package {}: {} -> {}",
|
||||
pkg_name, config.uid, uid
|
||||
);
|
||||
config.uid = uid;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!("Error parsing uid: {}", words[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if updated || removed_count > 0 {
|
||||
write_ap_package_config(&package_configs)?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Error reading packages.list: {}", e);
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Failed after max retries",
|
||||
))
|
||||
}
|
||||
185
apd/src/pty.rs
Normal file
185
apd/src/pty.rs
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
use std::ffi::c_int;
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Write, stderr, stdin, stdout};
|
||||
use std::mem::MaybeUninit;
|
||||
use std::os::fd::{AsFd, AsRawFd, OwnedFd, RawFd};
|
||||
use std::process::exit;
|
||||
use std::ptr::null_mut;
|
||||
use std::thread;
|
||||
|
||||
use crate::defs::PTS_NAME;
|
||||
use crate::utils::get_tmp_path;
|
||||
use anyhow::{Ok, Result, bail};
|
||||
use libc::{
|
||||
__errno, EINTR, SIG_BLOCK, SIG_UNBLOCK, SIGWINCH, TIOCGWINSZ, TIOCSWINSZ, fork,
|
||||
pthread_sigmask, sigaddset, sigemptyset, sigset_t, sigwait, waitpid, winsize,
|
||||
};
|
||||
use rustix::fs::{Mode, OFlags, open};
|
||||
use rustix::io::dup;
|
||||
use rustix::ioctl::{Getter, ReadOpcode, ioctl};
|
||||
use rustix::process::setsid;
|
||||
use rustix::pty::{grantpt, unlockpt};
|
||||
use rustix::stdio::{dup2_stderr, dup2_stdin, dup2_stdout};
|
||||
use rustix::termios::{OptionalActions, Termios, isatty, tcgetattr, tcsetattr};
|
||||
use std::sync::Mutex;
|
||||
|
||||
// https://github.com/topjohnwu/Magisk/blob/5627053b7481618adfdf8fa3569b48275589915b/native/src/core/su/pts.cpp
|
||||
|
||||
fn get_pty_num<F: AsFd>(fd: F) -> Result<u32> {
|
||||
Ok(unsafe {
|
||||
let tiocgptn = Getter::<ReadOpcode<b'T', 0x30, u32>, u32>::new();
|
||||
ioctl(fd, tiocgptn)?
|
||||
})
|
||||
}
|
||||
|
||||
static OLD_STDIN: Mutex<Option<Termios>> = Mutex::new(None);
|
||||
|
||||
fn watch_sigwinch_async(slave: RawFd) {
|
||||
let mut winch = MaybeUninit::<sigset_t>::uninit();
|
||||
unsafe {
|
||||
sigemptyset(winch.as_mut_ptr());
|
||||
sigaddset(winch.as_mut_ptr(), SIGWINCH);
|
||||
pthread_sigmask(SIG_BLOCK, winch.as_mut_ptr(), null_mut());
|
||||
}
|
||||
|
||||
thread::spawn(move || unsafe {
|
||||
let mut winch = MaybeUninit::<sigset_t>::uninit();
|
||||
sigemptyset(winch.as_mut_ptr());
|
||||
sigaddset(winch.as_mut_ptr(), SIGWINCH);
|
||||
pthread_sigmask(SIG_UNBLOCK, winch.as_mut_ptr(), null_mut());
|
||||
let mut sig: c_int = 0;
|
||||
loop {
|
||||
let mut w = MaybeUninit::<winsize>::uninit();
|
||||
if libc::ioctl(1, TIOCGWINSZ, w.as_mut_ptr()) < 0 {
|
||||
continue;
|
||||
}
|
||||
libc::ioctl(slave, TIOCSWINSZ, w.as_mut_ptr());
|
||||
if sigwait(winch.as_mut_ptr(), &mut sig) != 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn set_stdin_raw() -> rustix::io::Result<()> {
|
||||
let mut termios = tcgetattr(stdin())?;
|
||||
|
||||
let mut guard = OLD_STDIN.lock().unwrap();
|
||||
*guard = Some(termios.clone());
|
||||
drop(guard);
|
||||
|
||||
termios.make_raw();
|
||||
tcsetattr(stdin(), OptionalActions::Flush, &termios)
|
||||
}
|
||||
|
||||
fn restore_stdin() -> Result<()> {
|
||||
let mut guard = OLD_STDIN.lock().unwrap();
|
||||
|
||||
if let Some(original_termios) = guard.take() {
|
||||
tcsetattr(stdin(), OptionalActions::Flush, &original_termios)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pump<R: Read, W: Write>(mut from: R, mut to: W) {
|
||||
let mut buf = [0u8; 4096];
|
||||
loop {
|
||||
match from.read(&mut buf) {
|
||||
Result::Ok(len) => {
|
||||
if len == 0 {
|
||||
return;
|
||||
}
|
||||
if to.write_all(&buf[0..len]).is_err() {
|
||||
return;
|
||||
}
|
||||
if to.flush().is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pump_stdin_async(mut ptmx: File) {
|
||||
let _ = set_stdin_raw();
|
||||
|
||||
thread::spawn(move || {
|
||||
let mut stdin = stdin();
|
||||
pump(&mut stdin, &mut ptmx);
|
||||
});
|
||||
}
|
||||
|
||||
fn pump_stdout_blocking(mut ptmx: File) {
|
||||
let mut stdout = stdout();
|
||||
pump(&mut ptmx, &mut stdout);
|
||||
|
||||
let _ = restore_stdin();
|
||||
}
|
||||
|
||||
fn create_transfer(ptmx: OwnedFd) -> Result<()> {
|
||||
let pid = unsafe { fork() };
|
||||
match pid {
|
||||
d if d < 0 => bail!("fork"),
|
||||
0 => return Ok(()),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let ptmx_r = ptmx;
|
||||
let ptmx_w = dup(&ptmx_r)?;
|
||||
|
||||
let ptmx_r = File::from(ptmx_r);
|
||||
let ptmx_w = File::from(ptmx_w);
|
||||
|
||||
watch_sigwinch_async(ptmx_w.as_raw_fd());
|
||||
pump_stdin_async(ptmx_r);
|
||||
pump_stdout_blocking(ptmx_w);
|
||||
|
||||
let mut status: c_int = -1;
|
||||
|
||||
unsafe {
|
||||
loop {
|
||||
if waitpid(pid, &mut status, 0) == -1 && *__errno() != EINTR {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
exit(status)
|
||||
}
|
||||
|
||||
pub fn prepare_pty() -> Result<()> {
|
||||
let tty_in = isatty(stdin());
|
||||
let tty_out = isatty(stdout());
|
||||
let tty_err = isatty(stderr());
|
||||
if !tty_in && !tty_out && !tty_err {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut pts_path = format!("{}/{}", get_tmp_path(), PTS_NAME);
|
||||
if !std::path::Path::new(&pts_path).exists() {
|
||||
pts_path = "/dev/pts".to_string();
|
||||
}
|
||||
let ptmx_path = format!("{}/ptmx", pts_path);
|
||||
let ptmx_fd = open(ptmx_path, OFlags::RDWR, Mode::empty())?;
|
||||
grantpt(&ptmx_fd)?;
|
||||
unlockpt(&ptmx_fd)?;
|
||||
let pty_num = get_pty_num(&ptmx_fd)?;
|
||||
create_transfer(ptmx_fd)?;
|
||||
setsid()?;
|
||||
let pty_fd = open(format!("{pts_path}/{pty_num}"), OFlags::RDWR, Mode::empty())?;
|
||||
if tty_in {
|
||||
dup2_stdin(&pty_fd)?;
|
||||
}
|
||||
if tty_out {
|
||||
dup2_stdout(&pty_fd)?;
|
||||
}
|
||||
if tty_err {
|
||||
dup2_stderr(&pty_fd)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
81
apd/src/restorecon.rs
Normal file
81
apd/src/restorecon.rs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
use crate::defs;
|
||||
use anyhow::Result;
|
||||
use jwalk::{Parallelism::Serial, WalkDir};
|
||||
use std::path::Path;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use anyhow::{Context, Ok};
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use extattr::{Flags as XattrFlags, lsetxattr};
|
||||
|
||||
pub const SYSTEM_CON: &str = "u:object_r:system_file:s0";
|
||||
pub const ADB_CON: &str = "u:object_r:adb_data_file:s0";
|
||||
pub const UNLABEL_CON: &str = "u:object_r:unlabeled:s0";
|
||||
|
||||
const SELINUX_XATTR: &str = "security.selinux";
|
||||
|
||||
pub fn lsetfilecon<P: AsRef<Path>>(path: P, con: &str) -> Result<()> {
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
lsetxattr(&path, SELINUX_XATTR, con, XattrFlags::empty()).with_context(|| {
|
||||
format!(
|
||||
"Failed to change SELinux context for {}",
|
||||
path.as_ref().display()
|
||||
)
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn lgetfilecon<P: AsRef<Path>>(path: P) -> Result<String> {
|
||||
let con = extattr::lgetxattr(&path, SELINUX_XATTR).with_context(|| {
|
||||
format!(
|
||||
"Failed to get SELinux context for {}",
|
||||
path.as_ref().display()
|
||||
)
|
||||
})?;
|
||||
let con = String::from_utf8_lossy(&con);
|
||||
Ok(con.to_string())
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn setsyscon<P: AsRef<Path>>(path: P) -> Result<()> {
|
||||
lsetfilecon(path, SYSTEM_CON)
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn setsyscon<P: AsRef<Path>>(path: P) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn lgetfilecon<P: AsRef<Path>>(path: P) -> Result<String> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn restore_syscon<P: AsRef<Path>>(dir: P) -> Result<()> {
|
||||
for dir_entry in WalkDir::new(dir).parallelism(Serial) {
|
||||
if let Some(path) = dir_entry.ok().map(|dir_entry| dir_entry.path()) {
|
||||
setsyscon(&path)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn restore_syscon_if_unlabeled<P: AsRef<Path>>(dir: P) -> Result<()> {
|
||||
for dir_entry in WalkDir::new(dir).parallelism(Serial) {
|
||||
if let Some(path) = dir_entry.ok().map(|dir_entry| dir_entry.path()) {
|
||||
if let Result::Ok(con) = lgetfilecon(&path) {
|
||||
if con == UNLABEL_CON || con.is_empty() {
|
||||
lsetfilecon(&path, SYSTEM_CON)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn restorecon() -> Result<()> {
|
||||
lsetfilecon(defs::DAEMON_PATH, ADB_CON)?;
|
||||
restore_syscon_if_unlabeled(defs::MODULE_DIR)?;
|
||||
Ok(())
|
||||
}
|
||||
703
apd/src/sepolicy.rs
Normal file
703
apd/src/sepolicy.rs
Normal file
|
|
@ -0,0 +1,703 @@
|
|||
use anyhow::{Result, bail};
|
||||
use derive_new::new;
|
||||
use nom::{
|
||||
AsChar, IResult, Parser,
|
||||
branch::alt,
|
||||
bytes::complete::{tag, take_while, take_while_m_n, take_while1},
|
||||
character::complete::{space0, space1},
|
||||
combinator::map,
|
||||
};
|
||||
use std::{ffi, path::Path, vec};
|
||||
|
||||
type SeObject<'a> = Vec<&'a str>;
|
||||
|
||||
fn is_sepolicy_char(c: char) -> bool {
|
||||
c.is_alphanum() || c == '_' || c == '-'
|
||||
}
|
||||
|
||||
fn parse_single_word(input: &str) -> IResult<&str, &str> {
|
||||
take_while1(is_sepolicy_char).parse(input)
|
||||
}
|
||||
|
||||
fn parse_bracket_objs(input: &str) -> IResult<&str, SeObject> {
|
||||
let (input, (_, words, _)) = (
|
||||
tag("{"),
|
||||
take_while_m_n(1, 100, |c: char| is_sepolicy_char(c) || c.is_whitespace()),
|
||||
tag("}"),
|
||||
)
|
||||
.parse(input)?;
|
||||
Ok((input, words.split_whitespace().collect()))
|
||||
}
|
||||
|
||||
fn parse_single_obj(input: &str) -> IResult<&str, SeObject> {
|
||||
let (input, word) = take_while1(is_sepolicy_char).parse(input)?;
|
||||
Ok((input, vec![word]))
|
||||
}
|
||||
|
||||
fn parse_star(input: &str) -> IResult<&str, SeObject> {
|
||||
let (input, _) = tag("*").parse(input)?;
|
||||
Ok((input, vec!["*"]))
|
||||
}
|
||||
|
||||
// 1. a single sepolicy word
|
||||
// 2. { obj1 obj2 obj3 ...}
|
||||
// 3. *
|
||||
fn parse_seobj(input: &str) -> IResult<&str, SeObject> {
|
||||
let (input, strs) = alt((parse_single_obj, parse_bracket_objs, parse_star)).parse(input)?;
|
||||
Ok((input, strs))
|
||||
}
|
||||
|
||||
fn parse_seobj_no_star(input: &str) -> IResult<&str, SeObject> {
|
||||
let (input, strs) = alt((parse_single_obj, parse_bracket_objs)).parse(input)?;
|
||||
Ok((input, strs))
|
||||
}
|
||||
|
||||
trait SeObjectParser<'a> {
|
||||
fn parse(input: &'a str) -> IResult<&'a str, Self>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, new)]
|
||||
struct NormalPerm<'a> {
|
||||
op: &'a str,
|
||||
source: SeObject<'a>,
|
||||
target: SeObject<'a>,
|
||||
class: SeObject<'a>,
|
||||
perm: SeObject<'a>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, new)]
|
||||
struct XPerm<'a> {
|
||||
op: &'a str,
|
||||
source: SeObject<'a>,
|
||||
target: SeObject<'a>,
|
||||
class: SeObject<'a>,
|
||||
operation: &'a str,
|
||||
perm_set: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, new)]
|
||||
struct TypeState<'a> {
|
||||
op: &'a str,
|
||||
stype: SeObject<'a>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, new)]
|
||||
struct TypeAttr<'a> {
|
||||
stype: SeObject<'a>,
|
||||
sattr: SeObject<'a>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, new)]
|
||||
struct Type<'a> {
|
||||
name: &'a str,
|
||||
attrs: SeObject<'a>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, new)]
|
||||
struct Attr<'a> {
|
||||
name: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, new)]
|
||||
struct TypeTransition<'a> {
|
||||
source: &'a str,
|
||||
target: &'a str,
|
||||
class: &'a str,
|
||||
default_type: &'a str,
|
||||
object_name: Option<&'a str>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, new)]
|
||||
struct TypeChange<'a> {
|
||||
op: &'a str,
|
||||
source: &'a str,
|
||||
target: &'a str,
|
||||
class: &'a str,
|
||||
default_type: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, new)]
|
||||
struct GenFsCon<'a> {
|
||||
fs_name: &'a str,
|
||||
partial_path: &'a str,
|
||||
fs_context: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum PolicyStatement<'a> {
|
||||
// "allow *source_type *target_type *class *perm_set"
|
||||
// "deny *source_type *target_type *class *perm_set"
|
||||
// "auditallow *source_type *target_type *class *perm_set"
|
||||
// "dontaudit *source_type *target_type *class *perm_set"
|
||||
NormalPerm(NormalPerm<'a>),
|
||||
|
||||
// "allowxperm *source_type *target_type *class operation xperm_set"
|
||||
// "auditallowxperm *source_type *target_type *class operation xperm_set"
|
||||
// "dontauditxperm *source_type *target_type *class operation xperm_set"
|
||||
XPerm(XPerm<'a>),
|
||||
|
||||
// "permissive ^type"
|
||||
// "enforce ^type"
|
||||
TypeState(TypeState<'a>),
|
||||
|
||||
// "type type_name ^(attribute)"
|
||||
Type(Type<'a>),
|
||||
|
||||
// "typeattribute ^type ^attribute"
|
||||
TypeAttr(TypeAttr<'a>),
|
||||
|
||||
// "attribute ^attribute"
|
||||
Attr(Attr<'a>),
|
||||
|
||||
// "type_transition source_type target_type class default_type (object_name)"
|
||||
TypeTransition(TypeTransition<'a>),
|
||||
|
||||
// "type_change source_type target_type class default_type"
|
||||
// "type_member source_type target_type class default_type"
|
||||
TypeChange(TypeChange<'a>),
|
||||
|
||||
// "genfscon fs_name partial_path fs_context"
|
||||
GenFsCon(GenFsCon<'a>),
|
||||
}
|
||||
|
||||
impl<'a> SeObjectParser<'a> for NormalPerm<'a> {
|
||||
fn parse(input: &'a str) -> IResult<&'a str, Self> {
|
||||
let (input, op) = alt((
|
||||
tag("allow"),
|
||||
tag("deny"),
|
||||
tag("auditallow"),
|
||||
tag("dontaudit"),
|
||||
))
|
||||
.parse(input)?;
|
||||
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, source) = parse_seobj(input)?;
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, target) = parse_seobj(input)?;
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, class) = parse_seobj(input)?;
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, perm) = parse_seobj(input)?;
|
||||
Ok((input, NormalPerm::new(op, source, target, class, perm)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SeObjectParser<'a> for XPerm<'a> {
|
||||
fn parse(input: &'a str) -> IResult<&'a str, Self> {
|
||||
let (input, op) = alt((
|
||||
tag("allowxperm"),
|
||||
tag("auditallowxperm"),
|
||||
tag("dontauditxperm"),
|
||||
))
|
||||
.parse(input)?;
|
||||
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, source) = parse_seobj(input)?;
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, target) = parse_seobj(input)?;
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, class) = parse_seobj(input)?;
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, operation) = parse_single_word(input)?;
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, perm_set) = parse_single_word(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
XPerm::new(op, source, target, class, operation, perm_set),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SeObjectParser<'a> for TypeState<'a> {
|
||||
fn parse(input: &'a str) -> IResult<&'a str, Self> {
|
||||
let (input, op) = alt((tag("permissive"), tag("enforce"))).parse(input)?;
|
||||
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, stype) = parse_seobj_no_star(input)?;
|
||||
|
||||
Ok((input, TypeState::new(op, stype)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SeObjectParser<'a> for Type<'a> {
|
||||
fn parse(input: &'a str) -> IResult<&'a str, Self> {
|
||||
let (input, _) = tag("type")(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, name) = parse_single_word(input)?;
|
||||
|
||||
if input.is_empty() {
|
||||
return Ok((input, Type::new(name, vec!["domain"]))); // default to domain
|
||||
}
|
||||
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, attrs) = parse_seobj_no_star(input)?;
|
||||
|
||||
Ok((input, Type::new(name, attrs)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SeObjectParser<'a> for TypeAttr<'a> {
|
||||
fn parse(input: &'a str) -> IResult<&'a str, Self> {
|
||||
let (input, _) = alt((tag("typeattribute"), tag("attradd"))).parse(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, stype) = parse_seobj_no_star(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, attr) = parse_seobj_no_star(input)?;
|
||||
|
||||
Ok((input, TypeAttr::new(stype, attr)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SeObjectParser<'a> for Attr<'a> {
|
||||
fn parse(input: &'a str) -> IResult<&'a str, Self> {
|
||||
let (input, _) = tag("attribute")(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, attr) = parse_single_word(input)?;
|
||||
|
||||
Ok((input, Attr::new(attr)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SeObjectParser<'a> for TypeTransition<'a> {
|
||||
fn parse(input: &'a str) -> IResult<&'a str, Self> {
|
||||
let (input, _) = alt((tag("type_transition"), tag("name_transition"))).parse(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, source) = parse_single_word(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, target) = parse_single_word(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, class) = parse_single_word(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, default) = parse_single_word(input)?;
|
||||
|
||||
if input.is_empty() {
|
||||
return Ok((
|
||||
input,
|
||||
TypeTransition::new(source, target, class, default, None),
|
||||
));
|
||||
}
|
||||
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, object) = parse_single_word(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
TypeTransition::new(source, target, class, default, Some(object)),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SeObjectParser<'a> for TypeChange<'a> {
|
||||
fn parse(input: &'a str) -> IResult<&'a str, Self> {
|
||||
let (input, op) = alt((tag("type_change"), tag("type_member"))).parse(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, source) = parse_single_word(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, target) = parse_single_word(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, class) = parse_single_word(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, default) = parse_single_word(input)?;
|
||||
|
||||
Ok((input, TypeChange::new(op, source, target, class, default)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SeObjectParser<'a> for GenFsCon<'a> {
|
||||
fn parse(input: &'a str) -> IResult<&'a str, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let (input, _) = tag("genfscon")(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, fs) = parse_single_word(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, path) = parse_single_word(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, context) = parse_single_word(input)?;
|
||||
Ok((input, GenFsCon::new(fs, path, context)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PolicyStatement<'a> {
|
||||
fn parse(input: &'a str) -> IResult<&'a str, Self> {
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, statement) = alt((
|
||||
map(NormalPerm::parse, PolicyStatement::NormalPerm),
|
||||
map(XPerm::parse, PolicyStatement::XPerm),
|
||||
map(TypeState::parse, PolicyStatement::TypeState),
|
||||
map(Type::parse, PolicyStatement::Type),
|
||||
map(TypeAttr::parse, PolicyStatement::TypeAttr),
|
||||
map(Attr::parse, PolicyStatement::Attr),
|
||||
map(TypeTransition::parse, PolicyStatement::TypeTransition),
|
||||
map(TypeChange::parse, PolicyStatement::TypeChange),
|
||||
map(GenFsCon::parse, PolicyStatement::GenFsCon),
|
||||
))
|
||||
.parse(input)?;
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, _) = take_while(|c| c == ';')(input)?;
|
||||
let (input, _) = space0(input)?;
|
||||
Ok((input, statement))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_sepolicy<'a, 'b>(input: &'b str, strict: bool) -> Result<Vec<PolicyStatement<'a>>>
|
||||
where
|
||||
'b: 'a,
|
||||
{
|
||||
let mut statements = vec![];
|
||||
|
||||
for line in input.split(['\n', ';']) {
|
||||
let trimmed_line = line.trim();
|
||||
if trimmed_line.is_empty() || trimmed_line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
if let Ok((_, statement)) = PolicyStatement::parse(trimmed_line) {
|
||||
statements.push(statement);
|
||||
} else if strict {
|
||||
bail!("Failed to parse policy statement: {}", line)
|
||||
}
|
||||
}
|
||||
Ok(statements)
|
||||
}
|
||||
|
||||
const SEPOLICY_MAX_LEN: usize = 128;
|
||||
|
||||
const CMD_NORMAL_PERM: u32 = 1;
|
||||
const CMD_XPERM: u32 = 2;
|
||||
const CMD_TYPE_STATE: u32 = 3;
|
||||
const CMD_TYPE: u32 = 4;
|
||||
const CMD_TYPE_ATTR: u32 = 5;
|
||||
const CMD_ATTR: u32 = 6;
|
||||
const CMD_TYPE_TRANSITION: u32 = 7;
|
||||
const CMD_TYPE_CHANGE: u32 = 8;
|
||||
const CMD_GENFSCON: u32 = 9;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
enum PolicyObject {
|
||||
All, // for "*", stand for all objects, and is NULL in ffi
|
||||
One([u8; SEPOLICY_MAX_LEN]),
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for PolicyObject {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(s: &str) -> Result<Self> {
|
||||
anyhow::ensure!(s.len() <= SEPOLICY_MAX_LEN, "policy object too long");
|
||||
if s == "*" {
|
||||
return Ok(PolicyObject::All);
|
||||
}
|
||||
let mut buf = [0u8; SEPOLICY_MAX_LEN];
|
||||
buf[..s.len()].copy_from_slice(s.as_bytes());
|
||||
Ok(PolicyObject::One(buf))
|
||||
}
|
||||
}
|
||||
|
||||
/// atomic statement, such as: allow domain1 domain2:file1 read;
|
||||
/// normal statement would be expanded to atomic statement, for example:
|
||||
/// allow domain1 domain2:file1 { read write }; would be expanded to two atomic statement
|
||||
/// allow domain1 domain2:file1 read;allow domain1 domain2:file1 write;
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[derive(Debug, new)]
|
||||
struct AtomicStatement {
|
||||
cmd: u32,
|
||||
subcmd: u32,
|
||||
sepol1: PolicyObject,
|
||||
sepol2: PolicyObject,
|
||||
sepol3: PolicyObject,
|
||||
sepol4: PolicyObject,
|
||||
sepol5: PolicyObject,
|
||||
sepol6: PolicyObject,
|
||||
sepol7: PolicyObject,
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a NormalPerm<'a>> for Vec<AtomicStatement> {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(perm: &'a NormalPerm<'a>) -> Result<Self> {
|
||||
let mut result = vec![];
|
||||
let subcmd = match perm.op {
|
||||
"allow" => 1,
|
||||
"deny" => 2,
|
||||
"auditallow" => 3,
|
||||
"dontaudit" => 4,
|
||||
_ => 0,
|
||||
};
|
||||
for &s in &perm.source {
|
||||
for &t in &perm.target {
|
||||
for &c in &perm.class {
|
||||
for &p in &perm.perm {
|
||||
result.push(AtomicStatement {
|
||||
cmd: CMD_NORMAL_PERM,
|
||||
subcmd,
|
||||
sepol1: s.try_into()?,
|
||||
sepol2: t.try_into()?,
|
||||
sepol3: c.try_into()?,
|
||||
sepol4: p.try_into()?,
|
||||
sepol5: PolicyObject::None,
|
||||
sepol6: PolicyObject::None,
|
||||
sepol7: PolicyObject::None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a XPerm<'a>> for Vec<AtomicStatement> {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(perm: &'a XPerm<'a>) -> Result<Self> {
|
||||
let mut result = vec![];
|
||||
let subcmd = match perm.op {
|
||||
"allowxperm" => 1,
|
||||
"auditallowxperm" => 2,
|
||||
"dontauditxperm" => 3,
|
||||
_ => 0,
|
||||
};
|
||||
for &s in &perm.source {
|
||||
for &t in &perm.target {
|
||||
for &c in &perm.class {
|
||||
result.push(AtomicStatement {
|
||||
cmd: CMD_XPERM,
|
||||
subcmd,
|
||||
sepol1: s.try_into()?,
|
||||
sepol2: t.try_into()?,
|
||||
sepol3: c.try_into()?,
|
||||
sepol4: perm.operation.try_into()?,
|
||||
sepol5: perm.perm_set.try_into()?,
|
||||
sepol6: PolicyObject::None,
|
||||
sepol7: PolicyObject::None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a TypeState<'a>> for Vec<AtomicStatement> {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(perm: &'a TypeState<'a>) -> Result<Self> {
|
||||
let mut result = vec![];
|
||||
let subcmd = match perm.op {
|
||||
"permissive" => 1,
|
||||
"enforcing" => 2,
|
||||
_ => 0,
|
||||
};
|
||||
for &t in &perm.stype {
|
||||
result.push(AtomicStatement {
|
||||
cmd: CMD_TYPE_STATE,
|
||||
subcmd,
|
||||
sepol1: t.try_into()?,
|
||||
sepol2: PolicyObject::None,
|
||||
sepol3: PolicyObject::None,
|
||||
sepol4: PolicyObject::None,
|
||||
sepol5: PolicyObject::None,
|
||||
sepol6: PolicyObject::None,
|
||||
sepol7: PolicyObject::None,
|
||||
});
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a Type<'a>> for Vec<AtomicStatement> {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(perm: &'a Type<'a>) -> Result<Self> {
|
||||
let mut result = vec![];
|
||||
for &attr in &perm.attrs {
|
||||
result.push(AtomicStatement {
|
||||
cmd: CMD_TYPE,
|
||||
subcmd: 0,
|
||||
sepol1: perm.name.try_into()?,
|
||||
sepol2: attr.try_into()?,
|
||||
sepol3: PolicyObject::None,
|
||||
sepol4: PolicyObject::None,
|
||||
sepol5: PolicyObject::None,
|
||||
sepol6: PolicyObject::None,
|
||||
sepol7: PolicyObject::None,
|
||||
});
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a TypeAttr<'a>> for Vec<AtomicStatement> {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(perm: &'a TypeAttr<'a>) -> Result<Self> {
|
||||
let mut result = vec![];
|
||||
for &t in &perm.stype {
|
||||
for &attr in &perm.sattr {
|
||||
result.push(AtomicStatement {
|
||||
cmd: CMD_TYPE_ATTR,
|
||||
subcmd: 0,
|
||||
sepol1: t.try_into()?,
|
||||
sepol2: attr.try_into()?,
|
||||
sepol3: PolicyObject::None,
|
||||
sepol4: PolicyObject::None,
|
||||
sepol5: PolicyObject::None,
|
||||
sepol6: PolicyObject::None,
|
||||
sepol7: PolicyObject::None,
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a Attr<'a>> for Vec<AtomicStatement> {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(perm: &'a Attr<'a>) -> Result<Self> {
|
||||
let result = vec![AtomicStatement {
|
||||
cmd: CMD_ATTR,
|
||||
subcmd: 0,
|
||||
sepol1: perm.name.try_into()?,
|
||||
sepol2: PolicyObject::None,
|
||||
sepol3: PolicyObject::None,
|
||||
sepol4: PolicyObject::None,
|
||||
sepol5: PolicyObject::None,
|
||||
sepol6: PolicyObject::None,
|
||||
sepol7: PolicyObject::None,
|
||||
}];
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a TypeTransition<'a>> for Vec<AtomicStatement> {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(perm: &'a TypeTransition<'a>) -> Result<Self> {
|
||||
let mut result = vec![];
|
||||
let obj = match perm.object_name {
|
||||
Some(obj) => obj.try_into()?,
|
||||
None => PolicyObject::None,
|
||||
};
|
||||
result.push(AtomicStatement {
|
||||
cmd: CMD_TYPE_TRANSITION,
|
||||
subcmd: 0,
|
||||
sepol1: perm.source.try_into()?,
|
||||
sepol2: perm.target.try_into()?,
|
||||
sepol3: perm.class.try_into()?,
|
||||
sepol4: perm.default_type.try_into()?,
|
||||
sepol5: obj,
|
||||
sepol6: PolicyObject::None,
|
||||
sepol7: PolicyObject::None,
|
||||
});
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a TypeChange<'a>> for Vec<AtomicStatement> {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(perm: &'a TypeChange<'a>) -> Result<Self> {
|
||||
let mut result = vec![];
|
||||
let subcmd = match perm.op {
|
||||
"type_change" => 1,
|
||||
"type_member" => 2,
|
||||
_ => 0,
|
||||
};
|
||||
result.push(AtomicStatement {
|
||||
cmd: CMD_TYPE_CHANGE,
|
||||
subcmd,
|
||||
sepol1: perm.source.try_into()?,
|
||||
sepol2: perm.target.try_into()?,
|
||||
sepol3: perm.class.try_into()?,
|
||||
sepol4: perm.default_type.try_into()?,
|
||||
sepol5: PolicyObject::None,
|
||||
sepol6: PolicyObject::None,
|
||||
sepol7: PolicyObject::None,
|
||||
});
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a GenFsCon<'a>> for Vec<AtomicStatement> {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(perm: &'a GenFsCon<'a>) -> Result<Self> {
|
||||
let result = vec![AtomicStatement {
|
||||
cmd: CMD_GENFSCON,
|
||||
subcmd: 0,
|
||||
sepol1: perm.fs_name.try_into()?,
|
||||
sepol2: perm.partial_path.try_into()?,
|
||||
sepol3: perm.fs_context.try_into()?,
|
||||
sepol4: PolicyObject::None,
|
||||
sepol5: PolicyObject::None,
|
||||
sepol6: PolicyObject::None,
|
||||
sepol7: PolicyObject::None,
|
||||
}];
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a PolicyStatement<'a>> for Vec<AtomicStatement> {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(value: &'a PolicyStatement) -> Result<Self> {
|
||||
match value {
|
||||
PolicyStatement::NormalPerm(perm) => perm.try_into(),
|
||||
PolicyStatement::XPerm(perm) => perm.try_into(),
|
||||
PolicyStatement::TypeState(perm) => perm.try_into(),
|
||||
PolicyStatement::Type(perm) => perm.try_into(),
|
||||
PolicyStatement::TypeAttr(perm) => perm.try_into(),
|
||||
PolicyStatement::Attr(perm) => perm.try_into(),
|
||||
PolicyStatement::TypeTransition(perm) => perm.try_into(),
|
||||
PolicyStatement::TypeChange(perm) => perm.try_into(),
|
||||
PolicyStatement::GenFsCon(perm) => perm.try_into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////
|
||||
/// for C FFI to call kernel interface
|
||||
///////////////////////////////////////////////////////////////
|
||||
|
||||
#[derive(Debug)]
|
||||
#[repr(C)]
|
||||
struct FfiPolicy {
|
||||
cmd: u32,
|
||||
subcmd: u32,
|
||||
sepol1: *const ffi::c_char,
|
||||
sepol2: *const ffi::c_char,
|
||||
sepol3: *const ffi::c_char,
|
||||
sepol4: *const ffi::c_char,
|
||||
sepol5: *const ffi::c_char,
|
||||
sepol6: *const ffi::c_char,
|
||||
sepol7: *const ffi::c_char,
|
||||
}
|
||||
|
||||
fn to_c_ptr(pol: &PolicyObject) -> *const ffi::c_char {
|
||||
match pol {
|
||||
PolicyObject::None | PolicyObject::All => std::ptr::null(),
|
||||
PolicyObject::One(s) => s.as_ptr().cast::<ffi::c_char>(),
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AtomicStatement> for FfiPolicy {
|
||||
fn from(policy: AtomicStatement) -> FfiPolicy {
|
||||
FfiPolicy {
|
||||
cmd: policy.cmd,
|
||||
subcmd: policy.subcmd,
|
||||
sepol1: to_c_ptr(&policy.sepol1),
|
||||
sepol2: to_c_ptr(&policy.sepol2),
|
||||
sepol3: to_c_ptr(&policy.sepol3),
|
||||
sepol4: to_c_ptr(&policy.sepol4),
|
||||
sepol5: to_c_ptr(&policy.sepol5),
|
||||
sepol6: to_c_ptr(&policy.sepol6),
|
||||
sepol7: to_c_ptr(&policy.sepol7),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_rule(policy: &str) -> Result<()> {
|
||||
let path = Path::new(policy);
|
||||
let policy = if path.exists() {
|
||||
std::fs::read_to_string(path)?
|
||||
} else {
|
||||
policy.to_string()
|
||||
};
|
||||
parse_sepolicy(policy.trim(), true)?;
|
||||
Ok(())
|
||||
}
|
||||
476
apd/src/supercall.rs
Normal file
476
apd/src/supercall.rs
Normal file
|
|
@ -0,0 +1,476 @@
|
|||
use crate::package::{read_ap_package_config, synchronize_package_uid};
|
||||
use errno::errno;
|
||||
use libc::{EINVAL, c_int, c_long, c_void, execv, fork, pid_t, setenv, syscall, uid_t, wait};
|
||||
use log::{error, info, warn};
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::fmt::Write;
|
||||
use std::fs::File;
|
||||
use std::io::{self, Read};
|
||||
|
||||
use std::process::exit;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use std::{process, ptr};
|
||||
|
||||
const MAJOR: c_long = 0;
|
||||
const MINOR: c_long = 11;
|
||||
const PATCH: c_long = 1;
|
||||
|
||||
const KSTORAGE_EXCLUDE_LIST_GROUP: i32 = 1;
|
||||
|
||||
const __NR_SUPERCALL: c_long = 45;
|
||||
const SUPERCALL_KLOG: c_long = 0x1004;
|
||||
const SUPERCALL_KERNELPATCH_VER: c_long = 0x1008;
|
||||
const SUPERCALL_KERNEL_VER: c_long = 0x1009;
|
||||
const SUPERCALL_SU: c_long = 0x1010;
|
||||
const SUPERCALL_KSTORAGE_WRITE: c_long = 0x1041;
|
||||
const SUPERCALL_SU_GRANT_UID: c_long = 0x1100;
|
||||
const SUPERCALL_SU_REVOKE_UID: c_long = 0x1101;
|
||||
const SUPERCALL_SU_NUMS: c_long = 0x1102;
|
||||
const SUPERCALL_SU_LIST: c_long = 0x1103;
|
||||
const SUPERCALL_SU_RESET_PATH: c_long = 0x1111;
|
||||
const SUPERCALL_SU_GET_SAFEMODE: c_long = 0x1112;
|
||||
|
||||
const SUPERCALL_SCONTEXT_LEN: usize = 0x60;
|
||||
|
||||
#[repr(C)]
|
||||
struct SuProfile {
|
||||
uid: i32,
|
||||
to_uid: i32,
|
||||
scontext: [u8; SUPERCALL_SCONTEXT_LEN],
|
||||
}
|
||||
|
||||
fn ver_and_cmd(cmd: c_long) -> c_long {
|
||||
let version_code: u32 = ((MAJOR << 16) + (MINOR << 8) + PATCH).try_into().unwrap();
|
||||
((version_code as c_long) << 32) | (0x1158 << 16) | (cmd & 0xFFFF)
|
||||
}
|
||||
|
||||
fn sc_su_revoke_uid(key: &CStr, uid: uid_t) -> c_long {
|
||||
if key.to_bytes().is_empty() {
|
||||
return (-EINVAL).into();
|
||||
}
|
||||
unsafe {
|
||||
syscall(
|
||||
__NR_SUPERCALL,
|
||||
key.as_ptr(),
|
||||
ver_and_cmd(SUPERCALL_SU_REVOKE_UID),
|
||||
uid,
|
||||
) as c_long
|
||||
}
|
||||
}
|
||||
|
||||
fn sc_su_grant_uid(key: &CStr, profile: &SuProfile) -> c_long {
|
||||
if key.to_bytes().is_empty() {
|
||||
return (-EINVAL).into();
|
||||
}
|
||||
unsafe {
|
||||
syscall(
|
||||
__NR_SUPERCALL,
|
||||
key.as_ptr(),
|
||||
ver_and_cmd(SUPERCALL_SU_GRANT_UID),
|
||||
profile,
|
||||
) as c_long
|
||||
}
|
||||
}
|
||||
|
||||
fn sc_kstorage_write(
|
||||
key: &CStr,
|
||||
gid: i32,
|
||||
did: i64,
|
||||
data: *mut c_void,
|
||||
offset: i32,
|
||||
dlen: i32,
|
||||
) -> c_long {
|
||||
if key.to_bytes().is_empty() {
|
||||
return (-EINVAL).into();
|
||||
}
|
||||
unsafe {
|
||||
syscall(
|
||||
__NR_SUPERCALL,
|
||||
key.as_ptr(),
|
||||
ver_and_cmd(SUPERCALL_KSTORAGE_WRITE),
|
||||
gid as c_long,
|
||||
did as c_long,
|
||||
data,
|
||||
(((offset as i64) << 32) | (dlen as i64)) as c_long,
|
||||
) as c_long
|
||||
}
|
||||
}
|
||||
|
||||
fn sc_set_ap_mod_exclude(key: &CStr, uid: i64, exclude: i32) -> c_long {
|
||||
sc_kstorage_write(
|
||||
key,
|
||||
KSTORAGE_EXCLUDE_LIST_GROUP,
|
||||
uid,
|
||||
&exclude as *const i32 as *mut c_void,
|
||||
0,
|
||||
size_of::<i32>() as i32,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn sc_su_get_safemode(key: &CStr) -> c_long {
|
||||
if key.to_bytes().is_empty() {
|
||||
warn!("[sc_su_get_safemode] null superkey, tell apd we are not in safemode!");
|
||||
return 0;
|
||||
}
|
||||
|
||||
let key_ptr = key.as_ptr();
|
||||
if key_ptr.is_null() {
|
||||
warn!("[sc_su_get_safemode] superkey pointer is null!");
|
||||
return 0;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
syscall(
|
||||
__NR_SUPERCALL,
|
||||
key_ptr,
|
||||
ver_and_cmd(SUPERCALL_SU_GET_SAFEMODE),
|
||||
) as c_long
|
||||
}
|
||||
}
|
||||
|
||||
fn sc_su(key: &CStr, profile: &SuProfile) -> c_long {
|
||||
if key.to_bytes().is_empty() {
|
||||
return (-EINVAL).into();
|
||||
}
|
||||
unsafe {
|
||||
syscall(
|
||||
__NR_SUPERCALL,
|
||||
key.as_ptr(),
|
||||
ver_and_cmd(SUPERCALL_SU),
|
||||
profile,
|
||||
) as c_long
|
||||
}
|
||||
}
|
||||
|
||||
fn sc_su_reset_path(key: &CStr, path: &CStr) -> c_long {
|
||||
if key.to_bytes().is_empty() || path.to_bytes().is_empty() {
|
||||
return (-EINVAL).into();
|
||||
}
|
||||
unsafe {
|
||||
syscall(
|
||||
__NR_SUPERCALL,
|
||||
key.as_ptr(),
|
||||
ver_and_cmd(SUPERCALL_SU_RESET_PATH),
|
||||
path.as_ptr(),
|
||||
) as c_long
|
||||
}
|
||||
}
|
||||
|
||||
fn sc_kp_ver(key: &CStr) -> Result<u32, i32> {
|
||||
if key.to_bytes().is_empty() {
|
||||
return Err(-EINVAL);
|
||||
}
|
||||
let ret = unsafe {
|
||||
syscall(
|
||||
__NR_SUPERCALL,
|
||||
key.as_ptr(),
|
||||
ver_and_cmd(SUPERCALL_KERNELPATCH_VER),
|
||||
)
|
||||
};
|
||||
Ok(ret as u32)
|
||||
}
|
||||
|
||||
fn sc_k_ver(key: &CStr) -> Result<u32, i32> {
|
||||
if key.to_bytes().is_empty() {
|
||||
return Err(-EINVAL);
|
||||
}
|
||||
let ret = unsafe {
|
||||
syscall(
|
||||
__NR_SUPERCALL,
|
||||
key.as_ptr(),
|
||||
ver_and_cmd(SUPERCALL_KERNEL_VER),
|
||||
)
|
||||
};
|
||||
Ok(ret as u32)
|
||||
}
|
||||
|
||||
fn sc_klog(key: &CStr, msg: &CStr) -> c_long {
|
||||
if key.to_bytes().is_empty() || msg.to_bytes().is_empty() {
|
||||
return (-EINVAL).into();
|
||||
}
|
||||
unsafe {
|
||||
syscall(
|
||||
__NR_SUPERCALL,
|
||||
key.as_ptr(),
|
||||
ver_and_cmd(SUPERCALL_KLOG),
|
||||
msg.as_ptr(),
|
||||
) as c_long
|
||||
}
|
||||
}
|
||||
|
||||
fn sc_su_uid_nums(key: &CStr) -> c_long {
|
||||
if key.to_bytes().is_empty() {
|
||||
return (-EINVAL).into();
|
||||
}
|
||||
unsafe { syscall(__NR_SUPERCALL, key.as_ptr(), ver_and_cmd(SUPERCALL_SU_NUMS)) as c_long }
|
||||
}
|
||||
|
||||
fn sc_su_allow_uids(key: &CStr, buf: &mut [uid_t]) -> c_long {
|
||||
if key.to_bytes().is_empty() {
|
||||
return (-EINVAL).into();
|
||||
}
|
||||
if buf.is_empty() {
|
||||
return (-EINVAL).into();
|
||||
}
|
||||
unsafe {
|
||||
syscall(
|
||||
__NR_SUPERCALL,
|
||||
key.as_ptr(),
|
||||
ver_and_cmd(SUPERCALL_SU_LIST),
|
||||
buf.as_mut_ptr(),
|
||||
buf.len() as i32,
|
||||
) as c_long
|
||||
}
|
||||
}
|
||||
|
||||
fn read_file_to_string(path: &str) -> io::Result<String> {
|
||||
let mut file = File::open(path)?;
|
||||
let mut content = String::new();
|
||||
file.read_to_string(&mut content)?;
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
fn convert_string_to_u8_array(s: &str) -> [u8; SUPERCALL_SCONTEXT_LEN] {
|
||||
let mut u8_array = [0u8; SUPERCALL_SCONTEXT_LEN];
|
||||
let bytes = s.as_bytes();
|
||||
let len = usize::min(SUPERCALL_SCONTEXT_LEN, bytes.len());
|
||||
u8_array[..len].copy_from_slice(&bytes[..len]);
|
||||
u8_array
|
||||
}
|
||||
|
||||
fn convert_superkey(s: &Option<String>) -> Option<CString> {
|
||||
s.as_ref().and_then(|s| CString::new(s.clone()).ok())
|
||||
}
|
||||
|
||||
pub fn refresh_ap_package_list(skey: &CStr, mutex: &Arc<Mutex<()>>) {
|
||||
let _lock = mutex.lock().unwrap();
|
||||
|
||||
let num = sc_su_uid_nums(skey);
|
||||
if num < 0 {
|
||||
error!("[refresh_su_list] Error getting number of UIDs: {}", num);
|
||||
return;
|
||||
}
|
||||
let num = num as usize;
|
||||
let mut uids = vec![0 as uid_t; num];
|
||||
let n = sc_su_allow_uids(skey, &mut uids);
|
||||
if n < 0 {
|
||||
error!("[refresh_su_list] Error getting su list");
|
||||
return;
|
||||
}
|
||||
for uid in &uids {
|
||||
if *uid == 0 || *uid == 2000 {
|
||||
warn!(
|
||||
"[refresh_ap_package_list] Skip revoking critical uid: {}",
|
||||
uid
|
||||
);
|
||||
continue;
|
||||
}
|
||||
info!(
|
||||
"[refresh_ap_package_list] Revoking {} root permission...",
|
||||
uid
|
||||
);
|
||||
let rc = sc_su_revoke_uid(skey, *uid);
|
||||
if rc != 0 {
|
||||
error!("[refresh_ap_package_list] Error revoking UID: {}", rc);
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = synchronize_package_uid() {
|
||||
error!("Failed to synchronize package UIDs: {}", e);
|
||||
}
|
||||
|
||||
let package_configs = read_ap_package_config();
|
||||
for config in package_configs {
|
||||
if config.allow == 1 && config.exclude == 0 {
|
||||
let profile = SuProfile {
|
||||
uid: config.uid,
|
||||
to_uid: config.to_uid,
|
||||
scontext: convert_string_to_u8_array(&config.sctx),
|
||||
};
|
||||
let result = sc_su_grant_uid(skey, &profile);
|
||||
info!(
|
||||
"[refresh_ap_package_list] Loading {}: result = {}",
|
||||
config.pkg, result
|
||||
);
|
||||
}
|
||||
if config.allow == 0 && config.exclude == 1 {
|
||||
let result = sc_set_ap_mod_exclude(skey, config.uid as i64, 1);
|
||||
info!(
|
||||
"[refresh_ap_package_list] Loading exclude {}: result = {}",
|
||||
config.pkg, result
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn privilege_apd_profile(superkey: &Option<String>) {
|
||||
let key = convert_superkey(superkey);
|
||||
|
||||
let all_allow_ctx = "u:r:magisk:s0";
|
||||
let profile = SuProfile {
|
||||
uid: process::id().try_into().expect("PID conversion failed"),
|
||||
to_uid: 0,
|
||||
scontext: convert_string_to_u8_array(all_allow_ctx),
|
||||
};
|
||||
if let Some(ref key) = key {
|
||||
let result = sc_su(key, &profile);
|
||||
info!("[privilege_apd_profile] result = {}", result);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_load_package_uid_config(superkey: &Option<String>) {
|
||||
let package_configs = read_ap_package_config();
|
||||
let key = convert_superkey(superkey);
|
||||
|
||||
for config in package_configs {
|
||||
if config.allow == 1 && config.exclude == 0 {
|
||||
match key {
|
||||
Some(ref key) => {
|
||||
let profile = SuProfile {
|
||||
uid: config.uid,
|
||||
to_uid: config.to_uid,
|
||||
scontext: convert_string_to_u8_array(&config.sctx),
|
||||
};
|
||||
let result = sc_su_grant_uid(key, &profile);
|
||||
info!("Processed {}: result = {}", config.pkg, result);
|
||||
}
|
||||
_ => {
|
||||
warn!("Superkey is None, skipping config: {}", config.pkg);
|
||||
}
|
||||
}
|
||||
}
|
||||
if config.allow == 0 && config.exclude == 1 {
|
||||
match key {
|
||||
Some(ref key) => {
|
||||
let result = sc_set_ap_mod_exclude(key, config.uid as i64, 1);
|
||||
info!("Processed exclude {}: result = {}", config.pkg, result);
|
||||
}
|
||||
_ => {
|
||||
warn!("Superkey is None, skipping config: {}", config.pkg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_load_su_path(superkey: &Option<String>) {
|
||||
let su_path_file = "/data/adb/ap/su_path";
|
||||
|
||||
match read_file_to_string(su_path_file) {
|
||||
Ok(su_path) => {
|
||||
let superkey_cstr = convert_superkey(superkey);
|
||||
|
||||
match superkey_cstr {
|
||||
Some(superkey_cstr) => match CString::new(su_path.trim()) {
|
||||
Ok(su_path_cstr) => {
|
||||
let result = sc_su_reset_path(&superkey_cstr, &su_path_cstr);
|
||||
if result == 0 {
|
||||
info!("suPath load successfully");
|
||||
} else {
|
||||
warn!("Failed to load su path, error code: {}", result);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to convert su_path: {}", e);
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
warn!("Superkey is None, skipping...");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to read su_path file: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_env_var(key: &str, value: &str) {
|
||||
let key_c = CString::new(key).expect("CString::new failed");
|
||||
let value_c = CString::new(value).expect("CString::new failed");
|
||||
unsafe {
|
||||
setenv(key_c.as_ptr(), value_c.as_ptr(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
fn log_kernel(key: &CStr, _fmt: &str, args: std::fmt::Arguments) -> c_long {
|
||||
let mut buf = String::with_capacity(1024);
|
||||
write!(&mut buf, "{}", args).expect("Error formatting string");
|
||||
|
||||
let c_buf = CString::new(buf).expect("CString::new failed");
|
||||
sc_klog(key, &c_buf)
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! log_kernel {
|
||||
($key:expr_2021, $fmt:expr_2021, $($arg:tt)*) => (
|
||||
log_kernel($key, $fmt, std::format_args!($fmt, $($arg)*))
|
||||
)
|
||||
}
|
||||
|
||||
pub fn fork_for_result(exec: &str, argv: &[&str], key: &Option<String>) {
|
||||
let mut cmd = String::new();
|
||||
for arg in argv {
|
||||
cmd.push_str(arg);
|
||||
cmd.push(' ');
|
||||
}
|
||||
|
||||
let superkey_cstr = convert_superkey(key);
|
||||
|
||||
match superkey_cstr {
|
||||
Some(superkey_cstr) => {
|
||||
unsafe {
|
||||
let pid: pid_t = fork();
|
||||
if pid < 0 {
|
||||
log_kernel!(
|
||||
&superkey_cstr,
|
||||
"{} fork {} error: {}\n",
|
||||
libc::getpid(),
|
||||
exec,
|
||||
-1
|
||||
);
|
||||
} else if pid == 0 {
|
||||
set_env_var("KERNELPATCH", "true");
|
||||
let kpver = format!("{:x}", sc_kp_ver(&superkey_cstr).unwrap_or(0));
|
||||
set_env_var("KERNELPATCH_VERSION", kpver.as_str());
|
||||
let kver = format!("{:x}", sc_k_ver(&superkey_cstr).unwrap_or(0));
|
||||
set_env_var("KERNEL_VERSION", kver.as_str());
|
||||
|
||||
let c_exec = CString::new(exec).expect("CString::new failed");
|
||||
let c_argv: Vec<CString> =
|
||||
argv.iter().map(|&arg| CString::new(arg).unwrap()).collect();
|
||||
let mut c_argv_ptrs: Vec<*const libc::c_char> =
|
||||
c_argv.iter().map(|arg| arg.as_ptr()).collect();
|
||||
c_argv_ptrs.push(ptr::null());
|
||||
|
||||
execv(c_exec.as_ptr(), c_argv_ptrs.as_ptr());
|
||||
|
||||
log_kernel!(
|
||||
&superkey_cstr,
|
||||
"{} exec {} error: {}\n",
|
||||
libc::getpid(),
|
||||
cmd,
|
||||
CStr::from_ptr(libc::strerror(errno().0))
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
);
|
||||
exit(1); // execv only returns on error
|
||||
} else {
|
||||
let mut status: c_int = 0;
|
||||
wait(&mut status);
|
||||
log_kernel!(
|
||||
&superkey_cstr,
|
||||
"{} wait {} status: 0x{}\n",
|
||||
libc::getpid(),
|
||||
cmd,
|
||||
status
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
warn!("[fork_for_result] SuperKey convert failed!");
|
||||
}
|
||||
}
|
||||
}
|
||||
198
apd/src/utils.rs
Normal file
198
apd/src/utils.rs
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
use anyhow::{Context, Error, Ok, Result, bail};
|
||||
use log::{info, warn};
|
||||
use std::ffi::CString;
|
||||
use std::{
|
||||
fs::{File, OpenOptions, create_dir_all},
|
||||
io::{BufRead, BufReader, ErrorKind::AlreadyExists, Write},
|
||||
path::Path,
|
||||
process::Stdio,
|
||||
};
|
||||
|
||||
use crate::defs;
|
||||
use std::fs::metadata;
|
||||
#[allow(unused_imports)]
|
||||
use std::fs::{Permissions, set_permissions};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::prelude::PermissionsExt;
|
||||
use std::process::Command;
|
||||
|
||||
use crate::supercall::sc_su_get_safemode;
|
||||
|
||||
pub fn ensure_clean_dir(dir: &str) -> Result<()> {
|
||||
let path = Path::new(dir);
|
||||
log::debug!("ensure_clean_dir: {}", path.display());
|
||||
if path.exists() {
|
||||
log::debug!("ensure_clean_dir: {} exists, remove it", path.display());
|
||||
std::fs::remove_dir_all(path)?;
|
||||
}
|
||||
Ok(create_dir_all(path)?)
|
||||
}
|
||||
|
||||
pub fn ensure_file_exists<T: AsRef<Path>>(file: T) -> Result<()> {
|
||||
match File::options().write(true).create_new(true).open(&file) {
|
||||
Result::Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
if err.kind() == AlreadyExists && file.as_ref().is_file() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::from(err))
|
||||
.with_context(|| format!("{} is not a regular file", file.as_ref().display()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ensure_dir_exists<T: AsRef<Path>>(dir: T) -> Result<()> {
|
||||
let result = create_dir_all(&dir).map_err(Error::from);
|
||||
if dir.as_ref().is_dir() {
|
||||
result
|
||||
} else if result.is_ok() {
|
||||
bail!("{} is not a regular directory", dir.as_ref().display())
|
||||
} else {
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
// todo: ensure
|
||||
pub fn ensure_binary<T: AsRef<Path>>(path: T) -> Result<()> {
|
||||
set_permissions(&path, Permissions::from_mode(0o755))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn getprop(prop: &str) -> Option<String> {
|
||||
android_properties::getprop(prop).value()
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn getprop(_prop: &str) -> Option<String> {
|
||||
unimplemented!()
|
||||
}
|
||||
pub fn run_command(
|
||||
command: &str,
|
||||
args: &[&str],
|
||||
stdout: Option<Stdio>,
|
||||
) -> Result<std::process::Child> {
|
||||
let mut command_builder = Command::new(command);
|
||||
command_builder.args(args);
|
||||
if let Some(out) = stdout {
|
||||
command_builder.stdout(out);
|
||||
}
|
||||
let child = command_builder.spawn()?;
|
||||
Ok(child)
|
||||
}
|
||||
pub fn is_safe_mode(superkey: Option<String>) -> bool {
|
||||
let safemode = getprop("persist.sys.safemode")
|
||||
.filter(|prop| prop == "1")
|
||||
.is_some()
|
||||
|| getprop("ro.sys.safemode")
|
||||
.filter(|prop| prop == "1")
|
||||
.is_some();
|
||||
info!("safemode: {}", safemode);
|
||||
if safemode {
|
||||
return true;
|
||||
}
|
||||
let safemode = superkey
|
||||
.as_ref()
|
||||
.and_then(|key_str| CString::new(key_str.as_str()).ok())
|
||||
.map_or_else(
|
||||
|| {
|
||||
warn!("[is_safe_mode] No valid superkey provided, assuming safemode as false.");
|
||||
false
|
||||
},
|
||||
|cstr| sc_su_get_safemode(&cstr) == 1,
|
||||
);
|
||||
info!("kernel_safemode: {}", safemode);
|
||||
safemode
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn switch_mnt_ns(pid: i32) -> Result<()> {
|
||||
use anyhow::ensure;
|
||||
use std::os::fd::AsRawFd;
|
||||
let path = format!("/proc/{pid}/ns/mnt");
|
||||
let fd = File::open(path)?;
|
||||
let current_dir = std::env::current_dir();
|
||||
let ret = unsafe { libc::setns(fd.as_raw_fd(), libc::CLONE_NEWNS) };
|
||||
if let Result::Ok(current_dir) = current_dir {
|
||||
let _ = std::env::set_current_dir(current_dir);
|
||||
}
|
||||
ensure!(ret == 0, "switch mnt ns failed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_overlayfs_supported() -> Result<bool> {
|
||||
let file =
|
||||
File::open("/proc/filesystems").with_context(|| "Failed to open /proc/filesystems")?;
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
let overlay_supported = reader.lines().any(|line| {
|
||||
if let Result::Ok(line) = line {
|
||||
line.contains("overlay")
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
Ok(overlay_supported)
|
||||
}
|
||||
|
||||
pub fn should_use_overlayfs() -> Result<bool> {
|
||||
let force_using_overlayfs = Path::new(defs::FORCE_OVERLAYFS_FILE).exists();
|
||||
let overlayfs_supported = is_overlayfs_supported()?;
|
||||
|
||||
Ok(force_using_overlayfs && overlayfs_supported)
|
||||
}
|
||||
|
||||
fn switch_cgroup(grp: &str, pid: u32) {
|
||||
let path = Path::new(grp).join("cgroup.procs");
|
||||
if !path.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
let fp = OpenOptions::new().append(true).open(path);
|
||||
if let Result::Ok(mut fp) = fp {
|
||||
let _ = write!(fp, "{pid}");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn switch_cgroups() {
|
||||
let pid = std::process::id();
|
||||
switch_cgroup("/acct", pid);
|
||||
switch_cgroup("/dev/cg2_bpf", pid);
|
||||
switch_cgroup("/sys/fs/cgroup", pid);
|
||||
|
||||
if getprop("ro.config.per_app_memcg")
|
||||
.filter(|prop| prop == "false")
|
||||
.is_none()
|
||||
{
|
||||
switch_cgroup("/dev/memcg/apps", pid);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn umask(mask: u32) {
|
||||
unsafe { libc::umask(mask) };
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn umask(_mask: u32) {
|
||||
unimplemented!("umask is not supported on this platform")
|
||||
}
|
||||
|
||||
pub fn has_magisk() -> bool {
|
||||
which::which("magisk").is_ok()
|
||||
}
|
||||
pub fn get_tmp_path() -> &'static str {
|
||||
if metadata(defs::TEMP_DIR_LEGACY).is_ok() {
|
||||
return defs::TEMP_DIR_LEGACY;
|
||||
}
|
||||
if metadata(defs::TEMP_DIR).is_ok() {
|
||||
return defs::TEMP_DIR;
|
||||
}
|
||||
""
|
||||
}
|
||||
pub fn get_work_dir() -> String {
|
||||
let tmp_path = get_tmp_path();
|
||||
format!("{}/workdir/", tmp_path)
|
||||
}
|
||||
2
app/.gitignore
vendored
Normal file
2
app/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/build
|
||||
/release/
|
||||
294
app/build.gradle.kts
Normal file
294
app/build.gradle.kts
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
@file:Suppress("UnstableApiUsage")
|
||||
|
||||
import com.android.build.gradle.tasks.PackageAndroidArtifact
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import java.net.URI
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.agp.app)
|
||||
alias(libs.plugins.kotlin)
|
||||
alias(libs.plugins.kotlin.compose.compiler)
|
||||
alias(libs.plugins.ksp)
|
||||
alias(libs.plugins.lsplugin.apksign)
|
||||
alias(libs.plugins.lsplugin.resopt)
|
||||
alias(libs.plugins.lsplugin.cmaker)
|
||||
id("kotlin-parcelize")
|
||||
}
|
||||
|
||||
val managerVersionCode: Int by rootProject.extra
|
||||
val managerVersionName: String by rootProject.extra
|
||||
val branchname: String by rootProject.extra
|
||||
val kernelPatchVersion: String by rootProject.extra
|
||||
|
||||
apksign {
|
||||
storeFileProperty = "KEYSTORE_FILE"
|
||||
storePasswordProperty = "KEYSTORE_PASSWORD"
|
||||
keyAliasProperty = "KEY_ALIAS"
|
||||
keyPasswordProperty = "KEY_PASSWORD"
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "me.bmax.apatch"
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
isDebuggable = true
|
||||
isMinifyEnabled = false
|
||||
isShrinkResources = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
isDebuggable = false
|
||||
multiDexEnabled = true
|
||||
vcsInfo.include = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
dependenciesInfo.includeInApk = false
|
||||
|
||||
// https://stackoverflow.com/a/77745844
|
||||
tasks.withType<PackageAndroidArtifact> {
|
||||
doFirst { appMetadata.asFile.orNull?.writeText("") }
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
aidl = true
|
||||
buildConfig = true
|
||||
compose = true
|
||||
prefab = true
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
buildConfigField("String", "buildKPV", "\"$kernelPatchVersion\"")
|
||||
|
||||
base.archivesName = "APatch_${managerVersionCode}_${managerVersionName}_on_${branchname}"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
targetCompatibility = JavaVersion.VERSION_21
|
||||
}
|
||||
|
||||
packaging {
|
||||
jniLibs {
|
||||
useLegacyPackaging = true
|
||||
}
|
||||
resources {
|
||||
excludes += "**"
|
||||
merges += "META-INF/com/google/android/**"
|
||||
}
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
version = "3.28.0+"
|
||||
path("src/main/cpp/CMakeLists.txt")
|
||||
}
|
||||
}
|
||||
|
||||
androidResources {
|
||||
generateLocaleConfig = true
|
||||
}
|
||||
|
||||
sourceSets["main"].jniLibs.srcDir("libs")
|
||||
|
||||
applicationVariants.all {
|
||||
kotlin.sourceSets {
|
||||
getByName(name) {
|
||||
kotlin.srcDir("build/generated/ksp/$name/kotlin")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(21)
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(21)
|
||||
compilerOptions {
|
||||
jvmTarget = JvmTarget.JVM_21
|
||||
}
|
||||
}
|
||||
|
||||
fun registerDownloadTask(
|
||||
taskName: String, srcUrl: String, destPath: String, project: Project
|
||||
) {
|
||||
project.tasks.register(taskName) {
|
||||
val destFile = File(destPath)
|
||||
|
||||
doLast {
|
||||
if (!destFile.exists() || isFileUpdated(srcUrl, destFile)) {
|
||||
println(" - Downloading $srcUrl to ${destFile.absolutePath}")
|
||||
downloadFile(srcUrl, destFile)
|
||||
println(" - Download completed.")
|
||||
} else {
|
||||
println(" - File is up-to-date, skipping download.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isFileUpdated(url: String, localFile: File): Boolean {
|
||||
val connection = URI.create(url).toURL().openConnection()
|
||||
val remoteLastModified = connection.getHeaderFieldDate("Last-Modified", 0L)
|
||||
return remoteLastModified > localFile.lastModified()
|
||||
}
|
||||
|
||||
fun downloadFile(url: String, destFile: File) {
|
||||
URI.create(url).toURL().openStream().use { input ->
|
||||
destFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerDownloadTask(
|
||||
taskName = "downloadKpimg",
|
||||
srcUrl = "https://github.com/bmax121/KernelPatch/releases/download/$kernelPatchVersion/kpimg-android",
|
||||
destPath = "${project.projectDir}/src/main/assets/kpimg",
|
||||
project = project
|
||||
)
|
||||
|
||||
registerDownloadTask(
|
||||
taskName = "downloadKptools",
|
||||
srcUrl = "https://github.com/bmax121/KernelPatch/releases/download/$kernelPatchVersion/kptools-android",
|
||||
destPath = "${project.projectDir}/libs/arm64-v8a/libkptools.so",
|
||||
project = project
|
||||
)
|
||||
|
||||
// Compat kp version less than 0.10.7
|
||||
// TODO: Remove in future
|
||||
registerDownloadTask(
|
||||
taskName = "downloadCompatKpatch",
|
||||
srcUrl = "https://github.com/bmax121/KernelPatch/releases/download/0.10.7/kpatch-android",
|
||||
destPath = "${project.projectDir}/libs/arm64-v8a/libkpatch.so",
|
||||
project = project
|
||||
)
|
||||
|
||||
tasks.register<Copy>("mergeScripts") {
|
||||
into("${project.projectDir}/src/main/resources/META-INF/com/google/android")
|
||||
from(rootProject.file("${project.rootDir}/scripts/update_binary.sh")) {
|
||||
rename { "update-binary" }
|
||||
}
|
||||
from(rootProject.file("${project.rootDir}/scripts/update_script.sh")) {
|
||||
rename { "updater-script" }
|
||||
}
|
||||
}
|
||||
|
||||
tasks.getByName("preBuild").dependsOn(
|
||||
"downloadKpimg",
|
||||
"downloadKptools",
|
||||
"downloadCompatKpatch",
|
||||
"mergeScripts",
|
||||
)
|
||||
|
||||
// https://github.com/bbqsrc/cargo-ndk
|
||||
// cargo ndk -t arm64-v8a build --release
|
||||
tasks.register<Exec>("cargoBuild") {
|
||||
executable("cargo")
|
||||
args("ndk", "-t", "arm64-v8a", "build", "--release")
|
||||
workingDir("${project.rootDir}/apd")
|
||||
}
|
||||
|
||||
tasks.register<Copy>("buildApd") {
|
||||
dependsOn("cargoBuild")
|
||||
from("${project.rootDir}/apd/target/aarch64-linux-android/release/apd")
|
||||
into("${project.projectDir}/libs/arm64-v8a")
|
||||
rename("apd", "libapd.so")
|
||||
}
|
||||
|
||||
tasks.configureEach {
|
||||
if (name == "mergeDebugJniLibFolders" || name == "mergeReleaseJniLibFolders") {
|
||||
dependsOn("buildApd")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register<Exec>("cargoClean") {
|
||||
executable("cargo")
|
||||
args("clean")
|
||||
workingDir("${project.rootDir}/apd")
|
||||
}
|
||||
|
||||
tasks.register<Delete>("apdClean") {
|
||||
dependsOn("cargoClean")
|
||||
delete(file("${project.projectDir}/libs/arm64-v8a/libapd.so"))
|
||||
}
|
||||
|
||||
tasks.clean {
|
||||
dependsOn("apdClean")
|
||||
}
|
||||
|
||||
ksp {
|
||||
arg("compose-destinations.defaultTransitions", "none")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.core.splashscreen)
|
||||
implementation(libs.androidx.webkit)
|
||||
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.compose.material.icons.extended)
|
||||
implementation(libs.androidx.compose.material)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.ui)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.compose.runtime.livedata)
|
||||
|
||||
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||
|
||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
|
||||
implementation(libs.compose.destinations.core)
|
||||
ksp(libs.compose.destinations.ksp)
|
||||
|
||||
implementation(libs.com.github.topjohnwu.libsu.core)
|
||||
implementation(libs.com.github.topjohnwu.libsu.service)
|
||||
implementation(libs.com.github.topjohnwu.libsu.nio)
|
||||
implementation(libs.com.github.topjohnwu.libsu.io)
|
||||
|
||||
implementation(libs.dev.rikka.rikkax.parcelablelist)
|
||||
|
||||
implementation(libs.io.coil.kt.coil.compose)
|
||||
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
|
||||
implementation(libs.me.zhanghai.android.appiconloader.coil)
|
||||
|
||||
implementation(libs.sheet.compose.dialogs.core)
|
||||
implementation(libs.sheet.compose.dialogs.list)
|
||||
implementation(libs.sheet.compose.dialogs.input)
|
||||
|
||||
implementation(libs.markdown)
|
||||
|
||||
implementation(libs.ini4j)
|
||||
|
||||
compileOnly(libs.cxx)
|
||||
}
|
||||
|
||||
cmaker {
|
||||
default {
|
||||
arguments += "-DANDROID_STL=none"
|
||||
arguments += "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
|
||||
abiFilters("arm64-v8a")
|
||||
cppFlags += "-std=c++2b"
|
||||
cFlags += "-std=c2x"
|
||||
}
|
||||
}
|
||||
4
app/libs/arm64-v8a/.gitignore
vendored
Normal file
4
app/libs/arm64-v8a/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
libkptools.so
|
||||
libapjni.so
|
||||
libkpatch.so
|
||||
libapd.so
|
||||
BIN
app/libs/arm64-v8a/libbootctl.so
Executable file
BIN
app/libs/arm64-v8a/libbootctl.so
Executable file
Binary file not shown.
BIN
app/libs/arm64-v8a/libbusybox.so
Executable file
BIN
app/libs/arm64-v8a/libbusybox.so
Executable file
Binary file not shown.
BIN
app/libs/arm64-v8a/libmagiskboot.so
Executable file
BIN
app/libs/arm64-v8a/libmagiskboot.so
Executable file
Binary file not shown.
BIN
app/libs/arm64-v8a/libmagiskpolicy.so
Executable file
BIN
app/libs/arm64-v8a/libmagiskpolicy.so
Executable file
Binary file not shown.
BIN
app/libs/arm64-v8a/libresetprop.so
Executable file
BIN
app/libs/arm64-v8a/libresetprop.so
Executable file
Binary file not shown.
26
app/proguard-rules.pro
vendored
Normal file
26
app/proguard-rules.pro
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
-dontwarn org.bouncycastle.jsse.BCSSLParameters
|
||||
-dontwarn org.bouncycastle.jsse.BCSSLSocket
|
||||
-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
|
||||
-dontwarn org.conscrypt.Conscrypt$Version
|
||||
-dontwarn org.conscrypt.Conscrypt
|
||||
-dontwarn org.conscrypt.ConscryptHostnameVerifier
|
||||
-dontwarn org.openjsse.javax.net.ssl.SSLParameters
|
||||
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
|
||||
-dontwarn org.openjsse.net.ssl.OpenJSSE
|
||||
-dontwarn java.beans.Introspector
|
||||
-dontwarn java.beans.VetoableChangeListener
|
||||
-dontwarn java.beans.VetoableChangeSupport
|
||||
|
||||
# Keep ini4j Service Provider Interface
|
||||
-keep,allowobfuscation,allowoptimization public class org.ini4j.spi.*
|
||||
|
||||
# Kotlin
|
||||
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
|
||||
public static void check*(...);
|
||||
public static void throw*(...);
|
||||
}
|
||||
|
||||
-repackageclasses
|
||||
-allowaccessmodification
|
||||
-overloadaggressively
|
||||
-renamesourcefileattribute SourceFile
|
||||
73
app/src/main/AndroidManifest.xml
Normal file
73
app/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="29" />
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
|
||||
<application
|
||||
android:name=".APApplication"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="APatch"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.APatch"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="34">
|
||||
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.Splash">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ui.WebUIActivity"
|
||||
android:autoRemoveFromRecents="true"
|
||||
android:documentLaunchMode="intoExisting"
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.APatch.WebUI" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.CrashHandleActivity"
|
||||
android:exported="false" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
<service
|
||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||
android:enabled="false"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="autoStoreLocales"
|
||||
android:value="true" />
|
||||
</service>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
9
app/src/main/aidl/me/bmax/apatch/IAPRootService.aidl
Normal file
9
app/src/main/aidl/me/bmax/apatch/IAPRootService.aidl
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
// IAPRootService.aidl
|
||||
package me.bmax.apatch;
|
||||
|
||||
import android.content.pm.PackageInfo;
|
||||
import rikka.parcelablelist.ParcelableListSlice;
|
||||
|
||||
interface IAPRootService {
|
||||
ParcelableListSlice<PackageInfo> getPackages(int flags);
|
||||
}
|
||||
2
app/src/main/assets/.gitignore
vendored
Normal file
2
app/src/main/assets/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
kpimg
|
||||
*.kpm
|
||||
116
app/src/main/assets/InstallAP.sh
Normal file
116
app/src/main/assets/InstallAP.sh
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
#!/bin/sh
|
||||
# By SakuraKyuo
|
||||
|
||||
OUTFD=/proc/self/fd/$2
|
||||
|
||||
function ui_print() {
|
||||
echo -e "ui_print $1\nui_print" >> $OUTFD
|
||||
}
|
||||
|
||||
function ui_printfile() {
|
||||
while IFS='' read -r line || $BB [[ -n "$line" ]]; do
|
||||
ui_print "$line";
|
||||
done < $1;
|
||||
}
|
||||
|
||||
function kernelFlagsErr(){
|
||||
ui_print "- Installation has Aborted!"
|
||||
ui_print "- APatch requires CONFIG_KALLSYMS to be Enabled."
|
||||
ui_print "- But your kernel seems NOT enabled it."
|
||||
exit
|
||||
}
|
||||
|
||||
function apatchNote(){
|
||||
ui_print "- APatch Patch Done"
|
||||
ui_print "- APatch Key is: Ap$skey"
|
||||
ui_print "- We do have saved Origin Boot image to /data"
|
||||
ui_print "- If you encounter bootloop, reboot into Recovery and flash it"
|
||||
exit
|
||||
}
|
||||
|
||||
function failed(){
|
||||
ui_printfile /dev/tmp/install/log
|
||||
ui_print "- APatch Patch Failed."
|
||||
ui_print "- Please feedback to the developer with the screenshots."
|
||||
exit
|
||||
}
|
||||
|
||||
function boot_execute_ab(){
|
||||
./lib/arm64-v8a/libmagiskboot.so unpack boot.img
|
||||
if [[ ! $(./lib/arm64-v8a/libkptools.so -i ./kernel -f | grep CONFIG_KALLSYMS=y) ]]; then
|
||||
kernelFlagsErr
|
||||
fi
|
||||
mv kernel kernel-origin
|
||||
./lib/arm64-v8a/libkptools.so -p --image kernel-origin --skey "Ap$skey" --kpimg ./assets/kpimg --out ./kernel 2>&1 | tee /dev/tmp/install/log
|
||||
if [[ ! $(cat /dev/tmp/install/log | grep "patch done") ]]; then
|
||||
failed
|
||||
fi
|
||||
ui_printfile /dev/tmp/install/log
|
||||
./lib/arm64-v8a/libmagiskboot.so repack boot.img
|
||||
dd if=/dev/tmp/install/new-boot.img of=/dev/block/by-name/boot$slot
|
||||
mv boot.img /data/boot.img
|
||||
apatchNote
|
||||
}
|
||||
|
||||
function boot_execute(){
|
||||
./lib/arm64-v8a/libmagiskboot.so unpack boot.img
|
||||
if [[ ! $(./lib/arm64-v8a/libkptools.so -i ./kernel -f | grep CONFIG_KALLSYMS=y) ]]; then
|
||||
kernelFlagsErr
|
||||
fi
|
||||
mv kernel kernel-origin
|
||||
./lib/arm64-v8a/libkptools.so -p --image kernel-origin --skey "Ap$skey" --kpimg ./assets/kpimg --out ./kernel 2>&1 | tee /dev/tmp/install/log
|
||||
if [[ ! $(cat /dev/tmp/install/log | grep "patch done") ]]; then
|
||||
failed
|
||||
fi
|
||||
ui_printfile /dev/tmp/install/log
|
||||
./lib/arm64-v8a/libmagiskboot.so repack boot.img
|
||||
dd if=/dev/tmp/install/new-boot.img of=/dev/block/by-name/boot$slot
|
||||
mv boot.img /data/boot.img
|
||||
apatchNote
|
||||
}
|
||||
|
||||
function main(){
|
||||
|
||||
cd /dev/tmp/install
|
||||
|
||||
chmod a+x ./assets/kpimg
|
||||
chmod a+x ./lib/arm64-v8a/libkptools.so
|
||||
chmod a+x ./lib/arm64-v8a/libmagiskboot.so
|
||||
|
||||
slot=$(getprop ro.boot.slot_suffix)
|
||||
|
||||
skey=$(cat /proc/sys/kernel/random/uuid | cut -d \- -f1)
|
||||
|
||||
if [[ ! "$slot" == "" ]]; then
|
||||
|
||||
ui_print ""
|
||||
ui_print "- You are using A/B device."
|
||||
|
||||
# Script author
|
||||
ui_print "- Install Script by SakuraKyuo"
|
||||
|
||||
# Get kernel
|
||||
ui_print ""
|
||||
dd if=/dev/block/by-name/boot$slot of=/dev/tmp/install/boot.img
|
||||
if [[ "$?" == 0 ]]; then
|
||||
ui_print "- Detected boot partition."
|
||||
boot_execute_ab
|
||||
fi
|
||||
|
||||
else
|
||||
|
||||
ui_print "You are using A Only device."
|
||||
|
||||
# Get kernel
|
||||
ui_print ""
|
||||
dd if=/dev/block/by-name/boot of=/dev/tmp/install/boot.img
|
||||
if [[ "$?" == 0 ]]; then
|
||||
ui_print "- Detected boot partition."
|
||||
boot_execute
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
main
|
||||
89
app/src/main/assets/UninstallAP.sh
Normal file
89
app/src/main/assets/UninstallAP.sh
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
#!/bin/sh
|
||||
# By SakuraKyuo
|
||||
|
||||
OUTFD=/proc/self/fd/$2
|
||||
|
||||
function ui_print() {
|
||||
echo -e "ui_print $1\nui_print" >> $OUTFD
|
||||
}
|
||||
|
||||
function ui_printfile() {
|
||||
while IFS='' read -r line || $BB [[ -n "$line" ]]; do
|
||||
ui_print "$line";
|
||||
done < $1;
|
||||
}
|
||||
|
||||
function apatchNote(){
|
||||
ui_print "- APatch Unpatch Done"
|
||||
exit
|
||||
}
|
||||
|
||||
function failed(){
|
||||
ui_print "- APatch Unpatch Failed."
|
||||
ui_print "- Please feedback to the developer with the screenshots."
|
||||
exit
|
||||
}
|
||||
|
||||
function boot_execute_ab(){
|
||||
./lib/arm64-v8a/libmagiskboot.so unpack boot.img
|
||||
mv kernel kernel-origin
|
||||
./lib/arm64-v8a/libkptools.so -u --image kernel-origin --out ./kernel
|
||||
if [[ ! "$?" == 0 ]]; then
|
||||
failed
|
||||
fi
|
||||
./lib/arm64-v8a/libmagiskboot.so repack boot.img
|
||||
dd if=/dev/tmp/install/new-boot.img of=/dev/block/by-name/boot$slot
|
||||
apatchNote
|
||||
}
|
||||
|
||||
function boot_execute(){
|
||||
./lib/arm64-v8a/libmagiskboot.so unpack boot.img
|
||||
mv kernel kernel-origin
|
||||
./lib/arm64-v8a/libkptools.so -u --image kernel-origin --out ./kernel
|
||||
if [[ ! "$?" == 0 ]]; then
|
||||
failed
|
||||
fi
|
||||
./lib/arm64-v8a/libmagiskboot.so repack boot.img
|
||||
dd if=/dev/tmp/install/new-boot.img of=/dev/block/by-name/boot
|
||||
apatchNote
|
||||
}
|
||||
|
||||
function main(){
|
||||
|
||||
cd /dev/tmp/install
|
||||
|
||||
chmod a+x ./lib/arm64-v8a/libkptools.so
|
||||
chmod a+x ./lib/arm64-v8a/libmagiskboot.so
|
||||
|
||||
slot=$(getprop ro.boot.slot_suffix)
|
||||
|
||||
if [[ ! "$slot" == "" ]]; then
|
||||
|
||||
ui_print ""
|
||||
ui_print "- You are using A/B device."
|
||||
|
||||
# Get kernel
|
||||
ui_print ""
|
||||
dd if=/dev/block/by-name/boot$slot of=/dev/tmp/install/boot.img
|
||||
if [[ "$?" == 0 ]]; then
|
||||
ui_print "- Detected boot partition."
|
||||
boot_execute_ab
|
||||
fi
|
||||
|
||||
else
|
||||
|
||||
ui_print "You are using A Only device."
|
||||
|
||||
# Get kernel
|
||||
ui_print ""
|
||||
dd if=/dev/block/by-name/boot of=/dev/tmp/install/boot.img
|
||||
if [[ "$?" == 0 ]]; then
|
||||
ui_print "- Detected boot partition."
|
||||
boot_execute
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
main
|
||||
20
app/src/main/assets/boot_extract.sh
Normal file
20
app/src/main/assets/boot_extract.sh
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
#!/system/bin/sh
|
||||
|
||||
ARCH=$(getprop ro.product.cpu.abi)
|
||||
|
||||
IS_INSTALL_NEXT_SLOT=$1
|
||||
|
||||
# Load utility functions
|
||||
. ./util_functions.sh
|
||||
|
||||
if [ "$IS_INSTALL_NEXT_SLOT" = "true" ]; then
|
||||
get_next_slot
|
||||
else
|
||||
get_current_slot
|
||||
fi
|
||||
|
||||
find_boot_image
|
||||
|
||||
[ -e "$BOOTIMAGE" ] || { >&2 echo "- can't find boot.img!"; exit 1; }
|
||||
|
||||
true
|
||||
107
app/src/main/assets/boot_patch.sh
Normal file
107
app/src/main/assets/boot_patch.sh
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
#!/system/bin/sh
|
||||
#######################################################################################
|
||||
# APatch Boot Image Patcher
|
||||
#######################################################################################
|
||||
#
|
||||
# Usage: boot_patch.sh <superkey> <bootimage> [ARGS_PASS_TO_KPTOOLS]
|
||||
#
|
||||
# This script should be placed in a directory with the following files:
|
||||
#
|
||||
# File name Type Description
|
||||
#
|
||||
# boot_patch.sh script A script to patch boot image for APatch.
|
||||
# (this file) The script will use files in its same
|
||||
# directory to complete the patching process.
|
||||
# bootimg binary The target boot image
|
||||
# kpimg binary KernelPatch core Image
|
||||
# kptools executable The KernelPatch tools binary to inject kpimg to kernel Image
|
||||
# magiskboot executable Magisk tool to unpack boot.img.
|
||||
#
|
||||
#######################################################################################
|
||||
|
||||
ARCH=$(getprop ro.product.cpu.abi)
|
||||
|
||||
# Load utility functions
|
||||
. ./util_functions.sh
|
||||
|
||||
echo "****************************"
|
||||
echo " APatch Boot Image Patcher"
|
||||
echo "****************************"
|
||||
|
||||
SUPERKEY="$1"
|
||||
BOOTIMAGE=$2
|
||||
FLASH_TO_DEVICE=$3
|
||||
shift 2
|
||||
|
||||
[ -z "$SUPERKEY" ] && { >&2 echo "- SuperKey empty!"; exit 1; }
|
||||
[ -e "$BOOTIMAGE" ] || { >&2 echo "- $BOOTIMAGE does not exist!"; exit 1; }
|
||||
|
||||
# Check for dependencies
|
||||
command -v ./magiskboot >/dev/null 2>&1 || { >&2 echo "- Command magiskboot not found!"; exit 1; }
|
||||
command -v ./kptools >/dev/null 2>&1 || { >&2 echo "- Command kptools not found!"; exit 1; }
|
||||
|
||||
if [ ! -f kernel ]; then
|
||||
echo "- Unpacking boot image"
|
||||
./magiskboot unpack "$BOOTIMAGE" >/dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
>&2 echo "- Unpack error: $?"
|
||||
exit $?
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! $(./kptools -i kernel -f | grep CONFIG_KALLSYMS=y) ]; then
|
||||
echo "- Patcher has Aborted!"
|
||||
echo "- APatch requires CONFIG_KALLSYMS to be Enabled."
|
||||
echo "- But your kernel seems NOT enabled it."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ $(./kptools -i kernel -l | grep patched=false) ]; then
|
||||
echo "- Backing boot.img "
|
||||
cp "$BOOTIMAGE" "ori.img" >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
mv kernel kernel.ori
|
||||
|
||||
echo "- Patching kernel"
|
||||
|
||||
set -x
|
||||
./kptools -p -i kernel.ori -S "$SUPERKEY" -k kpimg -o kernel "$@"
|
||||
patch_rc=$?
|
||||
set +x
|
||||
|
||||
if [ $patch_rc -ne 0 ]; then
|
||||
>&2 echo "- Patch kernel error: $patch_rc"
|
||||
exit $?
|
||||
fi
|
||||
|
||||
echo "- Repacking boot image"
|
||||
./magiskboot repack "$BOOTIMAGE" >/dev/null 2>&1
|
||||
|
||||
if [ ! $(./kptools -i kernel.ori -f | grep CONFIG_KALLSYMS_ALL=y) ]; then
|
||||
echo "- Detected CONFIG_KALLSYMS_ALL is not set!"
|
||||
echo "- APatch has patched but maybe your device won't boot."
|
||||
echo "- Make sure you have original boot image backup."
|
||||
fi
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
>&2 echo "- Repack error: $?"
|
||||
exit $?
|
||||
fi
|
||||
|
||||
if [ "$FLASH_TO_DEVICE" = "true" ]; then
|
||||
# flash
|
||||
if [ -b "$BOOTIMAGE" ] || [ -c "$BOOTIMAGE" ] && [ -f "new-boot.img" ]; then
|
||||
echo "- Flashing new boot image"
|
||||
flash_image new-boot.img "$BOOTIMAGE"
|
||||
if [ $? -ne 0 ]; then
|
||||
>&2 echo "- Flash error: $?"
|
||||
exit $?
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "- Successfully Flashed!"
|
||||
else
|
||||
echo "- Successfully Patched!"
|
||||
fi
|
||||
|
||||
74
app/src/main/assets/boot_unpatch.sh
Normal file
74
app/src/main/assets/boot_unpatch.sh
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
#!/system/bin/sh
|
||||
#######################################################################################
|
||||
# APatch Boot Image Unpatcher
|
||||
#######################################################################################
|
||||
|
||||
ARCH=$(getprop ro.product.cpu.abi)
|
||||
|
||||
# Load utility functions
|
||||
. ./util_functions.sh
|
||||
|
||||
echo "****************************"
|
||||
echo " APatch Boot Image Unpatcher"
|
||||
echo "****************************"
|
||||
|
||||
BOOTIMAGE=$1
|
||||
|
||||
[ -e "$BOOTIMAGE" ] || { echo "- $BOOTIMAGE does not exist!"; exit 1; }
|
||||
|
||||
echo "- Target image: $BOOTIMAGE"
|
||||
|
||||
# Check for dependencies
|
||||
command -v ./magiskboot >/dev/null 2>&1 || { echo "- Command magiskboot not found!"; exit 1; }
|
||||
command -v ./kptools >/dev/null 2>&1 || { echo "- Command kptools not found!"; exit 1; }
|
||||
|
||||
if [ ! -f kernel ]; then
|
||||
echo "- Unpacking boot image"
|
||||
./magiskboot unpack "$BOOTIMAGE" >/dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
>&2 echo "- Unpack error: $?"
|
||||
exit $?
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! $(./kptools -i kernel -l | grep patched=false) ]; then
|
||||
echo "- kernel has been patched "
|
||||
if [ -f "new-boot.img" ]; then
|
||||
echo "- found backup boot.img ,use it for recovery"
|
||||
else
|
||||
mv kernel kernel.ori
|
||||
echo "- Unpatching kernel"
|
||||
./kptools -u --image kernel.ori --out kernel
|
||||
if [ $? -ne 0 ]; then
|
||||
>&2 echo "- Unpatch error: $?"
|
||||
exit $?
|
||||
fi
|
||||
echo "- Repacking boot image"
|
||||
./magiskboot repack "$BOOTIMAGE" >/dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
>&2 echo "- Repack error: $?"
|
||||
exit $?
|
||||
fi
|
||||
fi
|
||||
|
||||
else
|
||||
echo "- no need unpatch"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
|
||||
if [ -f "new-boot.img" ]; then
|
||||
echo "- Flashing boot image"
|
||||
flash_image new-boot.img "$BOOTIMAGE"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
>&2 echo "- Flash error: $?"
|
||||
exit $?
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "- Flash successful"
|
||||
|
||||
# Reset any error code
|
||||
true
|
||||
537
app/src/main/assets/util_functions.sh
Normal file
537
app/src/main/assets/util_functions.sh
Normal file
|
|
@ -0,0 +1,537 @@
|
|||
#!/system/bin/sh
|
||||
#######################################################################################
|
||||
# Helper Functions (credits to topjohnwu)
|
||||
#######################################################################################
|
||||
APATCH_VER='0.10.4'
|
||||
APATCH_VER_CODE=164
|
||||
|
||||
ui_print() {
|
||||
if $BOOTMODE; then
|
||||
echo "$1"
|
||||
else
|
||||
echo -e "ui_print $1\nui_print" >> /proc/self/fd/$OUTFD
|
||||
fi
|
||||
}
|
||||
|
||||
toupper() {
|
||||
echo "$@" | tr '[:lower:]' '[:upper:]'
|
||||
}
|
||||
|
||||
grep_cmdline() {
|
||||
local REGEX="s/^$1=//p"
|
||||
{ echo $(cat /proc/cmdline)$(sed -e 's/[^"]//g' -e 's/""//g' /proc/cmdline) | xargs -n 1; \
|
||||
sed -e 's/ = /=/g' -e 's/, /,/g' -e 's/"//g' /proc/bootconfig; \
|
||||
} 2>/dev/null | sed -n "$REGEX"
|
||||
}
|
||||
|
||||
grep_prop() {
|
||||
local REGEX="s/^$1=//p"
|
||||
shift
|
||||
local FILES=$@
|
||||
[ -z "$FILES" ] && FILES='/system/build.prop'
|
||||
cat $FILES 2>/dev/null | dos2unix | sed -n "$REGEX" | head -n 1
|
||||
}
|
||||
|
||||
getvar() {
|
||||
local VARNAME=$1
|
||||
local VALUE
|
||||
local PROPPATH='/data/.magisk /cache/.magisk'
|
||||
[ ! -z $MAGISKTMP ] && PROPPATH="$MAGISKTMP/.magisk/config $PROPPATH"
|
||||
VALUE=$(grep_prop $VARNAME $PROPPATH)
|
||||
[ ! -z $VALUE ] && eval $VARNAME=\$VALUE
|
||||
}
|
||||
|
||||
is_mounted() {
|
||||
grep -q " $(readlink -f $1) " /proc/mounts 2>/dev/null
|
||||
return $?
|
||||
}
|
||||
abort() {
|
||||
ui_print "$1"
|
||||
$BOOTMODE || recovery_cleanup
|
||||
[ ! -z $MODPATH ] && rm -rf $MODPATH
|
||||
rm -rf $TMPDIR
|
||||
exit 1
|
||||
}
|
||||
set_nvbase() {
|
||||
NVBASE="$1"
|
||||
MAGISKBIN="$1/magisk"
|
||||
}
|
||||
|
||||
print_title() {
|
||||
local len line1len line2len bar
|
||||
line1len=$(echo -n $1 | wc -c)
|
||||
line2len=$(echo -n $2 | wc -c)
|
||||
len=$line2len
|
||||
[ $line1len -gt $line2len ] && len=$line1len
|
||||
len=$((len + 2))
|
||||
bar=$(printf "%${len}s" | tr ' ' '*')
|
||||
ui_print "$bar"
|
||||
ui_print " $1 "
|
||||
[ "$2" ] && ui_print " $2 "
|
||||
ui_print "$bar"
|
||||
}
|
||||
setup_flashable() {
|
||||
ensure_bb
|
||||
$BOOTMODE && return
|
||||
if [ -z $OUTFD ] || readlink /proc/$$/fd/$OUTFD | grep -q /tmp; then
|
||||
# We will have to manually find out OUTFD
|
||||
for FD in $(ls /proc/$$/fd); do
|
||||
if readlink /proc/$$/fd/$FD | grep -q pipe; then
|
||||
if ps | grep -v grep | grep -qE " 3 $FD |status_fd=$FD"; then
|
||||
OUTFD=$FD
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
recovery_actions
|
||||
}
|
||||
|
||||
ensure_bb() {
|
||||
if set -o | grep -q standalone; then
|
||||
# We are definitely in busybox ash
|
||||
set -o standalone
|
||||
return
|
||||
fi
|
||||
|
||||
# Find our busybox binary
|
||||
local bb
|
||||
if [ -f $TMPDIR/busybox ]; then
|
||||
bb=$TMPDIR/busybox
|
||||
elif [ -f $MAGISKBIN/busybox ]; then
|
||||
bb=$MAGISKBIN/busybox
|
||||
else
|
||||
abort "! Cannot find BusyBox"
|
||||
fi
|
||||
chmod 755 $bb
|
||||
|
||||
# Busybox could be a script, make sure /system/bin/sh exists
|
||||
if [ ! -f /system/bin/sh ]; then
|
||||
umount -l /system 2>/dev/null
|
||||
mkdir -p /system/bin
|
||||
ln -s $(command -v sh) /system/bin/sh
|
||||
fi
|
||||
|
||||
export ASH_STANDALONE=1
|
||||
|
||||
# Find our current arguments
|
||||
# Run in busybox environment to ensure consistent results
|
||||
# /proc/<pid>/cmdline shall be <interpreter> <script> <arguments...>
|
||||
local cmds="$($bb sh -c "
|
||||
for arg in \$(tr '\0' '\n' < /proc/$$/cmdline); do
|
||||
if [ -z \"\$cmds\" ]; then
|
||||
# Skip the first argument as we want to change the interpreter
|
||||
cmds=\"sh\"
|
||||
else
|
||||
cmds=\"\$cmds '\$arg'\"
|
||||
fi
|
||||
done
|
||||
echo \$cmds")"
|
||||
|
||||
# Re-exec our script
|
||||
echo $cmds | $bb xargs $bb
|
||||
exit
|
||||
}
|
||||
recovery_actions() {
|
||||
# Make sure random won't get blocked
|
||||
mount -o bind /dev/urandom /dev/random
|
||||
# Unset library paths
|
||||
OLD_LD_LIB=$LD_LIBRARY_PATH
|
||||
OLD_LD_PRE=$LD_PRELOAD
|
||||
OLD_LD_CFG=$LD_CONFIG_FILE
|
||||
unset LD_LIBRARY_PATH
|
||||
unset LD_PRELOAD
|
||||
unset LD_CONFIG_FILE
|
||||
}
|
||||
recovery_cleanup() {
|
||||
local DIR
|
||||
ui_print "- Unmounting partitions"
|
||||
(
|
||||
if [ ! -d /postinstall/tmp ]; then
|
||||
umount -l /system
|
||||
umount -l /system_root
|
||||
fi
|
||||
umount -l /vendor
|
||||
umount -l /persist
|
||||
umount -l /metadata
|
||||
for DIR in /apex /system /system_root; do
|
||||
if [ -L "${DIR}_link" ]; then
|
||||
rmdir $DIR
|
||||
mv -f ${DIR}_link $DIR
|
||||
fi
|
||||
done
|
||||
umount -l /dev/random
|
||||
) 2>/dev/null
|
||||
[ -z $OLD_LD_LIB ] || export LD_LIBRARY_PATH=$OLD_LD_LIB
|
||||
[ -z $OLD_LD_PRE ] || export LD_PRELOAD=$OLD_LD_PRE
|
||||
[ -z $OLD_LD_CFG ] || export LD_CONFIG_FILE=$OLD_LD_CFG
|
||||
}
|
||||
|
||||
find_block() {
|
||||
local BLOCK DEV DEVICE DEVNAME PARTNAME UEVENT
|
||||
for BLOCK in "$@"; do
|
||||
DEVICE=$(find /dev/block \( -type b -o -type c -o -type l \) -iname $BLOCK | head -n 1) 2>/dev/null
|
||||
if [ ! -z $DEVICE ]; then
|
||||
readlink -f $DEVICE
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
# Fallback by parsing sysfs uevents
|
||||
for UEVENT in /sys/dev/block/*/uevent; do
|
||||
DEVNAME=$(grep_prop DEVNAME $UEVENT)
|
||||
PARTNAME=$(grep_prop PARTNAME $UEVENT)
|
||||
for BLOCK in "$@"; do
|
||||
if [ "$(toupper $BLOCK)" = "$(toupper $PARTNAME)" ]; then
|
||||
echo /dev/block/$DEVNAME
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
done
|
||||
# Look just in /dev in case we're dealing with MTD/NAND without /dev/block devices/links
|
||||
for DEV in "$@"; do
|
||||
DEVICE=$(find /dev \( -type b -o -type c -o -type l \) -maxdepth 1 -iname $DEV | head -n 1) 2>/dev/null
|
||||
if [ ! -z $DEVICE ]; then
|
||||
readlink -f $DEVICE
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# After calling this method, the following variables will be set:
|
||||
# SLOT
|
||||
get_current_slot() {
|
||||
# Check A/B slot
|
||||
SLOT=$(grep_cmdline androidboot.slot_suffix)
|
||||
if [ -z $SLOT ]; then
|
||||
SLOT=$(grep_cmdline androidboot.slot)
|
||||
[ -z $SLOT ] || SLOT=_${SLOT}
|
||||
fi
|
||||
if [ -z $SLOT ]; then
|
||||
SLOT=$(getprop ro.boot.slot_suffix)
|
||||
fi
|
||||
[ "$SLOT" = "normal" ] && unset SLOT
|
||||
[ -z $SLOT ] || echo "SLOT=$SLOT"
|
||||
}
|
||||
|
||||
# After calling this method, the following variables will be set:
|
||||
# SLOT
|
||||
# This is used after OTA
|
||||
get_next_slot() {
|
||||
# Check A/B slot
|
||||
SLOT=$(grep_cmdline androidboot.slot_suffix)
|
||||
if [ -z $SLOT ]; then
|
||||
SLOT=$(grep_cmdline androidboot.slot)
|
||||
[ -z $SLOT ] || SLOT=_${SLOT}
|
||||
fi
|
||||
if [ -z $SLOT ]; then
|
||||
SLOT=$(getprop ro.boot.slot_suffix)
|
||||
fi
|
||||
[ -z $SLOT ] && { >&2 echo "can't determined next boot slot! check your devices is A/B"; exit 1; }
|
||||
[ "$SLOT" = "normal" ] && { >&2 echo "can't determined next boot slot! check your devices is A/B"; exit 1; }
|
||||
if [[ $SLOT == *_a ]]; then
|
||||
SLOT='_b'
|
||||
else
|
||||
SLOT='_a'
|
||||
fi
|
||||
echo "SLOT=$SLOT"
|
||||
}
|
||||
|
||||
find_boot_image() {
|
||||
if [ ! -z $SLOT ]; then
|
||||
BOOTIMAGE=$(find_block "boot$SLOT")
|
||||
fi
|
||||
if [ -z $BOOTIMAGE ]; then
|
||||
BOOTIMAGE=$(find_block kern-a android_boot kernel bootimg boot lnx boot_a)
|
||||
fi
|
||||
if [ -z $BOOTIMAGE ]; then
|
||||
# Lets see what fstabs tells me
|
||||
BOOTIMAGE=$(grep -v '#' /etc/*fstab* | grep -E '/boot(img)?[^a-zA-Z]' | grep -oE '/dev/[a-zA-Z0-9_./-]*' | head -n 1)
|
||||
fi
|
||||
[ -z $BOOTIMAGE ] || echo "BOOTIMAGE=$BOOTIMAGE"
|
||||
}
|
||||
|
||||
flash_image() {
|
||||
local CMD1
|
||||
case "$1" in
|
||||
*.gz) CMD1="gzip -d < '$1' 2>/dev/null";;
|
||||
*) CMD1="cat '$1'";;
|
||||
esac
|
||||
if [ -b "$2" ]; then {
|
||||
local img_sz=$(stat -c '%s' "$1")
|
||||
local blk_sz=$(blockdev --getsize64 "$2")
|
||||
local blk_bs=$(blockdev --getbsz "$2")
|
||||
[ "$img_sz" -gt "$blk_sz" ] && return 1
|
||||
blockdev --setrw "$2"
|
||||
local blk_ro=$(blockdev --getro "$2")
|
||||
[ "$blk_ro" -eq 1 ] && return 2
|
||||
eval "$CMD1" | dd of="$2" bs="$blk_bs" iflag=fullblock conv=notrunc,fsync 2>/dev/null
|
||||
sync
|
||||
} elif [ -c "$2" ]; then {
|
||||
flash_eraseall "$2" >&2
|
||||
eval "$CMD1" | nandwrite -p "$2" - >&2
|
||||
} else {
|
||||
echo "- Not block or char device, storing image"
|
||||
eval "$CMD1" > "$2" 2>/dev/null
|
||||
} fi
|
||||
return 0
|
||||
}
|
||||
|
||||
setup_mntpoint() {
|
||||
local POINT=$1
|
||||
[ -L $POINT ] && mv -f $POINT ${POINT}_link
|
||||
if [ ! -d $POINT ]; then
|
||||
rm -f $POINT
|
||||
mkdir -p $POINT
|
||||
fi
|
||||
}
|
||||
|
||||
mount_name() {
|
||||
local PART=$1
|
||||
local POINT=$2
|
||||
local FLAG=$3
|
||||
setup_mntpoint $POINT
|
||||
is_mounted $POINT && return
|
||||
# First try mounting with fstab
|
||||
mount $FLAG $POINT 2>/dev/null
|
||||
if ! is_mounted $POINT; then
|
||||
local BLOCK=$(find_block $PART)
|
||||
mount $FLAG $BLOCK $POINT || return
|
||||
fi
|
||||
ui_print "- Mounting $POINT"
|
||||
}
|
||||
|
||||
mount_ro_ensure() {
|
||||
# We handle ro partitions only in recovery
|
||||
$BOOTMODE && return
|
||||
local PART=$1
|
||||
local POINT=$2
|
||||
mount_name "$PART" $POINT '-o ro'
|
||||
is_mounted $POINT || abort "! Cannot mount $POINT"
|
||||
}
|
||||
|
||||
# After calling this method, the following variables will be set:
|
||||
# SLOT, SYSTEM_AS_ROOT, LEGACYSAR
|
||||
mount_partitions() {
|
||||
# Check A/B slot
|
||||
SLOT=$(grep_cmdline androidboot.slot_suffix)
|
||||
if [ -z $SLOT ]; then
|
||||
SLOT=$(grep_cmdline androidboot.slot)
|
||||
[ -z $SLOT ] || SLOT=_${SLOT}
|
||||
fi
|
||||
[ "$SLOT" = "normal" ] && unset SLOT
|
||||
[ -z $SLOT ] || ui_print "- Current boot slot: $SLOT"
|
||||
|
||||
# Mount ro partitions
|
||||
if is_mounted /system_root; then
|
||||
umount /system 2>/dev/null
|
||||
umount /system_root 2>/dev/null
|
||||
fi
|
||||
mount_ro_ensure "system$SLOT app$SLOT" /system
|
||||
if [ -f /system/init -o -L /system/init ]; then
|
||||
SYSTEM_AS_ROOT=true
|
||||
setup_mntpoint /system_root
|
||||
if ! mount --move /system /system_root; then
|
||||
umount /system
|
||||
umount -l /system 2>/dev/null
|
||||
mount_ro_ensure "system$SLOT app$SLOT" /system_root
|
||||
fi
|
||||
mount -o bind /system_root/system /system
|
||||
else
|
||||
if grep ' / ' /proc/mounts | grep -qv 'rootfs' || grep -q ' /system_root ' /proc/mounts; then
|
||||
SYSTEM_AS_ROOT=true
|
||||
else
|
||||
SYSTEM_AS_ROOT=false
|
||||
fi
|
||||
fi
|
||||
$SYSTEM_AS_ROOT && ui_print "- Device is system-as-root"
|
||||
|
||||
LEGACYSAR=false
|
||||
if $BOOTMODE; then
|
||||
grep ' / ' /proc/mounts | grep -q '/dev/root' && LEGACYSAR=true
|
||||
else
|
||||
# Recovery mode, assume devices that don't use dynamic partitions are legacy SAR
|
||||
local IS_DYNAMIC=false
|
||||
if grep -q 'androidboot.super_partition' /proc/cmdline; then
|
||||
IS_DYNAMIC=true
|
||||
elif [ -n "$(find_block super)" ]; then
|
||||
IS_DYNAMIC=true
|
||||
fi
|
||||
if $SYSTEM_AS_ROOT && ! $IS_DYNAMIC; then
|
||||
LEGACYSAR=true
|
||||
ui_print "- Legacy SAR, force kernel to load rootfs"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
get_flags() {
|
||||
if grep ' /data ' /proc/mounts | grep -q 'dm-'; then
|
||||
ISENCRYPTED=true
|
||||
elif [ "$(getprop ro.crypto.state)" = "encrypted" ]; then
|
||||
ISENCRYPTED=true
|
||||
elif [ "$DATA" = "false" ]; then
|
||||
# No data access means unable to decrypt in recovery
|
||||
ISENCRYPTED=true
|
||||
else
|
||||
ISENCRYPTED=false
|
||||
fi
|
||||
if [ -n "$(find_block vbmeta vbmeta_a)" ]; then
|
||||
PATCHVBMETAFLAG=false
|
||||
else
|
||||
PATCHVBMETAFLAG=true
|
||||
ui_print "- No vbmeta partition, patch vbmeta in boot image"
|
||||
fi
|
||||
|
||||
# Overridable config flags with safe defaults
|
||||
getvar KEEPVERITY
|
||||
getvar KEEPFORCEENCRYPT
|
||||
getvar RECOVERYMODE
|
||||
if [ -z $KEEPVERITY ]; then
|
||||
if $SYSTEM_AS_ROOT; then
|
||||
KEEPVERITY=true
|
||||
ui_print "- System-as-root, keep dm-verity"
|
||||
else
|
||||
KEEPVERITY=false
|
||||
fi
|
||||
fi
|
||||
if [ -z $KEEPFORCEENCRYPT ]; then
|
||||
if $ISENCRYPTED; then
|
||||
KEEPFORCEENCRYPT=true
|
||||
ui_print "- Encrypted data, keep forceencrypt"
|
||||
else
|
||||
KEEPFORCEENCRYPT=false
|
||||
fi
|
||||
fi
|
||||
[ -z $RECOVERYMODE ] && RECOVERYMODE=false
|
||||
}
|
||||
|
||||
install_apatch() {
|
||||
cd $MAGISKBIN
|
||||
|
||||
# Source the boot patcher
|
||||
SOURCEDMODE=true
|
||||
. ./boot_patch.sh "$BOOTIMAGE"
|
||||
ui_print "- Flashing new boot image"
|
||||
flash_image new-boot.img "$BOOTIMAGE"
|
||||
case $? in
|
||||
1)
|
||||
abort "! Insufficient partition size"
|
||||
;;
|
||||
2)
|
||||
abort "! $BOOTIMAGE is read only"
|
||||
;;
|
||||
esac
|
||||
./magiskboot cleanup
|
||||
rm -f new-boot.img
|
||||
|
||||
run_migrations
|
||||
}
|
||||
|
||||
check_data() {
|
||||
DATA=false
|
||||
DATA_DE=false
|
||||
if grep ' /data ' /proc/mounts | grep -vq 'tmpfs'; then
|
||||
# Test if data is writable
|
||||
touch /data/.rw && rm /data/.rw && DATA=true
|
||||
# Test if data is decrypted
|
||||
$DATA && [ -d /data/adb ] && touch /data/adb/.rw && rm /data/adb/.rw && DATA_DE=true
|
||||
$DATA_DE && [ -d /data/adb/magisk ] || mkdir /data/adb/magisk || DATA_DE=false
|
||||
fi
|
||||
set_nvbase "/data"
|
||||
$DATA || set_nvbase "/cache/data_adb"
|
||||
$DATA_DE && set_nvbase "/data/adb"
|
||||
}
|
||||
|
||||
api_level_arch_detect() {
|
||||
API=$(grep_get_prop ro.build.version.sdk)
|
||||
ABI=$(grep_get_prop ro.product.cpu.abi)
|
||||
if [ "$ABI" = "x86" ]; then
|
||||
ARCH=x86
|
||||
ABI32=x86
|
||||
IS64BIT=false
|
||||
elif [ "$ABI" = "arm64-v8a" ]; then
|
||||
ARCH=arm64
|
||||
ABI32=armeabi-v7a
|
||||
IS64BIT=true
|
||||
elif [ "$ABI" = "x86_64" ]; then
|
||||
ARCH=x64
|
||||
ABI32=x86
|
||||
IS64BIT=true
|
||||
else
|
||||
ARCH=arm
|
||||
ABI=armeabi-v7a
|
||||
ABI32=armeabi-v7a
|
||||
IS64BIT=false
|
||||
fi
|
||||
}
|
||||
|
||||
remove_system_su() {
|
||||
[ -d /postinstall/tmp ] && POSTINST=/postinstall
|
||||
cd $POSTINST/system
|
||||
if [ -f bin/su -o -f xbin/su ] && [ ! -f /su/bin/su ]; then
|
||||
ui_print "- Removing system installed root"
|
||||
blockdev --setrw /dev/block/mapper/system$SLOT 2>/dev/null
|
||||
mount -o rw,remount $POSTINST/system
|
||||
# SuperSU
|
||||
cd bin
|
||||
if [ -e .ext/.su ]; then
|
||||
mv -f app_process32_original app_process32 2>/dev/null
|
||||
mv -f app_process64_original app_process64 2>/dev/null
|
||||
mv -f install-recovery_original.sh install-recovery.sh 2>/dev/null
|
||||
if [ -e app_process64 ]; then
|
||||
ln -sf app_process64 app_process
|
||||
elif [ -e app_process32 ]; then
|
||||
ln -sf app_process32 app_process
|
||||
fi
|
||||
fi
|
||||
# More SuperSU, SuperUser & ROM su
|
||||
cd ..
|
||||
rm -rf .pin bin/.ext etc/.installed_su_daemon etc/.has_su_daemon \
|
||||
xbin/daemonsu xbin/su xbin/sugote xbin/sugote-mksh xbin/supolicy \
|
||||
bin/app_process_init bin/su /cache/su lib/libsupol.so lib64/libsupol.so \
|
||||
su.d etc/init.d/99SuperSUDaemon etc/install-recovery.sh /cache/install-recovery.sh \
|
||||
.supersu /cache/.supersu /data/.supersu \
|
||||
app/Superuser.apk app/SuperSU /cache/Superuser.apk
|
||||
elif [ -f /cache/su.img -o -f /data/su.img -o -d /data/su -o -d /data/adb/su ]; then
|
||||
ui_print "- Removing systemless installed root"
|
||||
umount -l /su 2>/dev/null
|
||||
rm -rf /cache/su.img /data/su.img /data/su /data/adb/su /data/adb/suhide \
|
||||
/cache/.supersu /data/.supersu /cache/supersu_install /data/supersu_install
|
||||
fi
|
||||
cd $TMPDIR
|
||||
}
|
||||
|
||||
run_migrations() {
|
||||
local LOCSHA1
|
||||
local TARGET
|
||||
# Legacy app installation
|
||||
local BACKUP=$MAGISKBIN/stock_boot*.gz
|
||||
if [ -f $BACKUP ]; then
|
||||
cp $BACKUP /data
|
||||
rm -f $BACKUP
|
||||
fi
|
||||
|
||||
# Legacy backup
|
||||
for gz in /data/stock_boot*.gz; do
|
||||
[ -f $gz ] || break
|
||||
LOCSHA1=$(basename $gz | sed -e 's/stock_boot_//' -e 's/.img.gz//')
|
||||
[ -z $LOCSHA1 ] && break
|
||||
mkdir /data/magisk_backup_${LOCSHA1} 2>/dev/null
|
||||
mv $gz /data/magisk_backup_${LOCSHA1}/boot.img.gz
|
||||
done
|
||||
|
||||
# Stock backups
|
||||
LOCSHA1=$SHA1
|
||||
for name in boot dtb dtbo dtbs; do
|
||||
BACKUP=$MAGISKBIN/stock_${name}.img
|
||||
[ -f $BACKUP ] || continue
|
||||
if [ $name = 'boot' ]; then
|
||||
LOCSHA1=$($MAGISKBIN/magiskboot sha1 $BACKUP)
|
||||
mkdir /data/magisk_backup_${LOCSHA1} 2>/dev/null
|
||||
fi
|
||||
TARGET=/data/magisk_backup_${LOCSHA1}/${name}.img
|
||||
cp $BACKUP $TARGET
|
||||
rm -f $BACKUP
|
||||
gzip -9f $TARGET
|
||||
done
|
||||
}
|
||||
29
app/src/main/cpp/CMakeLists.txt
Normal file
29
app/src/main/cpp/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
cmake_minimum_required(VERSION 3.28.0)
|
||||
project("apjni")
|
||||
|
||||
find_program(CCACHE ccache)
|
||||
|
||||
if (CCACHE)
|
||||
set(CMAKE_CXX_COMPILER_LAUNCHER ${CCACHE})
|
||||
set(CMAKE_C_COMPILER_LAUNCHER ${CCACHE})
|
||||
endif ()
|
||||
|
||||
find_package(cxx REQUIRED CONFIG)
|
||||
link_libraries(cxx::cxx)
|
||||
|
||||
macro(SET_OPTION option value)
|
||||
set(${option} ${value} CACHE INTERNAL "" FORCE)
|
||||
endmacro()
|
||||
|
||||
set(CHERISH_POLLY_FLAGS "-mllvm -polly -mllvm -polly-run-dce -mllvm -polly-run-inliner -mllvm -polly-reschedule=1 -mllvm -polly-loopfusion-greedy=1 -mllvm -polly-postopts=1 -mllvm -polly-num-threads=0 -mllvm -polly-omp-backend=LLVM -mllvm -polly-scheduling=dynamic -mllvm -polly-scheduling-chunksize=1 -mllvm -polly-isl-arg=--no-schedule-serialize-sccs -mllvm -polly-ast-use-context -mllvm -polly-detect-keep-going -mllvm -polly-position=before-vectorizer -mllvm -polly-vectorizer=stripmine -mllvm -polly-detect-profitability-min-per-loop-insts=40 -mllvm -polly-invariant-load-hoisting")
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${CHERISH_POLLY_FLAGS} -ffunction-sections -fdata-sections -fvisibility=hidden -fvisibility-inlines-hidden -O3 -flto -Wno-vla-cxx-extension")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${CHERISH_POLLY_FLAGS} -ffunction-sections -fdata-sections -fvisibility=hidden -fvisibility-inlines-hidden -fno-rtti -fno-exceptions -O3 -flto -Wno-vla-cxx-extension")
|
||||
set(CMAKE_CXX_STANDARD 23)
|
||||
|
||||
add_library(${PROJECT_NAME} SHARED apjni.cpp)
|
||||
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
|
||||
COMMAND ${CMAKE_STRIP} --strip-all --remove-section=.note.gnu.build-id --remove-section=.note.android.ident $<TARGET_FILE:${PROJECT_NAME}>)
|
||||
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE log)
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE -flto)
|
||||
target_link_options(${PROJECT_NAME} PRIVATE "-Wl,--build-id=none" "-Wl,-icf=all,--lto-O3" "-Wl,-s,-x,--gc-sections" "-Wl,--no-undefined")
|
||||
294
app/src/main/cpp/apjni.cpp
Normal file
294
app/src/main/cpp/apjni.cpp
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
/*
|
||||
* Copyright (C) 2023 bmax121. All Rights Reserved.
|
||||
* Copyright (C) 2024 GarfieldHan. All Rights Reserved.
|
||||
* Copyright (C) 2024 1f2003d5. All Rights Reserved.
|
||||
*/
|
||||
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "apjni.hpp"
|
||||
#include "supercall.h"
|
||||
|
||||
jboolean nativeReady(JNIEnv *env, jobject /* this */, jstring super_key_jstr) {
|
||||
ensureSuperKeyNonNull(super_key_jstr);
|
||||
|
||||
const auto super_key = JUTFString(env, super_key_jstr);
|
||||
return sc_ready(super_key.get());
|
||||
}
|
||||
|
||||
jlong nativeKernelPatchVersion(JNIEnv *env, jobject /* this */, jstring super_key_jstr) {
|
||||
ensureSuperKeyNonNull(super_key_jstr);
|
||||
|
||||
const auto super_key = JUTFString(env, super_key_jstr);
|
||||
|
||||
return sc_kp_ver(super_key.get());
|
||||
}
|
||||
|
||||
jstring nativeKernelPatchBuildTime(JNIEnv *env, jobject /* this */, jstring super_key_jstr) {
|
||||
ensureSuperKeyNonNull(super_key_jstr);
|
||||
|
||||
const auto super_key = JUTFString(env, super_key_jstr);
|
||||
char buf[4096] = { '\0' };
|
||||
|
||||
sc_get_build_time(super_key.get(), buf, sizeof(buf));
|
||||
return env->NewStringUTF(buf);
|
||||
}
|
||||
|
||||
jlong nativeSu(JNIEnv *env, jobject /* this */, jstring super_key_jstr, jint to_uid, jstring selinux_context_jstr) {
|
||||
ensureSuperKeyNonNull(super_key_jstr);
|
||||
|
||||
const auto super_key = JUTFString(env, super_key_jstr);
|
||||
const char *selinux_context = nullptr;
|
||||
if (selinux_context_jstr) selinux_context = JUTFString(env, selinux_context_jstr);
|
||||
struct su_profile profile{};
|
||||
profile.uid = getuid();
|
||||
profile.to_uid = (uid_t)to_uid;
|
||||
if (selinux_context) strncpy(profile.scontext, selinux_context, sizeof(profile.scontext) - 1);
|
||||
long rc = sc_su(super_key.get(), &profile);
|
||||
if (rc < 0) [[unlikely]] {
|
||||
LOGE("nativeSu error: %ld", rc);
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
jint nativeSetUidExclude(JNIEnv *env, jobject /* this */, jstring super_key_jstr, jint uid, jint exclude) {
|
||||
ensureSuperKeyNonNull(super_key_jstr);
|
||||
|
||||
const auto super_key = JUTFString(env, super_key_jstr);
|
||||
return static_cast<int>(sc_set_ap_mod_exclude(super_key.get(), (uid_t) uid, exclude));
|
||||
}
|
||||
|
||||
jint nativeGetUidExclude(JNIEnv *env, jobject /* this */, jstring super_key_jstr, uid_t uid) {
|
||||
ensureSuperKeyNonNull(super_key_jstr);
|
||||
|
||||
const auto super_key = JUTFString(env, super_key_jstr);
|
||||
return static_cast<int>(sc_get_ap_mod_exclude(super_key.get(), uid));
|
||||
}
|
||||
|
||||
jintArray nativeSuUids(JNIEnv *env, jobject /* this */, jstring super_key_jstr) {
|
||||
ensureSuperKeyNonNull(super_key_jstr);
|
||||
|
||||
const auto super_key = JUTFString(env, super_key_jstr);
|
||||
int num = static_cast<int>(sc_su_uid_nums(super_key.get()));
|
||||
|
||||
if (num <= 0) [[unlikely]] {
|
||||
LOGW("SuperUser Count less than 1, skip allocating vector...");
|
||||
return env->NewIntArray(0);
|
||||
}
|
||||
|
||||
std::vector<int> uids(num);
|
||||
|
||||
long n = sc_su_allow_uids(super_key.get(), (uid_t *) uids.data(), num);
|
||||
if (n > 0) [[unlikely]] {
|
||||
auto array = env->NewIntArray(n);
|
||||
env->SetIntArrayRegion(array, 0, n, uids.data());
|
||||
return array;
|
||||
}
|
||||
|
||||
return env->NewIntArray(0);
|
||||
}
|
||||
|
||||
jobject nativeSuProfile(JNIEnv *env, jobject /* this */, jstring super_key_jstr, jint uid) {
|
||||
ensureSuperKeyNonNull(super_key_jstr);
|
||||
|
||||
const auto super_key = JUTFString(env, super_key_jstr);
|
||||
struct su_profile profile{};
|
||||
long rc = sc_su_uid_profile(super_key.get(), (uid_t) uid, &profile);
|
||||
if (rc < 0) [[unlikely]] {
|
||||
LOGE("nativeSuProfile error: %ld\n", rc);
|
||||
return nullptr;
|
||||
}
|
||||
jclass cls = env->FindClass("me/bmax/apatch/Natives$Profile");
|
||||
jmethodID constructor = env->GetMethodID(cls, "<init>", "()V");
|
||||
jfieldID uidField = env->GetFieldID(cls, "uid", "I");
|
||||
jfieldID toUidField = env->GetFieldID(cls, "toUid", "I");
|
||||
jfieldID scontextFild = env->GetFieldID(cls, "scontext", "Ljava/lang/String;");
|
||||
|
||||
jobject obj = env->NewObject(cls, constructor);
|
||||
env->SetIntField(obj, uidField, (int) profile.uid);
|
||||
env->SetIntField(obj, toUidField, (int) profile.to_uid);
|
||||
env->SetObjectField(obj, scontextFild, env->NewStringUTF(profile.scontext));
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
jlong nativeLoadKernelPatchModule(JNIEnv *env, jobject /* this */, jstring super_key_jstr, jstring module_path_jstr, jstring args_jstr) {
|
||||
ensureSuperKeyNonNull(super_key_jstr);
|
||||
|
||||
const auto super_key = JUTFString(env, super_key_jstr);
|
||||
const auto module_path = JUTFString(env, module_path_jstr);
|
||||
const auto args = JUTFString(env, args_jstr);
|
||||
long rc = sc_kpm_load(super_key.get(), module_path.get(), args.get(), nullptr);
|
||||
if (rc < 0) [[unlikely]] {
|
||||
LOGE("nativeLoadKernelPatchModule error: %ld", rc);
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
jobject nativeControlKernelPatchModule(JNIEnv *env, jobject /* this */, jstring super_key_jstr, jstring module_name_jstr, jstring control_args_jstr) {
|
||||
ensureSuperKeyNonNull(super_key_jstr);
|
||||
|
||||
const auto super_key = JUTFString(env, super_key_jstr);
|
||||
const auto module_name = JUTFString(env, module_name_jstr);
|
||||
const auto control_args = JUTFString(env, control_args_jstr);
|
||||
|
||||
char buf[4096] = { '\0' };
|
||||
long rc = sc_kpm_control(super_key.get(), module_name.get(), control_args.get(), buf, sizeof(buf));
|
||||
if (rc < 0) [[unlikely]] {
|
||||
LOGE("nativeControlKernelPatchModule error: %ld", rc);
|
||||
}
|
||||
|
||||
jclass cls = env->FindClass("me/bmax/apatch/Natives$KPMCtlRes");
|
||||
jmethodID constructor = env->GetMethodID(cls, "<init>", "()V");
|
||||
jfieldID rcField = env->GetFieldID(cls, "rc", "J");
|
||||
jfieldID outMsg = env->GetFieldID(cls, "outMsg", "Ljava/lang/String;");
|
||||
|
||||
jobject obj = env->NewObject(cls, constructor);
|
||||
env->SetLongField(obj, rcField, rc);
|
||||
env->SetObjectField(obj, outMsg, env->NewStringUTF(buf));
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
jlong nativeUnloadKernelPatchModule(JNIEnv *env, jobject /* this */, jstring super_key_jstr, jstring module_name_jstr) {
|
||||
ensureSuperKeyNonNull(super_key_jstr);
|
||||
|
||||
const auto super_key = JUTFString(env, super_key_jstr);
|
||||
const auto module_name = JUTFString(env, module_name_jstr);
|
||||
long rc = sc_kpm_unload(super_key.get(), module_name.get(), nullptr);
|
||||
if (rc < 0) [[unlikely]] {
|
||||
LOGE("nativeUnloadKernelPatchModule error: %ld", rc);
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
jlong nativeKernelPatchModuleNum(JNIEnv *env, jobject /* this */, jstring super_key_jstr) {
|
||||
ensureSuperKeyNonNull(super_key_jstr);
|
||||
|
||||
const auto super_key = JUTFString(env, super_key_jstr);
|
||||
long rc = sc_kpm_nums(super_key.get());
|
||||
if (rc < 0) [[unlikely]] {
|
||||
LOGE("nativeKernelPatchModuleNum error: %ld", rc);
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
jstring nativeKernelPatchModuleList(JNIEnv *env, jobject /* this */, jstring super_key_jstr) {
|
||||
ensureSuperKeyNonNull(super_key_jstr);
|
||||
|
||||
const auto super_key = JUTFString(env, super_key_jstr);
|
||||
|
||||
char buf[4096] = { '\0' };
|
||||
long rc = sc_kpm_list(super_key.get(), buf, sizeof(buf));
|
||||
if (rc < 0) [[unlikely]] {
|
||||
LOGE("nativeKernelPatchModuleList error: %ld", rc);
|
||||
}
|
||||
|
||||
return env->NewStringUTF(buf);
|
||||
}
|
||||
|
||||
jstring nativeKernelPatchModuleInfo(JNIEnv *env, jobject /* this */, jstring super_key_jstr, jstring module_name_jstr) {
|
||||
ensureSuperKeyNonNull(super_key_jstr);
|
||||
|
||||
const auto super_key = JUTFString(env, super_key_jstr);
|
||||
const auto module_name = JUTFString(env, module_name_jstr);
|
||||
char buf[1024] = { '\0' };
|
||||
long rc = sc_kpm_info(super_key.get(), module_name.get(), buf, sizeof(buf));
|
||||
if (rc < 0) [[unlikely]] {
|
||||
LOGE("nativeKernelPatchModuleInfo error: %ld", rc);
|
||||
}
|
||||
|
||||
return env->NewStringUTF(buf);
|
||||
}
|
||||
|
||||
jlong nativeGrantSu(JNIEnv *env, jobject /* this */, jstring super_key_jstr, jint uid, jint to_uid, jstring selinux_context_jstr) {
|
||||
ensureSuperKeyNonNull(super_key_jstr);
|
||||
|
||||
const auto super_key = JUTFString(env, super_key_jstr);
|
||||
const auto selinux_context = JUTFString(env, selinux_context_jstr);
|
||||
struct su_profile profile{};
|
||||
profile.uid = uid;
|
||||
profile.to_uid = to_uid;
|
||||
if (selinux_context) strncpy(profile.scontext, selinux_context, sizeof(profile.scontext) - 1);
|
||||
return sc_su_grant_uid(super_key.get(), &profile);
|
||||
}
|
||||
|
||||
jlong nativeRevokeSu(JNIEnv *env, jobject /* this */, jstring super_key_jstr, jint uid) {
|
||||
ensureSuperKeyNonNull(super_key_jstr);
|
||||
|
||||
const auto super_key = JUTFString(env, super_key_jstr);
|
||||
return sc_su_revoke_uid(super_key.get(), (uid_t) uid);
|
||||
}
|
||||
|
||||
jstring nativeSuPath(JNIEnv *env, jobject /* this */, jstring super_key_jstr) {
|
||||
ensureSuperKeyNonNull(super_key_jstr);
|
||||
|
||||
const auto super_key = JUTFString(env, super_key_jstr);
|
||||
char buf[SU_PATH_MAX_LEN] = { '\0' };
|
||||
long rc = sc_su_get_path(super_key.get(), buf, sizeof(buf));
|
||||
if (rc < 0) [[unlikely]] {
|
||||
LOGE("nativeSuPath error: %ld", rc);
|
||||
}
|
||||
|
||||
return env->NewStringUTF(buf);
|
||||
}
|
||||
|
||||
jboolean nativeResetSuPath(JNIEnv *env, jobject /* this */, jstring super_key_jstr, jstring su_path_jstr) {
|
||||
ensureSuperKeyNonNull(super_key_jstr);
|
||||
|
||||
const auto super_key = JUTFString(env, super_key_jstr);
|
||||
const auto su_path = JUTFString(env, su_path_jstr);
|
||||
|
||||
return sc_su_reset_path(super_key.get(), su_path.get()) == 0;
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void * /*reserved*/) {
|
||||
LOGI("Enter OnLoad");
|
||||
|
||||
JNIEnv* env{};
|
||||
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) [[unlikely]] {
|
||||
LOGE("Get JNIEnv error!");
|
||||
return JNI_FALSE;
|
||||
}
|
||||
|
||||
auto clazz = JNI_FindClass(env, "me/bmax/apatch/Natives");
|
||||
if (clazz.get() == nullptr) [[unlikely]] {
|
||||
LOGE("Failed to find Natives class");
|
||||
return JNI_FALSE;
|
||||
}
|
||||
|
||||
const static JNINativeMethod gMethods[] = {
|
||||
{"nativeReady", "(Ljava/lang/String;)Z", reinterpret_cast<void *>(&nativeReady)},
|
||||
{"nativeKernelPatchVersion", "(Ljava/lang/String;)J", reinterpret_cast<void *>(&nativeKernelPatchVersion)},
|
||||
{"nativeKernelPatchBuildTime", "(Ljava/lang/String;)Ljava/lang/String;", reinterpret_cast<void *>(&nativeKernelPatchBuildTime)},
|
||||
{"nativeSu", "(Ljava/lang/String;ILjava/lang/String;)J", reinterpret_cast<void *>(&nativeSu)},
|
||||
{"nativeSetUidExclude", "(Ljava/lang/String;II)I", reinterpret_cast<void *>(&nativeSetUidExclude)},
|
||||
{"nativeGetUidExclude", "(Ljava/lang/String;I)I", reinterpret_cast<void *>(&nativeGetUidExclude)},
|
||||
{"nativeSuUids", "(Ljava/lang/String;)[I", reinterpret_cast<void *>(&nativeSuUids)},
|
||||
{"nativeSuProfile", "(Ljava/lang/String;I)Lme/bmax/apatch/Natives$Profile;", reinterpret_cast<void *>(&nativeSuProfile)},
|
||||
{"nativeLoadKernelPatchModule", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)J", reinterpret_cast<void *>(&nativeLoadKernelPatchModule)},
|
||||
{"nativeControlKernelPatchModule", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lme/bmax/apatch/Natives$KPMCtlRes;", reinterpret_cast<void *>(&nativeControlKernelPatchModule)},
|
||||
{"nativeUnloadKernelPatchModule", "(Ljava/lang/String;Ljava/lang/String;)J", reinterpret_cast<void *>(&nativeUnloadKernelPatchModule)},
|
||||
{"nativeKernelPatchModuleNum", "(Ljava/lang/String;)J", reinterpret_cast<void *>(&nativeKernelPatchModuleNum)},
|
||||
{"nativeKernelPatchModuleList", "(Ljava/lang/String;)Ljava/lang/String;", reinterpret_cast<void *>(&nativeKernelPatchModuleList)},
|
||||
{"nativeKernelPatchModuleInfo", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", reinterpret_cast<void *>(&nativeKernelPatchModuleInfo)},
|
||||
{"nativeGrantSu", "(Ljava/lang/String;IILjava/lang/String;)J", reinterpret_cast<void *>(&nativeGrantSu)},
|
||||
{"nativeRevokeSu", "(Ljava/lang/String;I)J", reinterpret_cast<void *>(&nativeRevokeSu)},
|
||||
{"nativeSuPath", "(Ljava/lang/String;)Ljava/lang/String;", reinterpret_cast<void *>(&nativeSuPath)},
|
||||
{"nativeResetSuPath", "(Ljava/lang/String;Ljava/lang/String;)Z", reinterpret_cast<void *>(&nativeResetSuPath)},
|
||||
};
|
||||
|
||||
if (JNI_RegisterNatives(env, clazz, gMethods, sizeof(gMethods) / sizeof(gMethods[0])) < 0) [[unlikely]] {
|
||||
LOGE("Failed to register native methods");
|
||||
return JNI_FALSE;
|
||||
}
|
||||
|
||||
LOGI("JNI_OnLoad Done!");
|
||||
return JNI_VERSION_1_6;
|
||||
}
|
||||
28
app/src/main/cpp/apjni.hpp
Normal file
28
app/src/main/cpp/apjni.hpp
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
//
|
||||
// Created by GarfieldHan on 2024/6/11.
|
||||
//
|
||||
|
||||
#ifndef APATCH_APJNI_HPP
|
||||
#define APATCH_APJNI_HPP
|
||||
|
||||
#include <jni.h>
|
||||
#include <android/log.h>
|
||||
#include "jni_helper.hpp"
|
||||
|
||||
using namespace lsplant;
|
||||
|
||||
#define LOG_TAG "APatchNative"
|
||||
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
|
||||
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
|
||||
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
|
||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
||||
#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
|
||||
|
||||
void ensureSuperKeyNonNull(jstring super_key_jstr) {
|
||||
if (!super_key_jstr) [[unlikely]] {
|
||||
LOGE("[%s] Super Key is null!", __PRETTY_FUNCTION__);
|
||||
abort();
|
||||
}
|
||||
}
|
||||
|
||||
#endif //APATCH_APJNI_HPP
|
||||
1334
app/src/main/cpp/jni_helper.hpp
Normal file
1334
app/src/main/cpp/jni_helper.hpp
Normal file
File diff suppressed because it is too large
Load diff
568
app/src/main/cpp/supercall.h
Normal file
568
app/src/main/cpp/supercall.h
Normal file
|
|
@ -0,0 +1,568 @@
|
|||
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
/*
|
||||
* Copyright (C) 2023 bmax121. All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _KPU_SUPERCALL_H_
|
||||
#define _KPU_SUPERCALL_H_
|
||||
|
||||
#include <unistd.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include "uapi/scdefs.h"
|
||||
#include "version"
|
||||
|
||||
/// KernelPatch version is greater than or equal to 0x0a05
|
||||
static inline long ver_and_cmd(const char *key, long cmd)
|
||||
{
|
||||
uint32_t version_code = (MAJOR << 16) + (MINOR << 8) + PATCH;
|
||||
return ((long)version_code << 32) | (0x1158 << 16) | (cmd & 0xFFFF);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief If KernelPatch installed, @see SUPERCALL_HELLO_ECHO will echoed.
|
||||
*
|
||||
* @param key : superkey or 'su' string if caller uid is su allowed
|
||||
* @return long
|
||||
*/
|
||||
static inline long sc_hello(const char *key)
|
||||
{
|
||||
if (!key || !key[0]) return -EINVAL;
|
||||
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_HELLO));
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Is KernelPatch installed?
|
||||
*
|
||||
* @param key : superkey or 'su' string if caller uid is su allowed
|
||||
* @return true
|
||||
* @return false
|
||||
*/
|
||||
static inline bool sc_ready(const char *key)
|
||||
{
|
||||
return sc_hello(key) == SUPERCALL_HELLO_MAGIC;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Print messages by printk in the kernel
|
||||
*
|
||||
* @param key : superkey or 'su' string if caller uid is su allowed
|
||||
* @param msg
|
||||
* @return long
|
||||
*/
|
||||
static inline long sc_klog(const char *key, const char *msg)
|
||||
{
|
||||
if (!key || !key[0]) return -EINVAL;
|
||||
if (!msg || strlen(msg) <= 0) return -EINVAL;
|
||||
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_KLOG), msg);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Print build kernel time
|
||||
*
|
||||
* @param key : superkey or 'su' string if caller uid is su allowed
|
||||
* @param buildtime
|
||||
* @param timestamp
|
||||
* @return long
|
||||
*/
|
||||
static inline long sc_get_build_time(const char *key, const char *buildtime, size_t len)
|
||||
{
|
||||
if (!key || !key[0]) return -EINVAL;
|
||||
if (!buildtime) return -EINVAL;
|
||||
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_BUILD_TIME), buildtime,len);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief KernelPatch version number
|
||||
*
|
||||
* @param key
|
||||
* @return uint32_t
|
||||
*/
|
||||
static inline uint32_t sc_kp_ver(const char *key)
|
||||
{
|
||||
if (!key || !key[0]) return -EINVAL;
|
||||
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_KERNELPATCH_VER));
|
||||
return (uint32_t)ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Kernel version number
|
||||
*
|
||||
* @param key : superkey or 'su' string if caller uid is su allowed
|
||||
* @return uint32_t
|
||||
*/
|
||||
static inline uint32_t sc_k_ver(const char *key)
|
||||
{
|
||||
if (!key || !key[0]) return -EINVAL;
|
||||
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_KERNEL_VER));
|
||||
return (uint32_t)ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Substitute user of current thread
|
||||
*
|
||||
* @param key : superkey or 'su' string if caller uid is su allowed
|
||||
* @param profile : if scontext is invalid or illegal, all selinux permission checks will bypass via hook
|
||||
* @see struct su_profile
|
||||
* @return long : 0 if succeed
|
||||
*/
|
||||
static inline long sc_su(const char *key, struct su_profile *profile)
|
||||
{
|
||||
if (!key || !key[0]) return -EINVAL;
|
||||
if (strlen(profile->scontext) >= SUPERCALL_SCONTEXT_LEN) return -EINVAL;
|
||||
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_SU), profile);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Substitute user of tid specfied thread
|
||||
*
|
||||
* @param key : superkey or 'su' string if caller uid is su allowed
|
||||
* @param tid : target thread id
|
||||
* @param profile : if scontext is invalid or illegal, all selinux permission checks will bypass via hook
|
||||
* @see struct su_profile
|
||||
* @return long : 0 if succeed
|
||||
*/
|
||||
static inline long sc_su_task(const char *key, pid_t tid, struct su_profile *profile)
|
||||
{
|
||||
if (!key || !key[0]) return -EINVAL;
|
||||
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_SU_TASK), tid, profile);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief
|
||||
*
|
||||
* @param key
|
||||
* @param gid group id
|
||||
* @param did data id
|
||||
* @param data
|
||||
* @param dlen
|
||||
* @return long
|
||||
*/
|
||||
static inline long sc_kstorage_write(const char *key, int gid, long did, void *data, int offset, int dlen)
|
||||
{
|
||||
if (!key || !key[0]) return -EINVAL;
|
||||
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_KSTORAGE_WRITE), gid, did, data, (((long)offset << 32) | dlen));
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief
|
||||
*
|
||||
* @param key
|
||||
* @param gid
|
||||
* @param did
|
||||
* @param out_data
|
||||
* @param dlen
|
||||
* @return long
|
||||
*/
|
||||
static inline long sc_kstorage_read(const char *key, int gid, long did, void *out_data, int offset, int dlen)
|
||||
{
|
||||
if (!key || !key[0]) return -EINVAL;
|
||||
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_KSTORAGE_READ), gid, did, out_data, (((long)offset << 32) | dlen));
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @brief
|
||||
*
|
||||
* @param key
|
||||
* @param gid
|
||||
* @param ids
|
||||
* @param ids_len
|
||||
* @return long numbers of listed ids
|
||||
*/
|
||||
static inline long sc_kstorage_list_ids(const char *key, int gid, long *ids, int ids_len)
|
||||
{
|
||||
if (!key || !key[0]) return -EINVAL;
|
||||
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_KSTORAGE_LIST_IDS), gid, ids, ids_len);
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @brief
|
||||
*
|
||||
* @param key
|
||||
* @param gid
|
||||
* @param did
|
||||
* @return long
|
||||
*/
|
||||
static inline long sc_kstorage_remove(const char *key, int gid, long did)
|
||||
{
|
||||
if (!key || !key[0]) return -EINVAL;
|
||||
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_KSTORAGE_REMOVE), gid, did);
|
||||
return ret;
|
||||
}
|
||||
|
||||
#ifdef ANDROID
|
||||
/**
|
||||
* @brief
|
||||
*
|
||||
* @param key
|
||||
* @param uid
|
||||
* @param exclude
|
||||
* @return long
|
||||
*/
|
||||
static inline long sc_set_ap_mod_exclude(const char *key, uid_t uid, int exclude)
|
||||
{
|
||||
if(exclude) {
|
||||
return sc_kstorage_write(key, KSTORAGE_EXCLUDE_LIST_GROUP, uid, &exclude, 0, sizeof(exclude));
|
||||
} else {
|
||||
return sc_kstorage_remove(key, KSTORAGE_EXCLUDE_LIST_GROUP, uid);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @brief
|
||||
*
|
||||
* @param key
|
||||
* @param uid
|
||||
* @param exclude
|
||||
* @return long
|
||||
*/
|
||||
static inline int sc_get_ap_mod_exclude(const char *key, uid_t uid)
|
||||
{
|
||||
int exclude = 0;
|
||||
int rc = sc_kstorage_read(key, KSTORAGE_EXCLUDE_LIST_GROUP, uid, &exclude, 0, sizeof(exclude));
|
||||
if (rc < 0) return 0;
|
||||
return exclude;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
static inline int sc_list_ap_mod_exclude(const char *key, uid_t *uids, int uids_len)
|
||||
{
|
||||
if(uids_len < 0 || uids_len > 512) return -E2BIG;
|
||||
long ids[uids_len];
|
||||
int rc = sc_kstorage_list_ids(key, KSTORAGE_EXCLUDE_LIST_GROUP, ids, uids_len);
|
||||
if (rc < 0) return 0;
|
||||
for(int i = 0; i < rc; i ++) {
|
||||
uids[i] = (uid_t)ids[i];
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Grant su permission
|
||||
*
|
||||
* @param key
|
||||
* @param profile : if scontext is invalid or illegal, all selinux permission checks will bypass via hook
|
||||
* @return long : 0 if succeed
|
||||
*/
|
||||
static inline long sc_su_grant_uid(const char *key, struct su_profile *profile)
|
||||
{
|
||||
if (!key || !key[0]) return -EINVAL;
|
||||
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_SU_GRANT_UID), profile);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Revoke su permission
|
||||
*
|
||||
* @param key
|
||||
* @param uid
|
||||
* @return long 0 if succeed
|
||||
*/
|
||||
static inline long sc_su_revoke_uid(const char *key, uid_t uid)
|
||||
{
|
||||
if (!key || !key[0]) return -EINVAL;
|
||||
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_SU_REVOKE_UID), uid);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get numbers of su allowed uids
|
||||
*
|
||||
* @param key
|
||||
* @return long
|
||||
*/
|
||||
static inline long sc_su_uid_nums(const char *key)
|
||||
{
|
||||
if (!key || !key[0]) return -EINVAL;
|
||||
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_SU_NUMS));
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief
|
||||
*
|
||||
* @param key : superkey or 'su' string if caller uid is su allowed
|
||||
* @param buf
|
||||
* @param num
|
||||
* @return long : The numbers of uids if succeed, nagative value if failed
|
||||
*/
|
||||
static inline long sc_su_allow_uids(const char *key, uid_t *buf, int num)
|
||||
{
|
||||
if (!key || !key[0]) return -EINVAL;
|
||||
if (!buf || num <= 0) return -EINVAL;
|
||||
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_SU_LIST), buf, num);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get su profile of specified uid
|
||||
*
|
||||
* @param key
|
||||
* @param uid
|
||||
* @param out_profile
|
||||
* @return long : 0 if succeed
|
||||
*/
|
||||
static inline long sc_su_uid_profile(const char *key, uid_t uid, struct su_profile *out_profile)
|
||||
{
|
||||
if (!key || !key[0]) return -EINVAL;
|
||||
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_SU_PROFILE), uid, out_profile);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get full path of current 'su' command
|
||||
*
|
||||
* @param key : superkey or 'su' string if caller uid is su allowed
|
||||
* @param out_path
|
||||
* @param path_len
|
||||
* @return long : The length of result string if succeed, negative if failed
|
||||
*/
|
||||
static inline long sc_su_get_path(const char *key, char *out_path, int path_len)
|
||||
{
|
||||
if (!key || !key[0]) return -EINVAL;
|
||||
if (!out_path || path_len <= 0) return -EINVAL;
|
||||
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_SU_GET_PATH), out_path, path_len);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Reset full path of 'su' command
|
||||
*
|
||||
* @param key
|
||||
* @param path
|
||||
* @return long : 0 if succeed
|
||||
*/
|
||||
static inline long sc_su_reset_path(const char *key, const char *path)
|
||||
{
|
||||
if (!key || !key[0]) return -EINVAL;
|
||||
if (!path || !path[0]) return -EINVAL;
|
||||
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_SU_RESET_PATH), path);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get current all-allowed selinux context
|
||||
*
|
||||
* @param key : superkey or 'su' string if caller uid is su allowed
|
||||
* @param out_sctx
|
||||
* @param sctx_len
|
||||
* @return long 0 if there is a all-allowed selinux context now
|
||||
*/
|
||||
static inline long sc_su_get_all_allow_sctx(const char *key, char *out_sctx, int sctx_len)
|
||||
{
|
||||
if (!key || !key[0]) return -EINVAL;
|
||||
if (!out_sctx) return -EINVAL;
|
||||
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_SU_GET_ALLOW_SCTX), out_sctx);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Reset current all-allowed selinux context
|
||||
*
|
||||
* @param key : superkey or 'su' string if caller uid is su allowed
|
||||
* @param sctx If sctx is empty string, clear all-allowed selinux,
|
||||
* otherwise, try to reset a new all-allowed selinux context
|
||||
* @return long 0 if succeed
|
||||
*/
|
||||
static inline long sc_su_reset_all_allow_sctx(const char *key, const char *sctx)
|
||||
{
|
||||
if (!key || !key[0]) return -EINVAL;
|
||||
if (!sctx) return -EINVAL;
|
||||
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_SU_SET_ALLOW_SCTX), sctx);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Load module
|
||||
*
|
||||
* @param key : superkey
|
||||
* @param path
|
||||
* @param args
|
||||
* @param reserved
|
||||
* @return long : 0 if succeed
|
||||
*/
|
||||
static inline long sc_kpm_load(const char *key, const char *path, const char *args, void *reserved)
|
||||
{
|
||||
if (!key || !key[0]) return -EINVAL;
|
||||
if (!path || strlen(path) <= 0) return -EINVAL;
|
||||
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_KPM_LOAD), path, args, reserved);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Control module with arguments
|
||||
*
|
||||
* @param key : superkey
|
||||
* @param name : module name
|
||||
* @param ctl_args : control argument
|
||||
* @param out_msg : output message buffer
|
||||
* @param outlen : buffer length of out_msg
|
||||
* @return long : 0 if succeed
|
||||
*/
|
||||
static inline long sc_kpm_control(const char *key, const char *name, const char *ctl_args, char *out_msg, long outlen)
|
||||
{
|
||||
if (!key || !key[0]) return -EINVAL;
|
||||
if (!name || strlen(name) <= 0) return -EINVAL;
|
||||
if (!ctl_args || strlen(ctl_args) <= 0) return -EINVAL;
|
||||
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_KPM_CONTROL), name, ctl_args, out_msg, outlen);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Unload module
|
||||
*
|
||||
* @param key : superkey
|
||||
* @param name : module name
|
||||
* @param reserved
|
||||
* @return long : 0 if succeed
|
||||
*/
|
||||
static inline long sc_kpm_unload(const char *key, const char *name, void *reserved)
|
||||
{
|
||||
if (!key || !key[0]) return -EINVAL;
|
||||
if (!name || strlen(name) <= 0) return -EINVAL;
|
||||
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_KPM_UNLOAD), name, reserved);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Current loaded module numbers
|
||||
*
|
||||
* @param key : superkey
|
||||
* @return long
|
||||
*/
|
||||
static inline long sc_kpm_nums(const char *key)
|
||||
{
|
||||
if (!key || !key[0]) return -EINVAL;
|
||||
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_KPM_NUMS));
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief List names of current loaded modules, splited with '\n'
|
||||
*
|
||||
* @param key : superkey
|
||||
* @param names_buf : output buffer
|
||||
* @param buf_len : the length of names_buf
|
||||
* @return long : the length of result string if succeed, negative if failed
|
||||
*/
|
||||
static inline long sc_kpm_list(const char *key, char *names_buf, int buf_len)
|
||||
{
|
||||
if (!key || !key[0]) return -EINVAL;
|
||||
if (!names_buf || buf_len <= 0) return -EINVAL;
|
||||
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_KPM_LIST), names_buf, buf_len);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get module information.
|
||||
*
|
||||
* @param key : superkey
|
||||
* @param name : module name
|
||||
* @param buf :
|
||||
* @param buf_len :
|
||||
* @return long : The length of result string if succeed, negative if failed
|
||||
*/
|
||||
static inline long sc_kpm_info(const char *key, const char *name, char *buf, int buf_len)
|
||||
{
|
||||
if (!key || !key[0]) return -EINVAL;
|
||||
if (!buf || buf_len <= 0) return -EINVAL;
|
||||
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_KPM_INFO), name, buf, buf_len);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get current superkey
|
||||
*
|
||||
* @param key : superkey
|
||||
* @param out_key
|
||||
* @param outlen
|
||||
* @return long : 0 if succeed
|
||||
*/
|
||||
static inline long sc_skey_get(const char *key, char *out_key, int outlen)
|
||||
{
|
||||
if (!key || !key[0]) return -EINVAL;
|
||||
if (outlen < SUPERCALL_KEY_MAX_LEN) return -EINVAL;
|
||||
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_SKEY_GET), out_key, outlen);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Reset current superkey
|
||||
*
|
||||
* @param key : superkey
|
||||
* @param new_key
|
||||
* @return long : 0 if succeed
|
||||
*/
|
||||
static inline long sc_skey_set(const char *key, const char *new_key)
|
||||
{
|
||||
if (!key || !key[0]) return -EINVAL;
|
||||
if (!new_key || !new_key[0]) return -EINVAL;
|
||||
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_SKEY_SET), new_key);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Whether to enable hash verification for root superkey.
|
||||
*
|
||||
* @param key : superkey
|
||||
* @param enable
|
||||
* @return long
|
||||
*/
|
||||
static inline long sc_skey_root_enable(const char *key, bool enable)
|
||||
{
|
||||
if (!key || !key[0]) return -EINVAL;
|
||||
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_SKEY_ROOT_ENABLE), (long)enable);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get whether in safe mode
|
||||
*
|
||||
* @param key
|
||||
* @return long
|
||||
*/
|
||||
static inline long sc_su_get_safemode(const char *key)
|
||||
{
|
||||
if (!key || !key[0]) return -EINVAL;
|
||||
return syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_SU_GET_SAFEMODE));
|
||||
}
|
||||
|
||||
|
||||
static inline long sc_bootlog(const char *key)
|
||||
{
|
||||
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_BOOTLOG));
|
||||
return ret;
|
||||
}
|
||||
|
||||
static inline long sc_panic(const char *key)
|
||||
{
|
||||
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_PANIC));
|
||||
return ret;
|
||||
}
|
||||
|
||||
static inline long __sc_test(const char *key, long a1, long a2, long a3)
|
||||
{
|
||||
long ret = syscall(__NR_supercall, key, ver_and_cmd(key, SUPERCALL_TEST), a1, a2, a3);
|
||||
return ret;
|
||||
}
|
||||
|
||||
#endif
|
||||
14
app/src/main/cpp/type_traits.hpp
Normal file
14
app/src/main/cpp/type_traits.hpp
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
#pragma once
|
||||
|
||||
#include <type_traits>
|
||||
|
||||
namespace lsplant {
|
||||
template <class, template <class, class...> class>
|
||||
struct is_instance : public std::false_type {};
|
||||
|
||||
template <class... Ts, template <class, class...> class U>
|
||||
struct is_instance<U<Ts...>, U> : public std::true_type {};
|
||||
|
||||
template <class T, template <class, class...> class U>
|
||||
inline constexpr bool is_instance_v = is_instance<T, U>::value;
|
||||
} // namespace lsplant
|
||||
118
app/src/main/cpp/uapi/scdefs.h
Normal file
118
app/src/main/cpp/uapi/scdefs.h
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
/*
|
||||
* Copyright (C) 2023 bmax121. All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _KP_UAPI_SCDEF_H_
|
||||
#define _KP_UAPI_SCDEF_H_
|
||||
|
||||
static inline long hash_key(const char *key)
|
||||
{
|
||||
long hash = 1000000007;
|
||||
for (int i = 0; key[i]; i++) {
|
||||
hash = hash * 31 + key[i];
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
#define SUPERCALL_HELLO_ECHO "hello1158"
|
||||
|
||||
// #define __NR_supercall __NR3264_truncate // 45
|
||||
#define __NR_supercall 45
|
||||
|
||||
#define SUPERCALL_HELLO 0x1000
|
||||
#define SUPERCALL_KLOG 0x1004
|
||||
|
||||
#define SUPERCALL_BUILD_TIME 0x1007
|
||||
#define SUPERCALL_KERNELPATCH_VER 0x1008
|
||||
#define SUPERCALL_KERNEL_VER 0x1009
|
||||
|
||||
#define SUPERCALL_SKEY_GET 0x100a
|
||||
#define SUPERCALL_SKEY_SET 0x100b
|
||||
#define SUPERCALL_SKEY_ROOT_ENABLE 0x100c
|
||||
|
||||
#define SUPERCALL_SU 0x1010
|
||||
#define SUPERCALL_SU_TASK 0x1011 // syscall(__NR_gettid)
|
||||
|
||||
#define SUPERCALL_KPM_LOAD 0x1020
|
||||
#define SUPERCALL_KPM_UNLOAD 0x1021
|
||||
#define SUPERCALL_KPM_CONTROL 0x1022
|
||||
|
||||
#define SUPERCALL_KPM_NUMS 0x1030
|
||||
#define SUPERCALL_KPM_LIST 0x1031
|
||||
#define SUPERCALL_KPM_INFO 0x1032
|
||||
|
||||
struct kernel_storage
|
||||
{
|
||||
void *data;
|
||||
int len;
|
||||
};
|
||||
|
||||
#define SUPERCALL_KSTORAGE_ALLOC_GROUP 0x1040
|
||||
#define SUPERCALL_KSTORAGE_WRITE 0x1041
|
||||
#define SUPERCALL_KSTORAGE_READ 0x1042
|
||||
#define SUPERCALL_KSTORAGE_LIST_IDS 0x1043
|
||||
#define SUPERCALL_KSTORAGE_REMOVE 0x1044
|
||||
#define SUPERCALL_KSTORAGE_REMOVE_GROUP 0x1045
|
||||
|
||||
#define KSTORAGE_SU_LIST_GROUP 0
|
||||
#define KSTORAGE_EXCLUDE_LIST_GROUP 1
|
||||
#define KSTORAGE_UNUSED_GROUP_2 2
|
||||
#define KSTORAGE_UNUSED_GROUP_3 3
|
||||
|
||||
#define SUPERCALL_BOOTLOG 0x10fd
|
||||
#define SUPERCALL_PANIC 0x10fe
|
||||
#define SUPERCALL_TEST 0x10ff
|
||||
|
||||
#define SUPERCALL_KEY_MAX_LEN 0x40
|
||||
#define SUPERCALL_SCONTEXT_LEN 0x60
|
||||
|
||||
struct su_profile
|
||||
{
|
||||
uid_t uid;
|
||||
uid_t to_uid;
|
||||
char scontext[SUPERCALL_SCONTEXT_LEN];
|
||||
};
|
||||
|
||||
#ifdef ANDROID
|
||||
#define SH_PATH "/system/bin/sh"
|
||||
#define SU_PATH "/system/bin/kp"
|
||||
#define LEGACY_SU_PATH "/system/bin/su"
|
||||
#define ECHO_PATH "/system/bin/echo"
|
||||
#define KERNELPATCH_DATA_DIR "/data/adb/kp"
|
||||
#define KERNELPATCH_MODULE_DATA_DIR KERNELPATCH_DATA_DIR "/modules"
|
||||
#define APD_PATH "/data/adb/apd"
|
||||
#define ALL_ALLOW_SCONTEXT "u:r:kp:s0"
|
||||
#define ALL_ALLOW_SCONTEXT_MAGISK "u:r:magisk:s0"
|
||||
#define ALL_ALLOW_SCONTEXT_KERNEL "u:r:kernel:s0"
|
||||
#else
|
||||
#define SH_PATH "/usr/bin/sh"
|
||||
#define ECHO_PATH "/usr/bin/echo"
|
||||
#define SU_PATH "/usr/bin/kp"
|
||||
#define ALL_ALLOW_SCONTEXT "u:r:kernel:s0"
|
||||
#endif
|
||||
|
||||
#define SU_PATH_MAX_LEN 128
|
||||
|
||||
#define SUPERCMD "/system/bin/truncate"
|
||||
|
||||
#define SAFE_MODE_FLAG_FILE "/dev/.safemode"
|
||||
|
||||
#define SUPERCALL_SU_GRANT_UID 0x1100
|
||||
#define SUPERCALL_SU_REVOKE_UID 0x1101
|
||||
#define SUPERCALL_SU_NUMS 0x1102
|
||||
#define SUPERCALL_SU_LIST 0x1103
|
||||
#define SUPERCALL_SU_PROFILE 0x1104
|
||||
#define SUPERCALL_SU_GET_ALLOW_SCTX 0x1105
|
||||
#define SUPERCALL_SU_SET_ALLOW_SCTX 0x1106
|
||||
#define SUPERCALL_SU_GET_PATH 0x1110
|
||||
#define SUPERCALL_SU_RESET_PATH 0x1111
|
||||
#define SUPERCALL_SU_GET_SAFEMODE 0x1112
|
||||
|
||||
#define SUPERCALL_MAX 0x1200
|
||||
|
||||
#define SUPERCALL_RES_SUCCEED 0
|
||||
|
||||
#define SUPERCALL_HELLO_MAGIC 0x11581158
|
||||
|
||||
#endif
|
||||
3
app/src/main/cpp/version
Normal file
3
app/src/main/cpp/version
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#define MAJOR 0
|
||||
#define MINOR 12
|
||||
#define PATCH 0
|
||||
BIN
app/src/main/ic_launcher-playstore.png
Normal file
BIN
app/src/main/ic_launcher-playstore.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
308
app/src/main/java/me/bmax/apatch/APatchApp.kt
Normal file
308
app/src/main/java/me/bmax/apatch/APatchApp.kt
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
package me.bmax.apatch
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.topjohnwu.superuser.CallbackList
|
||||
import me.bmax.apatch.ui.CrashHandleActivity
|
||||
import me.bmax.apatch.util.APatchCli
|
||||
import me.bmax.apatch.util.APatchKeyHelper
|
||||
import me.bmax.apatch.util.Version
|
||||
import me.bmax.apatch.util.getRootShell
|
||||
import me.bmax.apatch.util.rootShellForResult
|
||||
import me.bmax.apatch.util.verifyAppSignature
|
||||
import okhttp3.Cache
|
||||
import okhttp3.OkHttpClient
|
||||
import java.io.File
|
||||
import java.util.Locale
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
lateinit var apApp: APApplication
|
||||
|
||||
const val TAG = "APatch"
|
||||
|
||||
class APApplication : Application(), Thread.UncaughtExceptionHandler {
|
||||
lateinit var okhttpClient: OkHttpClient
|
||||
|
||||
init {
|
||||
Thread.setDefaultUncaughtExceptionHandler(this)
|
||||
}
|
||||
|
||||
enum class State {
|
||||
UNKNOWN_STATE,
|
||||
|
||||
KERNELPATCH_INSTALLED, KERNELPATCH_NEED_UPDATE, KERNELPATCH_NEED_REBOOT, KERNELPATCH_UNINSTALLING,
|
||||
|
||||
ANDROIDPATCH_NOT_INSTALLED, ANDROIDPATCH_INSTALLED, ANDROIDPATCH_INSTALLING, ANDROIDPATCH_NEED_UPDATE, ANDROIDPATCH_UNINSTALLING,
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
const val APD_PATH = "/data/adb/apd"
|
||||
|
||||
@Deprecated("No more KPatch ELF from 0.11.0-dev")
|
||||
const val KPATCH_PATH = "/data/adb/kpatch"
|
||||
const val SUPERCMD = "/system/bin/truncate"
|
||||
const val APATCH_FOLDER = "/data/adb/ap/"
|
||||
private const val APATCH_BIN_FOLDER = APATCH_FOLDER + "bin/"
|
||||
private const val APATCH_LOG_FOLDER = APATCH_FOLDER + "log/"
|
||||
private const val APD_LINK_PATH = APATCH_BIN_FOLDER + "apd"
|
||||
const val PACKAGE_CONFIG_FILE = APATCH_FOLDER + "package_config"
|
||||
const val SU_PATH_FILE = APATCH_FOLDER + "su_path"
|
||||
const val SAFEMODE_FILE = "/dev/.safemode"
|
||||
private const val NEED_REBOOT_FILE = "/dev/.need_reboot"
|
||||
const val GLOBAL_NAMESPACE_FILE = "/data/adb/.global_namespace_enable"
|
||||
const val LITE_MODE_FILE = "/data/adb/.litemode_enable"
|
||||
const val FORCE_OVERLAYFS_FILE = "/data/adb/.overlayfs_enable"
|
||||
const val KPMS_DIR = APATCH_FOLDER + "kpms/"
|
||||
|
||||
@Deprecated("Use 'apd -V'")
|
||||
const val APATCH_VERSION_PATH = APATCH_FOLDER + "version"
|
||||
private const val MAGISKPOLICY_BIN_PATH = APATCH_BIN_FOLDER + "magiskpolicy"
|
||||
private const val BUSYBOX_BIN_PATH = APATCH_BIN_FOLDER + "busybox"
|
||||
private const val RESETPROP_BIN_PATH = APATCH_BIN_FOLDER + "resetprop"
|
||||
private const val MAGISKBOOT_BIN_PATH = APATCH_BIN_FOLDER + "magiskboot"
|
||||
const val DEFAULT_SCONTEXT = "u:r:untrusted_app:s0"
|
||||
const val MAGISK_SCONTEXT = "u:r:magisk:s0"
|
||||
|
||||
private const val DEFAULT_SU_PATH = "/system/bin/kp"
|
||||
private const val LEGACY_SU_PATH = "/system/bin/su"
|
||||
|
||||
const val SP_NAME = "config"
|
||||
private const val SHOW_BACKUP_WARN = "show_backup_warning"
|
||||
lateinit var sharedPreferences: SharedPreferences
|
||||
|
||||
private val logCallback: CallbackList<String?> = object : CallbackList<String?>() {
|
||||
override fun onAddElement(s: String?) {
|
||||
Log.d(TAG, s.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private val _kpStateLiveData = MutableLiveData(State.UNKNOWN_STATE)
|
||||
val kpStateLiveData: LiveData<State> = _kpStateLiveData
|
||||
|
||||
private val _apStateLiveData = MutableLiveData(State.UNKNOWN_STATE)
|
||||
val apStateLiveData: LiveData<State> = _apStateLiveData
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun uninstallApatch() {
|
||||
if (_apStateLiveData.value != State.ANDROIDPATCH_INSTALLED) return
|
||||
_apStateLiveData.value = State.ANDROIDPATCH_UNINSTALLING
|
||||
|
||||
Natives.resetSuPath(DEFAULT_SU_PATH)
|
||||
|
||||
val cmds = arrayOf(
|
||||
"rm -f $APD_PATH",
|
||||
"rm -f $KPATCH_PATH",
|
||||
"rm -rf $APATCH_BIN_FOLDER",
|
||||
"rm -rf $APATCH_LOG_FOLDER",
|
||||
"rm -rf $APATCH_VERSION_PATH",
|
||||
)
|
||||
|
||||
val shell = getRootShell()
|
||||
shell.newJob().add(*cmds).to(logCallback, logCallback).exec()
|
||||
|
||||
Log.d(TAG, "APatch uninstalled...")
|
||||
if (_kpStateLiveData.value == State.UNKNOWN_STATE) {
|
||||
_apStateLiveData.postValue(State.UNKNOWN_STATE)
|
||||
} else {
|
||||
_apStateLiveData.postValue(State.ANDROIDPATCH_NOT_INSTALLED)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun installApatch() {
|
||||
val state = _apStateLiveData.value
|
||||
if (state != State.ANDROIDPATCH_NOT_INSTALLED && state != State.ANDROIDPATCH_NEED_UPDATE) {
|
||||
return
|
||||
}
|
||||
_apStateLiveData.value = State.ANDROIDPATCH_INSTALLING
|
||||
val nativeDir = apApp.applicationInfo.nativeLibraryDir
|
||||
|
||||
Natives.resetSuPath(LEGACY_SU_PATH)
|
||||
|
||||
val cmds = arrayOf(
|
||||
"mkdir -p $APATCH_BIN_FOLDER",
|
||||
"mkdir -p $APATCH_LOG_FOLDER",
|
||||
|
||||
"cp -f ${nativeDir}/libapd.so $APD_PATH",
|
||||
"chmod +x $APD_PATH",
|
||||
"ln -s $APD_PATH $APD_LINK_PATH",
|
||||
"restorecon $APD_PATH",
|
||||
|
||||
"cp -f ${nativeDir}/libmagiskpolicy.so $MAGISKPOLICY_BIN_PATH",
|
||||
"chmod +x $MAGISKPOLICY_BIN_PATH",
|
||||
"cp -f ${nativeDir}/libresetprop.so $RESETPROP_BIN_PATH",
|
||||
"chmod +x $RESETPROP_BIN_PATH",
|
||||
"cp -f ${nativeDir}/libbusybox.so $BUSYBOX_BIN_PATH",
|
||||
"chmod +x $BUSYBOX_BIN_PATH",
|
||||
"cp -f ${nativeDir}/libmagiskboot.so $MAGISKBOOT_BIN_PATH",
|
||||
"chmod +x $MAGISKBOOT_BIN_PATH",
|
||||
|
||||
|
||||
"touch $PACKAGE_CONFIG_FILE",
|
||||
"touch $SU_PATH_FILE",
|
||||
"[ -s $SU_PATH_FILE ] || echo $LEGACY_SU_PATH > $SU_PATH_FILE",
|
||||
"echo ${Version.getManagerVersion().second} > $APATCH_VERSION_PATH",
|
||||
"restorecon -R $APATCH_FOLDER",
|
||||
|
||||
"${nativeDir}/libmagiskpolicy.so --magisk --live",
|
||||
)
|
||||
|
||||
val shell = getRootShell()
|
||||
shell.newJob().add(*cmds).to(logCallback, logCallback).exec()
|
||||
|
||||
// clear shell cache
|
||||
APatchCli.refresh()
|
||||
|
||||
Log.d(TAG, "APatch installed...")
|
||||
_apStateLiveData.postValue(State.ANDROIDPATCH_INSTALLED)
|
||||
}
|
||||
|
||||
fun markNeedReboot() {
|
||||
val result = rootShellForResult("touch $NEED_REBOOT_FILE")
|
||||
_kpStateLiveData.postValue(State.KERNELPATCH_NEED_REBOOT)
|
||||
Log.d(TAG, "mark reboot ${result.code}")
|
||||
}
|
||||
|
||||
|
||||
var superKey: String = ""
|
||||
set(value) {
|
||||
field = value
|
||||
val ready = Natives.nativeReady(value)
|
||||
_kpStateLiveData.value =
|
||||
if (ready) State.KERNELPATCH_INSTALLED else State.UNKNOWN_STATE
|
||||
_apStateLiveData.value =
|
||||
if (ready) State.ANDROIDPATCH_NOT_INSTALLED else State.UNKNOWN_STATE
|
||||
Log.d(TAG, "state: " + _kpStateLiveData.value)
|
||||
if (!ready) return
|
||||
|
||||
APatchKeyHelper.writeSPSuperKey(value)
|
||||
|
||||
thread {
|
||||
val rc = Natives.su(0, null)
|
||||
if (!rc) {
|
||||
Log.e(TAG, "Native.su failed")
|
||||
return@thread
|
||||
}
|
||||
|
||||
// KernelPatch version
|
||||
//val buildV = Version.buildKPVUInt()
|
||||
//val installedV = Version.installedKPVUInt()
|
||||
//use build time to check update
|
||||
val buildV = Version.getKpImg()
|
||||
val installedV = Version.installedKPTime()
|
||||
|
||||
|
||||
Log.d(TAG, "kp installed version: ${installedV}, build version: $buildV")
|
||||
|
||||
// use != instead of > to enable downgrade,
|
||||
if (buildV != installedV) {
|
||||
_kpStateLiveData.postValue(State.KERNELPATCH_NEED_UPDATE)
|
||||
}
|
||||
Log.d(TAG, "kp state: " + _kpStateLiveData.value)
|
||||
|
||||
if (File(NEED_REBOOT_FILE).exists()) {
|
||||
_kpStateLiveData.postValue(State.KERNELPATCH_NEED_REBOOT)
|
||||
}
|
||||
Log.d(TAG, "kp state: " + _kpStateLiveData.value)
|
||||
|
||||
// AndroidPatch version
|
||||
val mgv = Version.getManagerVersion().second
|
||||
val installedApdVInt = Version.installedApdVUInt()
|
||||
Log.d(TAG, "manager version: $mgv, installed apd version: $installedApdVInt")
|
||||
|
||||
if (Version.installedApdVInt > 0) {
|
||||
_apStateLiveData.postValue(State.ANDROIDPATCH_INSTALLED)
|
||||
}
|
||||
|
||||
if (Version.installedApdVInt > 0 && mgv.toInt() != Version.installedApdVInt) {
|
||||
_apStateLiveData.postValue(State.ANDROIDPATCH_NEED_UPDATE)
|
||||
// su path
|
||||
val suPathFile = File(SU_PATH_FILE)
|
||||
if (suPathFile.exists()) {
|
||||
val suPath = suPathFile.readLines()[0].trim()
|
||||
if (Natives.suPath() != suPath) {
|
||||
Log.d(TAG, "su path: $suPath")
|
||||
Natives.resetSuPath(suPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "ap state: " + _apStateLiveData.value)
|
||||
|
||||
return@thread
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
apApp = this
|
||||
|
||||
val isArm64 = Build.SUPPORTED_ABIS.any { it == "arm64-v8a" }
|
||||
if (!isArm64) {
|
||||
Toast.makeText(applicationContext, "Unsupported architecture!", Toast.LENGTH_LONG)
|
||||
.show()
|
||||
Thread.sleep(5000)
|
||||
exitProcess(0)
|
||||
}
|
||||
|
||||
if (!BuildConfig.DEBUG && !verifyAppSignature("1x2twMoHvfWUODv7KkRRNKBzOfEqJwRKGzJpgaz18xk=")) {
|
||||
while (true) {
|
||||
val intent = Intent(Intent.ACTION_DELETE)
|
||||
intent.data = "package:$packageName".toUri()
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
|
||||
startActivity(intent)
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: We can't totally protect superkey from be stolen by root or LSPosed-like injection tools in user space, the only way is don't use superkey,
|
||||
// TODO: 1. make me root by kernel
|
||||
// TODO: 2. remove all usage of superkey
|
||||
sharedPreferences = getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
|
||||
APatchKeyHelper.setSharedPreferences(sharedPreferences)
|
||||
superKey = APatchKeyHelper.readSPSuperKey()
|
||||
|
||||
okhttpClient =
|
||||
OkHttpClient.Builder().cache(Cache(File(cacheDir, "okhttp"), 10 * 1024 * 1024))
|
||||
.addInterceptor { block ->
|
||||
block.proceed(
|
||||
block.request().newBuilder()
|
||||
.header("User-Agent", "APatch/${BuildConfig.VERSION_CODE}")
|
||||
.header("Accept-Language", Locale.getDefault().toLanguageTag()).build()
|
||||
)
|
||||
}.build()
|
||||
}
|
||||
|
||||
fun getBackupWarningState(): Boolean {
|
||||
return sharedPreferences.getBoolean(SHOW_BACKUP_WARN, true)
|
||||
}
|
||||
|
||||
fun updateBackupWarningState(state: Boolean) {
|
||||
sharedPreferences.edit { putBoolean(SHOW_BACKUP_WARN, state) }
|
||||
}
|
||||
|
||||
override fun uncaughtException(t: Thread, e: Throwable) {
|
||||
val exceptionMessage = Log.getStackTraceString(e)
|
||||
val threadName = t.name
|
||||
Log.e(TAG, "Error on thread $threadName:\n $exceptionMessage")
|
||||
val intent = Intent(this, CrashHandleActivity::class.java).apply {
|
||||
putExtra("exception_message", exceptionMessage)
|
||||
putExtra("thread", threadName)
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
startActivity(intent)
|
||||
exitProcess(10)
|
||||
}
|
||||
}
|
||||
155
app/src/main/java/me/bmax/apatch/Natives.kt
Normal file
155
app/src/main/java/me/bmax/apatch/Natives.kt
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
package me.bmax.apatch
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.Keep
|
||||
import androidx.compose.runtime.Immutable
|
||||
import dalvik.annotation.optimization.FastNative
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
object Natives {
|
||||
init {
|
||||
System.loadLibrary("apjni")
|
||||
}
|
||||
|
||||
@Immutable
|
||||
@Parcelize
|
||||
@Keep
|
||||
data class Profile(
|
||||
var uid: Int = 0,
|
||||
var toUid: Int = 0,
|
||||
var scontext: String = APApplication.DEFAULT_SCONTEXT,
|
||||
) : Parcelable
|
||||
|
||||
@Keep
|
||||
class KPMCtlRes {
|
||||
var rc: Long = 0
|
||||
var outMsg: String? = null
|
||||
|
||||
constructor()
|
||||
|
||||
constructor(rc: Long, outMsg: String?) {
|
||||
this.rc = rc
|
||||
this.outMsg = outMsg
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@FastNative
|
||||
private external fun nativeSu(superKey: String, toUid: Int, scontext: String?): Long
|
||||
|
||||
fun su(toUid: Int, scontext: String?): Boolean {
|
||||
return nativeSu(APApplication.superKey, toUid, scontext) == 0L
|
||||
}
|
||||
|
||||
fun su(): Boolean {
|
||||
return su(0, "")
|
||||
}
|
||||
|
||||
@FastNative
|
||||
external fun nativeReady(superKey: String): Boolean
|
||||
|
||||
@FastNative
|
||||
private external fun nativeSuPath(superKey: String): String
|
||||
|
||||
fun suPath(): String {
|
||||
return nativeSuPath(APApplication.superKey)
|
||||
}
|
||||
|
||||
@FastNative
|
||||
private external fun nativeSuUids(superKey: String): IntArray
|
||||
|
||||
fun suUids(): IntArray {
|
||||
return nativeSuUids(APApplication.superKey)
|
||||
}
|
||||
|
||||
@FastNative
|
||||
private external fun nativeKernelPatchVersion(superKey: String): Long
|
||||
fun kernelPatchVersion(): Long {
|
||||
return nativeKernelPatchVersion(APApplication.superKey)
|
||||
}
|
||||
|
||||
@FastNative
|
||||
private external fun nativeKernelPatchBuildTime(superKey: String): String
|
||||
fun kernelPatchBuildTime(): String {
|
||||
return nativeKernelPatchBuildTime(APApplication.superKey)
|
||||
}
|
||||
|
||||
private external fun nativeLoadKernelPatchModule(
|
||||
superKey: String, modulePath: String, args: String
|
||||
): Long
|
||||
|
||||
fun loadKernelPatchModule(modulePath: String, args: String): Long {
|
||||
return nativeLoadKernelPatchModule(APApplication.superKey, modulePath, args)
|
||||
}
|
||||
|
||||
private external fun nativeUnloadKernelPatchModule(superKey: String, moduleName: String): Long
|
||||
fun unloadKernelPatchModule(moduleName: String): Long {
|
||||
return nativeUnloadKernelPatchModule(APApplication.superKey, moduleName)
|
||||
}
|
||||
|
||||
@FastNative
|
||||
private external fun nativeKernelPatchModuleNum(superKey: String): Long
|
||||
|
||||
fun kernelPatchModuleNum(): Long {
|
||||
return nativeKernelPatchModuleNum(APApplication.superKey)
|
||||
}
|
||||
|
||||
@FastNative
|
||||
private external fun nativeKernelPatchModuleList(superKey: String): String
|
||||
fun kernelPatchModuleList(): String {
|
||||
return nativeKernelPatchModuleList(APApplication.superKey)
|
||||
}
|
||||
|
||||
@FastNative
|
||||
private external fun nativeKernelPatchModuleInfo(superKey: String, moduleName: String): String
|
||||
fun kernelPatchModuleInfo(moduleName: String): String {
|
||||
return nativeKernelPatchModuleInfo(APApplication.superKey, moduleName)
|
||||
}
|
||||
|
||||
private external fun nativeControlKernelPatchModule(
|
||||
superKey: String, modName: String, jctlargs: String
|
||||
): KPMCtlRes
|
||||
|
||||
fun kernelPatchModuleControl(moduleName: String, controlArg: String): KPMCtlRes {
|
||||
return nativeControlKernelPatchModule(APApplication.superKey, moduleName, controlArg)
|
||||
}
|
||||
|
||||
@FastNative
|
||||
private external fun nativeGrantSu(
|
||||
superKey: String, uid: Int, toUid: Int, scontext: String?
|
||||
): Long
|
||||
|
||||
fun grantSu(uid: Int, toUid: Int, scontext: String?): Long {
|
||||
return nativeGrantSu(APApplication.superKey, uid, toUid, scontext)
|
||||
}
|
||||
|
||||
@FastNative
|
||||
private external fun nativeRevokeSu(superKey: String, uid: Int): Long
|
||||
fun revokeSu(uid: Int): Long {
|
||||
return nativeRevokeSu(APApplication.superKey, uid)
|
||||
}
|
||||
|
||||
@FastNative
|
||||
private external fun nativeSetUidExclude(superKey: String, uid: Int, exclude: Int): Int
|
||||
fun setUidExclude(uid: Int, exclude: Int): Int {
|
||||
return nativeSetUidExclude(APApplication.superKey, uid, exclude)
|
||||
}
|
||||
|
||||
@FastNative
|
||||
private external fun nativeGetUidExclude(superKey: String, uid: Int): Int
|
||||
fun isUidExcluded(uid: Int): Int {
|
||||
return nativeGetUidExclude(APApplication.superKey, uid)
|
||||
}
|
||||
|
||||
@FastNative
|
||||
private external fun nativeSuProfile(superKey: String, uid: Int): Profile
|
||||
fun suProfile(uid: Int): Profile {
|
||||
return nativeSuProfile(APApplication.superKey, uid)
|
||||
}
|
||||
|
||||
@FastNative
|
||||
private external fun nativeResetSuPath(superKey: String, path: String): Boolean
|
||||
fun resetSuPath(path: String): Boolean {
|
||||
return nativeResetSuPath(APApplication.superKey, path)
|
||||
}
|
||||
}
|
||||
72
app/src/main/java/me/bmax/apatch/services/RootServices.java
Normal file
72
app/src/main/java/me/bmax/apatch/services/RootServices.java
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
package me.bmax.apatch.services;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.IBinder;
|
||||
import android.os.UserHandle;
|
||||
import android.os.UserManager;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.topjohnwu.superuser.ipc.RootService;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import me.bmax.apatch.IAPRootService;
|
||||
import rikka.parcelablelist.ParcelableListSlice;
|
||||
|
||||
public class RootServices extends RootService {
|
||||
private static final String TAG = "RootServices";
|
||||
|
||||
@Override
|
||||
public IBinder onBind(@NonNull Intent intent) {
|
||||
return new Stub();
|
||||
}
|
||||
|
||||
List<Integer> getUserIds() {
|
||||
List<Integer> result = new ArrayList<>();
|
||||
UserManager um = (UserManager) getSystemService(Context.USER_SERVICE);
|
||||
List<UserHandle> userProfiles = um.getUserProfiles();
|
||||
for (UserHandle userProfile : userProfiles) {
|
||||
int userId = userProfile.hashCode();
|
||||
result.add(userProfile.hashCode());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ArrayList<PackageInfo> getInstalledPackagesAll(int flags) {
|
||||
ArrayList<PackageInfo> packages = new ArrayList<>();
|
||||
for (Integer userId : getUserIds()) {
|
||||
Log.i(TAG, "getInstalledPackagesAll: " + userId);
|
||||
packages.addAll(getInstalledPackagesAsUser(flags, userId));
|
||||
}
|
||||
return packages;
|
||||
}
|
||||
|
||||
List<PackageInfo> getInstalledPackagesAsUser(int flags, int userId) {
|
||||
try {
|
||||
PackageManager pm = getPackageManager();
|
||||
Method getInstalledPackagesAsUser = pm.getClass().getDeclaredMethod("getInstalledPackagesAsUser", int.class, int.class);
|
||||
return (List<PackageInfo>) getInstalledPackagesAsUser.invoke(pm, flags, userId);
|
||||
} catch (Throwable e) {
|
||||
Log.e(TAG, "err", e);
|
||||
}
|
||||
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
class Stub extends IAPRootService.Stub {
|
||||
@Override
|
||||
public ParcelableListSlice<PackageInfo> getPackages(int flags) {
|
||||
List<PackageInfo> list = getInstalledPackagesAll(flags);
|
||||
Log.i(TAG, "getPackages: " + list.size());
|
||||
return new ParcelableListSlice<>(list);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
151
app/src/main/java/me/bmax/apatch/ui/CrashHandleActivity.kt
Normal file
151
app/src/main/java/me/bmax/apatch/ui/CrashHandleActivity.kt
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
package me.bmax.apatch.ui
|
||||
|
||||
import android.content.ClipData
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.ContentCopy
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LargeTopAppBar
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.ClipEntry
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.launch
|
||||
import me.bmax.apatch.BuildConfig
|
||||
import me.bmax.apatch.R
|
||||
import me.bmax.apatch.ui.theme.APatchTheme
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
|
||||
class CrashHandleActivity : ComponentActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
enableEdgeToEdge()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
window.isNavigationBarContrastEnforced = false
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val appName = getString(R.string.app_name)
|
||||
val versionName = BuildConfig.VERSION_NAME
|
||||
val versionCode = BuildConfig.VERSION_CODE
|
||||
|
||||
val deviceBrand = Build.BRAND
|
||||
val deviceModel = Build.MODEL
|
||||
val sdkLevel = Build.VERSION.SDK_INT
|
||||
val currentDateTime = Calendar.getInstance().time
|
||||
val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
|
||||
val formattedDateTime = formatter.format(currentDateTime)
|
||||
|
||||
val exceptionMessage = intent.getStringExtra("exception_message")
|
||||
val threadName = intent.getStringExtra("thread")
|
||||
|
||||
val message = buildString {
|
||||
append(appName).append(" version: ").append(versionName).append(" ($versionCode)")
|
||||
.append("\n\n")
|
||||
append("Brand: ").append(deviceBrand).append("\n")
|
||||
append("Model: ").append(deviceModel).append("\n")
|
||||
append("SDK Level: ").append(sdkLevel).append("\n")
|
||||
append("Time: ").append(formattedDateTime).append("\n\n")
|
||||
append("Thread: ").append(threadName).append("\n")
|
||||
append("Crash Info: \n").append(exceptionMessage)
|
||||
}
|
||||
|
||||
setContent {
|
||||
APatchTheme {
|
||||
CrashHandleScreen(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun CrashHandleScreen(
|
||||
message: String
|
||||
) {
|
||||
val scrollBehavior =
|
||||
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
|
||||
val clipboard = LocalClipboard.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Scaffold(contentWindowInsets = WindowInsets.safeDrawing, topBar = {
|
||||
LargeTopAppBar(
|
||||
title = { Text(text = stringResource(R.string.crash_handle_title)) },
|
||||
scrollBehavior = scrollBehavior,
|
||||
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
|
||||
)
|
||||
}, floatingActionButton = {
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
clipboard.setClipEntry(
|
||||
ClipEntry(ClipData.newPlainText("CrashLog", message)),
|
||||
)
|
||||
}
|
||||
}, text = { Text(text = stringResource(R.string.crash_handle_copy)) }, icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.ContentCopy, contentDescription = null
|
||||
)
|
||||
}, modifier = Modifier.windowInsetsPadding(
|
||||
WindowInsets.safeDrawing.only(WindowInsetsSides.End)
|
||||
)
|
||||
)
|
||||
}) {
|
||||
SelectionContainer(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(it)
|
||||
.padding(
|
||||
start = 16.dp, top = 16.dp, end = 16.dp, bottom = 16.dp + 56.dp + 16.dp
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = message, style = TextStyle(
|
||||
fontFamily = FontFamily.Monospace, fontSize = 11.sp
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun CrashHandleScreenPreview() {
|
||||
APatchTheme {
|
||||
CrashHandleScreen("Crash log here")
|
||||
}
|
||||
}
|
||||
209
app/src/main/java/me/bmax/apatch/ui/MainActivity.kt
Normal file
209
app/src/main/java/me/bmax/apatch/ui/MainActivity.kt
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
package me.bmax.apatch.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.scaleOut
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import coil.Coil
|
||||
import coil.ImageLoader
|
||||
import com.ramcosta.composedestinations.DestinationsNavHost
|
||||
import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle
|
||||
import com.ramcosta.composedestinations.generated.NavGraphs
|
||||
import com.ramcosta.composedestinations.rememberNavHostEngine
|
||||
import com.ramcosta.composedestinations.utils.isRouteOnBackStackAsState
|
||||
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
|
||||
import me.bmax.apatch.APApplication
|
||||
import me.bmax.apatch.ui.screen.BottomBarDestination
|
||||
import me.bmax.apatch.ui.theme.APatchTheme
|
||||
import me.bmax.apatch.util.ui.LocalSnackbarHost
|
||||
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
|
||||
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private var isLoading = true
|
||||
|
||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
installSplashScreen().setKeepOnScreenCondition { isLoading }
|
||||
|
||||
enableEdgeToEdge()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
window.isNavigationBarContrastEnforced = false
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContent {
|
||||
APatchTheme {
|
||||
val navController = rememberNavController()
|
||||
val snackBarHostState = remember { SnackbarHostState() }
|
||||
val bottomBarRoutes = remember {
|
||||
BottomBarDestination.entries.map { it.direction.route }.toSet()
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
bottomBar = { BottomBar(navController) }
|
||||
) { _ ->
|
||||
CompositionLocalProvider(
|
||||
LocalSnackbarHost provides snackBarHostState,
|
||||
) {
|
||||
DestinationsNavHost(
|
||||
modifier = Modifier.padding(bottom = 80.dp),
|
||||
navGraph = NavGraphs.root,
|
||||
navController = navController,
|
||||
engine = rememberNavHostEngine(navHostContentAlignment = Alignment.TopCenter),
|
||||
defaultTransitions = object : NavHostAnimatedDestinationStyle() {
|
||||
override val enterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition =
|
||||
{
|
||||
// If the target is a detail page (not a bottom navigation page), slide in from the right
|
||||
if (targetState.destination.route !in bottomBarRoutes) {
|
||||
slideInHorizontally(initialOffsetX = { it })
|
||||
} else {
|
||||
// Otherwise (switching between bottom navigation pages), use fade in
|
||||
fadeIn(animationSpec = tween(340))
|
||||
}
|
||||
}
|
||||
|
||||
override val exitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition =
|
||||
{
|
||||
// If navigating from the home page (bottom navigation page) to a detail page, slide out to the left
|
||||
if (initialState.destination.route in bottomBarRoutes && targetState.destination.route !in bottomBarRoutes) {
|
||||
slideOutHorizontally(targetOffsetX = { -it / 4 }) + fadeOut()
|
||||
} else {
|
||||
// Otherwise (switching between bottom navigation pages), use fade out
|
||||
fadeOut(animationSpec = tween(340))
|
||||
}
|
||||
}
|
||||
|
||||
override val popEnterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition =
|
||||
{
|
||||
// If returning to the home page (bottom navigation page), slide in from the left
|
||||
if (targetState.destination.route in bottomBarRoutes) {
|
||||
slideInHorizontally(initialOffsetX = { -it / 4 }) + fadeIn()
|
||||
} else {
|
||||
// Otherwise (e.g., returning between multiple detail pages), use default fade in
|
||||
fadeIn(animationSpec = tween(340))
|
||||
}
|
||||
}
|
||||
|
||||
override val popExitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition =
|
||||
{
|
||||
// If returning from a detail page (not a bottom navigation page), scale down and fade out
|
||||
if (initialState.destination.route !in bottomBarRoutes) {
|
||||
scaleOut(targetScale = 0.9f) + fadeOut()
|
||||
} else {
|
||||
// Otherwise, use default fade out
|
||||
fadeOut(animationSpec = tween(340))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize Coil
|
||||
val iconSize = resources.getDimensionPixelSize(android.R.dimen.app_icon_size)
|
||||
Coil.setImageLoader(
|
||||
ImageLoader.Builder(this)
|
||||
.components {
|
||||
add(AppIconKeyer())
|
||||
add(AppIconFetcher.Factory(iconSize, false, this@MainActivity))
|
||||
}
|
||||
.build()
|
||||
)
|
||||
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BottomBar(navController: NavHostController) {
|
||||
val state by APApplication.apStateLiveData.observeAsState(APApplication.State.UNKNOWN_STATE)
|
||||
val navigator = navController.rememberDestinationsNavigator()
|
||||
|
||||
Crossfade(
|
||||
targetState = state,
|
||||
label = "BottomBarStateCrossfade"
|
||||
) { state ->
|
||||
val kPatchReady = state != APApplication.State.UNKNOWN_STATE
|
||||
val aPatchReady = state == APApplication.State.ANDROIDPATCH_INSTALLED
|
||||
|
||||
NavigationBar(tonalElevation = 8.dp) {
|
||||
BottomBarDestination.entries.forEach { destination ->
|
||||
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
|
||||
|
||||
val hideDestination = (destination.kPatchRequired && !kPatchReady) || (destination.aPatchRequired && !aPatchReady)
|
||||
if (hideDestination) return@forEach
|
||||
|
||||
NavigationBarItem(
|
||||
selected = isCurrentDestOnBackStack,
|
||||
onClick = {
|
||||
if (isCurrentDestOnBackStack) {
|
||||
navigator.popBackStack(destination.direction, false)
|
||||
}
|
||||
navigator.navigate(destination.direction) {
|
||||
popUpTo(NavGraphs.root) {
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
if (isCurrentDestOnBackStack) {
|
||||
Icon(destination.iconSelected, stringResource(destination.label))
|
||||
} else {
|
||||
Icon(destination.iconNotSelected, stringResource(destination.label))
|
||||
}
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(destination.label),
|
||||
overflow = TextOverflow.Visible,
|
||||
maxLines = 1,
|
||||
softWrap = false
|
||||
)
|
||||
},
|
||||
alwaysShowLabel = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
89
app/src/main/java/me/bmax/apatch/ui/WebUIActivity.kt
Normal file
89
app/src/main/java/me/bmax/apatch/ui/WebUIActivity.kt
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
package me.bmax.apatch.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.ActivityManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.ViewGroup.MarginLayoutParams
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.webkit.WebViewAssetLoader
|
||||
import me.bmax.apatch.APApplication
|
||||
import me.bmax.apatch.ui.webui.SuFilePathHandler
|
||||
import me.bmax.apatch.ui.webui.WebViewInterface
|
||||
import java.io.File
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
class WebUIActivity : ComponentActivity() {
|
||||
private lateinit var webViewInterface: WebViewInterface
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
enableEdgeToEdge()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
window.isNavigationBarContrastEnforced = false
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val moduleId = intent.getStringExtra("id")!!
|
||||
val name = intent.getStringExtra("name")!!
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
@Suppress("DEPRECATION")
|
||||
setTaskDescription(ActivityManager.TaskDescription("APatch - $name"))
|
||||
} else {
|
||||
val taskDescription = ActivityManager.TaskDescription.Builder().setLabel("APatch - $name").build()
|
||||
setTaskDescription(taskDescription)
|
||||
}
|
||||
|
||||
val prefs = APApplication.sharedPreferences
|
||||
WebView.setWebContentsDebuggingEnabled(prefs.getBoolean("enable_web_debugging", false))
|
||||
|
||||
val webRoot = File("/data/adb/modules/${moduleId}/webroot")
|
||||
val webViewAssetLoader = WebViewAssetLoader.Builder()
|
||||
.setDomain("mui.kernelsu.org")
|
||||
.addPathHandler(
|
||||
"/",
|
||||
SuFilePathHandler(this, webRoot)
|
||||
)
|
||||
.build()
|
||||
|
||||
val webViewClient = object : WebViewClient() {
|
||||
override fun shouldInterceptRequest(
|
||||
view: WebView,
|
||||
request: WebResourceRequest
|
||||
): WebResourceResponse? {
|
||||
return webViewAssetLoader.shouldInterceptRequest(request.url)
|
||||
}
|
||||
}
|
||||
|
||||
val webView = WebView(this).apply {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets ->
|
||||
val inset = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
view.updateLayoutParams<MarginLayoutParams> {
|
||||
leftMargin = inset.left
|
||||
rightMargin = inset.right
|
||||
topMargin = inset.top
|
||||
bottomMargin = inset.bottom
|
||||
}
|
||||
return@setOnApplyWindowInsetsListener insets
|
||||
}
|
||||
settings.javaScriptEnabled = true
|
||||
settings.domStorageEnabled = true
|
||||
settings.allowFileAccess = false
|
||||
webViewInterface = WebViewInterface(this@WebUIActivity, this)
|
||||
addJavascriptInterface(webViewInterface, "ksu")
|
||||
setWebViewClient(webViewClient)
|
||||
loadUrl("https://mui.kernelsu.org/index.html")
|
||||
}
|
||||
|
||||
setContentView(webView)
|
||||
}
|
||||
}
|
||||
533
app/src/main/java/me/bmax/apatch/ui/component/Dialog.kt
Normal file
533
app/src/main/java/me/bmax/apatch/ui/component/Dialog.kt
Normal file
|
|
@ -0,0 +1,533 @@
|
|||
package me.bmax.apatch.ui.component
|
||||
|
||||
import android.graphics.text.LineBreaker
|
||||
import android.os.Build
|
||||
import android.os.Parcelable
|
||||
import android.text.Layout
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.util.Log
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.AlertDialogDefaults
|
||||
import androidx.compose.material3.BasicAlertDialog
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.compose.ui.window.DialogWindowProvider
|
||||
import androidx.compose.ui.window.SecureFlagPolicy
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.utils.NoCopySpannableFactory
|
||||
import kotlinx.coroutines.CancellableContinuation
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.ReceiveChannel
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlinx.coroutines.flow.consumeAsFlow
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import me.bmax.apatch.util.ui.APDialogBlurBehindUtils.Companion.setupWindowBlurListener
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
private const val TAG = "DialogComponent"
|
||||
|
||||
interface ConfirmDialogVisuals : Parcelable {
|
||||
val title: String
|
||||
val content: String
|
||||
val isMarkdown: Boolean
|
||||
val confirm: String?
|
||||
val dismiss: String?
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
private data class ConfirmDialogVisualsImpl(
|
||||
override val title: String,
|
||||
override val content: String,
|
||||
override val isMarkdown: Boolean,
|
||||
override val confirm: String?,
|
||||
override val dismiss: String?,
|
||||
) : ConfirmDialogVisuals {
|
||||
companion object {
|
||||
val Empty: ConfirmDialogVisuals = ConfirmDialogVisualsImpl("", "", false, null, null)
|
||||
}
|
||||
}
|
||||
|
||||
interface DialogHandle {
|
||||
val isShown: Boolean
|
||||
val dialogType: String
|
||||
fun show()
|
||||
fun hide()
|
||||
}
|
||||
|
||||
interface LoadingDialogHandle : DialogHandle {
|
||||
suspend fun <R> withLoading(block: suspend () -> R): R
|
||||
fun showLoading()
|
||||
}
|
||||
|
||||
sealed interface ConfirmResult {
|
||||
data object Confirmed : ConfirmResult
|
||||
data object Canceled : ConfirmResult
|
||||
}
|
||||
|
||||
interface ConfirmDialogHandle : DialogHandle {
|
||||
val visuals: ConfirmDialogVisuals
|
||||
|
||||
fun showConfirm(
|
||||
title: String,
|
||||
content: String,
|
||||
markdown: Boolean = false,
|
||||
confirm: String? = null,
|
||||
dismiss: String? = null
|
||||
)
|
||||
|
||||
suspend fun awaitConfirm(
|
||||
title: String,
|
||||
content: String,
|
||||
markdown: Boolean = false,
|
||||
confirm: String? = null,
|
||||
dismiss: String? = null
|
||||
): ConfirmResult
|
||||
}
|
||||
|
||||
private abstract class DialogHandleBase(
|
||||
protected val visible: MutableState<Boolean>,
|
||||
protected val coroutineScope: CoroutineScope
|
||||
) : DialogHandle {
|
||||
override val isShown: Boolean
|
||||
get() = visible.value
|
||||
|
||||
override fun show() {
|
||||
coroutineScope.launch {
|
||||
visible.value = true
|
||||
}
|
||||
}
|
||||
|
||||
final override fun hide() {
|
||||
coroutineScope.launch {
|
||||
visible.value = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return dialogType
|
||||
}
|
||||
}
|
||||
|
||||
private class LoadingDialogHandleImpl(
|
||||
visible: MutableState<Boolean>,
|
||||
coroutineScope: CoroutineScope
|
||||
) : LoadingDialogHandle, DialogHandleBase(visible, coroutineScope) {
|
||||
override suspend fun <R> withLoading(block: suspend () -> R): R {
|
||||
return coroutineScope.async {
|
||||
try {
|
||||
visible.value = true
|
||||
block()
|
||||
} finally {
|
||||
visible.value = false
|
||||
}
|
||||
}.await()
|
||||
}
|
||||
|
||||
override fun showLoading() {
|
||||
show()
|
||||
}
|
||||
|
||||
override val dialogType: String get() = "LoadingDialog"
|
||||
}
|
||||
|
||||
typealias NullableCallback = (() -> Unit)?
|
||||
|
||||
interface ConfirmCallback {
|
||||
|
||||
val onConfirm: NullableCallback
|
||||
|
||||
val onDismiss: NullableCallback
|
||||
|
||||
val isEmpty: Boolean get() = onConfirm == null && onDismiss == null
|
||||
|
||||
companion object {
|
||||
operator fun invoke(
|
||||
onConfirmProvider: () -> NullableCallback,
|
||||
onDismissProvider: () -> NullableCallback
|
||||
): ConfirmCallback {
|
||||
return object : ConfirmCallback {
|
||||
override val onConfirm: NullableCallback
|
||||
get() = onConfirmProvider()
|
||||
override val onDismiss: NullableCallback
|
||||
get() = onDismissProvider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ConfirmDialogHandleImpl(
|
||||
visible: MutableState<Boolean>,
|
||||
coroutineScope: CoroutineScope,
|
||||
callback: ConfirmCallback,
|
||||
override var visuals: ConfirmDialogVisuals = ConfirmDialogVisualsImpl.Empty,
|
||||
private val resultFlow: ReceiveChannel<ConfirmResult>
|
||||
) : ConfirmDialogHandle, DialogHandleBase(visible, coroutineScope) {
|
||||
private class ResultCollector(
|
||||
private val callback: ConfirmCallback
|
||||
) : FlowCollector<ConfirmResult> {
|
||||
fun handleResult(result: ConfirmResult) {
|
||||
Log.d(TAG, "handleResult: ${result.javaClass.simpleName}")
|
||||
when (result) {
|
||||
ConfirmResult.Confirmed -> onConfirm()
|
||||
ConfirmResult.Canceled -> onDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
fun onConfirm() {
|
||||
callback.onConfirm?.invoke()
|
||||
}
|
||||
|
||||
fun onDismiss() {
|
||||
callback.onDismiss?.invoke()
|
||||
}
|
||||
|
||||
override suspend fun emit(value: ConfirmResult) {
|
||||
handleResult(value)
|
||||
}
|
||||
}
|
||||
|
||||
private val resultCollector = ResultCollector(callback)
|
||||
|
||||
private var awaitContinuation: CancellableContinuation<ConfirmResult>? = null
|
||||
|
||||
private val isCallbackEmpty = callback.isEmpty
|
||||
|
||||
init {
|
||||
coroutineScope.launch {
|
||||
resultFlow
|
||||
.consumeAsFlow()
|
||||
.onEach { result ->
|
||||
awaitContinuation?.let {
|
||||
awaitContinuation = null
|
||||
if (it.isActive) {
|
||||
it.resume(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onEach { hide() }
|
||||
.collect(resultCollector)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun awaitResult(): ConfirmResult {
|
||||
return suspendCancellableCoroutine {
|
||||
awaitContinuation = it.apply {
|
||||
if (isCallbackEmpty) {
|
||||
invokeOnCancellation {
|
||||
visible.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateVisuals(visuals: ConfirmDialogVisuals) {
|
||||
this.visuals = visuals
|
||||
}
|
||||
|
||||
override fun show() {
|
||||
if (visuals !== ConfirmDialogVisualsImpl.Empty) {
|
||||
super.show()
|
||||
} else {
|
||||
throw UnsupportedOperationException("can't show confirm dialog with the Empty visuals")
|
||||
}
|
||||
}
|
||||
|
||||
override fun showConfirm(
|
||||
title: String,
|
||||
content: String,
|
||||
markdown: Boolean,
|
||||
confirm: String?,
|
||||
dismiss: String?
|
||||
) {
|
||||
coroutineScope.launch {
|
||||
updateVisuals(ConfirmDialogVisualsImpl(title, content, markdown, confirm, dismiss))
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun awaitConfirm(
|
||||
title: String,
|
||||
content: String,
|
||||
markdown: Boolean,
|
||||
confirm: String?,
|
||||
dismiss: String?
|
||||
): ConfirmResult {
|
||||
coroutineScope.launch {
|
||||
updateVisuals(ConfirmDialogVisualsImpl(title, content, markdown, confirm, dismiss))
|
||||
show()
|
||||
}
|
||||
return awaitResult()
|
||||
}
|
||||
|
||||
override val dialogType: String get() = "ConfirmDialog"
|
||||
|
||||
override fun toString(): String {
|
||||
return "${super.toString()}(visuals: $visuals)"
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun Saver(
|
||||
visible: MutableState<Boolean>,
|
||||
coroutineScope: CoroutineScope,
|
||||
callback: ConfirmCallback,
|
||||
resultChannel: ReceiveChannel<ConfirmResult>
|
||||
) = Saver<ConfirmDialogHandle, ConfirmDialogVisuals>(
|
||||
save = {
|
||||
it.visuals
|
||||
},
|
||||
restore = {
|
||||
Log.d(TAG, "ConfirmDialog restore, visuals: $it")
|
||||
ConfirmDialogHandleImpl(visible, coroutineScope, callback, it, resultChannel)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class CustomDialogHandleImpl(
|
||||
visible: MutableState<Boolean>,
|
||||
coroutineScope: CoroutineScope
|
||||
) : DialogHandleBase(visible, coroutineScope) {
|
||||
override val dialogType: String get() = "CustomDialog"
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberLoadingDialog(): LoadingDialogHandle {
|
||||
val visible = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
if (visible.value) {
|
||||
LoadingDialog()
|
||||
}
|
||||
|
||||
return remember {
|
||||
LoadingDialogHandleImpl(visible, coroutineScope)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberConfirmDialog(
|
||||
visuals: ConfirmDialogVisuals,
|
||||
callback: ConfirmCallback
|
||||
): ConfirmDialogHandle {
|
||||
val visible = rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val resultChannel = remember {
|
||||
Channel<ConfirmResult>()
|
||||
}
|
||||
|
||||
val handle = rememberSaveable(
|
||||
saver = ConfirmDialogHandleImpl.Saver(visible, coroutineScope, callback, resultChannel),
|
||||
init = {
|
||||
ConfirmDialogHandleImpl(visible, coroutineScope, callback, visuals, resultChannel)
|
||||
}
|
||||
)
|
||||
|
||||
if (visible.value) {
|
||||
ConfirmDialog(
|
||||
handle.visuals,
|
||||
confirm = { coroutineScope.launch { resultChannel.send(ConfirmResult.Confirmed) } },
|
||||
dismiss = { coroutineScope.launch { resultChannel.send(ConfirmResult.Canceled) } }
|
||||
)
|
||||
}
|
||||
|
||||
return handle
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberConfirmCallback(
|
||||
onConfirm: NullableCallback,
|
||||
onDismiss: NullableCallback
|
||||
): ConfirmCallback {
|
||||
val currentOnConfirm by rememberUpdatedState(newValue = onConfirm)
|
||||
val currentOnDismiss by rememberUpdatedState(newValue = onDismiss)
|
||||
return remember {
|
||||
ConfirmCallback({ currentOnConfirm }, { currentOnDismiss })
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberConfirmDialog(
|
||||
onConfirm: NullableCallback = null,
|
||||
onDismiss: NullableCallback = null
|
||||
): ConfirmDialogHandle {
|
||||
return rememberConfirmDialog(rememberConfirmCallback(onConfirm, onDismiss))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberConfirmDialog(callback: ConfirmCallback): ConfirmDialogHandle {
|
||||
return rememberConfirmDialog(ConfirmDialogVisualsImpl.Empty, callback)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberCustomDialog(composable: @Composable (dismiss: () -> Unit) -> Unit): DialogHandle {
|
||||
val visible = rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
if (visible.value) {
|
||||
composable { visible.value = false }
|
||||
}
|
||||
return remember {
|
||||
CustomDialogHandleImpl(visible, coroutineScope)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadingDialog() {
|
||||
Dialog(
|
||||
onDismissRequest = {},
|
||||
properties = DialogProperties(
|
||||
dismissOnClickOutside = false,
|
||||
dismissOnBackPress = false,
|
||||
usePlatformDefaultWidth = false
|
||||
)
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.size(100.dp), shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
val dialogWindowProvider = LocalView.current.parent as DialogWindowProvider
|
||||
setupWindowBlurListener(dialogWindowProvider.window)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ConfirmDialog(visuals: ConfirmDialogVisuals, confirm: () -> Unit, dismiss: () -> Unit) {
|
||||
BasicAlertDialog(
|
||||
onDismissRequest = {
|
||||
dismiss()
|
||||
},
|
||||
properties = DialogProperties(
|
||||
decorFitsSystemWindows = true,
|
||||
usePlatformDefaultWidth = false,
|
||||
securePolicy = SecureFlagPolicy.SecureOff
|
||||
)
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.width(320.dp)
|
||||
.wrapContentHeight(),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
tonalElevation = AlertDialogDefaults.TonalElevation,
|
||||
color = AlertDialogDefaults.containerColor,
|
||||
) {
|
||||
Column(modifier = Modifier.padding(PaddingValues(all = 24.dp))) {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(PaddingValues(bottom = 16.dp))
|
||||
.align(Alignment.Start)
|
||||
) {
|
||||
Text(text = visuals.title, style = MaterialTheme.typography.headlineSmall)
|
||||
}
|
||||
Box(
|
||||
Modifier
|
||||
.weight(weight = 1f, fill = false)
|
||||
.padding(PaddingValues(bottom = 24.dp))
|
||||
.align(Alignment.Start)
|
||||
) {
|
||||
|
||||
if (visuals.isMarkdown) {
|
||||
MarkdownContent(content = visuals.content)
|
||||
} else {
|
||||
Text(text = visuals.content, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
TextButton(onClick = dismiss) {
|
||||
Text(text = visuals.dismiss ?: stringResource(id = android.R.string.cancel))
|
||||
}
|
||||
|
||||
TextButton(onClick = confirm) {
|
||||
Text(text = visuals.confirm ?: stringResource(id = android.R.string.ok))
|
||||
}
|
||||
}
|
||||
}
|
||||
val dialogWindowProvider = LocalView.current.parent as DialogWindowProvider
|
||||
setupWindowBlurListener(dialogWindowProvider.window)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MarkdownContent(content: String) {
|
||||
val contentColor = LocalContentColor.current
|
||||
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
TextView(context).apply {
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
setSpannableFactory(NoCopySpannableFactory.getInstance())
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
breakStrategy = LineBreaker.BREAK_STRATEGY_SIMPLE
|
||||
hyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE
|
||||
}
|
||||
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
update = {
|
||||
Markwon.create(it.context).setMarkdown(it, content)
|
||||
it.setTextColor(contentColor.toArgb())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package me.bmax.apatch.ui.component
|
||||
|
||||
import androidx.compose.foundation.shape.CornerBasedShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun ProvideMenuShape(
|
||||
value: CornerBasedShape = RoundedCornerShape(8.dp), content: @Composable () -> Unit
|
||||
) = MaterialTheme(
|
||||
shapes = MaterialTheme.shapes.copy(extraSmall = value), content = content
|
||||
)
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package me.bmax.apatch.ui.component
|
||||
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.input.key.KeyEvent
|
||||
import androidx.compose.ui.input.key.onKeyEvent
|
||||
|
||||
@Composable
|
||||
fun KeyEventBlocker(predicate: (KeyEvent) -> Boolean) {
|
||||
val requester = remember { FocusRequester() }
|
||||
Box(
|
||||
Modifier
|
||||
.onKeyEvent {
|
||||
predicate(it)
|
||||
}
|
||||
.focusRequester(requester)
|
||||
.focusable()
|
||||
)
|
||||
LaunchedEffect(Unit) {
|
||||
requester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
package me.bmax.apatch.ui.component
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import me.bmax.apatch.R
|
||||
|
||||
@Composable
|
||||
fun ModuleUpdateButton(
|
||||
onClick: () -> Unit
|
||||
) = FilledTonalButton(
|
||||
onClick = onClick, enabled = true, contentPadding = PaddingValues(horizontal = 12.dp)
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
painter = painterResource(id = R.drawable.device_mobile_down),
|
||||
contentDescription = null
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.apm_update),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Visible,
|
||||
softWrap = false
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ModuleRemoveButton(
|
||||
enabled: Boolean, onClick: () -> Unit
|
||||
) = FilledTonalButton(
|
||||
onClick = onClick, enabled = enabled, contentPadding = PaddingValues(horizontal = 12.dp)
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
painter = painterResource(id = R.drawable.trash),
|
||||
contentDescription = null
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.apm_remove),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Visible,
|
||||
softWrap = false
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun KPModuleRemoveButton(
|
||||
enabled: Boolean, onClick: () -> Unit
|
||||
) = FilledTonalButton(
|
||||
onClick = onClick, enabled = enabled, contentPadding = PaddingValues(horizontal = 12.dp)
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
painter = painterResource(id = R.drawable.trash),
|
||||
contentDescription = null
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.kpm_unload),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Visible,
|
||||
softWrap = false
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ModuleStateIndicator(
|
||||
@DrawableRes icon: Int, color: Color = MaterialTheme.colorScheme.outline
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.requiredSize(150.dp),
|
||||
painter = painterResource(id = icon),
|
||||
contentDescription = null,
|
||||
alpha = 0.1f,
|
||||
colorFilter = ColorFilter.tint(color)
|
||||
)
|
||||
}
|
||||
169
app/src/main/java/me/bmax/apatch/ui/component/SearchBar.kt
Normal file
169
app/src/main/java/me/bmax/apatch/ui/component/SearchBar.kt
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
package me.bmax.apatch.ui.component
|
||||
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
private const val TAG = "SearchBar"
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SearchAppBar(
|
||||
title: @Composable () -> Unit,
|
||||
searchText: String,
|
||||
onSearchTextChange: (String) -> Unit,
|
||||
onClearClick: () -> Unit,
|
||||
onBackClick: (() -> Unit)? = null,
|
||||
onConfirm: (() -> Unit)? = null,
|
||||
dropdownContent: @Composable (() -> Unit)? = null,
|
||||
) {
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
var onSearch by remember { mutableStateOf(false) }
|
||||
|
||||
if (onSearch) {
|
||||
LaunchedEffect(Unit) { focusRequester.requestFocus() }
|
||||
}
|
||||
|
||||
BackHandler(
|
||||
enabled = onSearch,
|
||||
onBack = {
|
||||
keyboardController?.hide()
|
||||
onClearClick()
|
||||
onSearch = !onSearch
|
||||
}
|
||||
)
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
keyboardController?.hide()
|
||||
}
|
||||
}
|
||||
|
||||
TopAppBar(
|
||||
title = {
|
||||
Box {
|
||||
AnimatedVisibility(
|
||||
modifier = Modifier.align(Alignment.CenterStart),
|
||||
visible = !onSearch,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
content = { title() }
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = onSearch,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
) {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
top = 2.dp,
|
||||
bottom = 2.dp,
|
||||
end = if (onBackClick != null) 0.dp else 14.dp
|
||||
)
|
||||
.focusRequester(focusRequester)
|
||||
.onFocusChanged { focusState ->
|
||||
if (focusState.isFocused) onSearch = true
|
||||
Log.d(TAG, "onFocusChanged: $focusState")
|
||||
},
|
||||
value = searchText,
|
||||
onValueChange = onSearchTextChange,
|
||||
shape = RoundedCornerShape(15.dp),
|
||||
trailingIcon = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
onSearch = false
|
||||
keyboardController?.hide()
|
||||
onClearClick()
|
||||
},
|
||||
content = { Icon(Icons.Filled.Close, null) }
|
||||
)
|
||||
},
|
||||
maxLines = 1,
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Text,
|
||||
imeAction = ImeAction.Search
|
||||
),
|
||||
keyboardActions = KeyboardActions {
|
||||
defaultKeyboardAction(ImeAction.Search)
|
||||
keyboardController?.hide()
|
||||
onConfirm?.invoke()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
if (onBackClick != null) {
|
||||
IconButton(
|
||||
onClick = onBackClick,
|
||||
content = { Icon(Icons.AutoMirrored.Outlined.ArrowBack, null) }
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
AnimatedVisibility(
|
||||
visible = !onSearch
|
||||
) {
|
||||
IconButton(
|
||||
onClick = { onSearch = true },
|
||||
content = { Icon(Icons.Filled.Search, null) }
|
||||
)
|
||||
}
|
||||
|
||||
dropdownContent?.invoke()
|
||||
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun SearchAppBarPreview() {
|
||||
var searchText by remember { mutableStateOf("") }
|
||||
SearchAppBar(
|
||||
title = { Text("Search text") },
|
||||
searchText = searchText,
|
||||
onSearchTextChange = { searchText = it },
|
||||
onClearClick = { searchText = "" }
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
package me.bmax.apatch.ui.component
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.semantics.Role
|
||||
|
||||
@Composable
|
||||
fun SwitchItem(
|
||||
icon: ImageVector? = null,
|
||||
title: String,
|
||||
summary: String? = null,
|
||||
checked: Boolean,
|
||||
enabled: Boolean = true,
|
||||
onCheckedChange: (Boolean) -> Unit
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
||||
ListItem(
|
||||
modifier = Modifier.toggleable(
|
||||
value = checked,
|
||||
interactionSource = interactionSource,
|
||||
role = Role.Switch,
|
||||
enabled = enabled,
|
||||
indication = LocalIndication.current,
|
||||
onValueChange = onCheckedChange
|
||||
),
|
||||
headlineContent = {
|
||||
Text(
|
||||
title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = LocalContentColor.current
|
||||
)
|
||||
},
|
||||
leadingContent = icon?.let {
|
||||
{ Icon(icon, title) }
|
||||
},
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = checked,
|
||||
enabled = enabled,
|
||||
onCheckedChange = onCheckedChange,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
if (summary != null) {
|
||||
Text(
|
||||
summary,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RadioItem(
|
||||
title: String,
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(title)
|
||||
},
|
||||
leadingContent = {
|
||||
RadioButton(selected = selected, onClick = onClick)
|
||||
},
|
||||
)
|
||||
}
|
||||
604
app/src/main/java/me/bmax/apatch/ui/screen/APM.kt
Normal file
604
app/src/main/java/me/bmax/apatch/ui/screen/APM.kt
Normal file
|
|
@ -0,0 +1,604 @@
|
|||
package me.bmax.apatch.ui.screen
|
||||
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.util.Patterns
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.generated.destinations.ExecuteAPMActionScreenDestination
|
||||
import com.ramcosta.composedestinations.generated.destinations.InstallScreenDestination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.bmax.apatch.APApplication
|
||||
import me.bmax.apatch.R
|
||||
import me.bmax.apatch.apApp
|
||||
import me.bmax.apatch.ui.WebUIActivity
|
||||
import me.bmax.apatch.ui.component.ConfirmResult
|
||||
import me.bmax.apatch.ui.component.ModuleRemoveButton
|
||||
import me.bmax.apatch.ui.component.ModuleStateIndicator
|
||||
import me.bmax.apatch.ui.component.ModuleUpdateButton
|
||||
import me.bmax.apatch.ui.component.rememberConfirmDialog
|
||||
import me.bmax.apatch.ui.component.rememberLoadingDialog
|
||||
import me.bmax.apatch.ui.viewmodel.APModuleViewModel
|
||||
import me.bmax.apatch.util.DownloadListener
|
||||
import me.bmax.apatch.util.download
|
||||
import me.bmax.apatch.util.hasMagisk
|
||||
import me.bmax.apatch.util.reboot
|
||||
import me.bmax.apatch.util.toggleModule
|
||||
import me.bmax.apatch.util.ui.LocalSnackbarHost
|
||||
import me.bmax.apatch.util.uninstallModule
|
||||
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun APModuleScreen(navigator: DestinationsNavigator) {
|
||||
val snackBarHost = LocalSnackbarHost.current
|
||||
val context = LocalContext.current
|
||||
|
||||
val state by APApplication.apStateLiveData.observeAsState(APApplication.State.UNKNOWN_STATE)
|
||||
if (state != APApplication.State.ANDROIDPATCH_INSTALLED && state != APApplication.State.ANDROIDPATCH_NEED_UPDATE) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(12.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Row {
|
||||
Text(
|
||||
text = stringResource(id = R.string.apm_not_installed),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val viewModel = viewModel<APModuleViewModel>()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (viewModel.moduleList.isEmpty() || viewModel.isNeedRefresh) {
|
||||
viewModel.fetchModuleList()
|
||||
}
|
||||
}
|
||||
val webUILauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult()
|
||||
) { viewModel.fetchModuleList() }
|
||||
//TODO: FIXME -> val isSafeMode = Natives.getSafeMode()
|
||||
val isSafeMode = false
|
||||
val hasMagisk = hasMagisk()
|
||||
val hideInstallButton = isSafeMode || hasMagisk
|
||||
|
||||
val moduleListState = rememberLazyListState()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBar()
|
||||
}, floatingActionButton = if (hideInstallButton) {
|
||||
{ /* Empty */ }
|
||||
} else {
|
||||
{
|
||||
val selectZipLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
if (it.resultCode != RESULT_OK) {
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
val data = it.data ?: return@rememberLauncherForActivityResult
|
||||
val uri = data.data ?: return@rememberLauncherForActivityResult
|
||||
|
||||
Log.i("ModuleScreen", "select zip result: $uri")
|
||||
|
||||
navigator.navigate(InstallScreenDestination(uri, MODULE_TYPE.APM))
|
||||
|
||||
viewModel.markNeedRefresh()
|
||||
}
|
||||
|
||||
FloatingActionButton(
|
||||
contentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
onClick = {
|
||||
// select the zip file to install
|
||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
||||
intent.type = "application/zip"
|
||||
selectZipLauncher.launch(intent)
|
||||
}) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.package_import),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}, snackbarHost = { SnackbarHost(snackBarHost) }) { innerPadding ->
|
||||
when {
|
||||
hasMagisk -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.apm_magisk_conflict),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
ModuleList(
|
||||
navigator,
|
||||
viewModel = viewModel,
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxSize(),
|
||||
state = moduleListState,
|
||||
onInstallModule = {
|
||||
navigator.navigate(InstallScreenDestination(it, MODULE_TYPE.APM))
|
||||
},
|
||||
onClickModule = { id, name, hasWebUi ->
|
||||
if (hasWebUi) {
|
||||
webUILauncher.launch(
|
||||
Intent(
|
||||
context, WebUIActivity::class.java
|
||||
).setData("apatch://webui/$id".toUri()).putExtra("id", id)
|
||||
.putExtra("name", name)
|
||||
)
|
||||
}
|
||||
},
|
||||
snackBarHost = snackBarHost,
|
||||
context = context
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ModuleList(
|
||||
navigator: DestinationsNavigator,
|
||||
viewModel: APModuleViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
state: LazyListState,
|
||||
onInstallModule: (Uri) -> Unit,
|
||||
onClickModule: (id: String, name: String, hasWebUi: Boolean) -> Unit,
|
||||
snackBarHost: SnackbarHostState,
|
||||
context: Context
|
||||
) {
|
||||
val failedEnable = stringResource(R.string.apm_failed_to_enable)
|
||||
val failedDisable = stringResource(R.string.apm_failed_to_disable)
|
||||
val failedUninstall = stringResource(R.string.apm_uninstall_failed)
|
||||
val successUninstall = stringResource(R.string.apm_uninstall_success)
|
||||
val reboot = stringResource(id = R.string.reboot)
|
||||
val rebootToApply = stringResource(id = R.string.apm_reboot_to_apply)
|
||||
val moduleStr = stringResource(id = R.string.apm)
|
||||
val uninstall = stringResource(id = R.string.apm_remove)
|
||||
val cancel = stringResource(id = android.R.string.cancel)
|
||||
val moduleUninstallConfirm = stringResource(id = R.string.apm_uninstall_confirm)
|
||||
val updateText = stringResource(R.string.apm_update)
|
||||
val changelogText = stringResource(R.string.apm_changelog)
|
||||
val downloadingText = stringResource(R.string.apm_downloading)
|
||||
val startDownloadingText = stringResource(R.string.apm_start_downloading)
|
||||
|
||||
val loadingDialog = rememberLoadingDialog()
|
||||
val confirmDialog = rememberConfirmDialog()
|
||||
|
||||
suspend fun onModuleUpdate(
|
||||
module: APModuleViewModel.ModuleInfo,
|
||||
changelogUrl: String,
|
||||
downloadUrl: String,
|
||||
fileName: String
|
||||
) {
|
||||
val changelog = loadingDialog.withLoading {
|
||||
withContext(Dispatchers.IO) {
|
||||
if (Patterns.WEB_URL.matcher(changelogUrl).matches()) {
|
||||
apApp.okhttpClient.newCall(
|
||||
okhttp3.Request.Builder().url(changelogUrl).build()
|
||||
).execute().body!!.string()
|
||||
} else {
|
||||
changelogUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (changelog.isNotEmpty()) {
|
||||
// changelog is not empty, show it and wait for confirm
|
||||
val confirmResult = confirmDialog.awaitConfirm(
|
||||
changelogText,
|
||||
content = changelog,
|
||||
markdown = true,
|
||||
confirm = updateText,
|
||||
)
|
||||
|
||||
if (confirmResult != ConfirmResult.Confirmed) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
context, startDownloadingText.format(module.name), Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
val downloading = downloadingText.format(module.name)
|
||||
withContext(Dispatchers.IO) {
|
||||
download(
|
||||
context,
|
||||
downloadUrl,
|
||||
fileName,
|
||||
downloading,
|
||||
onDownloaded = onInstallModule,
|
||||
onDownloading = {
|
||||
launch(Dispatchers.Main) {
|
||||
Toast.makeText(context, downloading, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun onModuleUninstall(module: APModuleViewModel.ModuleInfo) {
|
||||
val confirmResult = confirmDialog.awaitConfirm(
|
||||
moduleStr,
|
||||
content = moduleUninstallConfirm.format(module.name),
|
||||
confirm = uninstall,
|
||||
dismiss = cancel
|
||||
)
|
||||
if (confirmResult != ConfirmResult.Confirmed) {
|
||||
return
|
||||
}
|
||||
|
||||
val success = loadingDialog.withLoading {
|
||||
withContext(Dispatchers.IO) {
|
||||
uninstallModule(module.id)
|
||||
}
|
||||
}
|
||||
|
||||
if (success) {
|
||||
viewModel.fetchModuleList()
|
||||
}
|
||||
val message = if (success) {
|
||||
successUninstall.format(module.name)
|
||||
} else {
|
||||
failedUninstall.format(module.name)
|
||||
}
|
||||
val actionLabel = if (success) {
|
||||
reboot
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val result = snackBarHost.showSnackbar(
|
||||
message = message, actionLabel = actionLabel, duration = SnackbarDuration.Long
|
||||
)
|
||||
if (result == SnackbarResult.ActionPerformed) {
|
||||
reboot()
|
||||
}
|
||||
}
|
||||
|
||||
PullToRefreshBox(
|
||||
modifier = modifier,
|
||||
onRefresh = { viewModel.fetchModuleList() },
|
||||
isRefreshing = viewModel.isRefreshing
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = state,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
contentPadding = remember {
|
||||
PaddingValues(
|
||||
start = 16.dp,
|
||||
top = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp + 16.dp + 56.dp /* Scaffold Fab Spacing + Fab container height */
|
||||
)
|
||||
},
|
||||
) {
|
||||
when {
|
||||
viewModel.moduleList.isEmpty() -> {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier.fillParentMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.apm_empty), textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
items(viewModel.moduleList) { module ->
|
||||
var isChecked by rememberSaveable(module) { mutableStateOf(module.enabled) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val updatedModule by produceState(initialValue = Triple("", "", "")) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
value = viewModel.checkUpdate(module)
|
||||
}
|
||||
}
|
||||
|
||||
ModuleItem(
|
||||
navigator,
|
||||
module,
|
||||
isChecked,
|
||||
updatedModule.first,
|
||||
onUninstall = {
|
||||
scope.launch { onModuleUninstall(module) }
|
||||
},
|
||||
onCheckChanged = {
|
||||
scope.launch {
|
||||
val success = loadingDialog.withLoading {
|
||||
withContext(Dispatchers.IO) {
|
||||
toggleModule(module.id, !isChecked)
|
||||
}
|
||||
}
|
||||
if (success) {
|
||||
isChecked = it
|
||||
viewModel.fetchModuleList()
|
||||
|
||||
val result = snackBarHost.showSnackbar(
|
||||
message = rebootToApply,
|
||||
actionLabel = reboot,
|
||||
duration = SnackbarDuration.Long
|
||||
)
|
||||
if (result == SnackbarResult.ActionPerformed) {
|
||||
reboot()
|
||||
}
|
||||
} else {
|
||||
val message = if (isChecked) failedDisable else failedEnable
|
||||
snackBarHost.showSnackbar(message.format(module.name))
|
||||
}
|
||||
}
|
||||
},
|
||||
onUpdate = {
|
||||
scope.launch {
|
||||
onModuleUpdate(
|
||||
module,
|
||||
updatedModule.third,
|
||||
updatedModule.first,
|
||||
"${module.name}-${updatedModule.second}.zip"
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
onClickModule(it.id, it.name, it.hasWebUi)
|
||||
})
|
||||
// fix last item shadow incomplete in LazyColumn
|
||||
Spacer(Modifier.height(1.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DownloadListener(context, onInstallModule)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar() {
|
||||
TopAppBar(title = { Text(stringResource(R.string.apm)) })
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ModuleItem(
|
||||
navigator: DestinationsNavigator,
|
||||
module: APModuleViewModel.ModuleInfo,
|
||||
isChecked: Boolean,
|
||||
updateUrl: String,
|
||||
onUninstall: (APModuleViewModel.ModuleInfo) -> Unit,
|
||||
onCheckChanged: (Boolean) -> Unit,
|
||||
onUpdate: (APModuleViewModel.ModuleInfo) -> Unit,
|
||||
onClick: (APModuleViewModel.ModuleInfo) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
alpha: Float = 1f,
|
||||
) {
|
||||
val decoration = if (!module.remove) TextDecoration.None else TextDecoration.LineThrough
|
||||
val moduleAuthor = stringResource(id = R.string.apm_author)
|
||||
val viewModel = viewModel<APModuleViewModel>()
|
||||
Surface(
|
||||
modifier = modifier,
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
tonalElevation = 1.dp,
|
||||
shape = RoundedCornerShape(20.dp)
|
||||
) {
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onClick(module) },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(all = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.alpha(alpha = alpha)
|
||||
.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
Text(
|
||||
text = module.name,
|
||||
style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.Bold),
|
||||
maxLines = 2,
|
||||
textDecoration = decoration,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "${module.version}, $moduleAuthor ${module.author}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
textDecoration = decoration,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
Switch(
|
||||
enabled = !module.update,
|
||||
checked = isChecked,
|
||||
onCheckedChange = onCheckChanged
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.alpha(alpha = alpha)
|
||||
.padding(horizontal = 16.dp),
|
||||
text = module.description,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
textDecoration = decoration,
|
||||
color = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
|
||||
HorizontalDivider(
|
||||
thickness = 1.5.dp,
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
if (updateUrl.isNotEmpty()) {
|
||||
ModuleUpdateButton(onClick = { onUpdate(module) })
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
}
|
||||
|
||||
if (module.hasWebUi) {
|
||||
FilledTonalButton(
|
||||
onClick = { onClick(module) },
|
||||
enabled = true,
|
||||
contentPadding = PaddingValues(horizontal = 12.dp)
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
painter = painterResource(id = R.drawable.settings),
|
||||
contentDescription = null
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.apm_webui_open),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Visible,
|
||||
softWrap = false
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
}
|
||||
|
||||
if (module.hasActionScript) {
|
||||
FilledTonalButton(
|
||||
onClick = {
|
||||
navigator.navigate(ExecuteAPMActionScreenDestination(module.id))
|
||||
viewModel.markNeedRefresh()
|
||||
}, enabled = true, contentPadding = PaddingValues(horizontal = 12.dp)
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
painter = painterResource(id = R.drawable.settings),
|
||||
contentDescription = null
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.apm_action),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Visible,
|
||||
softWrap = false
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
}
|
||||
ModuleRemoveButton(enabled = !module.remove, onClick = { onUninstall(module) })
|
||||
}
|
||||
}
|
||||
|
||||
if (module.remove) {
|
||||
ModuleStateIndicator(R.drawable.trash)
|
||||
}
|
||||
if (module.update) {
|
||||
ModuleStateIndicator(R.drawable.device_mobile_down)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
193
app/src/main/java/me/bmax/apatch/ui/screen/AboutScreen.kt
Normal file
193
app/src/main/java/me/bmax/apatch/ui/screen/AboutScreen.kt
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
package me.bmax.apatch.ui.screen
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.dropUnlessResumed
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import me.bmax.apatch.BuildConfig
|
||||
import me.bmax.apatch.R
|
||||
import me.bmax.apatch.util.Version
|
||||
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun AboutScreen(navigator: DestinationsNavigator) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBar(onBack = dropUnlessResumed { navigator.popBackStack() })
|
||||
}
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
Surface(
|
||||
modifier = Modifier.size(95.dp),
|
||||
color = colorResource(id = R.color.ic_launcher_background),
|
||||
shape = CircleShape
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_launcher_foreground),
|
||||
contentDescription = "icon",
|
||||
modifier = Modifier.scale(1.4f)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.app_name),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.about_app_version,
|
||||
if (BuildConfig.VERSION_NAME.contains(BuildConfig.VERSION_CODE.toString())) "${BuildConfig.VERSION_CODE}" else "${BuildConfig.VERSION_CODE} (${BuildConfig.VERSION_NAME})"
|
||||
),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 5.dp)
|
||||
)
|
||||
Text(
|
||||
|
||||
text = stringResource(
|
||||
id = R.string.about_powered_by,
|
||||
"KernelPatch (${Version.buildKPVString()})"
|
||||
),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 5.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
FilledTonalButton(
|
||||
onClick = { uriHandler.openUri("https://github.com/bmax121/APatch") }
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.github),
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing))
|
||||
Text(text = stringResource(id = R.string.about_github))
|
||||
}
|
||||
|
||||
FilledTonalButton(
|
||||
onClick = { uriHandler.openUri("https://t.me/APatchChannel") }
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.telegram),
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing))
|
||||
Text(text = stringResource(id = R.string.about_telegram_channel))
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
FilledTonalButton(
|
||||
onClick = { uriHandler.openUri("https://hosted.weblate.org/engage/APatch") }
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.weblate),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(ButtonDefaults.IconSize)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing))
|
||||
Text(text = stringResource(id = R.string.about_weblate))
|
||||
}
|
||||
|
||||
FilledTonalButton(
|
||||
onClick = { uriHandler.openUri("https://t.me/apatch_discuss") }
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.telegram),
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing))
|
||||
Text(text = stringResource(id = R.string.about_telegram_group))
|
||||
}
|
||||
}
|
||||
|
||||
OutlinedCard(
|
||||
modifier = Modifier.padding(vertical = 30.dp, horizontal = 20.dp),
|
||||
shape = RoundedCornerShape(15.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(all = 12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.about_app_desc),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar(onBack: () -> Unit = {}) {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.about)) },
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onBack
|
||||
) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package me.bmax.apatch.ui.screen
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Apps
|
||||
import androidx.compose.material.icons.filled.Build
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.filled.Security
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.outlined.Apps
|
||||
import androidx.compose.material.icons.outlined.Build
|
||||
import androidx.compose.material.icons.outlined.Home
|
||||
import androidx.compose.material.icons.outlined.Security
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import com.ramcosta.composedestinations.generated.destinations.APModuleScreenDestination
|
||||
import com.ramcosta.composedestinations.generated.destinations.HomeScreenDestination
|
||||
import com.ramcosta.composedestinations.generated.destinations.KPModuleScreenDestination
|
||||
import com.ramcosta.composedestinations.generated.destinations.SettingScreenDestination
|
||||
import com.ramcosta.composedestinations.generated.destinations.SuperUserScreenDestination
|
||||
import com.ramcosta.composedestinations.spec.DirectionDestinationSpec
|
||||
import me.bmax.apatch.R
|
||||
|
||||
enum class BottomBarDestination(
|
||||
val direction: DirectionDestinationSpec,
|
||||
@param:StringRes val label: Int,
|
||||
val iconSelected: ImageVector,
|
||||
val iconNotSelected: ImageVector,
|
||||
val kPatchRequired: Boolean,
|
||||
val aPatchRequired: Boolean,
|
||||
) {
|
||||
Home(
|
||||
HomeScreenDestination,
|
||||
R.string.home,
|
||||
Icons.Filled.Home,
|
||||
Icons.Outlined.Home,
|
||||
false,
|
||||
false
|
||||
),
|
||||
KModule(
|
||||
KPModuleScreenDestination,
|
||||
R.string.kpm,
|
||||
Icons.Filled.Build,
|
||||
Icons.Outlined.Build,
|
||||
true,
|
||||
false
|
||||
),
|
||||
SuperUser(
|
||||
SuperUserScreenDestination,
|
||||
R.string.su_title,
|
||||
Icons.Filled.Security,
|
||||
Icons.Outlined.Security,
|
||||
true,
|
||||
false
|
||||
),
|
||||
AModule(
|
||||
APModuleScreenDestination,
|
||||
R.string.apm,
|
||||
Icons.Filled.Apps,
|
||||
Icons.Outlined.Apps,
|
||||
false,
|
||||
true
|
||||
),
|
||||
Settings(
|
||||
SettingScreenDestination,
|
||||
R.string.settings,
|
||||
Icons.Filled.Settings,
|
||||
Icons.Outlined.Settings,
|
||||
false,
|
||||
false
|
||||
)
|
||||
}
|
||||
152
app/src/main/java/me/bmax/apatch/ui/screen/ExecuteAPMAction.kt
Normal file
152
app/src/main/java/me/bmax/apatch/ui/screen/ExecuteAPMAction.kt
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
package me.bmax.apatch.ui.screen
|
||||
|
||||
import android.os.Environment
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Save
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.dropUnlessResumed
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.bmax.apatch.R
|
||||
import me.bmax.apatch.ui.component.KeyEventBlocker
|
||||
import me.bmax.apatch.util.runAPModuleAction
|
||||
import me.bmax.apatch.util.ui.LocalSnackbarHost
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
@Destination<RootGraph>
|
||||
fun ExecuteAPMActionScreen(navigator: DestinationsNavigator, moduleId: String) {
|
||||
var text by rememberSaveable { mutableStateOf("") }
|
||||
var tempText: String
|
||||
val logContent = rememberSaveable { StringBuilder() }
|
||||
val snackBarHost = LocalSnackbarHost.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val scrollState = rememberScrollState()
|
||||
var actionResult: Boolean
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (text.isNotEmpty()) {
|
||||
return@LaunchedEffect
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
runAPModuleAction(
|
||||
moduleId,
|
||||
onStdout = {
|
||||
tempText = "$it\n"
|
||||
if (tempText.startsWith("[H[J")) { // clear command
|
||||
text = tempText.substring(6)
|
||||
} else {
|
||||
text += tempText
|
||||
}
|
||||
logContent.append(it).append("\n")
|
||||
},
|
||||
onStderr = {
|
||||
logContent.append(it).append("\n")
|
||||
}
|
||||
).let {
|
||||
actionResult = it
|
||||
}
|
||||
}
|
||||
if (actionResult) {
|
||||
navigator.popBackStack()
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBar(
|
||||
onBack = dropUnlessResumed {
|
||||
navigator.popBackStack()
|
||||
},
|
||||
onSave = {
|
||||
scope.launch {
|
||||
val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault())
|
||||
val date = format.format(Date())
|
||||
val file = File(
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||
"APatch_apm_action_log_${date}.log"
|
||||
)
|
||||
file.writeText(logContent.toString())
|
||||
snackBarHost.showSnackbar("Log saved to ${file.absolutePath}")
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackBarHost) }
|
||||
) { innerPadding ->
|
||||
KeyEventBlocker {
|
||||
it.key == Key.VolumeDown || it.key == Key.VolumeUp
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(1f)
|
||||
.padding(innerPadding)
|
||||
.verticalScroll(scrollState),
|
||||
) {
|
||||
LaunchedEffect(text) {
|
||||
scrollState.animateScrollTo(scrollState.maxValue)
|
||||
}
|
||||
Text(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
text = text,
|
||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar(onBack: () -> Unit = {}, onSave: () -> Unit = {}) {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.apm_action)) },
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onBack
|
||||
) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) }
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = onSave) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Save,
|
||||
contentDescription = "Save log"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
996
app/src/main/java/me/bmax/apatch/ui/screen/Home.kt
Normal file
996
app/src/main/java/me/bmax/apatch/ui/screen/Home.kt
Normal file
|
|
@ -0,0 +1,996 @@
|
|||
package me.bmax.apatch.ui.screen
|
||||
|
||||
import android.os.Build
|
||||
import android.system.Os
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.InstallMobile
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material.icons.outlined.Block
|
||||
import androidx.compose.material.icons.outlined.Cached
|
||||
import androidx.compose.material.icons.outlined.CheckCircle
|
||||
import androidx.compose.material.icons.outlined.Clear
|
||||
import androidx.compose.material.icons.outlined.InstallMobile
|
||||
import androidx.compose.material.icons.outlined.SystemUpdate
|
||||
import androidx.compose.material3.AlertDialogDefaults
|
||||
import androidx.compose.material3.BasicAlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.compose.ui.window.DialogWindowProvider
|
||||
import androidx.compose.ui.window.SecureFlagPolicy
|
||||
import androidx.lifecycle.compose.dropUnlessResumed
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.generated.destinations.AboutScreenDestination
|
||||
import com.ramcosta.composedestinations.generated.destinations.InstallModeSelectScreenDestination
|
||||
import com.ramcosta.composedestinations.generated.destinations.PatchesDestination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.bmax.apatch.APApplication
|
||||
import me.bmax.apatch.Natives
|
||||
import me.bmax.apatch.R
|
||||
import me.bmax.apatch.apApp
|
||||
import me.bmax.apatch.ui.component.ProvideMenuShape
|
||||
import me.bmax.apatch.ui.component.rememberConfirmDialog
|
||||
import me.bmax.apatch.ui.viewmodel.PatchesViewModel
|
||||
import me.bmax.apatch.util.LatestVersionInfo
|
||||
import me.bmax.apatch.util.Version
|
||||
import me.bmax.apatch.util.Version.getManagerVersion
|
||||
import me.bmax.apatch.util.checkNewVersion
|
||||
import me.bmax.apatch.util.getSELinuxStatus
|
||||
import me.bmax.apatch.util.reboot
|
||||
import me.bmax.apatch.util.ui.APDialogBlurBehindUtils
|
||||
|
||||
private val managerVersion = getManagerVersion()
|
||||
|
||||
@Destination<RootGraph>(start = true)
|
||||
@Composable
|
||||
fun HomeScreen(navigator: DestinationsNavigator) {
|
||||
var showPatchFloatAction by remember { mutableStateOf(true) }
|
||||
|
||||
val kpState by APApplication.kpStateLiveData.observeAsState(APApplication.State.UNKNOWN_STATE)
|
||||
val apState by APApplication.apStateLiveData.observeAsState(APApplication.State.UNKNOWN_STATE)
|
||||
|
||||
if (kpState != APApplication.State.UNKNOWN_STATE) {
|
||||
showPatchFloatAction = false
|
||||
}
|
||||
|
||||
Scaffold(topBar = {
|
||||
TopBar(onInstallClick = dropUnlessResumed {
|
||||
navigator.navigate(InstallModeSelectScreenDestination)
|
||||
}, navigator, kpState)
|
||||
}) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.padding(horizontal = 16.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Spacer(Modifier.height(0.dp))
|
||||
WarningCard()
|
||||
KStatusCard(kpState, apState, navigator)
|
||||
if (kpState != APApplication.State.UNKNOWN_STATE && apState != APApplication.State.ANDROIDPATCH_INSTALLED) {
|
||||
AStatusCard(apState)
|
||||
}
|
||||
val checkUpdate = APApplication.sharedPreferences.getBoolean("check_update", true)
|
||||
if (checkUpdate) {
|
||||
UpdateCard()
|
||||
}
|
||||
InfoCard(kpState, apState)
|
||||
LearnMoreCard()
|
||||
Spacer(Modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun UninstallDialog(showDialog: MutableState<Boolean>, navigator: DestinationsNavigator) {
|
||||
BasicAlertDialog(
|
||||
onDismissRequest = { showDialog.value = false }, properties = DialogProperties(
|
||||
decorFitsSystemWindows = true,
|
||||
usePlatformDefaultWidth = false,
|
||||
)
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.width(320.dp)
|
||||
.wrapContentHeight(),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
tonalElevation = AlertDialogDefaults.TonalElevation,
|
||||
color = AlertDialogDefaults.containerColor,
|
||||
) {
|
||||
Column(modifier = Modifier.padding(PaddingValues(all = 24.dp))) {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(PaddingValues(bottom = 16.dp))
|
||||
.align(Alignment.CenterHorizontally)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.home_dialog_uninstall_title),
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
TextButton(onClick = {
|
||||
showDialog.value = false
|
||||
APApplication.uninstallApatch()
|
||||
}) {
|
||||
Text(text = stringResource(id = R.string.home_dialog_uninstall_ap_only))
|
||||
}
|
||||
|
||||
TextButton(onClick = {
|
||||
showDialog.value = false
|
||||
APApplication.uninstallApatch()
|
||||
navigator.navigate(PatchesDestination(PatchesViewModel.PatchMode.UNPATCH))
|
||||
}) {
|
||||
Text(text = stringResource(id = R.string.home_dialog_uninstall_all))
|
||||
}
|
||||
}
|
||||
}
|
||||
val dialogWindowProvider = LocalView.current.parent as DialogWindowProvider
|
||||
APDialogBlurBehindUtils.setupWindowBlurListener(dialogWindowProvider.window)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AuthFailedTipDialog(showDialog: MutableState<Boolean>) {
|
||||
BasicAlertDialog(
|
||||
onDismissRequest = { showDialog.value = false }, properties = DialogProperties(
|
||||
decorFitsSystemWindows = true,
|
||||
usePlatformDefaultWidth = false,
|
||||
securePolicy = SecureFlagPolicy.SecureOff
|
||||
)
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.width(320.dp)
|
||||
.wrapContentHeight(),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
tonalElevation = AlertDialogDefaults.TonalElevation,
|
||||
color = AlertDialogDefaults.containerColor,
|
||||
) {
|
||||
Column(modifier = Modifier.padding(PaddingValues(all = 24.dp))) {
|
||||
// Title
|
||||
Box(
|
||||
Modifier
|
||||
.padding(PaddingValues(bottom = 16.dp))
|
||||
.align(Alignment.Start)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.home_dialog_auth_fail_title),
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
}
|
||||
|
||||
// Content
|
||||
Box(
|
||||
Modifier
|
||||
.weight(weight = 1f, fill = false)
|
||||
.padding(PaddingValues(bottom = 24.dp))
|
||||
.align(Alignment.Start)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.home_dialog_auth_fail_content),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
|
||||
// Buttons
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
TextButton(onClick = { showDialog.value = false }) {
|
||||
Text(text = stringResource(id = android.R.string.ok))
|
||||
}
|
||||
}
|
||||
}
|
||||
val dialogWindowProvider = LocalView.current.parent as DialogWindowProvider
|
||||
APDialogBlurBehindUtils.setupWindowBlurListener(dialogWindowProvider.window)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
val checkSuperKeyValidation: (superKey: String) -> Boolean = { superKey ->
|
||||
superKey.length in 8..63 && superKey.any { it.isDigit() } && superKey.any { it.isLetter() }
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AuthSuperKey(showDialog: MutableState<Boolean>, showFailedDialog: MutableState<Boolean>) {
|
||||
var key by remember { mutableStateOf("") }
|
||||
var keyVisible by remember { mutableStateOf(false) }
|
||||
var enable by remember { mutableStateOf(false) }
|
||||
|
||||
BasicAlertDialog(
|
||||
onDismissRequest = { showDialog.value = false }, properties = DialogProperties(
|
||||
decorFitsSystemWindows = true,
|
||||
usePlatformDefaultWidth = false,
|
||||
securePolicy = SecureFlagPolicy.SecureOff
|
||||
)
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.width(310.dp)
|
||||
.wrapContentHeight(),
|
||||
shape = RoundedCornerShape(30.dp),
|
||||
tonalElevation = AlertDialogDefaults.TonalElevation,
|
||||
color = AlertDialogDefaults.containerColor,
|
||||
) {
|
||||
Column(modifier = Modifier.padding(PaddingValues(all = 24.dp))) {
|
||||
// Title
|
||||
Box(
|
||||
Modifier
|
||||
.padding(PaddingValues(bottom = 16.dp))
|
||||
.align(Alignment.Start)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.home_auth_key_title),
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
}
|
||||
|
||||
// Content
|
||||
Box(
|
||||
Modifier
|
||||
.weight(weight = 1f, fill = false)
|
||||
.align(Alignment.Start)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.home_auth_key_desc),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
|
||||
// Content2
|
||||
Box(
|
||||
contentAlignment = Alignment.CenterEnd,
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = key,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 6.dp),
|
||||
onValueChange = {
|
||||
key = it
|
||||
enable = checkSuperKeyValidation(key)
|
||||
},
|
||||
shape = RoundedCornerShape(50.0f),
|
||||
label = { Text(stringResource(id = R.string.super_key)) },
|
||||
visualTransformation = if (keyVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password)
|
||||
)
|
||||
IconButton(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.padding(top = 15.dp, end = 5.dp),
|
||||
onClick = { keyVisible = !keyVisible }) {
|
||||
Icon(
|
||||
imageVector = if (keyVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff,
|
||||
contentDescription = null,
|
||||
tint = Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
// Buttons
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
TextButton(onClick = { showDialog.value = false }) {
|
||||
Text(stringResource(id = android.R.string.cancel))
|
||||
}
|
||||
|
||||
Button(onClick = {
|
||||
showDialog.value = false
|
||||
|
||||
val preVerifyKey = Natives.nativeReady(key)
|
||||
if (preVerifyKey) {
|
||||
APApplication.superKey = key
|
||||
} else {
|
||||
showFailedDialog.value = true
|
||||
}
|
||||
|
||||
}, enabled = enable) {
|
||||
Text(stringResource(id = android.R.string.ok))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val dialogWindowProvider = LocalView.current.parent as DialogWindowProvider
|
||||
APDialogBlurBehindUtils.setupWindowBlurListener(dialogWindowProvider.window)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RebootDropdownItem(@StringRes id: Int, reason: String = "") {
|
||||
DropdownMenuItem(text = {
|
||||
Text(stringResource(id))
|
||||
}, onClick = {
|
||||
reboot(reason)
|
||||
})
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar(
|
||||
onInstallClick: () -> Unit, navigator: DestinationsNavigator, kpState: APApplication.State
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
var showDropdownMoreOptions by remember { mutableStateOf(false) }
|
||||
var showDropdownReboot by remember { mutableStateOf(false) }
|
||||
|
||||
TopAppBar(title = {
|
||||
Text(stringResource(R.string.app_name))
|
||||
}, actions = {
|
||||
IconButton(onClick = onInstallClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.InstallMobile,
|
||||
contentDescription = stringResource(id = R.string.mode_select_page_title)
|
||||
)
|
||||
}
|
||||
|
||||
if (kpState != APApplication.State.UNKNOWN_STATE) {
|
||||
IconButton(onClick = {
|
||||
showDropdownReboot = true
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Refresh,
|
||||
contentDescription = stringResource(id = R.string.reboot)
|
||||
)
|
||||
|
||||
ProvideMenuShape(RoundedCornerShape(10.dp)) {
|
||||
DropdownMenu(expanded = showDropdownReboot, onDismissRequest = {
|
||||
showDropdownReboot = false
|
||||
}) {
|
||||
RebootDropdownItem(id = R.string.reboot)
|
||||
RebootDropdownItem(id = R.string.reboot_recovery, reason = "recovery")
|
||||
RebootDropdownItem(id = R.string.reboot_bootloader, reason = "bootloader")
|
||||
RebootDropdownItem(id = R.string.reboot_download, reason = "download")
|
||||
RebootDropdownItem(id = R.string.reboot_edl, reason = "edl")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box {
|
||||
IconButton(onClick = { showDropdownMoreOptions = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.MoreVert,
|
||||
contentDescription = stringResource(id = R.string.settings)
|
||||
)
|
||||
ProvideMenuShape(RoundedCornerShape(10.dp)) {
|
||||
DropdownMenu(expanded = showDropdownMoreOptions, onDismissRequest = {
|
||||
showDropdownMoreOptions = false
|
||||
}) {
|
||||
DropdownMenuItem(text = {
|
||||
Text(stringResource(R.string.home_more_menu_feedback_or_suggestion))
|
||||
}, onClick = {
|
||||
showDropdownMoreOptions = false
|
||||
uriHandler.openUri("https://github.com/bmax121/APatch/issues/new/choose")
|
||||
})
|
||||
DropdownMenuItem(text = {
|
||||
Text(stringResource(R.string.home_more_menu_about))
|
||||
}, onClick = {
|
||||
navigator.navigate(AboutScreenDestination)
|
||||
showDropdownMoreOptions = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun KStatusCard(
|
||||
kpState: APApplication.State, apState: APApplication.State, navigator: DestinationsNavigator
|
||||
) {
|
||||
|
||||
val showAuthFailedTipDialog = remember { mutableStateOf(false) }
|
||||
if (showAuthFailedTipDialog.value) {
|
||||
AuthFailedTipDialog(showDialog = showAuthFailedTipDialog)
|
||||
}
|
||||
|
||||
val showAuthKeyDialog = remember { mutableStateOf(false) }
|
||||
if (showAuthKeyDialog.value) {
|
||||
AuthSuperKey(showDialog = showAuthKeyDialog, showFailedDialog = showAuthFailedTipDialog)
|
||||
}
|
||||
|
||||
val showUninstallDialog = remember { mutableStateOf(false) }
|
||||
if (showUninstallDialog.value) {
|
||||
UninstallDialog(showDialog = showUninstallDialog, navigator)
|
||||
}
|
||||
|
||||
val cardBackgroundColor = when (kpState) {
|
||||
APApplication.State.KERNELPATCH_INSTALLED -> {
|
||||
MaterialTheme.colorScheme.primary
|
||||
}
|
||||
|
||||
APApplication.State.KERNELPATCH_NEED_UPDATE, APApplication.State.KERNELPATCH_NEED_REBOOT -> {
|
||||
MaterialTheme.colorScheme.secondary
|
||||
}
|
||||
|
||||
else -> {
|
||||
MaterialTheme.colorScheme.secondaryContainer
|
||||
}
|
||||
}
|
||||
|
||||
ElevatedCard(
|
||||
onClick = {
|
||||
if (kpState != APApplication.State.KERNELPATCH_INSTALLED) {
|
||||
navigator.navigate(InstallModeSelectScreenDestination)
|
||||
}
|
||||
},
|
||||
colors = CardDefaults.elevatedCardColors(containerColor = cardBackgroundColor),
|
||||
elevation = CardDefaults.cardElevation(
|
||||
defaultElevation = if (kpState == APApplication.State.UNKNOWN_STATE) 0.dp else 6.dp
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
if (kpState == APApplication.State.KERNELPATCH_NEED_UPDATE) {
|
||||
Row {
|
||||
Text(
|
||||
text = stringResource(R.string.kernel_patch),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
when (kpState) {
|
||||
APApplication.State.KERNELPATCH_INSTALLED -> {
|
||||
Icon(Icons.Filled.CheckCircle, stringResource(R.string.home_working))
|
||||
}
|
||||
|
||||
APApplication.State.KERNELPATCH_NEED_UPDATE, APApplication.State.KERNELPATCH_NEED_REBOOT -> {
|
||||
Icon(Icons.Outlined.SystemUpdate, stringResource(R.string.home_need_update))
|
||||
}
|
||||
|
||||
else -> {
|
||||
Icon(Icons.AutoMirrored.Outlined.HelpOutline, "Unknown")
|
||||
}
|
||||
}
|
||||
Column(
|
||||
Modifier
|
||||
.weight(2f)
|
||||
.padding(start = 16.dp)
|
||||
) {
|
||||
when (kpState) {
|
||||
APApplication.State.KERNELPATCH_INSTALLED -> {
|
||||
Text(
|
||||
text = stringResource(R.string.home_working),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
|
||||
APApplication.State.KERNELPATCH_NEED_UPDATE, APApplication.State.KERNELPATCH_NEED_REBOOT -> {
|
||||
Text(
|
||||
text = stringResource(R.string.home_need_update),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Spacer(Modifier.height(6.dp))
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.kpatch_version_update,
|
||||
Version.installedKPVString(),
|
||||
Version.buildKPVString()
|
||||
), style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Text(
|
||||
text = stringResource(R.string.home_install_unknown),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.home_install_unknown_summary),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
if (kpState != APApplication.State.UNKNOWN_STATE && kpState != APApplication.State.KERNELPATCH_NEED_UPDATE && kpState != APApplication.State.KERNELPATCH_NEED_REBOOT) {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "${Version.installedKPVString()} (${managerVersion.second}) - " + if (apState != APApplication.State.ANDROIDPATCH_NOT_INSTALLED) "Full" else "KernelPatch",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.align(Alignment.CenterVertically)
|
||||
) {
|
||||
Button(onClick = {
|
||||
when (kpState) {
|
||||
APApplication.State.UNKNOWN_STATE -> {
|
||||
showAuthKeyDialog.value = true
|
||||
}
|
||||
|
||||
APApplication.State.KERNELPATCH_NEED_UPDATE -> {
|
||||
// todo: remove legacy compact for kp < 0.9.0
|
||||
if (Version.installedKPVUInt() < 0x900u) {
|
||||
navigator.navigate(PatchesDestination(PatchesViewModel.PatchMode.PATCH_ONLY))
|
||||
} else {
|
||||
navigator.navigate(InstallModeSelectScreenDestination)
|
||||
}
|
||||
}
|
||||
|
||||
APApplication.State.KERNELPATCH_NEED_REBOOT -> {
|
||||
reboot()
|
||||
}
|
||||
|
||||
APApplication.State.KERNELPATCH_UNINSTALLING -> {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
else -> {
|
||||
if (apState == APApplication.State.ANDROIDPATCH_INSTALLED || apState == APApplication.State.ANDROIDPATCH_NEED_UPDATE) {
|
||||
showUninstallDialog.value = true
|
||||
} else {
|
||||
navigator.navigate(PatchesDestination(PatchesViewModel.PatchMode.UNPATCH))
|
||||
}
|
||||
}
|
||||
}
|
||||
}, content = {
|
||||
when (kpState) {
|
||||
APApplication.State.UNKNOWN_STATE -> {
|
||||
Text(text = stringResource(id = R.string.super_key))
|
||||
}
|
||||
|
||||
APApplication.State.KERNELPATCH_NEED_UPDATE -> {
|
||||
Text(text = stringResource(id = R.string.home_ap_cando_update))
|
||||
}
|
||||
|
||||
APApplication.State.KERNELPATCH_NEED_REBOOT -> {
|
||||
Text(text = stringResource(id = R.string.home_ap_cando_reboot))
|
||||
}
|
||||
|
||||
APApplication.State.KERNELPATCH_UNINSTALLING -> {
|
||||
Icon(Icons.Outlined.Cached, contentDescription = "busy")
|
||||
}
|
||||
|
||||
else -> {
|
||||
Text(text = stringResource(id = R.string.home_ap_cando_uninstall))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AStatusCard(apState: APApplication.State) {
|
||||
ElevatedCard(
|
||||
colors = CardDefaults.elevatedCardColors(containerColor = run {
|
||||
MaterialTheme.colorScheme.secondaryContainer
|
||||
})
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Row {
|
||||
Text(
|
||||
text = stringResource(R.string.android_patch),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
when (apState) {
|
||||
APApplication.State.ANDROIDPATCH_NOT_INSTALLED -> {
|
||||
Icon(Icons.Outlined.Block, stringResource(R.string.home_not_installed))
|
||||
}
|
||||
|
||||
APApplication.State.ANDROIDPATCH_INSTALLING -> {
|
||||
Icon(Icons.Outlined.InstallMobile, stringResource(R.string.home_installing))
|
||||
}
|
||||
|
||||
APApplication.State.ANDROIDPATCH_INSTALLED -> {
|
||||
Icon(Icons.Outlined.CheckCircle, stringResource(R.string.home_working))
|
||||
}
|
||||
|
||||
APApplication.State.ANDROIDPATCH_NEED_UPDATE -> {
|
||||
Icon(Icons.Outlined.SystemUpdate, stringResource(R.string.home_need_update))
|
||||
}
|
||||
|
||||
else -> {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Outlined.HelpOutline,
|
||||
stringResource(R.string.home_install_unknown)
|
||||
)
|
||||
}
|
||||
}
|
||||
Column(
|
||||
Modifier
|
||||
.weight(2f)
|
||||
.padding(start = 16.dp)
|
||||
) {
|
||||
|
||||
when (apState) {
|
||||
APApplication.State.ANDROIDPATCH_NOT_INSTALLED -> {
|
||||
Text(
|
||||
text = stringResource(R.string.home_not_installed),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
|
||||
APApplication.State.ANDROIDPATCH_INSTALLING -> {
|
||||
Text(
|
||||
text = stringResource(R.string.home_installing),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
|
||||
APApplication.State.ANDROIDPATCH_INSTALLED -> {
|
||||
Text(
|
||||
text = stringResource(R.string.home_working),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
|
||||
APApplication.State.ANDROIDPATCH_NEED_UPDATE -> {
|
||||
Text(
|
||||
text = stringResource(R.string.home_need_update),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Spacer(Modifier.height(6.dp))
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.apatch_version_update,
|
||||
Version.installedApdVString,
|
||||
managerVersion.second
|
||||
), style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Text(
|
||||
text = stringResource(R.string.home_install_unknown),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (apState != APApplication.State.UNKNOWN_STATE) {
|
||||
Column(
|
||||
modifier = Modifier.align(Alignment.CenterVertically)
|
||||
) {
|
||||
Button(onClick = {
|
||||
when (apState) {
|
||||
APApplication.State.ANDROIDPATCH_NOT_INSTALLED, APApplication.State.ANDROIDPATCH_NEED_UPDATE -> {
|
||||
APApplication.installApatch()
|
||||
}
|
||||
|
||||
APApplication.State.ANDROIDPATCH_UNINSTALLING -> {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
else -> {
|
||||
APApplication.uninstallApatch()
|
||||
}
|
||||
}
|
||||
}, content = {
|
||||
when (apState) {
|
||||
APApplication.State.ANDROIDPATCH_NOT_INSTALLED -> {
|
||||
Text(text = stringResource(id = R.string.home_ap_cando_install))
|
||||
}
|
||||
|
||||
APApplication.State.ANDROIDPATCH_NEED_UPDATE -> {
|
||||
Text(text = stringResource(id = R.string.home_ap_cando_update))
|
||||
}
|
||||
|
||||
APApplication.State.ANDROIDPATCH_UNINSTALLING -> {
|
||||
Icon(Icons.Outlined.Cached, contentDescription = "busy")
|
||||
}
|
||||
|
||||
else -> {
|
||||
Text(text = stringResource(id = R.string.home_ap_cando_uninstall))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun WarningCard() {
|
||||
var show by rememberSaveable { mutableStateOf(apApp.getBackupWarningState()) }
|
||||
if (show) {
|
||||
ElevatedCard(
|
||||
elevation = CardDefaults.cardElevation(
|
||||
defaultElevation = 6.dp
|
||||
), colors = CardDefaults.elevatedCardColors(containerColor = run {
|
||||
MaterialTheme.colorScheme.error
|
||||
})
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(Icons.Filled.Warning, contentDescription = "warning")
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.CenterHorizontally),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = stringResource(id = R.string.patch_warnning),
|
||||
)
|
||||
|
||||
Spacer(Modifier.width(12.dp))
|
||||
|
||||
Icon(
|
||||
Icons.Outlined.Clear,
|
||||
contentDescription = "",
|
||||
modifier = Modifier.clickable {
|
||||
show = false
|
||||
apApp.updateBackupWarningState(false)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSystemVersion(): String {
|
||||
return "${Build.VERSION.RELEASE} ${if (Build.VERSION.PREVIEW_SDK_INT != 0) "Preview" else ""} (API ${Build.VERSION.SDK_INT})"
|
||||
}
|
||||
|
||||
private fun getDeviceInfo(): String {
|
||||
var manufacturer =
|
||||
Build.MANUFACTURER[0].uppercaseChar().toString() + Build.MANUFACTURER.substring(1)
|
||||
if (!Build.BRAND.equals(Build.MANUFACTURER, ignoreCase = true)) {
|
||||
manufacturer += " " + Build.BRAND[0].uppercaseChar() + Build.BRAND.substring(1)
|
||||
}
|
||||
manufacturer += " " + Build.MODEL + " "
|
||||
return manufacturer
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InfoCard(kpState: APApplication.State, apState: APApplication.State) {
|
||||
ElevatedCard {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 24.dp, top = 24.dp, end = 24.dp, bottom = 16.dp)
|
||||
) {
|
||||
val contents = StringBuilder()
|
||||
val uname = Os.uname()
|
||||
|
||||
@Composable
|
||||
fun InfoCardItem(label: String, content: String) {
|
||||
contents.appendLine(label).appendLine(content).appendLine()
|
||||
Text(text = label, style = MaterialTheme.typography.bodyLarge)
|
||||
Text(text = content, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
|
||||
if (kpState != APApplication.State.UNKNOWN_STATE) {
|
||||
InfoCardItem(
|
||||
stringResource(R.string.home_kpatch_version), Version.installedKPVString()
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
InfoCardItem(stringResource(R.string.home_su_path), Natives.suPath())
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
if (apState != APApplication.State.UNKNOWN_STATE && apState != APApplication.State.ANDROIDPATCH_NOT_INSTALLED) {
|
||||
InfoCardItem(
|
||||
stringResource(R.string.home_apatch_version), managerVersion.second.toString()
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
InfoCardItem(stringResource(R.string.home_device_info), getDeviceInfo())
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
InfoCardItem(stringResource(R.string.home_kernel), uname.release)
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
InfoCardItem(stringResource(R.string.home_system_version), getSystemVersion())
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
InfoCardItem(stringResource(R.string.home_fingerprint), Build.FINGERPRINT)
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
InfoCardItem(stringResource(R.string.home_selinux_status), getSELinuxStatus())
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WarningCard(
|
||||
message: String, color: Color = MaterialTheme.colorScheme.error, onClick: (() -> Unit)? = null
|
||||
) {
|
||||
ElevatedCard(
|
||||
colors = CardDefaults.elevatedCardColors(
|
||||
containerColor = color
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(onClick?.let { Modifier.clickable { it() } } ?: Modifier)
|
||||
.padding(24.dp)) {
|
||||
Text(
|
||||
text = message, style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UpdateCard() {
|
||||
val latestVersionInfo = LatestVersionInfo()
|
||||
val newVersion by produceState(initialValue = latestVersionInfo) {
|
||||
value = withContext(Dispatchers.IO) {
|
||||
checkNewVersion()
|
||||
}
|
||||
}
|
||||
val currentVersionCode = managerVersion.second
|
||||
val newVersionCode = newVersion.versionCode
|
||||
val newVersionUrl = newVersion.downloadUrl
|
||||
val changelog = newVersion.changelog
|
||||
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val title = stringResource(id = R.string.apm_changelog)
|
||||
val updateText = stringResource(id = R.string.apm_update)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = newVersionCode > currentVersionCode,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = shrinkVertically() + fadeOut()
|
||||
) {
|
||||
val updateDialog = rememberConfirmDialog(onConfirm = { uriHandler.openUri(newVersionUrl) })
|
||||
WarningCard(
|
||||
message = stringResource(id = R.string.home_new_apatch_found).format(newVersionCode),
|
||||
MaterialTheme.colorScheme.outlineVariant
|
||||
) {
|
||||
if (changelog.isEmpty()) {
|
||||
uriHandler.openUri(newVersionUrl)
|
||||
} else {
|
||||
updateDialog.showConfirm(
|
||||
title = title, content = changelog, markdown = true, confirm = updateText
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LearnMoreCard() {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
ElevatedCard {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
uriHandler.openUri("https://apatch.dev")
|
||||
}
|
||||
.padding(24.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.home_learn_apatch),
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.home_click_to_learn_apatch),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
175
app/src/main/java/me/bmax/apatch/ui/screen/Install.kt
Normal file
175
app/src/main/java/me/bmax/apatch/ui/screen/Install.kt
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
package me.bmax.apatch.ui.screen
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.Save
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.dropUnlessResumed
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.bmax.apatch.R
|
||||
import me.bmax.apatch.ui.component.KeyEventBlocker
|
||||
import me.bmax.apatch.util.installModule
|
||||
import me.bmax.apatch.util.reboot
|
||||
import me.bmax.apatch.util.ui.LocalSnackbarHost
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
enum class MODULE_TYPE {
|
||||
KPM, APM
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Destination<RootGraph>
|
||||
fun InstallScreen(navigator: DestinationsNavigator, uri: Uri, type: MODULE_TYPE) {
|
||||
var text by remember { mutableStateOf("") }
|
||||
var tempText: String
|
||||
val logContent = remember { StringBuilder() }
|
||||
var showFloatAction by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val snackBarHost = LocalSnackbarHost.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (text.isNotEmpty()) {
|
||||
return@LaunchedEffect
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
installModule(uri, type, onFinish = { success ->
|
||||
if (success) {
|
||||
showFloatAction = true
|
||||
}
|
||||
}, onStdout = {
|
||||
tempText = "$it\n"
|
||||
if (tempText.startsWith("[H[J")) { // clear command
|
||||
text = tempText.substring(6)
|
||||
} else {
|
||||
text += tempText
|
||||
}
|
||||
logContent.append(it).append("\n")
|
||||
}, onStderr = {
|
||||
tempText = "$it\n"
|
||||
if (tempText.startsWith("[H[J")) { // clear command
|
||||
text = tempText.substring(6)
|
||||
} else {
|
||||
text += tempText
|
||||
}
|
||||
logContent.append(it).append("\n")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(topBar = {
|
||||
TopBar(onBack = dropUnlessResumed {
|
||||
navigator.popBackStack()
|
||||
}, onSave = {
|
||||
scope.launch {
|
||||
val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault())
|
||||
val date = format.format(Date())
|
||||
val file = File(
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||
"APatch_install_${type}_log_${date}.log"
|
||||
)
|
||||
file.writeText(logContent.toString())
|
||||
snackBarHost.showSnackbar("Log saved to ${file.absolutePath}")
|
||||
}
|
||||
})
|
||||
}, floatingActionButton = {
|
||||
if (showFloatAction) {
|
||||
val reboot = stringResource(id = R.string.reboot)
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
reboot()
|
||||
}
|
||||
}
|
||||
},
|
||||
icon = { Icon(Icons.Filled.Refresh, reboot) },
|
||||
text = { Text(text = reboot) },
|
||||
)
|
||||
}
|
||||
|
||||
}, snackbarHost = { SnackbarHost(snackBarHost) }) { innerPadding ->
|
||||
KeyEventBlocker {
|
||||
it.key == Key.VolumeDown || it.key == Key.VolumeUp
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(1f)
|
||||
.padding(innerPadding)
|
||||
.verticalScroll(scrollState),
|
||||
) {
|
||||
LaunchedEffect(text) {
|
||||
scrollState.animateScrollTo(scrollState.maxValue)
|
||||
}
|
||||
Text(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
text = text,
|
||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar(onBack: () -> Unit = {}, onSave: () -> Unit = {}) {
|
||||
TopAppBar(title = { Text(stringResource(R.string.apm_install)) }, navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onBack
|
||||
) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) }
|
||||
}, actions = {
|
||||
IconButton(onClick = onSave) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Save, contentDescription = "Localized description"
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun InstallPreview() {
|
||||
// InstallScreen(DestinationsNavigator(), uri = Uri.EMPTY)
|
||||
}
|
||||
197
app/src/main/java/me/bmax/apatch/ui/screen/InstallModeSelect.kt
Normal file
197
app/src/main/java/me/bmax/apatch/ui/screen/InstallModeSelect.kt
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
package me.bmax.apatch.ui.screen
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.dropUnlessResumed
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.generated.destinations.PatchesDestination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import me.bmax.apatch.R
|
||||
import me.bmax.apatch.ui.component.rememberConfirmDialog
|
||||
import me.bmax.apatch.ui.viewmodel.PatchesViewModel
|
||||
import me.bmax.apatch.util.isABDevice
|
||||
import me.bmax.apatch.util.rootAvailable
|
||||
|
||||
var selectedBootImage: Uri? = null
|
||||
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun InstallModeSelectScreen(navigator: DestinationsNavigator) {
|
||||
var installMethod by remember {
|
||||
mutableStateOf<InstallMethod?>(null)
|
||||
}
|
||||
|
||||
Scaffold(topBar = {
|
||||
TopBar(
|
||||
onBack = dropUnlessResumed { navigator.popBackStack() },
|
||||
)
|
||||
}) {
|
||||
Column(modifier = Modifier.padding(it)) {
|
||||
SelectInstallMethod(
|
||||
onSelected = { method ->
|
||||
installMethod = method
|
||||
},
|
||||
navigator = navigator
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class InstallMethod {
|
||||
data class SelectFile(
|
||||
val uri: Uri? = null,
|
||||
@param:StringRes override val label: Int = R.string.mode_select_page_select_file,
|
||||
) : InstallMethod()
|
||||
|
||||
data object DirectInstall : InstallMethod() {
|
||||
override val label: Int
|
||||
get() = R.string.mode_select_page_patch_and_install
|
||||
}
|
||||
|
||||
data object DirectInstallToInactiveSlot : InstallMethod() {
|
||||
override val label: Int
|
||||
get() = R.string.mode_select_page_install_inactive_slot
|
||||
}
|
||||
|
||||
abstract val label: Int
|
||||
open val summary: String? = null
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SelectInstallMethod(
|
||||
onSelected: (InstallMethod) -> Unit = {},
|
||||
navigator: DestinationsNavigator
|
||||
) {
|
||||
val rootAvailable = rootAvailable()
|
||||
val isAbDevice = isABDevice()
|
||||
|
||||
val radioOptions =
|
||||
mutableListOf<InstallMethod>(InstallMethod.SelectFile())
|
||||
if (rootAvailable) {
|
||||
radioOptions.add(InstallMethod.DirectInstall)
|
||||
if (isAbDevice) {
|
||||
radioOptions.add(InstallMethod.DirectInstallToInactiveSlot)
|
||||
}
|
||||
}
|
||||
|
||||
var selectedOption by remember { mutableStateOf<InstallMethod?>(null) }
|
||||
val selectImageLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
if (it.resultCode == Activity.RESULT_OK) {
|
||||
it.data?.data?.let { uri ->
|
||||
val option = InstallMethod.SelectFile(uri)
|
||||
selectedOption = option
|
||||
onSelected(option)
|
||||
selectedBootImage = option.uri
|
||||
navigator.navigate(PatchesDestination(PatchesViewModel.PatchMode.PATCH_ONLY))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val confirmDialog = rememberConfirmDialog(onConfirm = {
|
||||
selectedOption = InstallMethod.DirectInstallToInactiveSlot
|
||||
onSelected(InstallMethod.DirectInstallToInactiveSlot)
|
||||
navigator.navigate(PatchesDestination(PatchesViewModel.PatchMode.INSTALL_TO_NEXT_SLOT))
|
||||
}, onDismiss = null)
|
||||
val dialogTitle = stringResource(id = android.R.string.dialog_alert_title)
|
||||
val dialogContent = stringResource(id = R.string.mode_select_page_install_inactive_slot_warning)
|
||||
|
||||
val onClick = { option: InstallMethod ->
|
||||
when (option) {
|
||||
is InstallMethod.SelectFile -> {
|
||||
// Reset before selecting
|
||||
selectedBootImage = null
|
||||
selectImageLauncher.launch(
|
||||
Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
type = "application/octet-stream"
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
is InstallMethod.DirectInstall -> {
|
||||
selectedOption = option
|
||||
onSelected(option)
|
||||
navigator.navigate(PatchesDestination(PatchesViewModel.PatchMode.PATCH_AND_INSTALL))
|
||||
}
|
||||
|
||||
is InstallMethod.DirectInstallToInactiveSlot -> {
|
||||
confirmDialog.showConfirm(dialogTitle, dialogContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
radioOptions.forEach { option ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
onClick(option)
|
||||
}) {
|
||||
RadioButton(selected = option.javaClass == selectedOption?.javaClass, onClick = {
|
||||
onClick(option)
|
||||
})
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(id = option.label),
|
||||
fontSize = MaterialTheme.typography.titleMedium.fontSize,
|
||||
fontFamily = MaterialTheme.typography.titleMedium.fontFamily,
|
||||
fontStyle = MaterialTheme.typography.titleMedium.fontStyle
|
||||
)
|
||||
option.summary?.let {
|
||||
Text(
|
||||
text = it,
|
||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
|
||||
fontStyle = MaterialTheme.typography.bodySmall.fontStyle
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar(onBack: () -> Unit = {}) {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.mode_select_page_title)) },
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onBack
|
||||
) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) }
|
||||
},
|
||||
)
|
||||
}
|
||||
607
app/src/main/java/me/bmax/apatch/ui/screen/KPM.kt
Normal file
607
app/src/main/java/me/bmax/apatch/ui/screen/KPM.kt
Normal file
|
|
@ -0,0 +1,607 @@
|
|||
package me.bmax.apatch.ui.screen
|
||||
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material3.AlertDialogDefaults
|
||||
import androidx.compose.material3.BasicAlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.compose.ui.window.DialogWindowProvider
|
||||
import androidx.compose.ui.window.PopupProperties
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.generated.destinations.InstallScreenDestination
|
||||
import com.ramcosta.composedestinations.generated.destinations.PatchesDestination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import com.topjohnwu.superuser.nio.ExtendedFile
|
||||
import com.topjohnwu.superuser.nio.FileSystemManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.bmax.apatch.APApplication
|
||||
import me.bmax.apatch.Natives
|
||||
import me.bmax.apatch.R
|
||||
import me.bmax.apatch.apApp
|
||||
import me.bmax.apatch.ui.component.ConfirmResult
|
||||
import me.bmax.apatch.ui.component.KPModuleRemoveButton
|
||||
import me.bmax.apatch.ui.component.LoadingDialogHandle
|
||||
import me.bmax.apatch.ui.component.ProvideMenuShape
|
||||
import me.bmax.apatch.ui.component.rememberConfirmDialog
|
||||
import me.bmax.apatch.ui.component.rememberLoadingDialog
|
||||
import me.bmax.apatch.ui.viewmodel.KPModel
|
||||
import me.bmax.apatch.ui.viewmodel.KPModuleViewModel
|
||||
import me.bmax.apatch.ui.viewmodel.PatchesViewModel
|
||||
import me.bmax.apatch.util.inputStream
|
||||
import me.bmax.apatch.util.ui.APDialogBlurBehindUtils
|
||||
import me.bmax.apatch.util.writeTo
|
||||
import java.io.IOException
|
||||
|
||||
private const val TAG = "KernelPatchModule"
|
||||
private lateinit var targetKPMToControl: KPModel.KPMInfo
|
||||
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun KPModuleScreen(navigator: DestinationsNavigator) {
|
||||
val state by APApplication.apStateLiveData.observeAsState(APApplication.State.UNKNOWN_STATE)
|
||||
if (state == APApplication.State.UNKNOWN_STATE) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(12.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Row {
|
||||
Text(
|
||||
text = stringResource(id = R.string.kpm_kp_not_installed),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val viewModel = viewModel<KPModuleViewModel>()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (viewModel.moduleList.isEmpty() || viewModel.isNeedRefresh) {
|
||||
viewModel.fetchModuleList()
|
||||
}
|
||||
}
|
||||
|
||||
val kpModuleListState = rememberLazyListState()
|
||||
|
||||
Scaffold(topBar = {
|
||||
TopBar()
|
||||
}, floatingActionButton = run {
|
||||
{
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
||||
val moduleLoad = stringResource(id = R.string.kpm_load)
|
||||
val moduleInstall = stringResource(id = R.string.kpm_install)
|
||||
val moduleEmbed = stringResource(id = R.string.kpm_embed)
|
||||
val successToastText = stringResource(id = R.string.kpm_load_toast_succ)
|
||||
val failToastText = stringResource(id = R.string.kpm_load_toast_failed)
|
||||
val loadingDialog = rememberLoadingDialog()
|
||||
|
||||
val selectZipLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
if (it.resultCode != RESULT_OK) {
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
val data = it.data ?: return@rememberLauncherForActivityResult
|
||||
val uri = data.data ?: return@rememberLauncherForActivityResult
|
||||
|
||||
Log.i(TAG, "select zip result: $uri")
|
||||
|
||||
navigator.navigate(InstallScreenDestination(uri, MODULE_TYPE.KPM))
|
||||
}
|
||||
|
||||
val selectKpmLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
if (it.resultCode != RESULT_OK) {
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
val data = it.data ?: return@rememberLauncherForActivityResult
|
||||
val uri = data.data ?: return@rememberLauncherForActivityResult
|
||||
|
||||
// todo: args
|
||||
scope.launch {
|
||||
val rc = loadModule(loadingDialog, uri, "") == 0
|
||||
val toastText = if (rc) successToastText else failToastText
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
context, toastText, Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
viewModel.markNeedRefresh()
|
||||
viewModel.fetchModuleList()
|
||||
}
|
||||
}
|
||||
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
val options = listOf(moduleEmbed, moduleInstall, moduleLoad)
|
||||
|
||||
Column {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
expanded = !expanded
|
||||
},
|
||||
contentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.package_import),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
|
||||
ProvideMenuShape(RoundedCornerShape(10.dp)) {
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
properties = PopupProperties(focusable = true)
|
||||
) {
|
||||
options.forEach { label ->
|
||||
DropdownMenuItem(text = { Text(label) }, onClick = {
|
||||
expanded = false
|
||||
when (label) {
|
||||
moduleEmbed -> {
|
||||
navigator.navigate(PatchesDestination(PatchesViewModel.PatchMode.PATCH_AND_INSTALL))
|
||||
}
|
||||
|
||||
moduleInstall -> {
|
||||
// val intent = Intent(Intent.ACTION_GET_CONTENT)
|
||||
// intent.type = "application/zip"
|
||||
// selectZipLauncher.launch(intent)
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Under development",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
moduleLoad -> {
|
||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
||||
intent.type = "*/*"
|
||||
selectKpmLauncher.launch(intent)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}) { innerPadding ->
|
||||
|
||||
KPModuleList(
|
||||
viewModel = viewModel,
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxSize(),
|
||||
state = kpModuleListState
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadModule(loadingDialog: LoadingDialogHandle, uri: Uri, args: String): Int {
|
||||
val rc = loadingDialog.withLoading {
|
||||
withContext(Dispatchers.IO) {
|
||||
run {
|
||||
val kpmDir: ExtendedFile =
|
||||
FileSystemManager.getLocal().getFile(apApp.filesDir.parent, "kpm")
|
||||
kpmDir.deleteRecursively()
|
||||
kpmDir.mkdirs()
|
||||
val rand = (1..4).map { ('a'..'z').random() }.joinToString("")
|
||||
val kpm = kpmDir.getChildFile("${rand}.kpm")
|
||||
Log.d(TAG, "save tmp kpm: ${kpm.path}")
|
||||
var rc = -1
|
||||
try {
|
||||
uri.inputStream().buffered().writeTo(kpm)
|
||||
rc = Natives.loadKernelPatchModule(kpm.path, args).toInt()
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Copy kpm error: $e")
|
||||
}
|
||||
Log.d(TAG, "load ${kpm.path} rc: $rc")
|
||||
rc
|
||||
}
|
||||
}
|
||||
}
|
||||
return rc
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun KPMControlDialog(showDialog: MutableState<Boolean>) {
|
||||
var controlParam by remember { mutableStateOf("") }
|
||||
var enable by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val loadingDialog = rememberLoadingDialog()
|
||||
val context = LocalContext.current
|
||||
val outMsgStringRes = stringResource(id = R.string.kpm_control_outMsg)
|
||||
val okStringRes = stringResource(id = R.string.kpm_control_ok)
|
||||
val failedStringRes = stringResource(id = R.string.kpm_control_failed)
|
||||
|
||||
lateinit var controlResult: Natives.KPMCtlRes
|
||||
|
||||
suspend fun onModuleControl(module: KPModel.KPMInfo) {
|
||||
loadingDialog.withLoading {
|
||||
withContext(Dispatchers.IO) {
|
||||
controlResult = Natives.kernelPatchModuleControl(module.name, controlParam)
|
||||
}
|
||||
}
|
||||
|
||||
if (controlResult.rc >= 0) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"$okStringRes\n${outMsgStringRes}: ${controlResult.outMsg}",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"$failedStringRes\n${outMsgStringRes}: ${controlResult.outMsg}",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
BasicAlertDialog(
|
||||
onDismissRequest = { showDialog.value = false }, properties = DialogProperties(
|
||||
decorFitsSystemWindows = true,
|
||||
usePlatformDefaultWidth = false,
|
||||
)
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.width(310.dp)
|
||||
.wrapContentHeight(),
|
||||
shape = RoundedCornerShape(30.dp),
|
||||
tonalElevation = AlertDialogDefaults.TonalElevation,
|
||||
color = AlertDialogDefaults.containerColor,
|
||||
) {
|
||||
Column(modifier = Modifier.padding(PaddingValues(all = 24.dp))) {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(PaddingValues(bottom = 16.dp))
|
||||
.align(Alignment.Start)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.kpm_control_dialog_title),
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
Modifier
|
||||
.weight(weight = 1f, fill = false)
|
||||
.align(Alignment.Start)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.kpm_control_dialog_content),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
contentAlignment = Alignment.CenterEnd,
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = controlParam,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 6.dp),
|
||||
onValueChange = {
|
||||
controlParam = it
|
||||
enable = controlParam.isNotBlank()
|
||||
},
|
||||
shape = RoundedCornerShape(50.0f),
|
||||
label = { Text(stringResource(id = R.string.kpm_control_paramters)) },
|
||||
visualTransformation = VisualTransformation.None,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
TextButton(onClick = { showDialog.value = false }) {
|
||||
Text(stringResource(id = android.R.string.cancel))
|
||||
}
|
||||
|
||||
Button(onClick = {
|
||||
showDialog.value = false
|
||||
|
||||
scope.launch { onModuleControl(targetKPMToControl) }
|
||||
|
||||
}, enabled = enable) {
|
||||
Text(stringResource(id = android.R.string.ok))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val dialogWindowProvider = LocalView.current.parent as DialogWindowProvider
|
||||
APDialogBlurBehindUtils.setupWindowBlurListener(dialogWindowProvider.window)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun KPModuleList(
|
||||
viewModel: KPModuleViewModel, modifier: Modifier = Modifier, state: LazyListState
|
||||
) {
|
||||
val moduleStr = stringResource(id = R.string.kpm)
|
||||
val moduleUninstallConfirm = stringResource(id = R.string.kpm_unload_confirm)
|
||||
val uninstall = stringResource(id = R.string.kpm_unload)
|
||||
val cancel = stringResource(id = android.R.string.cancel)
|
||||
|
||||
val confirmDialog = rememberConfirmDialog()
|
||||
val loadingDialog = rememberLoadingDialog()
|
||||
|
||||
val showKPMControlDialog = remember { mutableStateOf(false) }
|
||||
if (showKPMControlDialog.value) {
|
||||
KPMControlDialog(showDialog = showKPMControlDialog)
|
||||
}
|
||||
|
||||
suspend fun onModuleUninstall(module: KPModel.KPMInfo) {
|
||||
val confirmResult = confirmDialog.awaitConfirm(
|
||||
moduleStr,
|
||||
content = moduleUninstallConfirm.format(module.name),
|
||||
confirm = uninstall,
|
||||
dismiss = cancel
|
||||
)
|
||||
if (confirmResult != ConfirmResult.Confirmed) {
|
||||
return
|
||||
}
|
||||
|
||||
val success = loadingDialog.withLoading {
|
||||
withContext(Dispatchers.IO) {
|
||||
Natives.unloadKernelPatchModule(module.name) == 0L
|
||||
}
|
||||
}
|
||||
|
||||
if (success) {
|
||||
viewModel.fetchModuleList()
|
||||
}
|
||||
}
|
||||
|
||||
PullToRefreshBox(
|
||||
modifier = modifier,
|
||||
onRefresh = { viewModel.fetchModuleList() },
|
||||
isRefreshing = viewModel.isRefreshing
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = state,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
contentPadding = remember {
|
||||
PaddingValues(
|
||||
start = 16.dp,
|
||||
top = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp + 16.dp + 56.dp /* Scaffold Fab Spacing + Fab container height */
|
||||
)
|
||||
},
|
||||
) {
|
||||
when {
|
||||
viewModel.moduleList.isEmpty() -> {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier.fillParentMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.kpm_apm_empty), textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
items(viewModel.moduleList) { module ->
|
||||
val scope = rememberCoroutineScope()
|
||||
KPModuleItem(
|
||||
module,
|
||||
onUninstall = {
|
||||
scope.launch { onModuleUninstall(module) }
|
||||
},
|
||||
onControl = {
|
||||
targetKPMToControl = module
|
||||
showKPMControlDialog.value = true
|
||||
},
|
||||
)
|
||||
|
||||
// fix last item shadow incomplete in LazyColumn
|
||||
Spacer(Modifier.height(1.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar() {
|
||||
TopAppBar(title = { Text(stringResource(R.string.kpm)) })
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun KPModuleItem(
|
||||
module: KPModel.KPMInfo,
|
||||
onUninstall: (KPModel.KPMInfo) -> Unit,
|
||||
onControl: (KPModel.KPMInfo) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
alpha: Float = 1f,
|
||||
) {
|
||||
val moduleAuthor = stringResource(id = R.string.kpm_author)
|
||||
val moduleArgs = stringResource(id = R.string.kpm_args)
|
||||
val decoration = TextDecoration.None
|
||||
|
||||
Surface(
|
||||
modifier = modifier,
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
tonalElevation = 1.dp,
|
||||
shape = RoundedCornerShape(20.dp)
|
||||
) {
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(all = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.alpha(alpha = alpha)
|
||||
.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
Text(
|
||||
text = module.name,
|
||||
style = MaterialTheme.typography.titleSmall.copy(fontWeight = FontWeight.Bold),
|
||||
maxLines = 2,
|
||||
textDecoration = decoration,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "${module.version}, $moduleAuthor ${module.author}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
textDecoration = decoration,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "$moduleArgs: ${module.args}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
textDecoration = decoration,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.alpha(alpha = alpha)
|
||||
.padding(horizontal = 16.dp),
|
||||
text = module.description,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
textDecoration = decoration,
|
||||
color = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
|
||||
HorizontalDivider(
|
||||
thickness = 1.5.dp,
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
FilledTonalButton(
|
||||
onClick = { onControl(module) },
|
||||
enabled = true,
|
||||
contentPadding = PaddingValues(horizontal = 12.dp)
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
painter = painterResource(id = R.drawable.settings),
|
||||
contentDescription = null
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.kpm_control),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Visible,
|
||||
softWrap = false
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
KPModuleRemoveButton(enabled = true, onClick = { onUninstall(module) })
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
624
app/src/main/java/me/bmax/apatch/ui/screen/Patches.kt
Normal file
624
app/src/main/java/me/bmax/apatch/ui/screen/Patches.kt
Normal file
|
|
@ -0,0 +1,624 @@
|
|||
package me.bmax.apatch.ui.screen
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.bmax.apatch.R
|
||||
import me.bmax.apatch.ui.viewmodel.KPModel
|
||||
import me.bmax.apatch.ui.viewmodel.PatchesViewModel
|
||||
import me.bmax.apatch.util.Version
|
||||
import me.bmax.apatch.util.reboot
|
||||
|
||||
private const val TAG = "Patches"
|
||||
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun Patches(mode: PatchesViewModel.PatchMode) {
|
||||
val scrollState = rememberScrollState()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val viewModel = viewModel<PatchesViewModel>()
|
||||
SideEffect {
|
||||
viewModel.prepare(mode)
|
||||
}
|
||||
|
||||
Scaffold(topBar = {
|
||||
TopBar()
|
||||
}, floatingActionButton = {
|
||||
if (viewModel.needReboot) {
|
||||
val reboot = stringResource(id = R.string.reboot)
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
reboot()
|
||||
}
|
||||
}
|
||||
},
|
||||
icon = { Icon(Icons.Filled.Refresh, reboot) },
|
||||
text = { Text(text = reboot) },
|
||||
)
|
||||
}
|
||||
}) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.padding(horizontal = 16.dp)
|
||||
.verticalScroll(scrollState),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
// request permissions
|
||||
val permissions = arrayOf(
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
)
|
||||
val permissionsToRequest = permissions.filter {
|
||||
ContextCompat.checkSelfPermission(context, it) != PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
if (permissionsToRequest.isNotEmpty()) {
|
||||
ActivityCompat.requestPermissions(
|
||||
context as Activity,
|
||||
permissionsToRequest.toTypedArray(),
|
||||
1001
|
||||
)
|
||||
}
|
||||
|
||||
PatchMode(mode)
|
||||
ErrorView(viewModel.error)
|
||||
KernelPatchImageView(viewModel.kpimgInfo)
|
||||
|
||||
if (mode == PatchesViewModel.PatchMode.PATCH_ONLY && selectedBootImage != null && viewModel.kimgInfo.banner.isEmpty()) {
|
||||
viewModel.copyAndParseBootimg(selectedBootImage!!)
|
||||
// Fix endless loop. It's not normal if (parse done && working thread is not working) but banner still null
|
||||
// Leave user re-choose
|
||||
if (!viewModel.running && viewModel.kimgInfo.banner.isEmpty()) {
|
||||
selectedBootImage = null
|
||||
}
|
||||
}
|
||||
|
||||
// select boot.img
|
||||
if (mode == PatchesViewModel.PatchMode.PATCH_ONLY && viewModel.kimgInfo.banner.isEmpty()) {
|
||||
SelectFileButton(
|
||||
text = stringResource(id = R.string.patch_select_bootimg_btn),
|
||||
onSelected = { data, uri ->
|
||||
Log.d(TAG, "select boot.img, data: $data, uri: $uri")
|
||||
viewModel.copyAndParseBootimg(uri)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (viewModel.bootSlot.isNotEmpty() || viewModel.bootDev.isNotEmpty()) {
|
||||
BootimgView(slot = viewModel.bootSlot, boot = viewModel.bootDev)
|
||||
}
|
||||
|
||||
if (viewModel.kimgInfo.banner.isNotEmpty()) {
|
||||
KernelImageView(viewModel.kimgInfo)
|
||||
}
|
||||
|
||||
if (mode != PatchesViewModel.PatchMode.UNPATCH && viewModel.kimgInfo.banner.isNotEmpty()) {
|
||||
SetSuperKeyView(viewModel)
|
||||
}
|
||||
|
||||
// existed extras
|
||||
if (mode == PatchesViewModel.PatchMode.PATCH_AND_INSTALL || mode == PatchesViewModel.PatchMode.INSTALL_TO_NEXT_SLOT) {
|
||||
viewModel.existedExtras.forEach(action = {
|
||||
ExtraItem(extra = it, true, onDelete = {
|
||||
viewModel.existedExtras.remove(it)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// add new extras
|
||||
if (mode != PatchesViewModel.PatchMode.UNPATCH) {
|
||||
viewModel.newExtras.forEach(action = {
|
||||
ExtraItem(extra = it, false, onDelete = {
|
||||
val idx = viewModel.newExtras.indexOf(it)
|
||||
viewModel.newExtras.remove(it)
|
||||
viewModel.newExtrasFileName.removeAt(idx)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// add new KPM
|
||||
if (viewModel.superkey.isNotEmpty() && !viewModel.patching && !viewModel.patchdone && mode != PatchesViewModel.PatchMode.UNPATCH) {
|
||||
SelectFileButton(
|
||||
text = stringResource(id = R.string.patch_embed_kpm_btn),
|
||||
onSelected = { data, uri ->
|
||||
Log.d(TAG, "select kpm, data: $data, uri: $uri")
|
||||
viewModel.embedKPM(uri)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// do patch, update, unpatch
|
||||
if (!viewModel.patching && !viewModel.patchdone) {
|
||||
// patch start
|
||||
if (mode != PatchesViewModel.PatchMode.UNPATCH && viewModel.superkey.isNotEmpty()) {
|
||||
StartButton(stringResource(id = R.string.patch_start_patch_btn)) {
|
||||
viewModel.doPatch(
|
||||
mode
|
||||
)
|
||||
}
|
||||
}
|
||||
// unpatch
|
||||
if (mode == PatchesViewModel.PatchMode.UNPATCH && viewModel.kimgInfo.banner.isNotEmpty()) {
|
||||
StartButton(stringResource(id = R.string.patch_start_unpatch_btn)) { viewModel.doUnpatch() }
|
||||
}
|
||||
}
|
||||
|
||||
// patch log
|
||||
if (viewModel.patching || viewModel.patchdone) {
|
||||
SelectionContainer {
|
||||
Text(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
text = viewModel.patchLog,
|
||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
|
||||
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
||||
)
|
||||
}
|
||||
LaunchedEffect(viewModel.patchLog) {
|
||||
scrollState.animateScrollTo(scrollState.maxValue)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// loading progress
|
||||
if (viewModel.running) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.align(Alignment.CenterHorizontally)
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.size(50.dp)
|
||||
.padding(16.dp)
|
||||
.align(Alignment.BottomCenter)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun StartButton(text: String, onClick: () -> Unit) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.End
|
||||
) {
|
||||
Button(
|
||||
onClick = onClick,
|
||||
content = {
|
||||
Text(text = text)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExtraItem(extra: KPModel.IExtraInfo, existed: Boolean, onDelete: () -> Unit) {
|
||||
ElevatedCard(
|
||||
colors = CardDefaults.elevatedCardColors(containerColor = run {
|
||||
MaterialTheme.colorScheme.secondaryContainer
|
||||
}),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
) {
|
||||
Row(modifier = Modifier.align(Alignment.CenterHorizontally)) {
|
||||
Text(
|
||||
text = stringResource(
|
||||
id =
|
||||
if (existed) R.string.patch_item_existed_extra_kpm else R.string.patch_item_new_extra_kpm
|
||||
) +
|
||||
" " + extra.type.toString().uppercase(),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.wrapContentWidth(Alignment.CenterHorizontally)
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = "Delete",
|
||||
modifier = Modifier
|
||||
.padding(end = 8.dp)
|
||||
.clickable { onDelete() })
|
||||
}
|
||||
if (extra.type == KPModel.ExtraType.KPM) {
|
||||
val kpmInfo: KPModel.KPMInfo = extra as KPModel.KPMInfo
|
||||
Text(
|
||||
text = "${stringResource(id = R.string.patch_item_extra_name) + " "} ${kpmInfo.name}",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Text(
|
||||
text = "${stringResource(id = R.string.patch_item_extra_version) + " "} ${kpmInfo.version}",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Text(
|
||||
text = "${stringResource(id = R.string.patch_item_extra_kpm_license) + " "} ${kpmInfo.license}",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Text(
|
||||
text = "${stringResource(id = R.string.patch_item_extra_author) + " "} ${kpmInfo.author}",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Text(
|
||||
text = "${stringResource(id = R.string.patch_item_extra_kpm_desciption) + " "} ${kpmInfo.description}",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
var event by remember { mutableStateOf(kpmInfo.event) }
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color.LightGray)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.patch_item_extra_event) + " ",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
BasicTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = event,
|
||||
textStyle = MaterialTheme.typography.bodyMedium,
|
||||
onValueChange = {
|
||||
event = it
|
||||
kpmInfo.event = it
|
||||
},
|
||||
)
|
||||
}
|
||||
var args by remember { mutableStateOf(kpmInfo.args) }
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(Color.LightGray)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.patch_item_extra_args) + " ",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
BasicTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = args,
|
||||
textStyle = MaterialTheme.typography.bodyMedium,
|
||||
onValueChange = {
|
||||
args = it
|
||||
kpmInfo.args = it
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun SetSuperKeyView(viewModel: PatchesViewModel) {
|
||||
var skey by remember { mutableStateOf(viewModel.superkey) }
|
||||
var showWarn by remember { mutableStateOf(!viewModel.checkSuperKeyValidation(skey)) }
|
||||
var keyVisible by remember { mutableStateOf(false) }
|
||||
ElevatedCard(
|
||||
colors = CardDefaults.elevatedCardColors(containerColor = run {
|
||||
MaterialTheme.colorScheme.secondaryContainer
|
||||
})
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.patch_item_skey),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
if (showWarn) {
|
||||
Spacer(modifier = Modifier.height(3.dp))
|
||||
Text(
|
||||
color = Color.Red,
|
||||
text = stringResource(id = R.string.patch_item_set_skey_label),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
Column {
|
||||
//Spacer(modifier = Modifier.height(8.dp))
|
||||
Box(
|
||||
contentAlignment = Alignment.CenterEnd,
|
||||
) {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 6.dp),
|
||||
value = skey,
|
||||
label = { Text(stringResource(id = R.string.patch_set_superkey)) },
|
||||
visualTransformation = if (keyVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||
shape = RoundedCornerShape(50.0f),
|
||||
onValueChange = {
|
||||
skey = it
|
||||
if (viewModel.checkSuperKeyValidation(it)) {
|
||||
viewModel.superkey = it
|
||||
showWarn = false
|
||||
} else {
|
||||
viewModel.superkey = ""
|
||||
showWarn = true
|
||||
}
|
||||
},
|
||||
)
|
||||
IconButton(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.padding(top = 15.dp, end = 5.dp),
|
||||
onClick = { keyVisible = !keyVisible }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (keyVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff,
|
||||
contentDescription = null,
|
||||
tint = Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun KernelPatchImageView(kpImgInfo: KPModel.KPImgInfo) {
|
||||
if (kpImgInfo.version.isEmpty()) return
|
||||
ElevatedCard(
|
||||
colors = CardDefaults.elevatedCardColors(containerColor = run {
|
||||
MaterialTheme.colorScheme.secondaryContainer
|
||||
})
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 12.dp, top = 12.dp, end = 12.dp, bottom = 12.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.patch_item_kpimg),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = stringResource(id = R.string.patch_item_kpimg_version) + " " + Version.uInt2String(
|
||||
kpImgInfo.version.substring(2).toUInt(16)
|
||||
), style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.patch_item_kpimg_comile_time) + " " + kpImgInfo.compileTime,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.patch_item_kpimg_config) + " " + kpImgInfo.config,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BootimgView(slot: String, boot: String) {
|
||||
ElevatedCard(
|
||||
colors = CardDefaults.elevatedCardColors(containerColor = run {
|
||||
MaterialTheme.colorScheme.secondaryContainer
|
||||
})
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.patch_item_bootimg),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
if (slot.isNotEmpty()) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.patch_item_bootimg_slot) + " " + slot,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = stringResource(id = R.string.patch_item_bootimg_dev) + " " + boot,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun KernelImageView(kImgInfo: KPModel.KImgInfo) {
|
||||
ElevatedCard(
|
||||
colors = CardDefaults.elevatedCardColors(containerColor = run {
|
||||
MaterialTheme.colorScheme.secondaryContainer
|
||||
})
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.patch_item_kernel),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
Text(text = kImgInfo.banner, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun SelectFileButton(text: String, onSelected: (data: Intent, uri: Uri) -> Unit) {
|
||||
val selectFileLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
if (it.resultCode != Activity.RESULT_OK) {
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
val data = it.data ?: return@rememberLauncherForActivityResult
|
||||
val uri = data.data ?: return@rememberLauncherForActivityResult
|
||||
onSelected(data, uri)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.End
|
||||
) {
|
||||
Button(
|
||||
onClick = {
|
||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
||||
intent.type = "*/*"
|
||||
selectFileLauncher.launch(intent)
|
||||
},
|
||||
content = { Text(text = text) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ErrorView(error: String) {
|
||||
if (error.isEmpty()) return
|
||||
ElevatedCard(
|
||||
colors = CardDefaults.elevatedCardColors(containerColor = run {
|
||||
MaterialTheme.colorScheme.error
|
||||
})
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 12.dp, top = 12.dp, end = 12.dp, bottom = 12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.patch_item_error),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
Text(text = error, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PatchMode(mode: PatchesViewModel.PatchMode) {
|
||||
ElevatedCard(
|
||||
colors = CardDefaults.elevatedCardColors(containerColor = run {
|
||||
MaterialTheme.colorScheme.secondaryContainer
|
||||
})
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(text = stringResource(id = mode.sId), style = MaterialTheme.typography.bodyLarge)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar() {
|
||||
TopAppBar(title = { Text(stringResource(R.string.patch_config_title)) })
|
||||
}
|
||||
751
app/src/main/java/me/bmax/apatch/ui/screen/Settings.kt
Normal file
751
app/src/main/java/me/bmax/apatch/ui/screen/Settings.kt
Normal file
|
|
@ -0,0 +1,751 @@
|
|||
package me.bmax.apatch.ui.screen
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.BugReport
|
||||
import androidx.compose.material.icons.filled.ColorLens
|
||||
import androidx.compose.material.icons.filled.Commit
|
||||
import androidx.compose.material.icons.filled.DarkMode
|
||||
import androidx.compose.material.icons.filled.DeveloperMode
|
||||
import androidx.compose.material.icons.filled.Engineering
|
||||
import androidx.compose.material.icons.filled.FilePresent
|
||||
import androidx.compose.material.icons.filled.FormatColorFill
|
||||
import androidx.compose.material.icons.filled.InvertColors
|
||||
import androidx.compose.material.icons.filled.Key
|
||||
import androidx.compose.material.icons.filled.RemoveFromQueue
|
||||
import androidx.compose.material.icons.filled.Save
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material.icons.filled.Translate
|
||||
import androidx.compose.material.icons.filled.Update
|
||||
import androidx.compose.material3.AlertDialogDefaults
|
||||
import androidx.compose.material3.BasicAlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.stringArrayResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.LineHeightStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.compose.ui.window.DialogWindowProvider
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.bmax.apatch.APApplication
|
||||
import me.bmax.apatch.BuildConfig
|
||||
import me.bmax.apatch.Natives
|
||||
import me.bmax.apatch.R
|
||||
import me.bmax.apatch.ui.component.SwitchItem
|
||||
import me.bmax.apatch.ui.component.rememberConfirmDialog
|
||||
import me.bmax.apatch.ui.component.rememberLoadingDialog
|
||||
import me.bmax.apatch.ui.theme.refreshTheme
|
||||
import me.bmax.apatch.util.APatchKeyHelper
|
||||
import me.bmax.apatch.util.getBugreportFile
|
||||
import me.bmax.apatch.util.isForceUsingOverlayFS
|
||||
import me.bmax.apatch.util.isGlobalNamespaceEnabled
|
||||
import me.bmax.apatch.util.isLiteModeEnabled
|
||||
import me.bmax.apatch.util.outputStream
|
||||
import me.bmax.apatch.util.overlayFsAvailable
|
||||
import me.bmax.apatch.util.rootShellForResult
|
||||
import me.bmax.apatch.util.setForceUsingOverlayFS
|
||||
import me.bmax.apatch.util.setGlobalNamespaceEnabled
|
||||
import me.bmax.apatch.util.setLiteMode
|
||||
import me.bmax.apatch.util.ui.APDialogBlurBehindUtils
|
||||
import me.bmax.apatch.util.ui.LocalSnackbarHost
|
||||
import me.bmax.apatch.util.ui.NavigationBarsSpacer
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Locale
|
||||
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
fun SettingScreen() {
|
||||
val state by APApplication.apStateLiveData.observeAsState(APApplication.State.UNKNOWN_STATE)
|
||||
val kPatchReady = state != APApplication.State.UNKNOWN_STATE
|
||||
val aPatchReady =
|
||||
(state == APApplication.State.ANDROIDPATCH_INSTALLING || state == APApplication.State.ANDROIDPATCH_INSTALLED || state == APApplication.State.ANDROIDPATCH_NEED_UPDATE)
|
||||
var isGlobalNamespaceEnabled by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
var isLiteModeEnabled by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
var forceUsingOverlayFS by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
var bSkipStoreSuperKey by rememberSaveable {
|
||||
mutableStateOf(APatchKeyHelper.shouldSkipStoreSuperKey())
|
||||
}
|
||||
val isOverlayFSAvailable by rememberSaveable {
|
||||
mutableStateOf(overlayFsAvailable())
|
||||
}
|
||||
if (kPatchReady && aPatchReady) {
|
||||
isGlobalNamespaceEnabled = isGlobalNamespaceEnabled()
|
||||
isLiteModeEnabled = isLiteModeEnabled()
|
||||
forceUsingOverlayFS = isForceUsingOverlayFS()
|
||||
}
|
||||
|
||||
val snackBarHost = LocalSnackbarHost.current
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.settings)) },
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackBarHost) }
|
||||
) { paddingValues ->
|
||||
|
||||
val loadingDialog = rememberLoadingDialog()
|
||||
val clearKeyDialog = rememberConfirmDialog(
|
||||
onConfirm = {
|
||||
APatchKeyHelper.clearConfigKey()
|
||||
APApplication.superKey = ""
|
||||
}
|
||||
)
|
||||
|
||||
val showLanguageDialog = rememberSaveable { mutableStateOf(false) }
|
||||
LanguageDialog(showLanguageDialog)
|
||||
|
||||
val showResetSuPathDialog = remember { mutableStateOf(false) }
|
||||
if (showResetSuPathDialog.value) {
|
||||
ResetSUPathDialog(showResetSuPathDialog)
|
||||
}
|
||||
|
||||
val showThemeChooseDialog = remember { mutableStateOf(false) }
|
||||
if (showThemeChooseDialog.value) {
|
||||
ThemeChooseDialog(showThemeChooseDialog)
|
||||
}
|
||||
|
||||
var showLogBottomSheet by remember { mutableStateOf(false) }
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val logSavedMessage = stringResource(R.string.log_saved)
|
||||
val exportBugreportLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.CreateDocument("application/gzip")
|
||||
) { uri: Uri? ->
|
||||
if (uri != null) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
loadingDialog.show()
|
||||
uri.outputStream().use { output ->
|
||||
getBugreportFile(context).inputStream().use {
|
||||
it.copyTo(output)
|
||||
}
|
||||
}
|
||||
loadingDialog.hide()
|
||||
snackBarHost.showSnackbar(message = logSavedMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val prefs = APApplication.sharedPreferences
|
||||
|
||||
// clear key
|
||||
if (kPatchReady) {
|
||||
val clearKeyDialogTitle = stringResource(id = R.string.clear_super_key)
|
||||
val clearKeyDialogContent =
|
||||
stringResource(id = R.string.settings_clear_super_key_dialog)
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Filled.Key, stringResource(id = R.string.super_key)
|
||||
)
|
||||
},
|
||||
headlineContent = { Text(stringResource(id = R.string.clear_super_key)) },
|
||||
modifier = Modifier.clickable {
|
||||
clearKeyDialog.showConfirm(
|
||||
title = clearKeyDialogTitle,
|
||||
content = clearKeyDialogContent,
|
||||
markdown = false,
|
||||
)
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
// store key local?
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.Key,
|
||||
title = stringResource(id = R.string.settings_donot_store_superkey),
|
||||
summary = stringResource(id = R.string.settings_donot_store_superkey_summary),
|
||||
checked = bSkipStoreSuperKey,
|
||||
onCheckedChange = {
|
||||
bSkipStoreSuperKey = it
|
||||
APatchKeyHelper.setShouldSkipStoreSuperKey(bSkipStoreSuperKey)
|
||||
})
|
||||
|
||||
// Global mount
|
||||
if (kPatchReady && aPatchReady) {
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.Engineering,
|
||||
title = stringResource(id = R.string.settings_global_namespace_mode),
|
||||
summary = stringResource(id = R.string.settings_global_namespace_mode_summary),
|
||||
checked = isGlobalNamespaceEnabled,
|
||||
onCheckedChange = {
|
||||
setGlobalNamespaceEnabled(
|
||||
if (isGlobalNamespaceEnabled) {
|
||||
"0"
|
||||
} else {
|
||||
"1"
|
||||
}
|
||||
)
|
||||
isGlobalNamespaceEnabled = it
|
||||
})
|
||||
}
|
||||
|
||||
// Lite Mode
|
||||
if (kPatchReady && aPatchReady) {
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.RemoveFromQueue,
|
||||
title = stringResource(id = R.string.settings_lite_mode),
|
||||
summary = stringResource(id = R.string.settings_lite_mode_mode_summary),
|
||||
checked = isLiteModeEnabled,
|
||||
onCheckedChange = {
|
||||
setLiteMode(it)
|
||||
isLiteModeEnabled = it
|
||||
})
|
||||
}
|
||||
|
||||
// Force OverlayFS
|
||||
if (kPatchReady && aPatchReady && isOverlayFSAvailable) {
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.FilePresent,
|
||||
title = stringResource(id = R.string.settings_force_overlayfs_mode),
|
||||
summary = stringResource(id = R.string.settings_force_overlayfs_mode_summary),
|
||||
checked = forceUsingOverlayFS,
|
||||
onCheckedChange = {
|
||||
setForceUsingOverlayFS(it)
|
||||
forceUsingOverlayFS = it
|
||||
})
|
||||
}
|
||||
|
||||
// WebView Debug
|
||||
if (aPatchReady) {
|
||||
var enableWebDebugging by rememberSaveable {
|
||||
mutableStateOf(
|
||||
prefs.getBoolean("enable_web_debugging", false)
|
||||
)
|
||||
}
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.DeveloperMode,
|
||||
title = stringResource(id = R.string.enable_web_debugging),
|
||||
summary = stringResource(id = R.string.enable_web_debugging_summary),
|
||||
checked = enableWebDebugging
|
||||
) {
|
||||
APApplication.sharedPreferences.edit {
|
||||
putBoolean("enable_web_debugging", it)
|
||||
}
|
||||
enableWebDebugging = it
|
||||
}
|
||||
}
|
||||
|
||||
// Check Update
|
||||
var checkUpdate by rememberSaveable {
|
||||
mutableStateOf(
|
||||
prefs.getBoolean("check_update", true)
|
||||
)
|
||||
}
|
||||
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.Update,
|
||||
title = stringResource(id = R.string.settings_check_update),
|
||||
summary = stringResource(id = R.string.settings_check_update_summary),
|
||||
checked = checkUpdate
|
||||
) {
|
||||
prefs.edit { putBoolean("check_update", it) }
|
||||
checkUpdate = it
|
||||
}
|
||||
|
||||
// Night Mode Follow System
|
||||
var nightFollowSystem by rememberSaveable {
|
||||
mutableStateOf(
|
||||
prefs.getBoolean("night_mode_follow_sys", true)
|
||||
)
|
||||
}
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.InvertColors,
|
||||
title = stringResource(id = R.string.settings_night_mode_follow_sys),
|
||||
summary = stringResource(id = R.string.settings_night_mode_follow_sys_summary),
|
||||
checked = nightFollowSystem
|
||||
) {
|
||||
prefs.edit { putBoolean("night_mode_follow_sys", it) }
|
||||
nightFollowSystem = it
|
||||
refreshTheme.value = true
|
||||
}
|
||||
|
||||
// Custom Night Theme Switch
|
||||
if (!nightFollowSystem) {
|
||||
var nightThemeEnabled by rememberSaveable {
|
||||
mutableStateOf(
|
||||
prefs.getBoolean("night_mode_enabled", false)
|
||||
)
|
||||
}
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.DarkMode,
|
||||
title = stringResource(id = R.string.settings_night_theme_enabled),
|
||||
checked = nightThemeEnabled
|
||||
) {
|
||||
prefs.edit { putBoolean("night_mode_enabled", it) }
|
||||
nightThemeEnabled = it
|
||||
refreshTheme.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// System dynamic color theme
|
||||
val isDynamicColorSupport = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
if (isDynamicColorSupport) {
|
||||
var useSystemDynamicColor by rememberSaveable {
|
||||
mutableStateOf(
|
||||
prefs.getBoolean("use_system_color_theme", true)
|
||||
)
|
||||
}
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.ColorLens,
|
||||
title = stringResource(id = R.string.settings_use_system_color_theme),
|
||||
summary = stringResource(id = R.string.settings_use_system_color_theme_summary),
|
||||
checked = useSystemDynamicColor
|
||||
) {
|
||||
prefs.edit { putBoolean("use_system_color_theme", it) }
|
||||
useSystemDynamicColor = it
|
||||
refreshTheme.value = true
|
||||
}
|
||||
|
||||
if (!useSystemDynamicColor) {
|
||||
ListItem(headlineContent = {
|
||||
Text(text = stringResource(id = R.string.settings_custom_color_theme))
|
||||
}, modifier = Modifier.clickable {
|
||||
showThemeChooseDialog.value = true
|
||||
}, supportingContent = {
|
||||
val colorMode = prefs.getString("custom_color", "blue")
|
||||
Text(
|
||||
text = stringResource(colorNameToString(colorMode.toString())),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
}, leadingContent = { Icon(Icons.Filled.FormatColorFill, null) })
|
||||
|
||||
}
|
||||
} else {
|
||||
ListItem(headlineContent = {
|
||||
Text(text = stringResource(id = R.string.settings_custom_color_theme))
|
||||
}, modifier = Modifier.clickable {
|
||||
showThemeChooseDialog.value = true
|
||||
}, supportingContent = {
|
||||
val colorMode = prefs.getString("custom_color", "blue")
|
||||
Text(
|
||||
text = stringResource(colorNameToString(colorMode.toString())),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
}, leadingContent = { Icon(Icons.Filled.FormatColorFill, null) })
|
||||
}
|
||||
|
||||
// su path
|
||||
if (kPatchReady) {
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Filled.Commit, stringResource(id = R.string.setting_reset_su_path)
|
||||
)
|
||||
},
|
||||
supportingContent = {},
|
||||
headlineContent = { Text(stringResource(id = R.string.setting_reset_su_path)) },
|
||||
modifier = Modifier.clickable {
|
||||
showResetSuPathDialog.value = true
|
||||
})
|
||||
}
|
||||
|
||||
// language
|
||||
ListItem(headlineContent = {
|
||||
Text(text = stringResource(id = R.string.settings_app_language))
|
||||
}, modifier = Modifier.clickable {
|
||||
showLanguageDialog.value = true
|
||||
}, supportingContent = {
|
||||
Text(text = AppCompatDelegate.getApplicationLocales()[0]?.displayLanguage?.replaceFirstChar {
|
||||
if (it.isLowerCase()) it.titlecase(
|
||||
Locale.getDefault()
|
||||
) else it.toString()
|
||||
} ?: stringResource(id = R.string.system_default),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.outline)
|
||||
}, leadingContent = { Icon(Icons.Filled.Translate, null) })
|
||||
|
||||
// log
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Filled.BugReport, stringResource(id = R.string.send_log)
|
||||
)
|
||||
},
|
||||
headlineContent = { Text(stringResource(id = R.string.send_log)) },
|
||||
modifier = Modifier.clickable {
|
||||
showLogBottomSheet = true
|
||||
})
|
||||
if (showLogBottomSheet) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { showLogBottomSheet = false },
|
||||
contentWindowInsets = { WindowInsets(0, 0, 0, 0) },
|
||||
content = {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(10.dp)
|
||||
.align(Alignment.CenterHorizontally)
|
||||
|
||||
) {
|
||||
Box {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.clickable {
|
||||
scope.launch {
|
||||
val formatter =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm")
|
||||
val current = LocalDateTime.now().format(formatter)
|
||||
exportBugreportLauncher.launch("APatch_bugreport_${current}.tar.gz")
|
||||
showLogBottomSheet = false
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Save,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.save_log),
|
||||
modifier = Modifier.padding(top = 16.dp),
|
||||
textAlign = TextAlign.Center.also {
|
||||
LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
Box {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.clickable {
|
||||
scope.launch {
|
||||
val bugreport = loadingDialog.withLoading {
|
||||
withContext(Dispatchers.IO) {
|
||||
getBugreportFile(context)
|
||||
}
|
||||
}
|
||||
|
||||
val uri: Uri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${BuildConfig.APPLICATION_ID}.fileprovider",
|
||||
bugreport
|
||||
)
|
||||
|
||||
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
setDataAndType(uri, "application/gzip")
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
|
||||
context.startActivity(
|
||||
Intent.createChooser(
|
||||
shareIntent,
|
||||
context.getString(R.string.send_log)
|
||||
)
|
||||
)
|
||||
showLogBottomSheet = false
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
Icons.Filled.Share,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.send_log),
|
||||
modifier = Modifier.padding(top = 16.dp),
|
||||
textAlign = TextAlign.Center.also {
|
||||
LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
NavigationBarsSpacer()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ThemeChooseDialog(showDialog: MutableState<Boolean>) {
|
||||
val prefs = APApplication.sharedPreferences
|
||||
|
||||
BasicAlertDialog(
|
||||
onDismissRequest = { showDialog.value = false }, properties = DialogProperties(
|
||||
decorFitsSystemWindows = true,
|
||||
usePlatformDefaultWidth = false,
|
||||
)
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.width(310.dp)
|
||||
.wrapContentHeight(),
|
||||
shape = RoundedCornerShape(30.dp),
|
||||
tonalElevation = AlertDialogDefaults.TonalElevation,
|
||||
color = AlertDialogDefaults.containerColor,
|
||||
) {
|
||||
LazyColumn {
|
||||
items(colorsList()) {
|
||||
ListItem(
|
||||
headlineContent = { Text(text = stringResource(it.nameId)) },
|
||||
modifier = Modifier.clickable {
|
||||
showDialog.value = false
|
||||
prefs.edit { putString("custom_color", it.name) }
|
||||
refreshTheme.value = true
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
val dialogWindowProvider = LocalView.current.parent as DialogWindowProvider
|
||||
APDialogBlurBehindUtils.setupWindowBlurListener(dialogWindowProvider.window)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private data class APColor(
|
||||
val name: String, @param:StringRes val nameId: Int
|
||||
)
|
||||
|
||||
private fun colorsList(): List<APColor> {
|
||||
return listOf(
|
||||
APColor("amber", R.string.amber_theme),
|
||||
APColor("blue_grey", R.string.blue_grey_theme),
|
||||
APColor("blue", R.string.blue_theme),
|
||||
APColor("brown", R.string.brown_theme),
|
||||
APColor("cyan", R.string.cyan_theme),
|
||||
APColor("deep_orange", R.string.deep_orange_theme),
|
||||
APColor("deep_purple", R.string.deep_purple_theme),
|
||||
APColor("green", R.string.green_theme),
|
||||
APColor("indigo", R.string.indigo_theme),
|
||||
APColor("light_blue", R.string.light_blue_theme),
|
||||
APColor("light_green", R.string.light_green_theme),
|
||||
APColor("lime", R.string.lime_theme),
|
||||
APColor("orange", R.string.orange_theme),
|
||||
APColor("pink", R.string.pink_theme),
|
||||
APColor("purple", R.string.purple_theme),
|
||||
APColor("red", R.string.red_theme),
|
||||
APColor("sakura", R.string.sakura_theme),
|
||||
APColor("teal", R.string.teal_theme),
|
||||
APColor("yellow", R.string.yellow_theme),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun colorNameToString(colorName: String): Int {
|
||||
return colorsList().find { it.name == colorName }?.nameId ?: R.string.blue_theme
|
||||
}
|
||||
|
||||
val suPathChecked: (path: String) -> Boolean = {
|
||||
it.startsWith("/") && it.trim().length > 1
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ResetSUPathDialog(showDialog: MutableState<Boolean>) {
|
||||
val context = LocalContext.current
|
||||
var suPath by remember { mutableStateOf(Natives.suPath()) }
|
||||
BasicAlertDialog(
|
||||
onDismissRequest = { showDialog.value = false }, properties = DialogProperties(
|
||||
decorFitsSystemWindows = true,
|
||||
usePlatformDefaultWidth = false,
|
||||
)
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.width(310.dp)
|
||||
.wrapContentHeight(),
|
||||
shape = RoundedCornerShape(30.dp),
|
||||
tonalElevation = AlertDialogDefaults.TonalElevation,
|
||||
color = AlertDialogDefaults.containerColor,
|
||||
) {
|
||||
Column(modifier = Modifier.padding(PaddingValues(all = 24.dp))) {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(PaddingValues(bottom = 16.dp))
|
||||
.align(Alignment.Start)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.setting_reset_su_path),
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
}
|
||||
Box(
|
||||
Modifier
|
||||
.weight(weight = 1f, fill = false)
|
||||
.padding(PaddingValues(bottom = 12.dp))
|
||||
.align(Alignment.Start)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = suPath,
|
||||
onValueChange = {
|
||||
suPath = it
|
||||
},
|
||||
label = { Text(stringResource(id = R.string.setting_reset_su_new_path)) },
|
||||
visualTransformation = VisualTransformation.None,
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
TextButton(onClick = { showDialog.value = false }) {
|
||||
|
||||
Text(stringResource(id = android.R.string.cancel))
|
||||
}
|
||||
|
||||
Button(enabled = suPathChecked(suPath), onClick = {
|
||||
showDialog.value = false
|
||||
val success = Natives.resetSuPath(suPath)
|
||||
Toast.makeText(
|
||||
context,
|
||||
if (success) R.string.success else R.string.failure,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
rootShellForResult("echo $suPath > ${APApplication.SU_PATH_FILE}")
|
||||
}) {
|
||||
Text(stringResource(id = android.R.string.ok))
|
||||
}
|
||||
}
|
||||
}
|
||||
val dialogWindowProvider = LocalView.current.parent as DialogWindowProvider
|
||||
APDialogBlurBehindUtils.setupWindowBlurListener(dialogWindowProvider.window)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LanguageDialog(showLanguageDialog: MutableState<Boolean>) {
|
||||
|
||||
val languages = stringArrayResource(id = R.array.languages)
|
||||
val languagesValues = stringArrayResource(id = R.array.languages_values)
|
||||
|
||||
if (showLanguageDialog.value) {
|
||||
BasicAlertDialog(
|
||||
onDismissRequest = { showLanguageDialog.value = false }
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.width(150.dp)
|
||||
.wrapContentHeight(),
|
||||
shape = RoundedCornerShape(28.dp),
|
||||
tonalElevation = AlertDialogDefaults.TonalElevation,
|
||||
color = AlertDialogDefaults.containerColor,
|
||||
) {
|
||||
LazyColumn {
|
||||
itemsIndexed(languages) { index, item ->
|
||||
ListItem(
|
||||
headlineContent = { Text(item) },
|
||||
modifier = Modifier.clickable {
|
||||
showLanguageDialog.value = false
|
||||
if (index == 0) {
|
||||
AppCompatDelegate.setApplicationLocales(
|
||||
LocaleListCompat.getEmptyLocaleList()
|
||||
)
|
||||
} else {
|
||||
AppCompatDelegate.setApplicationLocales(
|
||||
LocaleListCompat.forLanguageTags(
|
||||
languagesValues[index]
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
val dialogWindowProvider = LocalView.current.parent as DialogWindowProvider
|
||||
APDialogBlurBehindUtils.setupWindowBlurListener(dialogWindowProvider.window)
|
||||
}
|
||||
}
|
||||
}
|
||||
267
app/src/main/java/me/bmax/apatch/ui/screen/SuperUser.kt
Normal file
267
app/src/main/java/me/bmax/apatch/ui/screen/SuperUser.kt
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
package me.bmax.apatch.ui.screen
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Security
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import kotlinx.coroutines.launch
|
||||
import me.bmax.apatch.APApplication
|
||||
import me.bmax.apatch.Natives
|
||||
import me.bmax.apatch.R
|
||||
import me.bmax.apatch.ui.component.ProvideMenuShape
|
||||
import me.bmax.apatch.ui.component.SearchAppBar
|
||||
import me.bmax.apatch.ui.component.SwitchItem
|
||||
import me.bmax.apatch.ui.viewmodel.SuperUserViewModel
|
||||
import me.bmax.apatch.util.PkgConfig
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun SuperUserScreen() {
|
||||
val viewModel = viewModel<SuperUserViewModel>()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (viewModel.appList.isEmpty()) {
|
||||
viewModel.fetchAppList()
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
SearchAppBar(
|
||||
title = { Text(stringResource(R.string.su_title)) },
|
||||
searchText = viewModel.search,
|
||||
onSearchTextChange = { viewModel.search = it },
|
||||
onClearClick = { viewModel.search = "" },
|
||||
dropdownContent = {
|
||||
var showDropdown by remember { mutableStateOf(false) }
|
||||
|
||||
IconButton(
|
||||
onClick = { showDropdown = true },
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.MoreVert,
|
||||
contentDescription = stringResource(id = R.string.settings)
|
||||
)
|
||||
|
||||
ProvideMenuShape(RoundedCornerShape(10.dp)) {
|
||||
DropdownMenu(expanded = showDropdown, onDismissRequest = {
|
||||
showDropdown = false
|
||||
}) {
|
||||
DropdownMenuItem(text = {
|
||||
Text(stringResource(R.string.su_refresh))
|
||||
}, onClick = {
|
||||
scope.launch {
|
||||
viewModel.fetchAppList()
|
||||
}
|
||||
showDropdown = false
|
||||
})
|
||||
|
||||
DropdownMenuItem(text = {
|
||||
Text(
|
||||
if (viewModel.showSystemApps) {
|
||||
stringResource(R.string.su_hide_system_apps)
|
||||
} else {
|
||||
stringResource(R.string.su_show_system_apps)
|
||||
}
|
||||
)
|
||||
}, onClick = {
|
||||
viewModel.showSystemApps = !viewModel.showSystemApps
|
||||
showDropdown = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
|
||||
PullToRefreshBox(
|
||||
modifier = Modifier.padding(innerPadding),
|
||||
onRefresh = { scope.launch { viewModel.fetchAppList() } },
|
||||
isRefreshing = viewModel.isRefreshing
|
||||
) {
|
||||
LazyColumn(Modifier.fillMaxSize()) {
|
||||
items(viewModel.appList, key = { it.packageName + it.uid }) { app ->
|
||||
AppItem(app)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun AppItem(
|
||||
app: SuperUserViewModel.AppInfo,
|
||||
) {
|
||||
val config = app.config
|
||||
var showEditProfile by remember { mutableStateOf(false) }
|
||||
var rootGranted by remember { mutableStateOf(config.allow != 0) }
|
||||
var excludeApp by remember { mutableIntStateOf(config.exclude) }
|
||||
|
||||
ListItem(
|
||||
modifier = Modifier.clickable(onClick = {
|
||||
if (!rootGranted) {
|
||||
showEditProfile = !showEditProfile
|
||||
} else {
|
||||
rootGranted = false
|
||||
config.allow = 0
|
||||
Natives.revokeSu(app.uid)
|
||||
PkgConfig.changeConfig(config)
|
||||
}
|
||||
}),
|
||||
headlineContent = { Text(app.label) },
|
||||
leadingContent = {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current).data(app.packageInfo)
|
||||
.crossfade(true).build(),
|
||||
contentDescription = app.label,
|
||||
modifier = Modifier
|
||||
.padding(4.dp)
|
||||
.width(48.dp)
|
||||
.height(48.dp)
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
|
||||
Column {
|
||||
Text(app.packageName)
|
||||
FlowRow {
|
||||
|
||||
if (excludeApp == 1) {
|
||||
LabelText(label = stringResource(id = R.string.su_pkg_excluded_label))
|
||||
}
|
||||
if (rootGranted) {
|
||||
LabelText(label = config.profile.uid.toString())
|
||||
LabelText(label = config.profile.toUid.toString())
|
||||
LabelText(
|
||||
label = when {
|
||||
// todo: valid scontext ?
|
||||
config.profile.scontext.isNotEmpty() -> config.profile.scontext
|
||||
else -> stringResource(id = R.string.su_selinux_via_hook)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
trailingContent = {
|
||||
Switch(checked = rootGranted, onCheckedChange = {
|
||||
rootGranted = !rootGranted
|
||||
if (rootGranted) {
|
||||
excludeApp = 0
|
||||
config.allow = 1
|
||||
config.exclude = 0
|
||||
config.profile.scontext = APApplication.MAGISK_SCONTEXT
|
||||
} else {
|
||||
config.allow = 0
|
||||
}
|
||||
config.profile.uid = app.uid
|
||||
PkgConfig.changeConfig(config)
|
||||
if (config.allow == 1) {
|
||||
Natives.grantSu(app.uid, 0, config.profile.scontext)
|
||||
Natives.setUidExclude(app.uid, 0)
|
||||
} else {
|
||||
Natives.revokeSu(app.uid)
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = showEditProfile && !rootGranted,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp)
|
||||
) {
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.Security,
|
||||
title = stringResource(id = R.string.su_pkg_excluded_setting_title),
|
||||
summary = stringResource(id = R.string.su_pkg_excluded_setting_summary),
|
||||
checked = excludeApp == 1,
|
||||
onCheckedChange = {
|
||||
if (it) {
|
||||
excludeApp = 1
|
||||
config.allow = 0
|
||||
config.profile.scontext = APApplication.DEFAULT_SCONTEXT
|
||||
Natives.revokeSu(app.uid)
|
||||
} else {
|
||||
excludeApp = 0
|
||||
}
|
||||
config.exclude = excludeApp
|
||||
config.profile.uid = app.uid
|
||||
PkgConfig.changeConfig(config)
|
||||
Natives.setUidExclude(app.uid, excludeApp)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LabelText(label: String) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(top = 4.dp, end = 4.dp)
|
||||
.background(
|
||||
Color.Black, shape = RoundedCornerShape(4.dp)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
modifier = Modifier.padding(vertical = 2.dp, horizontal = 5.dp),
|
||||
style = TextStyle(
|
||||
fontSize = 8.sp,
|
||||
color = Color.White,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
131
app/src/main/java/me/bmax/apatch/ui/theme/AmberTheme.kt
Normal file
131
app/src/main/java/me/bmax/apatch/ui/theme/AmberTheme.kt
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package me.bmax.apatch.ui.theme
|
||||
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
private val md_theme_light_primary = Color(0xFF785900)
|
||||
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_primaryContainer = Color(0xFFFFDF9E)
|
||||
private val md_theme_light_onPrimaryContainer = Color(0xFF261A00)
|
||||
private val md_theme_light_secondary = Color(0xFF6B5D3F)
|
||||
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_secondaryContainer = Color(0xFFF5E0BB)
|
||||
private val md_theme_light_onSecondaryContainer = Color(0xFF241A04)
|
||||
private val md_theme_light_tertiary = Color(0xFF4A6547)
|
||||
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_tertiaryContainer = Color(0xFFCCEBC4)
|
||||
private val md_theme_light_onTertiaryContainer = Color(0xFF072109)
|
||||
private val md_theme_light_error = Color(0xFFBA1A1A)
|
||||
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_onErrorContainer = Color(0xFF410002)
|
||||
private val md_theme_light_background = Color(0xFFFFFBFF)
|
||||
private val md_theme_light_onBackground = Color(0xFF1E1B16)
|
||||
private val md_theme_light_surface = Color(0xFFFFFBFF)
|
||||
private val md_theme_light_onSurface = Color(0xFF1E1B16)
|
||||
private val md_theme_light_surfaceVariant = Color(0xFFEDE1CF)
|
||||
private val md_theme_light_onSurfaceVariant = Color(0xFF4D4639)
|
||||
private val md_theme_light_outline = Color(0xFF7F7667)
|
||||
private val md_theme_light_inverseOnSurface = Color(0xFFF7EFE7)
|
||||
private val md_theme_light_inverseSurface = Color(0xFF33302A)
|
||||
private val md_theme_light_inversePrimary = Color(0xFFFABD00)
|
||||
private val md_theme_light_shadow = Color(0xFF000000)
|
||||
private val md_theme_light_surfaceTint = Color(0xFF785900)
|
||||
private val md_theme_light_outlineVariant = Color(0xFFD0C5B4)
|
||||
private val md_theme_light_scrim = Color(0xFF000000)
|
||||
|
||||
private val md_theme_dark_primary = Color(0xFFFABD00)
|
||||
private val md_theme_dark_onPrimary = Color(0xFF3F2E00)
|
||||
private val md_theme_dark_primaryContainer = Color(0xFF5B4300)
|
||||
private val md_theme_dark_onPrimaryContainer = Color(0xFFFFDF9E)
|
||||
private val md_theme_dark_secondary = Color(0xFFD8C4A0)
|
||||
private val md_theme_dark_onSecondary = Color(0xFF3A2F15)
|
||||
private val md_theme_dark_secondaryContainer = Color(0xFF52452A)
|
||||
private val md_theme_dark_onSecondaryContainer = Color(0xFFF5E0BB)
|
||||
private val md_theme_dark_tertiary = Color(0xFFB0CFAA)
|
||||
private val md_theme_dark_onTertiary = Color(0xFF1D361C)
|
||||
private val md_theme_dark_tertiaryContainer = Color(0xFF334D31)
|
||||
private val md_theme_dark_onTertiaryContainer = Color(0xFFCCEBC4)
|
||||
private val md_theme_dark_error = Color(0xFFFFB4AB)
|
||||
private val md_theme_dark_errorContainer = Color(0xFF93000A)
|
||||
private val md_theme_dark_onError = Color(0xFF690005)
|
||||
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_dark_background = Color(0xFF1E1B16)
|
||||
private val md_theme_dark_onBackground = Color(0xFFE9E1D8)
|
||||
private val md_theme_dark_surface = Color(0xFF1E1B16)
|
||||
private val md_theme_dark_onSurface = Color(0xFFE9E1D8)
|
||||
private val md_theme_dark_surfaceVariant = Color(0xFF4D4639)
|
||||
private val md_theme_dark_onSurfaceVariant = Color(0xFFD0C5B4)
|
||||
private val md_theme_dark_outline = Color(0xFF998F80)
|
||||
private val md_theme_dark_inverseOnSurface = Color(0xFF1E1B16)
|
||||
private val md_theme_dark_inverseSurface = Color(0xFFE9E1D8)
|
||||
private val md_theme_dark_inversePrimary = Color(0xFF785900)
|
||||
private val md_theme_dark_shadow = Color(0xFF000000)
|
||||
private val md_theme_dark_surfaceTint = Color(0xFFFABD00)
|
||||
private val md_theme_dark_outlineVariant = Color(0xFF4D4639)
|
||||
private val md_theme_dark_scrim = Color(0xFF000000)
|
||||
|
||||
val LightAmberTheme = lightColorScheme(
|
||||
primary = md_theme_light_primary,
|
||||
onPrimary = md_theme_light_onPrimary,
|
||||
primaryContainer = md_theme_light_primaryContainer,
|
||||
onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
||||
secondary = md_theme_light_secondary,
|
||||
onSecondary = md_theme_light_onSecondary,
|
||||
secondaryContainer = md_theme_light_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
||||
tertiary = md_theme_light_tertiary,
|
||||
onTertiary = md_theme_light_onTertiary,
|
||||
tertiaryContainer = md_theme_light_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
||||
error = md_theme_light_error,
|
||||
errorContainer = md_theme_light_errorContainer,
|
||||
onError = md_theme_light_onError,
|
||||
onErrorContainer = md_theme_light_onErrorContainer,
|
||||
background = md_theme_light_background,
|
||||
onBackground = md_theme_light_onBackground,
|
||||
surface = md_theme_light_surface,
|
||||
onSurface = md_theme_light_onSurface,
|
||||
surfaceVariant = md_theme_light_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
||||
outline = md_theme_light_outline,
|
||||
inverseOnSurface = md_theme_light_inverseOnSurface,
|
||||
inverseSurface = md_theme_light_inverseSurface,
|
||||
inversePrimary = md_theme_light_inversePrimary,
|
||||
surfaceTint = md_theme_light_surfaceTint,
|
||||
outlineVariant = md_theme_light_outlineVariant,
|
||||
scrim = md_theme_light_scrim,
|
||||
)
|
||||
|
||||
val DarkAmberTheme = darkColorScheme(
|
||||
primary = md_theme_dark_primary,
|
||||
onPrimary = md_theme_dark_onPrimary,
|
||||
primaryContainer = md_theme_dark_primaryContainer,
|
||||
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
|
||||
secondary = md_theme_dark_secondary,
|
||||
onSecondary = md_theme_dark_onSecondary,
|
||||
secondaryContainer = md_theme_dark_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
|
||||
tertiary = md_theme_dark_tertiary,
|
||||
onTertiary = md_theme_dark_onTertiary,
|
||||
tertiaryContainer = md_theme_dark_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
|
||||
error = md_theme_dark_error,
|
||||
errorContainer = md_theme_dark_errorContainer,
|
||||
onError = md_theme_dark_onError,
|
||||
onErrorContainer = md_theme_dark_onErrorContainer,
|
||||
background = md_theme_dark_background,
|
||||
onBackground = md_theme_dark_onBackground,
|
||||
surface = md_theme_dark_surface,
|
||||
onSurface = md_theme_dark_onSurface,
|
||||
surfaceVariant = md_theme_dark_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
|
||||
outline = md_theme_dark_outline,
|
||||
inverseOnSurface = md_theme_dark_inverseOnSurface,
|
||||
inverseSurface = md_theme_dark_inverseSurface,
|
||||
inversePrimary = md_theme_dark_inversePrimary,
|
||||
surfaceTint = md_theme_dark_surfaceTint,
|
||||
outlineVariant = md_theme_dark_outlineVariant,
|
||||
scrim = md_theme_dark_scrim,
|
||||
)
|
||||
131
app/src/main/java/me/bmax/apatch/ui/theme/BlueGreyTheme.kt
Normal file
131
app/src/main/java/me/bmax/apatch/ui/theme/BlueGreyTheme.kt
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package me.bmax.apatch.ui.theme
|
||||
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
private val md_theme_light_primary = Color(0xFF00668A)
|
||||
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_primaryContainer = Color(0xFFC4E7FF)
|
||||
private val md_theme_light_onPrimaryContainer = Color(0xFF001E2C)
|
||||
private val md_theme_light_secondary = Color(0xFF4E616D)
|
||||
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_secondaryContainer = Color(0xFFD1E5F4)
|
||||
private val md_theme_light_onSecondaryContainer = Color(0xFF0A1E28)
|
||||
private val md_theme_light_tertiary = Color(0xFF605A7D)
|
||||
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_tertiaryContainer = Color(0xFFE6DEFF)
|
||||
private val md_theme_light_onTertiaryContainer = Color(0xFF1D1736)
|
||||
private val md_theme_light_error = Color(0xFFBA1A1A)
|
||||
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_onErrorContainer = Color(0xFF410002)
|
||||
private val md_theme_light_background = Color(0xFFFBFCFF)
|
||||
private val md_theme_light_onBackground = Color(0xFF191C1E)
|
||||
private val md_theme_light_surface = Color(0xFFFBFCFF)
|
||||
private val md_theme_light_onSurface = Color(0xFF191C1E)
|
||||
private val md_theme_light_surfaceVariant = Color(0xFFDCE3E9)
|
||||
private val md_theme_light_onSurfaceVariant = Color(0xFF41484D)
|
||||
private val md_theme_light_outline = Color(0xFF71787D)
|
||||
private val md_theme_light_inverseOnSurface = Color(0xFFF0F1F3)
|
||||
private val md_theme_light_inverseSurface = Color(0xFF2E3133)
|
||||
private val md_theme_light_inversePrimary = Color(0xFF7BD0FF)
|
||||
private val md_theme_light_shadow = Color(0xFF000000)
|
||||
private val md_theme_light_surfaceTint = Color(0xFF00668A)
|
||||
private val md_theme_light_outlineVariant = Color(0xFFC0C7CD)
|
||||
private val md_theme_light_scrim = Color(0xFF000000)
|
||||
|
||||
private val md_theme_dark_primary = Color(0xFF7BD0FF)
|
||||
private val md_theme_dark_onPrimary = Color(0xFF003549)
|
||||
private val md_theme_dark_primaryContainer = Color(0xFF004C69)
|
||||
private val md_theme_dark_onPrimaryContainer = Color(0xFFC4E7FF)
|
||||
private val md_theme_dark_secondary = Color(0xFFB5C9D7)
|
||||
private val md_theme_dark_onSecondary = Color(0xFF20333E)
|
||||
private val md_theme_dark_secondaryContainer = Color(0xFF374955)
|
||||
private val md_theme_dark_onSecondaryContainer = Color(0xFFD1E5F4)
|
||||
private val md_theme_dark_tertiary = Color(0xFFCAC1E9)
|
||||
private val md_theme_dark_onTertiary = Color(0xFF322C4C)
|
||||
private val md_theme_dark_tertiaryContainer = Color(0xFF484264)
|
||||
private val md_theme_dark_onTertiaryContainer = Color(0xFFE6DEFF)
|
||||
private val md_theme_dark_error = Color(0xFFFFB4AB)
|
||||
private val md_theme_dark_errorContainer = Color(0xFF93000A)
|
||||
private val md_theme_dark_onError = Color(0xFF690005)
|
||||
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_dark_background = Color(0xFF191C1E)
|
||||
private val md_theme_dark_onBackground = Color(0xFFE1E2E5)
|
||||
private val md_theme_dark_surface = Color(0xFF191C1E)
|
||||
private val md_theme_dark_onSurface = Color(0xFFE1E2E5)
|
||||
private val md_theme_dark_surfaceVariant = Color(0xFF41484D)
|
||||
private val md_theme_dark_onSurfaceVariant = Color(0xFFC0C7CD)
|
||||
private val md_theme_dark_outline = Color(0xFF8B9297)
|
||||
private val md_theme_dark_inverseOnSurface = Color(0xFF191C1E)
|
||||
private val md_theme_dark_inverseSurface = Color(0xFFE1E2E5)
|
||||
private val md_theme_dark_inversePrimary = Color(0xFF00668A)
|
||||
private val md_theme_dark_shadow = Color(0xFF000000)
|
||||
private val md_theme_dark_surfaceTint = Color(0xFF7BD0FF)
|
||||
private val md_theme_dark_outlineVariant = Color(0xFF41484D)
|
||||
private val md_theme_dark_scrim = Color(0xFF000000)
|
||||
|
||||
val LightBlueGreyTheme = lightColorScheme(
|
||||
primary = md_theme_light_primary,
|
||||
onPrimary = md_theme_light_onPrimary,
|
||||
primaryContainer = md_theme_light_primaryContainer,
|
||||
onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
||||
secondary = md_theme_light_secondary,
|
||||
onSecondary = md_theme_light_onSecondary,
|
||||
secondaryContainer = md_theme_light_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
||||
tertiary = md_theme_light_tertiary,
|
||||
onTertiary = md_theme_light_onTertiary,
|
||||
tertiaryContainer = md_theme_light_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
||||
error = md_theme_light_error,
|
||||
errorContainer = md_theme_light_errorContainer,
|
||||
onError = md_theme_light_onError,
|
||||
onErrorContainer = md_theme_light_onErrorContainer,
|
||||
background = md_theme_light_background,
|
||||
onBackground = md_theme_light_onBackground,
|
||||
surface = md_theme_light_surface,
|
||||
onSurface = md_theme_light_onSurface,
|
||||
surfaceVariant = md_theme_light_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
||||
outline = md_theme_light_outline,
|
||||
inverseOnSurface = md_theme_light_inverseOnSurface,
|
||||
inverseSurface = md_theme_light_inverseSurface,
|
||||
inversePrimary = md_theme_light_inversePrimary,
|
||||
surfaceTint = md_theme_light_surfaceTint,
|
||||
outlineVariant = md_theme_light_outlineVariant,
|
||||
scrim = md_theme_light_scrim,
|
||||
)
|
||||
|
||||
val DarkBlueGreyTheme = darkColorScheme(
|
||||
primary = md_theme_dark_primary,
|
||||
onPrimary = md_theme_dark_onPrimary,
|
||||
primaryContainer = md_theme_dark_primaryContainer,
|
||||
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
|
||||
secondary = md_theme_dark_secondary,
|
||||
onSecondary = md_theme_dark_onSecondary,
|
||||
secondaryContainer = md_theme_dark_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
|
||||
tertiary = md_theme_dark_tertiary,
|
||||
onTertiary = md_theme_dark_onTertiary,
|
||||
tertiaryContainer = md_theme_dark_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
|
||||
error = md_theme_dark_error,
|
||||
errorContainer = md_theme_dark_errorContainer,
|
||||
onError = md_theme_dark_onError,
|
||||
onErrorContainer = md_theme_dark_onErrorContainer,
|
||||
background = md_theme_dark_background,
|
||||
onBackground = md_theme_dark_onBackground,
|
||||
surface = md_theme_dark_surface,
|
||||
onSurface = md_theme_dark_onSurface,
|
||||
surfaceVariant = md_theme_dark_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
|
||||
outline = md_theme_dark_outline,
|
||||
inverseOnSurface = md_theme_dark_inverseOnSurface,
|
||||
inverseSurface = md_theme_dark_inverseSurface,
|
||||
inversePrimary = md_theme_dark_inversePrimary,
|
||||
surfaceTint = md_theme_dark_surfaceTint,
|
||||
outlineVariant = md_theme_dark_outlineVariant,
|
||||
scrim = md_theme_dark_scrim,
|
||||
)
|
||||
132
app/src/main/java/me/bmax/apatch/ui/theme/BlueTheme.kt
Normal file
132
app/src/main/java/me/bmax/apatch/ui/theme/BlueTheme.kt
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
package me.bmax.apatch.ui.theme
|
||||
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
private val md_theme_light_primary = Color(0xFF0061A4)
|
||||
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_primaryContainer = Color(0xFFD1E4FF)
|
||||
private val md_theme_light_onPrimaryContainer = Color(0xFF001D36)
|
||||
private val md_theme_light_secondary = Color(0xFF535F70)
|
||||
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_secondaryContainer = Color(0xFFD7E3F7)
|
||||
private val md_theme_light_onSecondaryContainer = Color(0xFF101C2B)
|
||||
private val md_theme_light_tertiary = Color(0xFF6B5778)
|
||||
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_tertiaryContainer = Color(0xFFF2DAFF)
|
||||
private val md_theme_light_onTertiaryContainer = Color(0xFF251431)
|
||||
private val md_theme_light_error = Color(0xFFBA1A1A)
|
||||
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_onErrorContainer = Color(0xFF410002)
|
||||
private val md_theme_light_background = Color(0xFFFDFCFF)
|
||||
private val md_theme_light_onBackground = Color(0xFF1A1C1E)
|
||||
private val md_theme_light_surface = Color(0xFFFDFCFF)
|
||||
private val md_theme_light_onSurface = Color(0xFF1A1C1E)
|
||||
private val md_theme_light_surfaceVariant = Color(0xFFDFE2EB)
|
||||
private val md_theme_light_onSurfaceVariant = Color(0xFF43474E)
|
||||
private val md_theme_light_outline = Color(0xFF73777F)
|
||||
private val md_theme_light_inverseOnSurface = Color(0xFFF1F0F4)
|
||||
private val md_theme_light_inverseSurface = Color(0xFF2F3033)
|
||||
private val md_theme_light_inversePrimary = Color(0xFF9ECAFF)
|
||||
private val md_theme_light_shadow = Color(0xFF000000)
|
||||
private val md_theme_light_surfaceTint = Color(0xFF0061A4)
|
||||
private val md_theme_light_outlineVariant = Color(0xFFC3C7CF)
|
||||
private val md_theme_light_scrim = Color(0xFF000000)
|
||||
|
||||
private val md_theme_dark_primary = Color(0xFF9ECAFF)
|
||||
private val md_theme_dark_onPrimary = Color(0xFF003258)
|
||||
private val md_theme_dark_primaryContainer = Color(0xFF00497D)
|
||||
private val md_theme_dark_onPrimaryContainer = Color(0xFFD1E4FF)
|
||||
private val md_theme_dark_secondary = Color(0xFFBBC7DB)
|
||||
private val md_theme_dark_onSecondary = Color(0xFF253140)
|
||||
private val md_theme_dark_secondaryContainer = Color(0xFF3B4858)
|
||||
private val md_theme_dark_onSecondaryContainer = Color(0xFFD7E3F7)
|
||||
private val md_theme_dark_tertiary = Color(0xFFD6BEE4)
|
||||
private val md_theme_dark_onTertiary = Color(0xFF3B2948)
|
||||
private val md_theme_dark_tertiaryContainer = Color(0xFF523F5F)
|
||||
private val md_theme_dark_onTertiaryContainer = Color(0xFFF2DAFF)
|
||||
private val md_theme_dark_error = Color(0xFFFFB4AB)
|
||||
private val md_theme_dark_errorContainer = Color(0xFF93000A)
|
||||
private val md_theme_dark_onError = Color(0xFF690005)
|
||||
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_dark_background = Color(0xFF1A1C1E)
|
||||
private val md_theme_dark_onBackground = Color(0xFFE2E2E6)
|
||||
private val md_theme_dark_surface = Color(0xFF1A1C1E)
|
||||
private val md_theme_dark_onSurface = Color(0xFFE2E2E6)
|
||||
private val md_theme_dark_surfaceVariant = Color(0xFF43474E)
|
||||
private val md_theme_dark_onSurfaceVariant = Color(0xFFC3C7CF)
|
||||
private val md_theme_dark_outline = Color(0xFF8D9199)
|
||||
private val md_theme_dark_inverseOnSurface = Color(0xFF1A1C1E)
|
||||
private val md_theme_dark_inverseSurface = Color(0xFFE2E2E6)
|
||||
private val md_theme_dark_inversePrimary = Color(0xFF0061A4)
|
||||
private val md_theme_dark_shadow = Color(0xFF000000)
|
||||
private val md_theme_dark_surfaceTint = Color(0xFF9ECAFF)
|
||||
private val md_theme_dark_outlineVariant = Color(0xFF43474E)
|
||||
private val md_theme_dark_scrim = Color(0xFF000000)
|
||||
|
||||
|
||||
val LightBlueTheme = lightColorScheme(
|
||||
primary = md_theme_light_primary,
|
||||
onPrimary = md_theme_light_onPrimary,
|
||||
primaryContainer = md_theme_light_primaryContainer,
|
||||
onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
||||
secondary = md_theme_light_secondary,
|
||||
onSecondary = md_theme_light_onSecondary,
|
||||
secondaryContainer = md_theme_light_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
||||
tertiary = md_theme_light_tertiary,
|
||||
onTertiary = md_theme_light_onTertiary,
|
||||
tertiaryContainer = md_theme_light_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
||||
error = md_theme_light_error,
|
||||
errorContainer = md_theme_light_errorContainer,
|
||||
onError = md_theme_light_onError,
|
||||
onErrorContainer = md_theme_light_onErrorContainer,
|
||||
background = md_theme_light_background,
|
||||
onBackground = md_theme_light_onBackground,
|
||||
surface = md_theme_light_surface,
|
||||
onSurface = md_theme_light_onSurface,
|
||||
surfaceVariant = md_theme_light_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
||||
outline = md_theme_light_outline,
|
||||
inverseOnSurface = md_theme_light_inverseOnSurface,
|
||||
inverseSurface = md_theme_light_inverseSurface,
|
||||
inversePrimary = md_theme_light_inversePrimary,
|
||||
surfaceTint = md_theme_light_surfaceTint,
|
||||
outlineVariant = md_theme_light_outlineVariant,
|
||||
scrim = md_theme_light_scrim,
|
||||
)
|
||||
|
||||
val DarkBlueTheme = darkColorScheme(
|
||||
primary = md_theme_dark_primary,
|
||||
onPrimary = md_theme_dark_onPrimary,
|
||||
primaryContainer = md_theme_dark_primaryContainer,
|
||||
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
|
||||
secondary = md_theme_dark_secondary,
|
||||
onSecondary = md_theme_dark_onSecondary,
|
||||
secondaryContainer = md_theme_dark_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
|
||||
tertiary = md_theme_dark_tertiary,
|
||||
onTertiary = md_theme_dark_onTertiary,
|
||||
tertiaryContainer = md_theme_dark_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
|
||||
error = md_theme_dark_error,
|
||||
errorContainer = md_theme_dark_errorContainer,
|
||||
onError = md_theme_dark_onError,
|
||||
onErrorContainer = md_theme_dark_onErrorContainer,
|
||||
background = md_theme_dark_background,
|
||||
onBackground = md_theme_dark_onBackground,
|
||||
surface = md_theme_dark_surface,
|
||||
onSurface = md_theme_dark_onSurface,
|
||||
surfaceVariant = md_theme_dark_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
|
||||
outline = md_theme_dark_outline,
|
||||
inverseOnSurface = md_theme_dark_inverseOnSurface,
|
||||
inverseSurface = md_theme_dark_inverseSurface,
|
||||
inversePrimary = md_theme_dark_inversePrimary,
|
||||
surfaceTint = md_theme_dark_surfaceTint,
|
||||
outlineVariant = md_theme_dark_outlineVariant,
|
||||
scrim = md_theme_dark_scrim,
|
||||
)
|
||||
131
app/src/main/java/me/bmax/apatch/ui/theme/BrownTheme.kt
Normal file
131
app/src/main/java/me/bmax/apatch/ui/theme/BrownTheme.kt
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package me.bmax.apatch.ui.theme
|
||||
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
private val md_theme_light_primary = Color(0xFF9A4522)
|
||||
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_primaryContainer = Color(0xFFFFDBCF)
|
||||
private val md_theme_light_onPrimaryContainer = Color(0xFF380D00)
|
||||
private val md_theme_light_secondary = Color(0xFF77574C)
|
||||
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_secondaryContainer = Color(0xFFFFDBCF)
|
||||
private val md_theme_light_onSecondaryContainer = Color(0xFF2C160D)
|
||||
private val md_theme_light_tertiary = Color(0xFF695E2F)
|
||||
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_tertiaryContainer = Color(0xFFF2E2A8)
|
||||
private val md_theme_light_onTertiaryContainer = Color(0xFF211B00)
|
||||
private val md_theme_light_error = Color(0xFFBA1A1A)
|
||||
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_onErrorContainer = Color(0xFF410002)
|
||||
private val md_theme_light_background = Color(0xFFFFFBFF)
|
||||
private val md_theme_light_onBackground = Color(0xFF201A18)
|
||||
private val md_theme_light_surface = Color(0xFFFFFBFF)
|
||||
private val md_theme_light_onSurface = Color(0xFF201A18)
|
||||
private val md_theme_light_surfaceVariant = Color(0xFFF5DED6)
|
||||
private val md_theme_light_onSurfaceVariant = Color(0xFF53433E)
|
||||
private val md_theme_light_outline = Color(0xFF85736D)
|
||||
private val md_theme_light_inverseOnSurface = Color(0xFFFBEEEA)
|
||||
private val md_theme_light_inverseSurface = Color(0xFF362F2C)
|
||||
private val md_theme_light_inversePrimary = Color(0xFFFFB59A)
|
||||
private val md_theme_light_shadow = Color(0xFF000000)
|
||||
private val md_theme_light_surfaceTint = Color(0xFF9A4522)
|
||||
private val md_theme_light_outlineVariant = Color(0xFFD8C2BB)
|
||||
private val md_theme_light_scrim = Color(0xFF000000)
|
||||
|
||||
private val md_theme_dark_primary = Color(0xFFFFB59A)
|
||||
private val md_theme_dark_onPrimary = Color(0xFF5B1B00)
|
||||
private val md_theme_dark_primaryContainer = Color(0xFF7B2E0D)
|
||||
private val md_theme_dark_onPrimaryContainer = Color(0xFFFFDBCF)
|
||||
private val md_theme_dark_secondary = Color(0xFFE7BEAF)
|
||||
private val md_theme_dark_onSecondary = Color(0xFF442A20)
|
||||
private val md_theme_dark_secondaryContainer = Color(0xFF5D4035)
|
||||
private val md_theme_dark_onSecondaryContainer = Color(0xFFFFDBCF)
|
||||
private val md_theme_dark_tertiary = Color(0xFFD5C68E)
|
||||
private val md_theme_dark_onTertiary = Color(0xFF393005)
|
||||
private val md_theme_dark_tertiaryContainer = Color(0xFF50471A)
|
||||
private val md_theme_dark_onTertiaryContainer = Color(0xFFF2E2A8)
|
||||
private val md_theme_dark_error = Color(0xFFFFB4AB)
|
||||
private val md_theme_dark_errorContainer = Color(0xFF93000A)
|
||||
private val md_theme_dark_onError = Color(0xFF690005)
|
||||
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_dark_background = Color(0xFF201A18)
|
||||
private val md_theme_dark_onBackground = Color(0xFFEDE0DC)
|
||||
private val md_theme_dark_surface = Color(0xFF201A18)
|
||||
private val md_theme_dark_onSurface = Color(0xFFEDE0DC)
|
||||
private val md_theme_dark_surfaceVariant = Color(0xFF53433E)
|
||||
private val md_theme_dark_onSurfaceVariant = Color(0xFFD8C2BB)
|
||||
private val md_theme_dark_outline = Color(0xFFA08D86)
|
||||
private val md_theme_dark_inverseOnSurface = Color(0xFF201A18)
|
||||
private val md_theme_dark_inverseSurface = Color(0xFFEDE0DC)
|
||||
private val md_theme_dark_inversePrimary = Color(0xFF9A4522)
|
||||
private val md_theme_dark_shadow = Color(0xFF000000)
|
||||
private val md_theme_dark_surfaceTint = Color(0xFFFFB59A)
|
||||
private val md_theme_dark_outlineVariant = Color(0xFF53433E)
|
||||
private val md_theme_dark_scrim = Color(0xFF000000)
|
||||
|
||||
val LightBrownTheme = lightColorScheme(
|
||||
primary = md_theme_light_primary,
|
||||
onPrimary = md_theme_light_onPrimary,
|
||||
primaryContainer = md_theme_light_primaryContainer,
|
||||
onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
||||
secondary = md_theme_light_secondary,
|
||||
onSecondary = md_theme_light_onSecondary,
|
||||
secondaryContainer = md_theme_light_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
||||
tertiary = md_theme_light_tertiary,
|
||||
onTertiary = md_theme_light_onTertiary,
|
||||
tertiaryContainer = md_theme_light_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
||||
error = md_theme_light_error,
|
||||
errorContainer = md_theme_light_errorContainer,
|
||||
onError = md_theme_light_onError,
|
||||
onErrorContainer = md_theme_light_onErrorContainer,
|
||||
background = md_theme_light_background,
|
||||
onBackground = md_theme_light_onBackground,
|
||||
surface = md_theme_light_surface,
|
||||
onSurface = md_theme_light_onSurface,
|
||||
surfaceVariant = md_theme_light_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
||||
outline = md_theme_light_outline,
|
||||
inverseOnSurface = md_theme_light_inverseOnSurface,
|
||||
inverseSurface = md_theme_light_inverseSurface,
|
||||
inversePrimary = md_theme_light_inversePrimary,
|
||||
surfaceTint = md_theme_light_surfaceTint,
|
||||
outlineVariant = md_theme_light_outlineVariant,
|
||||
scrim = md_theme_light_scrim,
|
||||
)
|
||||
|
||||
val DarkBrownTheme = darkColorScheme(
|
||||
primary = md_theme_dark_primary,
|
||||
onPrimary = md_theme_dark_onPrimary,
|
||||
primaryContainer = md_theme_dark_primaryContainer,
|
||||
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
|
||||
secondary = md_theme_dark_secondary,
|
||||
onSecondary = md_theme_dark_onSecondary,
|
||||
secondaryContainer = md_theme_dark_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
|
||||
tertiary = md_theme_dark_tertiary,
|
||||
onTertiary = md_theme_dark_onTertiary,
|
||||
tertiaryContainer = md_theme_dark_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
|
||||
error = md_theme_dark_error,
|
||||
errorContainer = md_theme_dark_errorContainer,
|
||||
onError = md_theme_dark_onError,
|
||||
onErrorContainer = md_theme_dark_onErrorContainer,
|
||||
background = md_theme_dark_background,
|
||||
onBackground = md_theme_dark_onBackground,
|
||||
surface = md_theme_dark_surface,
|
||||
onSurface = md_theme_dark_onSurface,
|
||||
surfaceVariant = md_theme_dark_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
|
||||
outline = md_theme_dark_outline,
|
||||
inverseOnSurface = md_theme_dark_inverseOnSurface,
|
||||
inverseSurface = md_theme_dark_inverseSurface,
|
||||
inversePrimary = md_theme_dark_inversePrimary,
|
||||
surfaceTint = md_theme_dark_surfaceTint,
|
||||
outlineVariant = md_theme_dark_outlineVariant,
|
||||
scrim = md_theme_dark_scrim,
|
||||
)
|
||||
131
app/src/main/java/me/bmax/apatch/ui/theme/CyanTheme.kt
Normal file
131
app/src/main/java/me/bmax/apatch/ui/theme/CyanTheme.kt
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package me.bmax.apatch.ui.theme
|
||||
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
private val md_theme_light_primary = Color(0xFF006876)
|
||||
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_primaryContainer = Color(0xFFA1EFFF)
|
||||
private val md_theme_light_onPrimaryContainer = Color(0xFF001F25)
|
||||
private val md_theme_light_secondary = Color(0xFF4A6268)
|
||||
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_secondaryContainer = Color(0xFFCDE7ED)
|
||||
private val md_theme_light_onSecondaryContainer = Color(0xFF051F23)
|
||||
private val md_theme_light_tertiary = Color(0xFF545D7E)
|
||||
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_tertiaryContainer = Color(0xFFDBE1FF)
|
||||
private val md_theme_light_onTertiaryContainer = Color(0xFF101A37)
|
||||
private val md_theme_light_error = Color(0xFFBA1A1A)
|
||||
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_onErrorContainer = Color(0xFF410002)
|
||||
private val md_theme_light_background = Color(0xFFFBFCFD)
|
||||
private val md_theme_light_onBackground = Color(0xFF191C1D)
|
||||
private val md_theme_light_surface = Color(0xFFFBFCFD)
|
||||
private val md_theme_light_onSurface = Color(0xFF191C1D)
|
||||
private val md_theme_light_surfaceVariant = Color(0xFFDBE4E6)
|
||||
private val md_theme_light_onSurfaceVariant = Color(0xFF3F484A)
|
||||
private val md_theme_light_outline = Color(0xFF6F797B)
|
||||
private val md_theme_light_inverseOnSurface = Color(0xFFEFF1F2)
|
||||
private val md_theme_light_inverseSurface = Color(0xFF2E3132)
|
||||
private val md_theme_light_inversePrimary = Color(0xFF44D8F1)
|
||||
private val md_theme_light_shadow = Color(0xFF000000)
|
||||
private val md_theme_light_surfaceTint = Color(0xFF006876)
|
||||
private val md_theme_light_outlineVariant = Color(0xFFBFC8CA)
|
||||
private val md_theme_light_scrim = Color(0xFF000000)
|
||||
|
||||
private val md_theme_dark_primary = Color(0xFF44D8F1)
|
||||
private val md_theme_dark_onPrimary = Color(0xFF00363E)
|
||||
private val md_theme_dark_primaryContainer = Color(0xFF004E59)
|
||||
private val md_theme_dark_onPrimaryContainer = Color(0xFFA1EFFF)
|
||||
private val md_theme_dark_secondary = Color(0xFFB1CBD1)
|
||||
private val md_theme_dark_onSecondary = Color(0xFF1C3439)
|
||||
private val md_theme_dark_secondaryContainer = Color(0xFF334A50)
|
||||
private val md_theme_dark_onSecondaryContainer = Color(0xFFCDE7ED)
|
||||
private val md_theme_dark_tertiary = Color(0xFFBCC5EB)
|
||||
private val md_theme_dark_onTertiary = Color(0xFF262F4D)
|
||||
private val md_theme_dark_tertiaryContainer = Color(0xFF3C4665)
|
||||
private val md_theme_dark_onTertiaryContainer = Color(0xFFDBE1FF)
|
||||
private val md_theme_dark_error = Color(0xFFFFB4AB)
|
||||
private val md_theme_dark_errorContainer = Color(0xFF93000A)
|
||||
private val md_theme_dark_onError = Color(0xFF690005)
|
||||
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_dark_background = Color(0xFF191C1D)
|
||||
private val md_theme_dark_onBackground = Color(0xFFE1E3E3)
|
||||
private val md_theme_dark_surface = Color(0xFF191C1D)
|
||||
private val md_theme_dark_onSurface = Color(0xFFE1E3E3)
|
||||
private val md_theme_dark_surfaceVariant = Color(0xFF3F484A)
|
||||
private val md_theme_dark_onSurfaceVariant = Color(0xFFBFC8CA)
|
||||
private val md_theme_dark_outline = Color(0xFF899295)
|
||||
private val md_theme_dark_inverseOnSurface = Color(0xFF191C1D)
|
||||
private val md_theme_dark_inverseSurface = Color(0xFFE1E3E3)
|
||||
private val md_theme_dark_inversePrimary = Color(0xFF006876)
|
||||
private val md_theme_dark_shadow = Color(0xFF000000)
|
||||
private val md_theme_dark_surfaceTint = Color(0xFF44D8F1)
|
||||
private val md_theme_dark_outlineVariant = Color(0xFF3F484A)
|
||||
private val md_theme_dark_scrim = Color(0xFF000000)
|
||||
|
||||
val LightCyanTheme = lightColorScheme(
|
||||
primary = md_theme_light_primary,
|
||||
onPrimary = md_theme_light_onPrimary,
|
||||
primaryContainer = md_theme_light_primaryContainer,
|
||||
onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
||||
secondary = md_theme_light_secondary,
|
||||
onSecondary = md_theme_light_onSecondary,
|
||||
secondaryContainer = md_theme_light_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
||||
tertiary = md_theme_light_tertiary,
|
||||
onTertiary = md_theme_light_onTertiary,
|
||||
tertiaryContainer = md_theme_light_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
||||
error = md_theme_light_error,
|
||||
errorContainer = md_theme_light_errorContainer,
|
||||
onError = md_theme_light_onError,
|
||||
onErrorContainer = md_theme_light_onErrorContainer,
|
||||
background = md_theme_light_background,
|
||||
onBackground = md_theme_light_onBackground,
|
||||
surface = md_theme_light_surface,
|
||||
onSurface = md_theme_light_onSurface,
|
||||
surfaceVariant = md_theme_light_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
||||
outline = md_theme_light_outline,
|
||||
inverseOnSurface = md_theme_light_inverseOnSurface,
|
||||
inverseSurface = md_theme_light_inverseSurface,
|
||||
inversePrimary = md_theme_light_inversePrimary,
|
||||
surfaceTint = md_theme_light_surfaceTint,
|
||||
outlineVariant = md_theme_light_outlineVariant,
|
||||
scrim = md_theme_light_scrim,
|
||||
)
|
||||
|
||||
val DarkCyanTheme = darkColorScheme(
|
||||
primary = md_theme_dark_primary,
|
||||
onPrimary = md_theme_dark_onPrimary,
|
||||
primaryContainer = md_theme_dark_primaryContainer,
|
||||
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
|
||||
secondary = md_theme_dark_secondary,
|
||||
onSecondary = md_theme_dark_onSecondary,
|
||||
secondaryContainer = md_theme_dark_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
|
||||
tertiary = md_theme_dark_tertiary,
|
||||
onTertiary = md_theme_dark_onTertiary,
|
||||
tertiaryContainer = md_theme_dark_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
|
||||
error = md_theme_dark_error,
|
||||
errorContainer = md_theme_dark_errorContainer,
|
||||
onError = md_theme_dark_onError,
|
||||
onErrorContainer = md_theme_dark_onErrorContainer,
|
||||
background = md_theme_dark_background,
|
||||
onBackground = md_theme_dark_onBackground,
|
||||
surface = md_theme_dark_surface,
|
||||
onSurface = md_theme_dark_onSurface,
|
||||
surfaceVariant = md_theme_dark_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
|
||||
outline = md_theme_dark_outline,
|
||||
inverseOnSurface = md_theme_dark_inverseOnSurface,
|
||||
inverseSurface = md_theme_dark_inverseSurface,
|
||||
inversePrimary = md_theme_dark_inversePrimary,
|
||||
surfaceTint = md_theme_dark_surfaceTint,
|
||||
outlineVariant = md_theme_dark_outlineVariant,
|
||||
scrim = md_theme_dark_scrim,
|
||||
)
|
||||
132
app/src/main/java/me/bmax/apatch/ui/theme/DeepOrangeTheme.kt
Normal file
132
app/src/main/java/me/bmax/apatch/ui/theme/DeepOrangeTheme.kt
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
package me.bmax.apatch.ui.theme
|
||||
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
private val md_theme_light_primary = Color(0xFFB02F00)
|
||||
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_primaryContainer = Color(0xFFFFDBD1)
|
||||
private val md_theme_light_onPrimaryContainer = Color(0xFF3B0900)
|
||||
private val md_theme_light_secondary = Color(0xFF77574E)
|
||||
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_secondaryContainer = Color(0xFFFFDBD1)
|
||||
private val md_theme_light_onSecondaryContainer = Color(0xFF2C150F)
|
||||
private val md_theme_light_tertiary = Color(0xFF6C5D2F)
|
||||
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_tertiaryContainer = Color(0xFFF5E1A7)
|
||||
private val md_theme_light_onTertiaryContainer = Color(0xFF231B00)
|
||||
private val md_theme_light_error = Color(0xFFBA1A1A)
|
||||
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_onErrorContainer = Color(0xFF410002)
|
||||
private val md_theme_light_background = Color(0xFFFFFBFF)
|
||||
private val md_theme_light_onBackground = Color(0xFF201A18)
|
||||
private val md_theme_light_surface = Color(0xFFFFFBFF)
|
||||
private val md_theme_light_onSurface = Color(0xFF201A18)
|
||||
private val md_theme_light_surfaceVariant = Color(0xFFF5DED8)
|
||||
private val md_theme_light_onSurfaceVariant = Color(0xFF53433F)
|
||||
private val md_theme_light_outline = Color(0xFF85736E)
|
||||
private val md_theme_light_inverseOnSurface = Color(0xFFFBEEEB)
|
||||
private val md_theme_light_inverseSurface = Color(0xFF362F2D)
|
||||
private val md_theme_light_inversePrimary = Color(0xFFFFB5A0)
|
||||
private val md_theme_light_shadow = Color(0xFF000000)
|
||||
private val md_theme_light_surfaceTint = Color(0xFFB02F00)
|
||||
private val md_theme_light_outlineVariant = Color(0xFFD8C2BC)
|
||||
private val md_theme_light_scrim = Color(0xFF000000)
|
||||
|
||||
private val md_theme_dark_primary = Color(0xFFFFB5A0)
|
||||
private val md_theme_dark_onPrimary = Color(0xFF5F1500)
|
||||
private val md_theme_dark_primaryContainer = Color(0xFF862200)
|
||||
private val md_theme_dark_onPrimaryContainer = Color(0xFFFFDBD1)
|
||||
private val md_theme_dark_secondary = Color(0xFFE7BDB2)
|
||||
private val md_theme_dark_onSecondary = Color(0xFF442A22)
|
||||
private val md_theme_dark_secondaryContainer = Color(0xFF5D4037)
|
||||
private val md_theme_dark_onSecondaryContainer = Color(0xFFFFDBD1)
|
||||
private val md_theme_dark_tertiary = Color(0xFFD8C58D)
|
||||
private val md_theme_dark_onTertiary = Color(0xFF3B2F05)
|
||||
private val md_theme_dark_tertiaryContainer = Color(0xFF534619)
|
||||
private val md_theme_dark_onTertiaryContainer = Color(0xFFF5E1A7)
|
||||
private val md_theme_dark_error = Color(0xFFFFB4AB)
|
||||
private val md_theme_dark_errorContainer = Color(0xFF93000A)
|
||||
private val md_theme_dark_onError = Color(0xFF690005)
|
||||
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_dark_background = Color(0xFF201A18)
|
||||
private val md_theme_dark_onBackground = Color(0xFFEDE0DC)
|
||||
private val md_theme_dark_surface = Color(0xFF201A18)
|
||||
private val md_theme_dark_onSurface = Color(0xFFEDE0DC)
|
||||
private val md_theme_dark_surfaceVariant = Color(0xFF53433F)
|
||||
private val md_theme_dark_onSurfaceVariant = Color(0xFFD8C2BC)
|
||||
private val md_theme_dark_outline = Color(0xFFA08C87)
|
||||
private val md_theme_dark_inverseOnSurface = Color(0xFF201A18)
|
||||
private val md_theme_dark_inverseSurface = Color(0xFFEDE0DC)
|
||||
private val md_theme_dark_inversePrimary = Color(0xFFB02F00)
|
||||
private val md_theme_dark_shadow = Color(0xFF000000)
|
||||
private val md_theme_dark_surfaceTint = Color(0xFFFFB5A0)
|
||||
private val md_theme_dark_outlineVariant = Color(0xFF53433F)
|
||||
private val md_theme_dark_scrim = Color(0xFF000000)
|
||||
|
||||
|
||||
val LightDeepOrangeTheme = lightColorScheme(
|
||||
primary = md_theme_light_primary,
|
||||
onPrimary = md_theme_light_onPrimary,
|
||||
primaryContainer = md_theme_light_primaryContainer,
|
||||
onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
||||
secondary = md_theme_light_secondary,
|
||||
onSecondary = md_theme_light_onSecondary,
|
||||
secondaryContainer = md_theme_light_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
||||
tertiary = md_theme_light_tertiary,
|
||||
onTertiary = md_theme_light_onTertiary,
|
||||
tertiaryContainer = md_theme_light_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
||||
error = md_theme_light_error,
|
||||
errorContainer = md_theme_light_errorContainer,
|
||||
onError = md_theme_light_onError,
|
||||
onErrorContainer = md_theme_light_onErrorContainer,
|
||||
background = md_theme_light_background,
|
||||
onBackground = md_theme_light_onBackground,
|
||||
surface = md_theme_light_surface,
|
||||
onSurface = md_theme_light_onSurface,
|
||||
surfaceVariant = md_theme_light_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
||||
outline = md_theme_light_outline,
|
||||
inverseOnSurface = md_theme_light_inverseOnSurface,
|
||||
inverseSurface = md_theme_light_inverseSurface,
|
||||
inversePrimary = md_theme_light_inversePrimary,
|
||||
surfaceTint = md_theme_light_surfaceTint,
|
||||
outlineVariant = md_theme_light_outlineVariant,
|
||||
scrim = md_theme_light_scrim,
|
||||
)
|
||||
|
||||
val DarkDeepOrangeTheme = darkColorScheme(
|
||||
primary = md_theme_dark_primary,
|
||||
onPrimary = md_theme_dark_onPrimary,
|
||||
primaryContainer = md_theme_dark_primaryContainer,
|
||||
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
|
||||
secondary = md_theme_dark_secondary,
|
||||
onSecondary = md_theme_dark_onSecondary,
|
||||
secondaryContainer = md_theme_dark_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
|
||||
tertiary = md_theme_dark_tertiary,
|
||||
onTertiary = md_theme_dark_onTertiary,
|
||||
tertiaryContainer = md_theme_dark_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
|
||||
error = md_theme_dark_error,
|
||||
errorContainer = md_theme_dark_errorContainer,
|
||||
onError = md_theme_dark_onError,
|
||||
onErrorContainer = md_theme_dark_onErrorContainer,
|
||||
background = md_theme_dark_background,
|
||||
onBackground = md_theme_dark_onBackground,
|
||||
surface = md_theme_dark_surface,
|
||||
onSurface = md_theme_dark_onSurface,
|
||||
surfaceVariant = md_theme_dark_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
|
||||
outline = md_theme_dark_outline,
|
||||
inverseOnSurface = md_theme_dark_inverseOnSurface,
|
||||
inverseSurface = md_theme_dark_inverseSurface,
|
||||
inversePrimary = md_theme_dark_inversePrimary,
|
||||
surfaceTint = md_theme_dark_surfaceTint,
|
||||
outlineVariant = md_theme_dark_outlineVariant,
|
||||
scrim = md_theme_dark_scrim,
|
||||
)
|
||||
131
app/src/main/java/me/bmax/apatch/ui/theme/DeepPurpleTheme.kt
Normal file
131
app/src/main/java/me/bmax/apatch/ui/theme/DeepPurpleTheme.kt
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package me.bmax.apatch.ui.theme
|
||||
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
private val md_theme_light_primary = Color(0xFF6F43C0)
|
||||
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_primaryContainer = Color(0xFFEBDDFF)
|
||||
private val md_theme_light_onPrimaryContainer = Color(0xFF250059)
|
||||
private val md_theme_light_secondary = Color(0xFF635B70)
|
||||
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_secondaryContainer = Color(0xFFE9DEF8)
|
||||
private val md_theme_light_onSecondaryContainer = Color(0xFF1F182B)
|
||||
private val md_theme_light_tertiary = Color(0xFF7E525D)
|
||||
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_tertiaryContainer = Color(0xFFFFD9E1)
|
||||
private val md_theme_light_onTertiaryContainer = Color(0xFF31101B)
|
||||
private val md_theme_light_error = Color(0xFFBA1A1A)
|
||||
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_onErrorContainer = Color(0xFF410002)
|
||||
private val md_theme_light_background = Color(0xFFFFFBFF)
|
||||
private val md_theme_light_onBackground = Color(0xFF1D1B1E)
|
||||
private val md_theme_light_surface = Color(0xFFFFFBFF)
|
||||
private val md_theme_light_onSurface = Color(0xFF1D1B1E)
|
||||
private val md_theme_light_surfaceVariant = Color(0xFFE7E0EB)
|
||||
private val md_theme_light_onSurfaceVariant = Color(0xFF49454E)
|
||||
private val md_theme_light_outline = Color(0xFF7A757F)
|
||||
private val md_theme_light_inverseOnSurface = Color(0xFFF5EFF4)
|
||||
private val md_theme_light_inverseSurface = Color(0xFF323033)
|
||||
private val md_theme_light_inversePrimary = Color(0xFFD3BBFF)
|
||||
private val md_theme_light_shadow = Color(0xFF000000)
|
||||
private val md_theme_light_surfaceTint = Color(0xFF6F43C0)
|
||||
private val md_theme_light_outlineVariant = Color(0xFFCBC4CF)
|
||||
private val md_theme_light_scrim = Color(0xFF000000)
|
||||
|
||||
private val md_theme_dark_primary = Color(0xFFD3BBFF)
|
||||
private val md_theme_dark_onPrimary = Color(0xFF3F008D)
|
||||
private val md_theme_dark_primaryContainer = Color(0xFF5727A6)
|
||||
private val md_theme_dark_onPrimaryContainer = Color(0xFFEBDDFF)
|
||||
private val md_theme_dark_secondary = Color(0xFFCDC2DB)
|
||||
private val md_theme_dark_onSecondary = Color(0xFF342D40)
|
||||
private val md_theme_dark_secondaryContainer = Color(0xFF4B4358)
|
||||
private val md_theme_dark_onSecondaryContainer = Color(0xFFE9DEF8)
|
||||
private val md_theme_dark_tertiary = Color(0xFFF0B7C5)
|
||||
private val md_theme_dark_onTertiary = Color(0xFF4A2530)
|
||||
private val md_theme_dark_tertiaryContainer = Color(0xFF643B46)
|
||||
private val md_theme_dark_onTertiaryContainer = Color(0xFFFFD9E1)
|
||||
private val md_theme_dark_error = Color(0xFFFFB4AB)
|
||||
private val md_theme_dark_errorContainer = Color(0xFF93000A)
|
||||
private val md_theme_dark_onError = Color(0xFF690005)
|
||||
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_dark_background = Color(0xFF1D1B1E)
|
||||
private val md_theme_dark_onBackground = Color(0xFFE6E1E6)
|
||||
private val md_theme_dark_surface = Color(0xFF1D1B1E)
|
||||
private val md_theme_dark_onSurface = Color(0xFFE6E1E6)
|
||||
private val md_theme_dark_surfaceVariant = Color(0xFF49454E)
|
||||
private val md_theme_dark_onSurfaceVariant = Color(0xFFCBC4CF)
|
||||
private val md_theme_dark_outline = Color(0xFF948F99)
|
||||
private val md_theme_dark_inverseOnSurface = Color(0xFF1D1B1E)
|
||||
private val md_theme_dark_inverseSurface = Color(0xFFE6E1E6)
|
||||
private val md_theme_dark_inversePrimary = Color(0xFF6F43C0)
|
||||
private val md_theme_dark_shadow = Color(0xFF000000)
|
||||
private val md_theme_dark_surfaceTint = Color(0xFFD3BBFF)
|
||||
private val md_theme_dark_outlineVariant = Color(0xFF49454E)
|
||||
private val md_theme_dark_scrim = Color(0xFF000000)
|
||||
|
||||
val LightDeepPurpleTheme = lightColorScheme(
|
||||
primary = md_theme_light_primary,
|
||||
onPrimary = md_theme_light_onPrimary,
|
||||
primaryContainer = md_theme_light_primaryContainer,
|
||||
onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
||||
secondary = md_theme_light_secondary,
|
||||
onSecondary = md_theme_light_onSecondary,
|
||||
secondaryContainer = md_theme_light_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
||||
tertiary = md_theme_light_tertiary,
|
||||
onTertiary = md_theme_light_onTertiary,
|
||||
tertiaryContainer = md_theme_light_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
||||
error = md_theme_light_error,
|
||||
errorContainer = md_theme_light_errorContainer,
|
||||
onError = md_theme_light_onError,
|
||||
onErrorContainer = md_theme_light_onErrorContainer,
|
||||
background = md_theme_light_background,
|
||||
onBackground = md_theme_light_onBackground,
|
||||
surface = md_theme_light_surface,
|
||||
onSurface = md_theme_light_onSurface,
|
||||
surfaceVariant = md_theme_light_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
||||
outline = md_theme_light_outline,
|
||||
inverseOnSurface = md_theme_light_inverseOnSurface,
|
||||
inverseSurface = md_theme_light_inverseSurface,
|
||||
inversePrimary = md_theme_light_inversePrimary,
|
||||
surfaceTint = md_theme_light_surfaceTint,
|
||||
outlineVariant = md_theme_light_outlineVariant,
|
||||
scrim = md_theme_light_scrim,
|
||||
)
|
||||
|
||||
val DarkDeepPurpleTheme = darkColorScheme(
|
||||
primary = md_theme_dark_primary,
|
||||
onPrimary = md_theme_dark_onPrimary,
|
||||
primaryContainer = md_theme_dark_primaryContainer,
|
||||
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
|
||||
secondary = md_theme_dark_secondary,
|
||||
onSecondary = md_theme_dark_onSecondary,
|
||||
secondaryContainer = md_theme_dark_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
|
||||
tertiary = md_theme_dark_tertiary,
|
||||
onTertiary = md_theme_dark_onTertiary,
|
||||
tertiaryContainer = md_theme_dark_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
|
||||
error = md_theme_dark_error,
|
||||
errorContainer = md_theme_dark_errorContainer,
|
||||
onError = md_theme_dark_onError,
|
||||
onErrorContainer = md_theme_dark_onErrorContainer,
|
||||
background = md_theme_dark_background,
|
||||
onBackground = md_theme_dark_onBackground,
|
||||
surface = md_theme_dark_surface,
|
||||
onSurface = md_theme_dark_onSurface,
|
||||
surfaceVariant = md_theme_dark_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
|
||||
outline = md_theme_dark_outline,
|
||||
inverseOnSurface = md_theme_dark_inverseOnSurface,
|
||||
inverseSurface = md_theme_dark_inverseSurface,
|
||||
inversePrimary = md_theme_dark_inversePrimary,
|
||||
surfaceTint = md_theme_dark_surfaceTint,
|
||||
outlineVariant = md_theme_dark_outlineVariant,
|
||||
scrim = md_theme_dark_scrim,
|
||||
)
|
||||
131
app/src/main/java/me/bmax/apatch/ui/theme/GreenTheme.kt
Normal file
131
app/src/main/java/me/bmax/apatch/ui/theme/GreenTheme.kt
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package me.bmax.apatch.ui.theme
|
||||
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
private val md_theme_light_primary = Color(0xFF006E1A)
|
||||
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_primaryContainer = Color(0xFF96F990)
|
||||
private val md_theme_light_onPrimaryContainer = Color(0xFF002203)
|
||||
private val md_theme_light_secondary = Color(0xFF53634F)
|
||||
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_secondaryContainer = Color(0xFFD6E8CE)
|
||||
private val md_theme_light_onSecondaryContainer = Color(0xFF111F0F)
|
||||
private val md_theme_light_tertiary = Color(0xFF38656A)
|
||||
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_tertiaryContainer = Color(0xFFBCEBF0)
|
||||
private val md_theme_light_onTertiaryContainer = Color(0xFF002023)
|
||||
private val md_theme_light_error = Color(0xFFBA1A1A)
|
||||
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_onErrorContainer = Color(0xFF410002)
|
||||
private val md_theme_light_background = Color(0xFFFCFDF6)
|
||||
private val md_theme_light_onBackground = Color(0xFF1A1C19)
|
||||
private val md_theme_light_surface = Color(0xFFFCFDF6)
|
||||
private val md_theme_light_onSurface = Color(0xFF1A1C19)
|
||||
private val md_theme_light_surfaceVariant = Color(0xFFDEE5D8)
|
||||
private val md_theme_light_onSurfaceVariant = Color(0xFF424940)
|
||||
private val md_theme_light_outline = Color(0xFF72796F)
|
||||
private val md_theme_light_inverseOnSurface = Color(0xFFF1F1EB)
|
||||
private val md_theme_light_inverseSurface = Color(0xFF2F312D)
|
||||
private val md_theme_light_inversePrimary = Color(0xFF7ADC77)
|
||||
private val md_theme_light_shadow = Color(0xFF000000)
|
||||
private val md_theme_light_surfaceTint = Color(0xFF006E1A)
|
||||
private val md_theme_light_outlineVariant = Color(0xFFC2C8BD)
|
||||
private val md_theme_light_scrim = Color(0xFF000000)
|
||||
|
||||
private val md_theme_dark_primary = Color(0xFF7ADC77)
|
||||
private val md_theme_dark_onPrimary = Color(0xFF003909)
|
||||
private val md_theme_dark_primaryContainer = Color(0xFF005311)
|
||||
private val md_theme_dark_onPrimaryContainer = Color(0xFF96F990)
|
||||
private val md_theme_dark_secondary = Color(0xFFBACCB3)
|
||||
private val md_theme_dark_onSecondary = Color(0xFF253423)
|
||||
private val md_theme_dark_secondaryContainer = Color(0xFF3B4B38)
|
||||
private val md_theme_dark_onSecondaryContainer = Color(0xFFD6E8CE)
|
||||
private val md_theme_dark_tertiary = Color(0xFFA0CFD4)
|
||||
private val md_theme_dark_onTertiary = Color(0xFF00363B)
|
||||
private val md_theme_dark_tertiaryContainer = Color(0xFF1E4D52)
|
||||
private val md_theme_dark_onTertiaryContainer = Color(0xFFBCEBF0)
|
||||
private val md_theme_dark_error = Color(0xFFFFB4AB)
|
||||
private val md_theme_dark_errorContainer = Color(0xFF93000A)
|
||||
private val md_theme_dark_onError = Color(0xFF690005)
|
||||
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_dark_background = Color(0xFF1A1C19)
|
||||
private val md_theme_dark_onBackground = Color(0xFFE2E3DD)
|
||||
private val md_theme_dark_surface = Color(0xFF1A1C19)
|
||||
private val md_theme_dark_onSurface = Color(0xFFE2E3DD)
|
||||
private val md_theme_dark_surfaceVariant = Color(0xFF424940)
|
||||
private val md_theme_dark_onSurfaceVariant = Color(0xFFC2C8BD)
|
||||
private val md_theme_dark_outline = Color(0xFF8C9388)
|
||||
private val md_theme_dark_inverseOnSurface = Color(0xFF1A1C19)
|
||||
private val md_theme_dark_inverseSurface = Color(0xFFE2E3DD)
|
||||
private val md_theme_dark_inversePrimary = Color(0xFF006E1A)
|
||||
private val md_theme_dark_shadow = Color(0xFF000000)
|
||||
private val md_theme_dark_surfaceTint = Color(0xFF7ADC77)
|
||||
private val md_theme_dark_outlineVariant = Color(0xFF424940)
|
||||
private val md_theme_dark_scrim = Color(0xFF000000)
|
||||
|
||||
val LightGreenTheme = lightColorScheme(
|
||||
primary = md_theme_light_primary,
|
||||
onPrimary = md_theme_light_onPrimary,
|
||||
primaryContainer = md_theme_light_primaryContainer,
|
||||
onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
||||
secondary = md_theme_light_secondary,
|
||||
onSecondary = md_theme_light_onSecondary,
|
||||
secondaryContainer = md_theme_light_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
||||
tertiary = md_theme_light_tertiary,
|
||||
onTertiary = md_theme_light_onTertiary,
|
||||
tertiaryContainer = md_theme_light_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
||||
error = md_theme_light_error,
|
||||
errorContainer = md_theme_light_errorContainer,
|
||||
onError = md_theme_light_onError,
|
||||
onErrorContainer = md_theme_light_onErrorContainer,
|
||||
background = md_theme_light_background,
|
||||
onBackground = md_theme_light_onBackground,
|
||||
surface = md_theme_light_surface,
|
||||
onSurface = md_theme_light_onSurface,
|
||||
surfaceVariant = md_theme_light_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
||||
outline = md_theme_light_outline,
|
||||
inverseOnSurface = md_theme_light_inverseOnSurface,
|
||||
inverseSurface = md_theme_light_inverseSurface,
|
||||
inversePrimary = md_theme_light_inversePrimary,
|
||||
surfaceTint = md_theme_light_surfaceTint,
|
||||
outlineVariant = md_theme_light_outlineVariant,
|
||||
scrim = md_theme_light_scrim,
|
||||
)
|
||||
|
||||
val DarkGreenTheme = darkColorScheme(
|
||||
primary = md_theme_dark_primary,
|
||||
onPrimary = md_theme_dark_onPrimary,
|
||||
primaryContainer = md_theme_dark_primaryContainer,
|
||||
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
|
||||
secondary = md_theme_dark_secondary,
|
||||
onSecondary = md_theme_dark_onSecondary,
|
||||
secondaryContainer = md_theme_dark_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
|
||||
tertiary = md_theme_dark_tertiary,
|
||||
onTertiary = md_theme_dark_onTertiary,
|
||||
tertiaryContainer = md_theme_dark_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
|
||||
error = md_theme_dark_error,
|
||||
errorContainer = md_theme_dark_errorContainer,
|
||||
onError = md_theme_dark_onError,
|
||||
onErrorContainer = md_theme_dark_onErrorContainer,
|
||||
background = md_theme_dark_background,
|
||||
onBackground = md_theme_dark_onBackground,
|
||||
surface = md_theme_dark_surface,
|
||||
onSurface = md_theme_dark_onSurface,
|
||||
surfaceVariant = md_theme_dark_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
|
||||
outline = md_theme_dark_outline,
|
||||
inverseOnSurface = md_theme_dark_inverseOnSurface,
|
||||
inverseSurface = md_theme_dark_inverseSurface,
|
||||
inversePrimary = md_theme_dark_inversePrimary,
|
||||
surfaceTint = md_theme_dark_surfaceTint,
|
||||
outlineVariant = md_theme_dark_outlineVariant,
|
||||
scrim = md_theme_dark_scrim,
|
||||
)
|
||||
131
app/src/main/java/me/bmax/apatch/ui/theme/IndigoTheme.kt
Normal file
131
app/src/main/java/me/bmax/apatch/ui/theme/IndigoTheme.kt
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package me.bmax.apatch.ui.theme
|
||||
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
private val md_theme_light_primary = Color(0xFF4355B9)
|
||||
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_primaryContainer = Color(0xFFDEE0FF)
|
||||
private val md_theme_light_onPrimaryContainer = Color(0xFF00105C)
|
||||
private val md_theme_light_secondary = Color(0xFF5B5D72)
|
||||
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_secondaryContainer = Color(0xFFE0E1F9)
|
||||
private val md_theme_light_onSecondaryContainer = Color(0xFF181A2C)
|
||||
private val md_theme_light_tertiary = Color(0xFF77536D)
|
||||
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_tertiaryContainer = Color(0xFFFFD7F1)
|
||||
private val md_theme_light_onTertiaryContainer = Color(0xFF2D1228)
|
||||
private val md_theme_light_error = Color(0xFFBA1A1A)
|
||||
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_onErrorContainer = Color(0xFF410002)
|
||||
private val md_theme_light_background = Color(0xFFFEFBFF)
|
||||
private val md_theme_light_onBackground = Color(0xFF1B1B1F)
|
||||
private val md_theme_light_surface = Color(0xFFFEFBFF)
|
||||
private val md_theme_light_onSurface = Color(0xFF1B1B1F)
|
||||
private val md_theme_light_surfaceVariant = Color(0xFFE3E1EC)
|
||||
private val md_theme_light_onSurfaceVariant = Color(0xFF46464F)
|
||||
private val md_theme_light_outline = Color(0xFF767680)
|
||||
private val md_theme_light_inverseOnSurface = Color(0xFFF3F0F4)
|
||||
private val md_theme_light_inverseSurface = Color(0xFF303034)
|
||||
private val md_theme_light_inversePrimary = Color(0xFFBAC3FF)
|
||||
private val md_theme_light_shadow = Color(0xFF000000)
|
||||
private val md_theme_light_surfaceTint = Color(0xFF4355B9)
|
||||
private val md_theme_light_outlineVariant = Color(0xFFC7C5D0)
|
||||
private val md_theme_light_scrim = Color(0xFF000000)
|
||||
|
||||
private val md_theme_dark_primary = Color(0xFFBAC3FF)
|
||||
private val md_theme_dark_onPrimary = Color(0xFF08218A)
|
||||
private val md_theme_dark_primaryContainer = Color(0xFF293CA0)
|
||||
private val md_theme_dark_onPrimaryContainer = Color(0xFFDEE0FF)
|
||||
private val md_theme_dark_secondary = Color(0xFFC3C5DD)
|
||||
private val md_theme_dark_onSecondary = Color(0xFF2D2F42)
|
||||
private val md_theme_dark_secondaryContainer = Color(0xFF434659)
|
||||
private val md_theme_dark_onSecondaryContainer = Color(0xFFE0E1F9)
|
||||
private val md_theme_dark_tertiary = Color(0xFFE6BAD7)
|
||||
private val md_theme_dark_onTertiary = Color(0xFF44263D)
|
||||
private val md_theme_dark_tertiaryContainer = Color(0xFF5D3C55)
|
||||
private val md_theme_dark_onTertiaryContainer = Color(0xFFFFD7F1)
|
||||
private val md_theme_dark_error = Color(0xFFFFB4AB)
|
||||
private val md_theme_dark_errorContainer = Color(0xFF93000A)
|
||||
private val md_theme_dark_onError = Color(0xFF690005)
|
||||
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_dark_background = Color(0xFF1B1B1F)
|
||||
private val md_theme_dark_onBackground = Color(0xFFE4E1E6)
|
||||
private val md_theme_dark_surface = Color(0xFF1B1B1F)
|
||||
private val md_theme_dark_onSurface = Color(0xFFE4E1E6)
|
||||
private val md_theme_dark_surfaceVariant = Color(0xFF46464F)
|
||||
private val md_theme_dark_onSurfaceVariant = Color(0xFFC7C5D0)
|
||||
private val md_theme_dark_outline = Color(0xFF90909A)
|
||||
private val md_theme_dark_inverseOnSurface = Color(0xFF1B1B1F)
|
||||
private val md_theme_dark_inverseSurface = Color(0xFFE4E1E6)
|
||||
private val md_theme_dark_inversePrimary = Color(0xFF4355B9)
|
||||
private val md_theme_dark_shadow = Color(0xFF000000)
|
||||
private val md_theme_dark_surfaceTint = Color(0xFFBAC3FF)
|
||||
private val md_theme_dark_outlineVariant = Color(0xFF46464F)
|
||||
private val md_theme_dark_scrim = Color(0xFF000000)
|
||||
|
||||
val LightIndigoTheme = lightColorScheme(
|
||||
primary = md_theme_light_primary,
|
||||
onPrimary = md_theme_light_onPrimary,
|
||||
primaryContainer = md_theme_light_primaryContainer,
|
||||
onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
||||
secondary = md_theme_light_secondary,
|
||||
onSecondary = md_theme_light_onSecondary,
|
||||
secondaryContainer = md_theme_light_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
||||
tertiary = md_theme_light_tertiary,
|
||||
onTertiary = md_theme_light_onTertiary,
|
||||
tertiaryContainer = md_theme_light_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
||||
error = md_theme_light_error,
|
||||
errorContainer = md_theme_light_errorContainer,
|
||||
onError = md_theme_light_onError,
|
||||
onErrorContainer = md_theme_light_onErrorContainer,
|
||||
background = md_theme_light_background,
|
||||
onBackground = md_theme_light_onBackground,
|
||||
surface = md_theme_light_surface,
|
||||
onSurface = md_theme_light_onSurface,
|
||||
surfaceVariant = md_theme_light_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
||||
outline = md_theme_light_outline,
|
||||
inverseOnSurface = md_theme_light_inverseOnSurface,
|
||||
inverseSurface = md_theme_light_inverseSurface,
|
||||
inversePrimary = md_theme_light_inversePrimary,
|
||||
surfaceTint = md_theme_light_surfaceTint,
|
||||
outlineVariant = md_theme_light_outlineVariant,
|
||||
scrim = md_theme_light_scrim,
|
||||
)
|
||||
|
||||
val DarkIndigoTheme = darkColorScheme(
|
||||
primary = md_theme_dark_primary,
|
||||
onPrimary = md_theme_dark_onPrimary,
|
||||
primaryContainer = md_theme_dark_primaryContainer,
|
||||
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
|
||||
secondary = md_theme_dark_secondary,
|
||||
onSecondary = md_theme_dark_onSecondary,
|
||||
secondaryContainer = md_theme_dark_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
|
||||
tertiary = md_theme_dark_tertiary,
|
||||
onTertiary = md_theme_dark_onTertiary,
|
||||
tertiaryContainer = md_theme_dark_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
|
||||
error = md_theme_dark_error,
|
||||
errorContainer = md_theme_dark_errorContainer,
|
||||
onError = md_theme_dark_onError,
|
||||
onErrorContainer = md_theme_dark_onErrorContainer,
|
||||
background = md_theme_dark_background,
|
||||
onBackground = md_theme_dark_onBackground,
|
||||
surface = md_theme_dark_surface,
|
||||
onSurface = md_theme_dark_onSurface,
|
||||
surfaceVariant = md_theme_dark_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
|
||||
outline = md_theme_dark_outline,
|
||||
inverseOnSurface = md_theme_dark_inverseOnSurface,
|
||||
inverseSurface = md_theme_dark_inverseSurface,
|
||||
inversePrimary = md_theme_dark_inversePrimary,
|
||||
surfaceTint = md_theme_dark_surfaceTint,
|
||||
outlineVariant = md_theme_dark_outlineVariant,
|
||||
scrim = md_theme_dark_scrim,
|
||||
)
|
||||
132
app/src/main/java/me/bmax/apatch/ui/theme/LightBlueTheme.kt
Normal file
132
app/src/main/java/me/bmax/apatch/ui/theme/LightBlueTheme.kt
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
package me.bmax.apatch.ui.theme
|
||||
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
private val md_theme_light_primary = Color(0xFF006493)
|
||||
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_primaryContainer = Color(0xFFCAE6FF)
|
||||
private val md_theme_light_onPrimaryContainer = Color(0xFF001E30)
|
||||
private val md_theme_light_secondary = Color(0xFF50606E)
|
||||
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_secondaryContainer = Color(0xFFD3E5F5)
|
||||
private val md_theme_light_onSecondaryContainer = Color(0xFF0C1D29)
|
||||
private val md_theme_light_tertiary = Color(0xFF65587B)
|
||||
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_tertiaryContainer = Color(0xFFEBDDFF)
|
||||
private val md_theme_light_onTertiaryContainer = Color(0xFF201634)
|
||||
private val md_theme_light_error = Color(0xFFBA1A1A)
|
||||
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_onErrorContainer = Color(0xFF410002)
|
||||
private val md_theme_light_background = Color(0xFFFCFCFF)
|
||||
private val md_theme_light_onBackground = Color(0xFF1A1C1E)
|
||||
private val md_theme_light_surface = Color(0xFFFCFCFF)
|
||||
private val md_theme_light_onSurface = Color(0xFF1A1C1E)
|
||||
private val md_theme_light_surfaceVariant = Color(0xFFDDE3EA)
|
||||
private val md_theme_light_onSurfaceVariant = Color(0xFF41474D)
|
||||
private val md_theme_light_outline = Color(0xFF72787E)
|
||||
private val md_theme_light_inverseOnSurface = Color(0xFFF0F0F3)
|
||||
private val md_theme_light_inverseSurface = Color(0xFF2E3133)
|
||||
private val md_theme_light_inversePrimary = Color(0xFF8DCDFF)
|
||||
private val md_theme_light_shadow = Color(0xFF000000)
|
||||
private val md_theme_light_surfaceTint = Color(0xFF006493)
|
||||
private val md_theme_light_outlineVariant = Color(0xFFC1C7CE)
|
||||
private val md_theme_light_scrim = Color(0xFF000000)
|
||||
|
||||
private val md_theme_dark_primary = Color(0xFF8DCDFF)
|
||||
private val md_theme_dark_onPrimary = Color(0xFF00344F)
|
||||
private val md_theme_dark_primaryContainer = Color(0xFF004B70)
|
||||
private val md_theme_dark_onPrimaryContainer = Color(0xFFCAE6FF)
|
||||
private val md_theme_dark_secondary = Color(0xFFB7C9D9)
|
||||
private val md_theme_dark_onSecondary = Color(0xFF22323F)
|
||||
private val md_theme_dark_secondaryContainer = Color(0xFF384956)
|
||||
private val md_theme_dark_onSecondaryContainer = Color(0xFFD3E5F5)
|
||||
private val md_theme_dark_tertiary = Color(0xFFCFC0E8)
|
||||
private val md_theme_dark_onTertiary = Color(0xFF362B4B)
|
||||
private val md_theme_dark_tertiaryContainer = Color(0xFF4D4162)
|
||||
private val md_theme_dark_onTertiaryContainer = Color(0xFFEBDDFF)
|
||||
private val md_theme_dark_error = Color(0xFFFFB4AB)
|
||||
private val md_theme_dark_errorContainer = Color(0xFF93000A)
|
||||
private val md_theme_dark_onError = Color(0xFF690005)
|
||||
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_dark_background = Color(0xFF1A1C1E)
|
||||
private val md_theme_dark_onBackground = Color(0xFFE2E2E5)
|
||||
private val md_theme_dark_surface = Color(0xFF1A1C1E)
|
||||
private val md_theme_dark_onSurface = Color(0xFFE2E2E5)
|
||||
private val md_theme_dark_surfaceVariant = Color(0xFF41474D)
|
||||
private val md_theme_dark_onSurfaceVariant = Color(0xFFC1C7CE)
|
||||
private val md_theme_dark_outline = Color(0xFF8B9198)
|
||||
private val md_theme_dark_inverseOnSurface = Color(0xFF1A1C1E)
|
||||
private val md_theme_dark_inverseSurface = Color(0xFFE2E2E5)
|
||||
private val md_theme_dark_inversePrimary = Color(0xFF006493)
|
||||
private val md_theme_dark_shadow = Color(0xFF000000)
|
||||
private val md_theme_dark_surfaceTint = Color(0xFF8DCDFF)
|
||||
private val md_theme_dark_outlineVariant = Color(0xFF41474D)
|
||||
private val md_theme_dark_scrim = Color(0xFF000000)
|
||||
|
||||
|
||||
val LightLightBlueTheme = lightColorScheme(
|
||||
primary = md_theme_light_primary,
|
||||
onPrimary = md_theme_light_onPrimary,
|
||||
primaryContainer = md_theme_light_primaryContainer,
|
||||
onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
||||
secondary = md_theme_light_secondary,
|
||||
onSecondary = md_theme_light_onSecondary,
|
||||
secondaryContainer = md_theme_light_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
||||
tertiary = md_theme_light_tertiary,
|
||||
onTertiary = md_theme_light_onTertiary,
|
||||
tertiaryContainer = md_theme_light_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
||||
error = md_theme_light_error,
|
||||
errorContainer = md_theme_light_errorContainer,
|
||||
onError = md_theme_light_onError,
|
||||
onErrorContainer = md_theme_light_onErrorContainer,
|
||||
background = md_theme_light_background,
|
||||
onBackground = md_theme_light_onBackground,
|
||||
surface = md_theme_light_surface,
|
||||
onSurface = md_theme_light_onSurface,
|
||||
surfaceVariant = md_theme_light_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
||||
outline = md_theme_light_outline,
|
||||
inverseOnSurface = md_theme_light_inverseOnSurface,
|
||||
inverseSurface = md_theme_light_inverseSurface,
|
||||
inversePrimary = md_theme_light_inversePrimary,
|
||||
surfaceTint = md_theme_light_surfaceTint,
|
||||
outlineVariant = md_theme_light_outlineVariant,
|
||||
scrim = md_theme_light_scrim,
|
||||
)
|
||||
|
||||
val DarkLightBlueTheme = darkColorScheme(
|
||||
primary = md_theme_dark_primary,
|
||||
onPrimary = md_theme_dark_onPrimary,
|
||||
primaryContainer = md_theme_dark_primaryContainer,
|
||||
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
|
||||
secondary = md_theme_dark_secondary,
|
||||
onSecondary = md_theme_dark_onSecondary,
|
||||
secondaryContainer = md_theme_dark_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
|
||||
tertiary = md_theme_dark_tertiary,
|
||||
onTertiary = md_theme_dark_onTertiary,
|
||||
tertiaryContainer = md_theme_dark_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
|
||||
error = md_theme_dark_error,
|
||||
errorContainer = md_theme_dark_errorContainer,
|
||||
onError = md_theme_dark_onError,
|
||||
onErrorContainer = md_theme_dark_onErrorContainer,
|
||||
background = md_theme_dark_background,
|
||||
onBackground = md_theme_dark_onBackground,
|
||||
surface = md_theme_dark_surface,
|
||||
onSurface = md_theme_dark_onSurface,
|
||||
surfaceVariant = md_theme_dark_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
|
||||
outline = md_theme_dark_outline,
|
||||
inverseOnSurface = md_theme_dark_inverseOnSurface,
|
||||
inverseSurface = md_theme_dark_inverseSurface,
|
||||
inversePrimary = md_theme_dark_inversePrimary,
|
||||
surfaceTint = md_theme_dark_surfaceTint,
|
||||
outlineVariant = md_theme_dark_outlineVariant,
|
||||
scrim = md_theme_dark_scrim,
|
||||
)
|
||||
131
app/src/main/java/me/bmax/apatch/ui/theme/LightGreenTheme.kt
Normal file
131
app/src/main/java/me/bmax/apatch/ui/theme/LightGreenTheme.kt
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package me.bmax.apatch.ui.theme
|
||||
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
private val md_theme_light_primary = Color(0xFF006C48)
|
||||
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_primaryContainer = Color(0xFF8DF7C2)
|
||||
private val md_theme_light_onPrimaryContainer = Color(0xFF002113)
|
||||
private val md_theme_light_secondary = Color(0xFF4D6356)
|
||||
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_secondaryContainer = Color(0xFFD0E8D8)
|
||||
private val md_theme_light_onSecondaryContainer = Color(0xFF0A1F15)
|
||||
private val md_theme_light_tertiary = Color(0xFF3C6472)
|
||||
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_tertiaryContainer = Color(0xFFC0E9FA)
|
||||
private val md_theme_light_onTertiaryContainer = Color(0xFF001F28)
|
||||
private val md_theme_light_error = Color(0xFFBA1A1A)
|
||||
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_onErrorContainer = Color(0xFF410002)
|
||||
private val md_theme_light_background = Color(0xFFFBFDF8)
|
||||
private val md_theme_light_onBackground = Color(0xFF191C1A)
|
||||
private val md_theme_light_surface = Color(0xFFFBFDF8)
|
||||
private val md_theme_light_onSurface = Color(0xFF191C1A)
|
||||
private val md_theme_light_surfaceVariant = Color(0xFFDCE5DD)
|
||||
private val md_theme_light_onSurfaceVariant = Color(0xFF404943)
|
||||
private val md_theme_light_outline = Color(0xFF707973)
|
||||
private val md_theme_light_inverseOnSurface = Color(0xFFEFF1ED)
|
||||
private val md_theme_light_inverseSurface = Color(0xFF2E312F)
|
||||
private val md_theme_light_inversePrimary = Color(0xFF70DBA7)
|
||||
private val md_theme_light_shadow = Color(0xFF000000)
|
||||
private val md_theme_light_surfaceTint = Color(0xFF006C48)
|
||||
private val md_theme_light_outlineVariant = Color(0xFFC0C9C1)
|
||||
private val md_theme_light_scrim = Color(0xFF000000)
|
||||
|
||||
private val md_theme_dark_primary = Color(0xFF70DBA7)
|
||||
private val md_theme_dark_onPrimary = Color(0xFF003824)
|
||||
private val md_theme_dark_primaryContainer = Color(0xFF005235)
|
||||
private val md_theme_dark_onPrimaryContainer = Color(0xFF8DF7C2)
|
||||
private val md_theme_dark_secondary = Color(0xFFB4CCBC)
|
||||
private val md_theme_dark_onSecondary = Color(0xFF20352A)
|
||||
private val md_theme_dark_secondaryContainer = Color(0xFF364B3F)
|
||||
private val md_theme_dark_onSecondaryContainer = Color(0xFFD0E8D8)
|
||||
private val md_theme_dark_tertiary = Color(0xFFA4CDDE)
|
||||
private val md_theme_dark_onTertiary = Color(0xFF063543)
|
||||
private val md_theme_dark_tertiaryContainer = Color(0xFF234C5A)
|
||||
private val md_theme_dark_onTertiaryContainer = Color(0xFFC0E9FA)
|
||||
private val md_theme_dark_error = Color(0xFFFFB4AB)
|
||||
private val md_theme_dark_errorContainer = Color(0xFF93000A)
|
||||
private val md_theme_dark_onError = Color(0xFF690005)
|
||||
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_dark_background = Color(0xFF191C1A)
|
||||
private val md_theme_dark_onBackground = Color(0xFFE1E3DF)
|
||||
private val md_theme_dark_surface = Color(0xFF191C1A)
|
||||
private val md_theme_dark_onSurface = Color(0xFFE1E3DF)
|
||||
private val md_theme_dark_surfaceVariant = Color(0xFF404943)
|
||||
private val md_theme_dark_onSurfaceVariant = Color(0xFFC0C9C1)
|
||||
private val md_theme_dark_outline = Color(0xFF8A938C)
|
||||
private val md_theme_dark_inverseOnSurface = Color(0xFF191C1A)
|
||||
private val md_theme_dark_inverseSurface = Color(0xFFE1E3DF)
|
||||
private val md_theme_dark_inversePrimary = Color(0xFF006C48)
|
||||
private val md_theme_dark_shadow = Color(0xFF000000)
|
||||
private val md_theme_dark_surfaceTint = Color(0xFF70DBA7)
|
||||
private val md_theme_dark_outlineVariant = Color(0xFF404943)
|
||||
private val md_theme_dark_scrim = Color(0xFF000000)
|
||||
|
||||
val LightLightGreenTheme = lightColorScheme(
|
||||
primary = md_theme_light_primary,
|
||||
onPrimary = md_theme_light_onPrimary,
|
||||
primaryContainer = md_theme_light_primaryContainer,
|
||||
onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
||||
secondary = md_theme_light_secondary,
|
||||
onSecondary = md_theme_light_onSecondary,
|
||||
secondaryContainer = md_theme_light_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
||||
tertiary = md_theme_light_tertiary,
|
||||
onTertiary = md_theme_light_onTertiary,
|
||||
tertiaryContainer = md_theme_light_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
||||
error = md_theme_light_error,
|
||||
errorContainer = md_theme_light_errorContainer,
|
||||
onError = md_theme_light_onError,
|
||||
onErrorContainer = md_theme_light_onErrorContainer,
|
||||
background = md_theme_light_background,
|
||||
onBackground = md_theme_light_onBackground,
|
||||
surface = md_theme_light_surface,
|
||||
onSurface = md_theme_light_onSurface,
|
||||
surfaceVariant = md_theme_light_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
||||
outline = md_theme_light_outline,
|
||||
inverseOnSurface = md_theme_light_inverseOnSurface,
|
||||
inverseSurface = md_theme_light_inverseSurface,
|
||||
inversePrimary = md_theme_light_inversePrimary,
|
||||
surfaceTint = md_theme_light_surfaceTint,
|
||||
outlineVariant = md_theme_light_outlineVariant,
|
||||
scrim = md_theme_light_scrim,
|
||||
)
|
||||
|
||||
val DarkLightGreenTheme = darkColorScheme(
|
||||
primary = md_theme_dark_primary,
|
||||
onPrimary = md_theme_dark_onPrimary,
|
||||
primaryContainer = md_theme_dark_primaryContainer,
|
||||
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
|
||||
secondary = md_theme_dark_secondary,
|
||||
onSecondary = md_theme_dark_onSecondary,
|
||||
secondaryContainer = md_theme_dark_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
|
||||
tertiary = md_theme_dark_tertiary,
|
||||
onTertiary = md_theme_dark_onTertiary,
|
||||
tertiaryContainer = md_theme_dark_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
|
||||
error = md_theme_dark_error,
|
||||
errorContainer = md_theme_dark_errorContainer,
|
||||
onError = md_theme_dark_onError,
|
||||
onErrorContainer = md_theme_dark_onErrorContainer,
|
||||
background = md_theme_dark_background,
|
||||
onBackground = md_theme_dark_onBackground,
|
||||
surface = md_theme_dark_surface,
|
||||
onSurface = md_theme_dark_onSurface,
|
||||
surfaceVariant = md_theme_dark_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
|
||||
outline = md_theme_dark_outline,
|
||||
inverseOnSurface = md_theme_dark_inverseOnSurface,
|
||||
inverseSurface = md_theme_dark_inverseSurface,
|
||||
inversePrimary = md_theme_dark_inversePrimary,
|
||||
surfaceTint = md_theme_dark_surfaceTint,
|
||||
outlineVariant = md_theme_dark_outlineVariant,
|
||||
scrim = md_theme_dark_scrim,
|
||||
)
|
||||
131
app/src/main/java/me/bmax/apatch/ui/theme/LimeTheme.kt
Normal file
131
app/src/main/java/me/bmax/apatch/ui/theme/LimeTheme.kt
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package me.bmax.apatch.ui.theme
|
||||
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
private val md_theme_light_primary = Color(0xFF5B6300)
|
||||
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_primaryContainer = Color(0xFFDDED49)
|
||||
private val md_theme_light_onPrimaryContainer = Color(0xFF1A1D00)
|
||||
private val md_theme_light_secondary = Color(0xFF5E6044)
|
||||
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_secondaryContainer = Color(0xFFE4E5C1)
|
||||
private val md_theme_light_onSecondaryContainer = Color(0xFF1B1D07)
|
||||
private val md_theme_light_tertiary = Color(0xFF3C665A)
|
||||
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_tertiaryContainer = Color(0xFFBEECDC)
|
||||
private val md_theme_light_onTertiaryContainer = Color(0xFF002019)
|
||||
private val md_theme_light_error = Color(0xFFBA1A1A)
|
||||
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_onErrorContainer = Color(0xFF410002)
|
||||
private val md_theme_light_background = Color(0xFFFEFFD8)
|
||||
private val md_theme_light_onBackground = Color(0xFF1C1C17)
|
||||
private val md_theme_light_surface = Color(0xFFFEFFD8)
|
||||
private val md_theme_light_onSurface = Color(0xFF1C1C17)
|
||||
private val md_theme_light_surfaceVariant = Color(0xFFE5E3D2)
|
||||
private val md_theme_light_onSurfaceVariant = Color(0xFF47483B)
|
||||
private val md_theme_light_outline = Color(0xFF787869)
|
||||
private val md_theme_light_inverseOnSurface = Color(0xFFF3F1E8)
|
||||
private val md_theme_light_inverseSurface = Color(0xFF31312B)
|
||||
private val md_theme_light_inversePrimary = Color(0xFFC1D02C)
|
||||
private val md_theme_light_shadow = Color(0xFF000000)
|
||||
private val md_theme_light_surfaceTint = Color(0xFF5B6300)
|
||||
private val md_theme_light_outlineVariant = Color(0xFFC8C7B7)
|
||||
private val md_theme_light_scrim = Color(0xFF000000)
|
||||
|
||||
private val md_theme_dark_primary = Color(0xFFC1D02C)
|
||||
private val md_theme_dark_onPrimary = Color(0xFF2F3300)
|
||||
private val md_theme_dark_primaryContainer = Color(0xFF444B00)
|
||||
private val md_theme_dark_onPrimaryContainer = Color(0xFFDDED49)
|
||||
private val md_theme_dark_secondary = Color(0xFFC7C9A6)
|
||||
private val md_theme_dark_onSecondary = Color(0xFF30321A)
|
||||
private val md_theme_dark_secondaryContainer = Color(0xFF46492E)
|
||||
private val md_theme_dark_onSecondaryContainer = Color(0xFFE4E5C1)
|
||||
private val md_theme_dark_tertiary = Color(0xFFA2D0C1)
|
||||
private val md_theme_dark_onTertiary = Color(0xFF07372D)
|
||||
private val md_theme_dark_tertiaryContainer = Color(0xFF234E43)
|
||||
private val md_theme_dark_onTertiaryContainer = Color(0xFFBEECDC)
|
||||
private val md_theme_dark_error = Color(0xFFFFB4AB)
|
||||
private val md_theme_dark_errorContainer = Color(0xFF93000A)
|
||||
private val md_theme_dark_onError = Color(0xFF690005)
|
||||
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_dark_background = Color(0xFF1C1C17)
|
||||
private val md_theme_dark_onBackground = Color(0xFFE5E2DA)
|
||||
private val md_theme_dark_surface = Color(0xFF1C1C17)
|
||||
private val md_theme_dark_onSurface = Color(0xFFE5E2DA)
|
||||
private val md_theme_dark_surfaceVariant = Color(0xFF47483B)
|
||||
private val md_theme_dark_onSurfaceVariant = Color(0xFFC8C7B7)
|
||||
private val md_theme_dark_outline = Color(0xFF929282)
|
||||
private val md_theme_dark_inverseOnSurface = Color(0xFF1C1C17)
|
||||
private val md_theme_dark_inverseSurface = Color(0xFFE5E2DA)
|
||||
private val md_theme_dark_inversePrimary = Color(0xFF5B6300)
|
||||
private val md_theme_dark_shadow = Color(0xFF000000)
|
||||
private val md_theme_dark_surfaceTint = Color(0xFFC1D02C)
|
||||
private val md_theme_dark_outlineVariant = Color(0xFF47483B)
|
||||
private val md_theme_dark_scrim = Color(0xFF000000)
|
||||
|
||||
val LightLimeTheme = lightColorScheme(
|
||||
primary = md_theme_light_primary,
|
||||
onPrimary = md_theme_light_onPrimary,
|
||||
primaryContainer = md_theme_light_primaryContainer,
|
||||
onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
||||
secondary = md_theme_light_secondary,
|
||||
onSecondary = md_theme_light_onSecondary,
|
||||
secondaryContainer = md_theme_light_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
||||
tertiary = md_theme_light_tertiary,
|
||||
onTertiary = md_theme_light_onTertiary,
|
||||
tertiaryContainer = md_theme_light_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
||||
error = md_theme_light_error,
|
||||
errorContainer = md_theme_light_errorContainer,
|
||||
onError = md_theme_light_onError,
|
||||
onErrorContainer = md_theme_light_onErrorContainer,
|
||||
background = md_theme_light_background,
|
||||
onBackground = md_theme_light_onBackground,
|
||||
surface = md_theme_light_surface,
|
||||
onSurface = md_theme_light_onSurface,
|
||||
surfaceVariant = md_theme_light_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
||||
outline = md_theme_light_outline,
|
||||
inverseOnSurface = md_theme_light_inverseOnSurface,
|
||||
inverseSurface = md_theme_light_inverseSurface,
|
||||
inversePrimary = md_theme_light_inversePrimary,
|
||||
surfaceTint = md_theme_light_surfaceTint,
|
||||
outlineVariant = md_theme_light_outlineVariant,
|
||||
scrim = md_theme_light_scrim,
|
||||
)
|
||||
|
||||
val DarkLimeTheme = darkColorScheme(
|
||||
primary = md_theme_dark_primary,
|
||||
onPrimary = md_theme_dark_onPrimary,
|
||||
primaryContainer = md_theme_dark_primaryContainer,
|
||||
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
|
||||
secondary = md_theme_dark_secondary,
|
||||
onSecondary = md_theme_dark_onSecondary,
|
||||
secondaryContainer = md_theme_dark_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
|
||||
tertiary = md_theme_dark_tertiary,
|
||||
onTertiary = md_theme_dark_onTertiary,
|
||||
tertiaryContainer = md_theme_dark_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
|
||||
error = md_theme_dark_error,
|
||||
errorContainer = md_theme_dark_errorContainer,
|
||||
onError = md_theme_dark_onError,
|
||||
onErrorContainer = md_theme_dark_onErrorContainer,
|
||||
background = md_theme_dark_background,
|
||||
onBackground = md_theme_dark_onBackground,
|
||||
surface = md_theme_dark_surface,
|
||||
onSurface = md_theme_dark_onSurface,
|
||||
surfaceVariant = md_theme_dark_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
|
||||
outline = md_theme_dark_outline,
|
||||
inverseOnSurface = md_theme_dark_inverseOnSurface,
|
||||
inverseSurface = md_theme_dark_inverseSurface,
|
||||
inversePrimary = md_theme_dark_inversePrimary,
|
||||
surfaceTint = md_theme_dark_surfaceTint,
|
||||
outlineVariant = md_theme_dark_outlineVariant,
|
||||
scrim = md_theme_dark_scrim,
|
||||
)
|
||||
131
app/src/main/java/me/bmax/apatch/ui/theme/OrangeTheme.kt
Normal file
131
app/src/main/java/me/bmax/apatch/ui/theme/OrangeTheme.kt
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package me.bmax.apatch.ui.theme
|
||||
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
private val md_theme_light_primary = Color(0xFF8B5000)
|
||||
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_primaryContainer = Color(0xFFFFDCBE)
|
||||
private val md_theme_light_onPrimaryContainer = Color(0xFF2C1600)
|
||||
private val md_theme_light_secondary = Color(0xFF725A42)
|
||||
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_secondaryContainer = Color(0xFFFFDCBE)
|
||||
private val md_theme_light_onSecondaryContainer = Color(0xFF291806)
|
||||
private val md_theme_light_tertiary = Color(0xFF58633A)
|
||||
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_tertiaryContainer = Color(0xFFDCE8B4)
|
||||
private val md_theme_light_onTertiaryContainer = Color(0xFF161E01)
|
||||
private val md_theme_light_error = Color(0xFFBA1A1A)
|
||||
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_onErrorContainer = Color(0xFF410002)
|
||||
private val md_theme_light_background = Color(0xFFFFFBFF)
|
||||
private val md_theme_light_onBackground = Color(0xFF201B16)
|
||||
private val md_theme_light_surface = Color(0xFFFFFBFF)
|
||||
private val md_theme_light_onSurface = Color(0xFF201B16)
|
||||
private val md_theme_light_surfaceVariant = Color(0xFFF2DFD1)
|
||||
private val md_theme_light_onSurfaceVariant = Color(0xFF51453A)
|
||||
private val md_theme_light_outline = Color(0xFF837468)
|
||||
private val md_theme_light_inverseOnSurface = Color(0xFFFAEFE7)
|
||||
private val md_theme_light_inverseSurface = Color(0xFF352F2B)
|
||||
private val md_theme_light_inversePrimary = Color(0xFFFFB870)
|
||||
private val md_theme_light_shadow = Color(0xFF000000)
|
||||
private val md_theme_light_surfaceTint = Color(0xFF8B5000)
|
||||
private val md_theme_light_outlineVariant = Color(0xFFD5C3B5)
|
||||
private val md_theme_light_scrim = Color(0xFF000000)
|
||||
|
||||
private val md_theme_dark_primary = Color(0xFFFFB870)
|
||||
private val md_theme_dark_onPrimary = Color(0xFF4A2800)
|
||||
private val md_theme_dark_primaryContainer = Color(0xFF693C00)
|
||||
private val md_theme_dark_onPrimaryContainer = Color(0xFFFFDCBE)
|
||||
private val md_theme_dark_secondary = Color(0xFFE1C1A4)
|
||||
private val md_theme_dark_onSecondary = Color(0xFF402C18)
|
||||
private val md_theme_dark_secondaryContainer = Color(0xFF59422C)
|
||||
private val md_theme_dark_onSecondaryContainer = Color(0xFFFFDCBE)
|
||||
private val md_theme_dark_tertiary = Color(0xFFC0CC9A)
|
||||
private val md_theme_dark_onTertiary = Color(0xFF2B3410)
|
||||
private val md_theme_dark_tertiaryContainer = Color(0xFF414B24)
|
||||
private val md_theme_dark_onTertiaryContainer = Color(0xFFDCE8B4)
|
||||
private val md_theme_dark_error = Color(0xFFFFB4AB)
|
||||
private val md_theme_dark_errorContainer = Color(0xFF93000A)
|
||||
private val md_theme_dark_onError = Color(0xFF690005)
|
||||
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_dark_background = Color(0xFF201B16)
|
||||
private val md_theme_dark_onBackground = Color(0xFFEBE0D9)
|
||||
private val md_theme_dark_surface = Color(0xFF201B16)
|
||||
private val md_theme_dark_onSurface = Color(0xFFEBE0D9)
|
||||
private val md_theme_dark_surfaceVariant = Color(0xFF51453A)
|
||||
private val md_theme_dark_onSurfaceVariant = Color(0xFFD5C3B5)
|
||||
private val md_theme_dark_outline = Color(0xFF9D8E81)
|
||||
private val md_theme_dark_inverseOnSurface = Color(0xFF201B16)
|
||||
private val md_theme_dark_inverseSurface = Color(0xFFEBE0D9)
|
||||
private val md_theme_dark_inversePrimary = Color(0xFF8B5000)
|
||||
private val md_theme_dark_shadow = Color(0xFF000000)
|
||||
private val md_theme_dark_surfaceTint = Color(0xFFFFB870)
|
||||
private val md_theme_dark_outlineVariant = Color(0xFF51453A)
|
||||
private val md_theme_dark_scrim = Color(0xFF000000)
|
||||
|
||||
val LightOrangeTheme = lightColorScheme(
|
||||
primary = md_theme_light_primary,
|
||||
onPrimary = md_theme_light_onPrimary,
|
||||
primaryContainer = md_theme_light_primaryContainer,
|
||||
onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
||||
secondary = md_theme_light_secondary,
|
||||
onSecondary = md_theme_light_onSecondary,
|
||||
secondaryContainer = md_theme_light_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
||||
tertiary = md_theme_light_tertiary,
|
||||
onTertiary = md_theme_light_onTertiary,
|
||||
tertiaryContainer = md_theme_light_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
||||
error = md_theme_light_error,
|
||||
errorContainer = md_theme_light_errorContainer,
|
||||
onError = md_theme_light_onError,
|
||||
onErrorContainer = md_theme_light_onErrorContainer,
|
||||
background = md_theme_light_background,
|
||||
onBackground = md_theme_light_onBackground,
|
||||
surface = md_theme_light_surface,
|
||||
onSurface = md_theme_light_onSurface,
|
||||
surfaceVariant = md_theme_light_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
||||
outline = md_theme_light_outline,
|
||||
inverseOnSurface = md_theme_light_inverseOnSurface,
|
||||
inverseSurface = md_theme_light_inverseSurface,
|
||||
inversePrimary = md_theme_light_inversePrimary,
|
||||
surfaceTint = md_theme_light_surfaceTint,
|
||||
outlineVariant = md_theme_light_outlineVariant,
|
||||
scrim = md_theme_light_scrim,
|
||||
)
|
||||
|
||||
val DarkOrangeTheme = darkColorScheme(
|
||||
primary = md_theme_dark_primary,
|
||||
onPrimary = md_theme_dark_onPrimary,
|
||||
primaryContainer = md_theme_dark_primaryContainer,
|
||||
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
|
||||
secondary = md_theme_dark_secondary,
|
||||
onSecondary = md_theme_dark_onSecondary,
|
||||
secondaryContainer = md_theme_dark_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
|
||||
tertiary = md_theme_dark_tertiary,
|
||||
onTertiary = md_theme_dark_onTertiary,
|
||||
tertiaryContainer = md_theme_dark_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
|
||||
error = md_theme_dark_error,
|
||||
errorContainer = md_theme_dark_errorContainer,
|
||||
onError = md_theme_dark_onError,
|
||||
onErrorContainer = md_theme_dark_onErrorContainer,
|
||||
background = md_theme_dark_background,
|
||||
onBackground = md_theme_dark_onBackground,
|
||||
surface = md_theme_dark_surface,
|
||||
onSurface = md_theme_dark_onSurface,
|
||||
surfaceVariant = md_theme_dark_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
|
||||
outline = md_theme_dark_outline,
|
||||
inverseOnSurface = md_theme_dark_inverseOnSurface,
|
||||
inverseSurface = md_theme_dark_inverseSurface,
|
||||
inversePrimary = md_theme_dark_inversePrimary,
|
||||
surfaceTint = md_theme_dark_surfaceTint,
|
||||
outlineVariant = md_theme_dark_outlineVariant,
|
||||
scrim = md_theme_dark_scrim,
|
||||
)
|
||||
131
app/src/main/java/me/bmax/apatch/ui/theme/PinkTheme.kt
Normal file
131
app/src/main/java/me/bmax/apatch/ui/theme/PinkTheme.kt
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package me.bmax.apatch.ui.theme
|
||||
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
private val md_theme_light_primary = Color(0xFFBC004B)
|
||||
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_primaryContainer = Color(0xFFFFD9DE)
|
||||
private val md_theme_light_onPrimaryContainer = Color(0xFF400014)
|
||||
private val md_theme_light_secondary = Color(0xFF75565B)
|
||||
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_secondaryContainer = Color(0xFFFFD9DE)
|
||||
private val md_theme_light_onSecondaryContainer = Color(0xFF2C1519)
|
||||
private val md_theme_light_tertiary = Color(0xFF795831)
|
||||
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_tertiaryContainer = Color(0xFFFFDDBA)
|
||||
private val md_theme_light_onTertiaryContainer = Color(0xFF2B1700)
|
||||
private val md_theme_light_error = Color(0xFFBA1A1A)
|
||||
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_onErrorContainer = Color(0xFF410002)
|
||||
private val md_theme_light_background = Color(0xFFFFFBFF)
|
||||
private val md_theme_light_onBackground = Color(0xFF201A1B)
|
||||
private val md_theme_light_surface = Color(0xFFFFFBFF)
|
||||
private val md_theme_light_onSurface = Color(0xFF201A1B)
|
||||
private val md_theme_light_surfaceVariant = Color(0xFFF3DDDF)
|
||||
private val md_theme_light_onSurfaceVariant = Color(0xFF524345)
|
||||
private val md_theme_light_outline = Color(0xFF847375)
|
||||
private val md_theme_light_inverseOnSurface = Color(0xFFFBEEEE)
|
||||
private val md_theme_light_inverseSurface = Color(0xFF362F2F)
|
||||
private val md_theme_light_inversePrimary = Color(0xFFFFB2BE)
|
||||
private val md_theme_light_shadow = Color(0xFF000000)
|
||||
private val md_theme_light_surfaceTint = Color(0xFFBC004B)
|
||||
private val md_theme_light_outlineVariant = Color(0xFFD6C2C3)
|
||||
private val md_theme_light_scrim = Color(0xFF000000)
|
||||
|
||||
private val md_theme_dark_primary = Color(0xFFFFB2BE)
|
||||
private val md_theme_dark_onPrimary = Color(0xFF660025)
|
||||
private val md_theme_dark_primaryContainer = Color(0xFF900038)
|
||||
private val md_theme_dark_onPrimaryContainer = Color(0xFFFFD9DE)
|
||||
private val md_theme_dark_secondary = Color(0xFFE5BDC2)
|
||||
private val md_theme_dark_onSecondary = Color(0xFF43292D)
|
||||
private val md_theme_dark_secondaryContainer = Color(0xFF5C3F43)
|
||||
private val md_theme_dark_onSecondaryContainer = Color(0xFFFFD9DE)
|
||||
private val md_theme_dark_tertiary = Color(0xFFEBBF90)
|
||||
private val md_theme_dark_onTertiary = Color(0xFF452B08)
|
||||
private val md_theme_dark_tertiaryContainer = Color(0xFF5F411C)
|
||||
private val md_theme_dark_onTertiaryContainer = Color(0xFFFFDDBA)
|
||||
private val md_theme_dark_error = Color(0xFFFFB4AB)
|
||||
private val md_theme_dark_errorContainer = Color(0xFF93000A)
|
||||
private val md_theme_dark_onError = Color(0xFF690005)
|
||||
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_dark_background = Color(0xFF201A1B)
|
||||
private val md_theme_dark_onBackground = Color(0xFFECE0E0)
|
||||
private val md_theme_dark_surface = Color(0xFF201A1B)
|
||||
private val md_theme_dark_onSurface = Color(0xFFECE0E0)
|
||||
private val md_theme_dark_surfaceVariant = Color(0xFF524345)
|
||||
private val md_theme_dark_onSurfaceVariant = Color(0xFFD6C2C3)
|
||||
private val md_theme_dark_outline = Color(0xFF9F8C8E)
|
||||
private val md_theme_dark_inverseOnSurface = Color(0xFF201A1B)
|
||||
private val md_theme_dark_inverseSurface = Color(0xFFECE0E0)
|
||||
private val md_theme_dark_inversePrimary = Color(0xFFBC004B)
|
||||
private val md_theme_dark_shadow = Color(0xFF000000)
|
||||
private val md_theme_dark_surfaceTint = Color(0xFFFFB2BE)
|
||||
private val md_theme_dark_outlineVariant = Color(0xFF524345)
|
||||
private val md_theme_dark_scrim = Color(0xFF000000)
|
||||
|
||||
val LightPinkTheme = lightColorScheme(
|
||||
primary = md_theme_light_primary,
|
||||
onPrimary = md_theme_light_onPrimary,
|
||||
primaryContainer = md_theme_light_primaryContainer,
|
||||
onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
||||
secondary = md_theme_light_secondary,
|
||||
onSecondary = md_theme_light_onSecondary,
|
||||
secondaryContainer = md_theme_light_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
||||
tertiary = md_theme_light_tertiary,
|
||||
onTertiary = md_theme_light_onTertiary,
|
||||
tertiaryContainer = md_theme_light_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
||||
error = md_theme_light_error,
|
||||
errorContainer = md_theme_light_errorContainer,
|
||||
onError = md_theme_light_onError,
|
||||
onErrorContainer = md_theme_light_onErrorContainer,
|
||||
background = md_theme_light_background,
|
||||
onBackground = md_theme_light_onBackground,
|
||||
surface = md_theme_light_surface,
|
||||
onSurface = md_theme_light_onSurface,
|
||||
surfaceVariant = md_theme_light_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
||||
outline = md_theme_light_outline,
|
||||
inverseOnSurface = md_theme_light_inverseOnSurface,
|
||||
inverseSurface = md_theme_light_inverseSurface,
|
||||
inversePrimary = md_theme_light_inversePrimary,
|
||||
surfaceTint = md_theme_light_surfaceTint,
|
||||
outlineVariant = md_theme_light_outlineVariant,
|
||||
scrim = md_theme_light_scrim,
|
||||
)
|
||||
|
||||
val DarkPinkTheme = darkColorScheme(
|
||||
primary = md_theme_dark_primary,
|
||||
onPrimary = md_theme_dark_onPrimary,
|
||||
primaryContainer = md_theme_dark_primaryContainer,
|
||||
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
|
||||
secondary = md_theme_dark_secondary,
|
||||
onSecondary = md_theme_dark_onSecondary,
|
||||
secondaryContainer = md_theme_dark_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
|
||||
tertiary = md_theme_dark_tertiary,
|
||||
onTertiary = md_theme_dark_onTertiary,
|
||||
tertiaryContainer = md_theme_dark_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
|
||||
error = md_theme_dark_error,
|
||||
errorContainer = md_theme_dark_errorContainer,
|
||||
onError = md_theme_dark_onError,
|
||||
onErrorContainer = md_theme_dark_onErrorContainer,
|
||||
background = md_theme_dark_background,
|
||||
onBackground = md_theme_dark_onBackground,
|
||||
surface = md_theme_dark_surface,
|
||||
onSurface = md_theme_dark_onSurface,
|
||||
surfaceVariant = md_theme_dark_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
|
||||
outline = md_theme_dark_outline,
|
||||
inverseOnSurface = md_theme_dark_inverseOnSurface,
|
||||
inverseSurface = md_theme_dark_inverseSurface,
|
||||
inversePrimary = md_theme_dark_inversePrimary,
|
||||
surfaceTint = md_theme_dark_surfaceTint,
|
||||
outlineVariant = md_theme_dark_outlineVariant,
|
||||
scrim = md_theme_dark_scrim,
|
||||
)
|
||||
131
app/src/main/java/me/bmax/apatch/ui/theme/PurpleTheme.kt
Normal file
131
app/src/main/java/me/bmax/apatch/ui/theme/PurpleTheme.kt
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package me.bmax.apatch.ui.theme
|
||||
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
private val md_theme_light_primary = Color(0xFF9A25AE)
|
||||
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_primaryContainer = Color(0xFFFFD6FE)
|
||||
private val md_theme_light_onPrimaryContainer = Color(0xFF35003F)
|
||||
private val md_theme_light_secondary = Color(0xFF6B586B)
|
||||
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_secondaryContainer = Color(0xFFF4DBF1)
|
||||
private val md_theme_light_onSecondaryContainer = Color(0xFF251626)
|
||||
private val md_theme_light_tertiary = Color(0xFF82524A)
|
||||
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_tertiaryContainer = Color(0xFFFFDAD4)
|
||||
private val md_theme_light_onTertiaryContainer = Color(0xFF33110C)
|
||||
private val md_theme_light_error = Color(0xFFBA1A1A)
|
||||
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_onErrorContainer = Color(0xFF410002)
|
||||
private val md_theme_light_background = Color(0xFFFFFBFF)
|
||||
private val md_theme_light_onBackground = Color(0xFF1E1A1D)
|
||||
private val md_theme_light_surface = Color(0xFFFFFBFF)
|
||||
private val md_theme_light_onSurface = Color(0xFF1E1A1D)
|
||||
private val md_theme_light_surfaceVariant = Color(0xFFECDFE8)
|
||||
private val md_theme_light_onSurfaceVariant = Color(0xFF4D444C)
|
||||
private val md_theme_light_outline = Color(0xFF7F747D)
|
||||
private val md_theme_light_inverseOnSurface = Color(0xFFF7EEF3)
|
||||
private val md_theme_light_inverseSurface = Color(0xFF332F32)
|
||||
private val md_theme_light_inversePrimary = Color(0xFFF9ABFF)
|
||||
private val md_theme_light_shadow = Color(0xFF000000)
|
||||
private val md_theme_light_surfaceTint = Color(0xFF9A25AE)
|
||||
private val md_theme_light_outlineVariant = Color(0xFFD0C3CC)
|
||||
private val md_theme_light_scrim = Color(0xFF000000)
|
||||
|
||||
private val md_theme_dark_primary = Color(0xFFF9ABFF)
|
||||
private val md_theme_dark_onPrimary = Color(0xFF570066)
|
||||
private val md_theme_dark_primaryContainer = Color(0xFF7B008F)
|
||||
private val md_theme_dark_onPrimaryContainer = Color(0xFFFFD6FE)
|
||||
private val md_theme_dark_secondary = Color(0xFFD7BFD5)
|
||||
private val md_theme_dark_onSecondary = Color(0xFF3B2B3C)
|
||||
private val md_theme_dark_secondaryContainer = Color(0xFF534153)
|
||||
private val md_theme_dark_onSecondaryContainer = Color(0xFFF4DBF1)
|
||||
private val md_theme_dark_tertiary = Color(0xFFF6B8AD)
|
||||
private val md_theme_dark_onTertiary = Color(0xFF4C251F)
|
||||
private val md_theme_dark_tertiaryContainer = Color(0xFF673B34)
|
||||
private val md_theme_dark_onTertiaryContainer = Color(0xFFFFDAD4)
|
||||
private val md_theme_dark_error = Color(0xFFFFB4AB)
|
||||
private val md_theme_dark_errorContainer = Color(0xFF93000A)
|
||||
private val md_theme_dark_onError = Color(0xFF690005)
|
||||
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_dark_background = Color(0xFF1E1A1D)
|
||||
private val md_theme_dark_onBackground = Color(0xFFE9E0E4)
|
||||
private val md_theme_dark_surface = Color(0xFF1E1A1D)
|
||||
private val md_theme_dark_onSurface = Color(0xFFE9E0E4)
|
||||
private val md_theme_dark_surfaceVariant = Color(0xFF4D444C)
|
||||
private val md_theme_dark_onSurfaceVariant = Color(0xFFD0C3CC)
|
||||
private val md_theme_dark_outline = Color(0xFF998D96)
|
||||
private val md_theme_dark_inverseOnSurface = Color(0xFF1E1A1D)
|
||||
private val md_theme_dark_inverseSurface = Color(0xFFE9E0E4)
|
||||
private val md_theme_dark_inversePrimary = Color(0xFF9A25AE)
|
||||
private val md_theme_dark_shadow = Color(0xFF000000)
|
||||
private val md_theme_dark_surfaceTint = Color(0xFFF9ABFF)
|
||||
private val md_theme_dark_outlineVariant = Color(0xFF4D444C)
|
||||
private val md_theme_dark_scrim = Color(0xFF000000)
|
||||
|
||||
val LightPurpleTheme = lightColorScheme(
|
||||
primary = md_theme_light_primary,
|
||||
onPrimary = md_theme_light_onPrimary,
|
||||
primaryContainer = md_theme_light_primaryContainer,
|
||||
onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
||||
secondary = md_theme_light_secondary,
|
||||
onSecondary = md_theme_light_onSecondary,
|
||||
secondaryContainer = md_theme_light_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
||||
tertiary = md_theme_light_tertiary,
|
||||
onTertiary = md_theme_light_onTertiary,
|
||||
tertiaryContainer = md_theme_light_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
||||
error = md_theme_light_error,
|
||||
errorContainer = md_theme_light_errorContainer,
|
||||
onError = md_theme_light_onError,
|
||||
onErrorContainer = md_theme_light_onErrorContainer,
|
||||
background = md_theme_light_background,
|
||||
onBackground = md_theme_light_onBackground,
|
||||
surface = md_theme_light_surface,
|
||||
onSurface = md_theme_light_onSurface,
|
||||
surfaceVariant = md_theme_light_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
||||
outline = md_theme_light_outline,
|
||||
inverseOnSurface = md_theme_light_inverseOnSurface,
|
||||
inverseSurface = md_theme_light_inverseSurface,
|
||||
inversePrimary = md_theme_light_inversePrimary,
|
||||
surfaceTint = md_theme_light_surfaceTint,
|
||||
outlineVariant = md_theme_light_outlineVariant,
|
||||
scrim = md_theme_light_scrim,
|
||||
)
|
||||
|
||||
val DarkPurpleTheme = darkColorScheme(
|
||||
primary = md_theme_dark_primary,
|
||||
onPrimary = md_theme_dark_onPrimary,
|
||||
primaryContainer = md_theme_dark_primaryContainer,
|
||||
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
|
||||
secondary = md_theme_dark_secondary,
|
||||
onSecondary = md_theme_dark_onSecondary,
|
||||
secondaryContainer = md_theme_dark_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
|
||||
tertiary = md_theme_dark_tertiary,
|
||||
onTertiary = md_theme_dark_onTertiary,
|
||||
tertiaryContainer = md_theme_dark_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
|
||||
error = md_theme_dark_error,
|
||||
errorContainer = md_theme_dark_errorContainer,
|
||||
onError = md_theme_dark_onError,
|
||||
onErrorContainer = md_theme_dark_onErrorContainer,
|
||||
background = md_theme_dark_background,
|
||||
onBackground = md_theme_dark_onBackground,
|
||||
surface = md_theme_dark_surface,
|
||||
onSurface = md_theme_dark_onSurface,
|
||||
surfaceVariant = md_theme_dark_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
|
||||
outline = md_theme_dark_outline,
|
||||
inverseOnSurface = md_theme_dark_inverseOnSurface,
|
||||
inverseSurface = md_theme_dark_inverseSurface,
|
||||
inversePrimary = md_theme_dark_inversePrimary,
|
||||
surfaceTint = md_theme_dark_surfaceTint,
|
||||
outlineVariant = md_theme_dark_outlineVariant,
|
||||
scrim = md_theme_dark_scrim,
|
||||
)
|
||||
131
app/src/main/java/me/bmax/apatch/ui/theme/RedTheme.kt
Normal file
131
app/src/main/java/me/bmax/apatch/ui/theme/RedTheme.kt
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package me.bmax.apatch.ui.theme
|
||||
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
private val md_theme_light_primary = Color(0xFFBB1614)
|
||||
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_primaryContainer = Color(0xFFFFDAD5)
|
||||
private val md_theme_light_onPrimaryContainer = Color(0xFF410001)
|
||||
private val md_theme_light_secondary = Color(0xFF775652)
|
||||
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_secondaryContainer = Color(0xFFFFDAD5)
|
||||
private val md_theme_light_onSecondaryContainer = Color(0xFF2C1512)
|
||||
private val md_theme_light_tertiary = Color(0xFF705C2E)
|
||||
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_tertiaryContainer = Color(0xFFFCDFA6)
|
||||
private val md_theme_light_onTertiaryContainer = Color(0xFF261A00)
|
||||
private val md_theme_light_error = Color(0xFFBA1A1A)
|
||||
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_onErrorContainer = Color(0xFF410002)
|
||||
private val md_theme_light_background = Color(0xFFFFFBFF)
|
||||
private val md_theme_light_onBackground = Color(0xFF201A19)
|
||||
private val md_theme_light_surface = Color(0xFFFFFBFF)
|
||||
private val md_theme_light_onSurface = Color(0xFF201A19)
|
||||
private val md_theme_light_surfaceVariant = Color(0xFFF5DDDA)
|
||||
private val md_theme_light_onSurfaceVariant = Color(0xFF534341)
|
||||
private val md_theme_light_outline = Color(0xFF857370)
|
||||
private val md_theme_light_inverseOnSurface = Color(0xFFFBEEEC)
|
||||
private val md_theme_light_inverseSurface = Color(0xFF362F2E)
|
||||
private val md_theme_light_inversePrimary = Color(0xFFFFB4A9)
|
||||
private val md_theme_light_shadow = Color(0xFF000000)
|
||||
private val md_theme_light_surfaceTint = Color(0xFFBB1614)
|
||||
private val md_theme_light_outlineVariant = Color(0xFFD8C2BE)
|
||||
private val md_theme_light_scrim = Color(0xFF000000)
|
||||
|
||||
private val md_theme_dark_primary = Color(0xFFFFB4A9)
|
||||
private val md_theme_dark_onPrimary = Color(0xFF690002)
|
||||
private val md_theme_dark_primaryContainer = Color(0xFF930005)
|
||||
private val md_theme_dark_onPrimaryContainer = Color(0xFFFFDAD5)
|
||||
private val md_theme_dark_secondary = Color(0xFFE7BDB7)
|
||||
private val md_theme_dark_onSecondary = Color(0xFF442926)
|
||||
private val md_theme_dark_secondaryContainer = Color(0xFF5D3F3B)
|
||||
private val md_theme_dark_onSecondaryContainer = Color(0xFFFFDAD5)
|
||||
private val md_theme_dark_tertiary = Color(0xFFDFC38C)
|
||||
private val md_theme_dark_onTertiary = Color(0xFF3E2E04)
|
||||
private val md_theme_dark_tertiaryContainer = Color(0xFF574419)
|
||||
private val md_theme_dark_onTertiaryContainer = Color(0xFFFCDFA6)
|
||||
private val md_theme_dark_error = Color(0xFFFFB4AB)
|
||||
private val md_theme_dark_errorContainer = Color(0xFF93000A)
|
||||
private val md_theme_dark_onError = Color(0xFF690005)
|
||||
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_dark_background = Color(0xFF201A19)
|
||||
private val md_theme_dark_onBackground = Color(0xFFEDE0DE)
|
||||
private val md_theme_dark_surface = Color(0xFF201A19)
|
||||
private val md_theme_dark_onSurface = Color(0xFFEDE0DE)
|
||||
private val md_theme_dark_surfaceVariant = Color(0xFF534341)
|
||||
private val md_theme_dark_onSurfaceVariant = Color(0xFFD8C2BE)
|
||||
private val md_theme_dark_outline = Color(0xFFA08C89)
|
||||
private val md_theme_dark_inverseOnSurface = Color(0xFF201A19)
|
||||
private val md_theme_dark_inverseSurface = Color(0xFFEDE0DE)
|
||||
private val md_theme_dark_inversePrimary = Color(0xFFBB1614)
|
||||
private val md_theme_dark_shadow = Color(0xFF000000)
|
||||
private val md_theme_dark_surfaceTint = Color(0xFFFFB4A9)
|
||||
private val md_theme_dark_outlineVariant = Color(0xFF534341)
|
||||
private val md_theme_dark_scrim = Color(0xFF000000)
|
||||
|
||||
val LightRedTheme = lightColorScheme(
|
||||
primary = md_theme_light_primary,
|
||||
onPrimary = md_theme_light_onPrimary,
|
||||
primaryContainer = md_theme_light_primaryContainer,
|
||||
onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
||||
secondary = md_theme_light_secondary,
|
||||
onSecondary = md_theme_light_onSecondary,
|
||||
secondaryContainer = md_theme_light_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
||||
tertiary = md_theme_light_tertiary,
|
||||
onTertiary = md_theme_light_onTertiary,
|
||||
tertiaryContainer = md_theme_light_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
||||
error = md_theme_light_error,
|
||||
errorContainer = md_theme_light_errorContainer,
|
||||
onError = md_theme_light_onError,
|
||||
onErrorContainer = md_theme_light_onErrorContainer,
|
||||
background = md_theme_light_background,
|
||||
onBackground = md_theme_light_onBackground,
|
||||
surface = md_theme_light_surface,
|
||||
onSurface = md_theme_light_onSurface,
|
||||
surfaceVariant = md_theme_light_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
||||
outline = md_theme_light_outline,
|
||||
inverseOnSurface = md_theme_light_inverseOnSurface,
|
||||
inverseSurface = md_theme_light_inverseSurface,
|
||||
inversePrimary = md_theme_light_inversePrimary,
|
||||
surfaceTint = md_theme_light_surfaceTint,
|
||||
outlineVariant = md_theme_light_outlineVariant,
|
||||
scrim = md_theme_light_scrim,
|
||||
)
|
||||
|
||||
val DarkRedTheme = darkColorScheme(
|
||||
primary = md_theme_dark_primary,
|
||||
onPrimary = md_theme_dark_onPrimary,
|
||||
primaryContainer = md_theme_dark_primaryContainer,
|
||||
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
|
||||
secondary = md_theme_dark_secondary,
|
||||
onSecondary = md_theme_dark_onSecondary,
|
||||
secondaryContainer = md_theme_dark_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
|
||||
tertiary = md_theme_dark_tertiary,
|
||||
onTertiary = md_theme_dark_onTertiary,
|
||||
tertiaryContainer = md_theme_dark_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
|
||||
error = md_theme_dark_error,
|
||||
errorContainer = md_theme_dark_errorContainer,
|
||||
onError = md_theme_dark_onError,
|
||||
onErrorContainer = md_theme_dark_onErrorContainer,
|
||||
background = md_theme_dark_background,
|
||||
onBackground = md_theme_dark_onBackground,
|
||||
surface = md_theme_dark_surface,
|
||||
onSurface = md_theme_dark_onSurface,
|
||||
surfaceVariant = md_theme_dark_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
|
||||
outline = md_theme_dark_outline,
|
||||
inverseOnSurface = md_theme_dark_inverseOnSurface,
|
||||
inverseSurface = md_theme_dark_inverseSurface,
|
||||
inversePrimary = md_theme_dark_inversePrimary,
|
||||
surfaceTint = md_theme_dark_surfaceTint,
|
||||
outlineVariant = md_theme_dark_outlineVariant,
|
||||
scrim = md_theme_dark_scrim,
|
||||
)
|
||||
131
app/src/main/java/me/bmax/apatch/ui/theme/SakuraTheme.kt
Normal file
131
app/src/main/java/me/bmax/apatch/ui/theme/SakuraTheme.kt
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package me.bmax.apatch.ui.theme
|
||||
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
private val md_theme_light_primary = Color(0xFF9B404F)
|
||||
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_primaryContainer = Color(0xFFFFD9DC)
|
||||
private val md_theme_light_onPrimaryContainer = Color(0xFF400011)
|
||||
private val md_theme_light_secondary = Color(0xFF765659)
|
||||
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_secondaryContainer = Color(0xFFFFD9DC)
|
||||
private val md_theme_light_onSecondaryContainer = Color(0xFF2C1518)
|
||||
private val md_theme_light_tertiary = Color(0xFF785830)
|
||||
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_tertiaryContainer = Color(0xFFFFDDB7)
|
||||
private val md_theme_light_onTertiaryContainer = Color(0xFF2A1700)
|
||||
private val md_theme_light_error = Color(0xFFBA1A1A)
|
||||
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_onErrorContainer = Color(0xFF410002)
|
||||
private val md_theme_light_background = Color(0xFFFFFBFF)
|
||||
private val md_theme_light_onBackground = Color(0xFF201A1A)
|
||||
private val md_theme_light_surface = Color(0xFFFFFBFF)
|
||||
private val md_theme_light_onSurface = Color(0xFF201A1A)
|
||||
private val md_theme_light_surfaceVariant = Color(0xFFF4DDDE)
|
||||
private val md_theme_light_onSurfaceVariant = Color(0xFF524344)
|
||||
private val md_theme_light_outline = Color(0xFF847374)
|
||||
private val md_theme_light_inverseOnSurface = Color(0xFFFBEEEE)
|
||||
private val md_theme_light_inverseSurface = Color(0xFF362F2F)
|
||||
private val md_theme_light_inversePrimary = Color(0xFFFFB2BA)
|
||||
private val md_theme_light_shadow = Color(0xFF000000)
|
||||
private val md_theme_light_surfaceTint = Color(0xFF9B404F)
|
||||
private val md_theme_light_outlineVariant = Color(0xFFD7C1C3)
|
||||
private val md_theme_light_scrim = Color(0xFF000000)
|
||||
|
||||
private val md_theme_dark_primary = Color(0xFFFFB2BA)
|
||||
private val md_theme_dark_onPrimary = Color(0xFF5F1223)
|
||||
private val md_theme_dark_primaryContainer = Color(0xFF7D2939)
|
||||
private val md_theme_dark_onPrimaryContainer = Color(0xFFFFD9DC)
|
||||
private val md_theme_dark_secondary = Color(0xFFE5BDC0)
|
||||
private val md_theme_dark_onSecondary = Color(0xFF43292C)
|
||||
private val md_theme_dark_secondaryContainer = Color(0xFF5C3F42)
|
||||
private val md_theme_dark_onSecondaryContainer = Color(0xFFFFD9DC)
|
||||
private val md_theme_dark_tertiary = Color(0xFFE9BF8F)
|
||||
private val md_theme_dark_onTertiary = Color(0xFF442B07)
|
||||
private val md_theme_dark_tertiaryContainer = Color(0xFF5E411B)
|
||||
private val md_theme_dark_onTertiaryContainer = Color(0xFFFFDDB7)
|
||||
private val md_theme_dark_error = Color(0xFFFFB4AB)
|
||||
private val md_theme_dark_errorContainer = Color(0xFF93000A)
|
||||
private val md_theme_dark_onError = Color(0xFF690005)
|
||||
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_dark_background = Color(0xFF201A1A)
|
||||
private val md_theme_dark_onBackground = Color(0xFFECE0E0)
|
||||
private val md_theme_dark_surface = Color(0xFF201A1A)
|
||||
private val md_theme_dark_onSurface = Color(0xFFECE0E0)
|
||||
private val md_theme_dark_surfaceVariant = Color(0xFF524344)
|
||||
private val md_theme_dark_onSurfaceVariant = Color(0xFFD7C1C3)
|
||||
private val md_theme_dark_outline = Color(0xFF9F8C8D)
|
||||
private val md_theme_dark_inverseOnSurface = Color(0xFF201A1A)
|
||||
private val md_theme_dark_inverseSurface = Color(0xFFECE0E0)
|
||||
private val md_theme_dark_inversePrimary = Color(0xFF9B404F)
|
||||
private val md_theme_dark_shadow = Color(0xFF000000)
|
||||
private val md_theme_dark_surfaceTint = Color(0xFFFFB2BA)
|
||||
private val md_theme_dark_outlineVariant = Color(0xFF524344)
|
||||
private val md_theme_dark_scrim = Color(0xFF000000)
|
||||
|
||||
val LightSakuraTheme = lightColorScheme(
|
||||
primary = md_theme_light_primary,
|
||||
onPrimary = md_theme_light_onPrimary,
|
||||
primaryContainer = md_theme_light_primaryContainer,
|
||||
onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
||||
secondary = md_theme_light_secondary,
|
||||
onSecondary = md_theme_light_onSecondary,
|
||||
secondaryContainer = md_theme_light_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
||||
tertiary = md_theme_light_tertiary,
|
||||
onTertiary = md_theme_light_onTertiary,
|
||||
tertiaryContainer = md_theme_light_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
||||
error = md_theme_light_error,
|
||||
errorContainer = md_theme_light_errorContainer,
|
||||
onError = md_theme_light_onError,
|
||||
onErrorContainer = md_theme_light_onErrorContainer,
|
||||
background = md_theme_light_background,
|
||||
onBackground = md_theme_light_onBackground,
|
||||
surface = md_theme_light_surface,
|
||||
onSurface = md_theme_light_onSurface,
|
||||
surfaceVariant = md_theme_light_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
||||
outline = md_theme_light_outline,
|
||||
inverseOnSurface = md_theme_light_inverseOnSurface,
|
||||
inverseSurface = md_theme_light_inverseSurface,
|
||||
inversePrimary = md_theme_light_inversePrimary,
|
||||
surfaceTint = md_theme_light_surfaceTint,
|
||||
outlineVariant = md_theme_light_outlineVariant,
|
||||
scrim = md_theme_light_scrim,
|
||||
)
|
||||
|
||||
val DarkSakuraTheme = darkColorScheme(
|
||||
primary = md_theme_dark_primary,
|
||||
onPrimary = md_theme_dark_onPrimary,
|
||||
primaryContainer = md_theme_dark_primaryContainer,
|
||||
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
|
||||
secondary = md_theme_dark_secondary,
|
||||
onSecondary = md_theme_dark_onSecondary,
|
||||
secondaryContainer = md_theme_dark_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
|
||||
tertiary = md_theme_dark_tertiary,
|
||||
onTertiary = md_theme_dark_onTertiary,
|
||||
tertiaryContainer = md_theme_dark_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
|
||||
error = md_theme_dark_error,
|
||||
errorContainer = md_theme_dark_errorContainer,
|
||||
onError = md_theme_dark_onError,
|
||||
onErrorContainer = md_theme_dark_onErrorContainer,
|
||||
background = md_theme_dark_background,
|
||||
onBackground = md_theme_dark_onBackground,
|
||||
surface = md_theme_dark_surface,
|
||||
onSurface = md_theme_dark_onSurface,
|
||||
surfaceVariant = md_theme_dark_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
|
||||
outline = md_theme_dark_outline,
|
||||
inverseOnSurface = md_theme_dark_inverseOnSurface,
|
||||
inverseSurface = md_theme_dark_inverseSurface,
|
||||
inversePrimary = md_theme_dark_inversePrimary,
|
||||
surfaceTint = md_theme_dark_surfaceTint,
|
||||
outlineVariant = md_theme_dark_outlineVariant,
|
||||
scrim = md_theme_dark_scrim,
|
||||
)
|
||||
132
app/src/main/java/me/bmax/apatch/ui/theme/TealTheme.kt
Normal file
132
app/src/main/java/me/bmax/apatch/ui/theme/TealTheme.kt
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
package me.bmax.apatch.ui.theme
|
||||
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
private val md_theme_light_primary = Color(0xFF006A60)
|
||||
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_primaryContainer = Color(0xFF74F8E5)
|
||||
private val md_theme_light_onPrimaryContainer = Color(0xFF00201C)
|
||||
private val md_theme_light_secondary = Color(0xFF4A635F)
|
||||
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_secondaryContainer = Color(0xFFCCE8E2)
|
||||
private val md_theme_light_onSecondaryContainer = Color(0xFF05201C)
|
||||
private val md_theme_light_tertiary = Color(0xFF456179)
|
||||
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_tertiaryContainer = Color(0xFFCCE5FF)
|
||||
private val md_theme_light_onTertiaryContainer = Color(0xFF001E31)
|
||||
private val md_theme_light_error = Color(0xFFBA1A1A)
|
||||
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_onErrorContainer = Color(0xFF410002)
|
||||
private val md_theme_light_background = Color(0xFFFAFDFB)
|
||||
private val md_theme_light_onBackground = Color(0xFF191C1B)
|
||||
private val md_theme_light_surface = Color(0xFFFAFDFB)
|
||||
private val md_theme_light_onSurface = Color(0xFF191C1B)
|
||||
private val md_theme_light_surfaceVariant = Color(0xFFDAE5E1)
|
||||
private val md_theme_light_onSurfaceVariant = Color(0xFF3F4947)
|
||||
private val md_theme_light_outline = Color(0xFF6F7977)
|
||||
private val md_theme_light_inverseOnSurface = Color(0xFFEFF1EF)
|
||||
private val md_theme_light_inverseSurface = Color(0xFF2D3130)
|
||||
private val md_theme_light_inversePrimary = Color(0xFF53DBC9)
|
||||
private val md_theme_light_shadow = Color(0xFF000000)
|
||||
private val md_theme_light_surfaceTint = Color(0xFF006A60)
|
||||
private val md_theme_light_outlineVariant = Color(0xFFBEC9C6)
|
||||
private val md_theme_light_scrim = Color(0xFF000000)
|
||||
|
||||
private val md_theme_dark_primary = Color(0xFF53DBC9)
|
||||
private val md_theme_dark_onPrimary = Color(0xFF003731)
|
||||
private val md_theme_dark_primaryContainer = Color(0xFF005048)
|
||||
private val md_theme_dark_onPrimaryContainer = Color(0xFF74F8E5)
|
||||
private val md_theme_dark_secondary = Color(0xFFB1CCC6)
|
||||
private val md_theme_dark_onSecondary = Color(0xFF1C3531)
|
||||
private val md_theme_dark_secondaryContainer = Color(0xFF334B47)
|
||||
private val md_theme_dark_onSecondaryContainer = Color(0xFFCCE8E2)
|
||||
private val md_theme_dark_tertiary = Color(0xFFADCAE6)
|
||||
private val md_theme_dark_onTertiary = Color(0xFF153349)
|
||||
private val md_theme_dark_tertiaryContainer = Color(0xFF2D4961)
|
||||
private val md_theme_dark_onTertiaryContainer = Color(0xFFCCE5FF)
|
||||
private val md_theme_dark_error = Color(0xFFFFB4AB)
|
||||
private val md_theme_dark_errorContainer = Color(0xFF93000A)
|
||||
private val md_theme_dark_onError = Color(0xFF690005)
|
||||
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_dark_background = Color(0xFF191C1B)
|
||||
private val md_theme_dark_onBackground = Color(0xFFE0E3E1)
|
||||
private val md_theme_dark_surface = Color(0xFF191C1B)
|
||||
private val md_theme_dark_onSurface = Color(0xFFE0E3E1)
|
||||
private val md_theme_dark_surfaceVariant = Color(0xFF3F4947)
|
||||
private val md_theme_dark_onSurfaceVariant = Color(0xFFBEC9C6)
|
||||
private val md_theme_dark_outline = Color(0xFF899390)
|
||||
private val md_theme_dark_inverseOnSurface = Color(0xFF191C1B)
|
||||
private val md_theme_dark_inverseSurface = Color(0xFFE0E3E1)
|
||||
private val md_theme_dark_inversePrimary = Color(0xFF006A60)
|
||||
private val md_theme_dark_shadow = Color(0xFF000000)
|
||||
private val md_theme_dark_surfaceTint = Color(0xFF53DBC9)
|
||||
private val md_theme_dark_outlineVariant = Color(0xFF3F4947)
|
||||
private val md_theme_dark_scrim = Color(0xFF000000)
|
||||
|
||||
|
||||
val LightTealTheme = lightColorScheme(
|
||||
primary = md_theme_light_primary,
|
||||
onPrimary = md_theme_light_onPrimary,
|
||||
primaryContainer = md_theme_light_primaryContainer,
|
||||
onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
||||
secondary = md_theme_light_secondary,
|
||||
onSecondary = md_theme_light_onSecondary,
|
||||
secondaryContainer = md_theme_light_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
||||
tertiary = md_theme_light_tertiary,
|
||||
onTertiary = md_theme_light_onTertiary,
|
||||
tertiaryContainer = md_theme_light_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
||||
error = md_theme_light_error,
|
||||
errorContainer = md_theme_light_errorContainer,
|
||||
onError = md_theme_light_onError,
|
||||
onErrorContainer = md_theme_light_onErrorContainer,
|
||||
background = md_theme_light_background,
|
||||
onBackground = md_theme_light_onBackground,
|
||||
surface = md_theme_light_surface,
|
||||
onSurface = md_theme_light_onSurface,
|
||||
surfaceVariant = md_theme_light_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
||||
outline = md_theme_light_outline,
|
||||
inverseOnSurface = md_theme_light_inverseOnSurface,
|
||||
inverseSurface = md_theme_light_inverseSurface,
|
||||
inversePrimary = md_theme_light_inversePrimary,
|
||||
surfaceTint = md_theme_light_surfaceTint,
|
||||
outlineVariant = md_theme_light_outlineVariant,
|
||||
scrim = md_theme_light_scrim,
|
||||
)
|
||||
|
||||
val DarkTealTheme = darkColorScheme(
|
||||
primary = md_theme_dark_primary,
|
||||
onPrimary = md_theme_dark_onPrimary,
|
||||
primaryContainer = md_theme_dark_primaryContainer,
|
||||
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
|
||||
secondary = md_theme_dark_secondary,
|
||||
onSecondary = md_theme_dark_onSecondary,
|
||||
secondaryContainer = md_theme_dark_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
|
||||
tertiary = md_theme_dark_tertiary,
|
||||
onTertiary = md_theme_dark_onTertiary,
|
||||
tertiaryContainer = md_theme_dark_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
|
||||
error = md_theme_dark_error,
|
||||
errorContainer = md_theme_dark_errorContainer,
|
||||
onError = md_theme_dark_onError,
|
||||
onErrorContainer = md_theme_dark_onErrorContainer,
|
||||
background = md_theme_dark_background,
|
||||
onBackground = md_theme_dark_onBackground,
|
||||
surface = md_theme_dark_surface,
|
||||
onSurface = md_theme_dark_onSurface,
|
||||
surfaceVariant = md_theme_dark_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
|
||||
outline = md_theme_dark_outline,
|
||||
inverseOnSurface = md_theme_dark_inverseOnSurface,
|
||||
inverseSurface = md_theme_dark_inverseSurface,
|
||||
inversePrimary = md_theme_dark_inversePrimary,
|
||||
surfaceTint = md_theme_dark_surfaceTint,
|
||||
outlineVariant = md_theme_dark_outlineVariant,
|
||||
scrim = md_theme_dark_scrim,
|
||||
)
|
||||
172
app/src/main/java/me/bmax/apatch/ui/theme/Theme.kt
Normal file
172
app/src/main/java/me/bmax/apatch/ui/theme/Theme.kt
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
package me.bmax.apatch.ui.theme
|
||||
|
||||
import android.os.Build
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import me.bmax.apatch.APApplication
|
||||
|
||||
@Composable
|
||||
private fun SystemBarStyle(
|
||||
darkMode: Boolean,
|
||||
statusBarScrim: Color = Color.Transparent,
|
||||
navigationBarScrim: Color = Color.Transparent
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val activity = context as ComponentActivity
|
||||
|
||||
SideEffect {
|
||||
activity.enableEdgeToEdge(
|
||||
statusBarStyle = SystemBarStyle.auto(
|
||||
statusBarScrim.toArgb(),
|
||||
statusBarScrim.toArgb(),
|
||||
) { darkMode }, navigationBarStyle = when {
|
||||
darkMode -> SystemBarStyle.dark(
|
||||
navigationBarScrim.toArgb()
|
||||
)
|
||||
|
||||
else -> SystemBarStyle.light(
|
||||
navigationBarScrim.toArgb(),
|
||||
navigationBarScrim.toArgb(),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val refreshTheme = MutableLiveData(false)
|
||||
|
||||
@Composable
|
||||
fun APatchTheme(
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val prefs = APApplication.sharedPreferences
|
||||
|
||||
var darkThemeFollowSys by remember {
|
||||
mutableStateOf(
|
||||
prefs.getBoolean(
|
||||
"night_mode_follow_sys",
|
||||
true
|
||||
)
|
||||
)
|
||||
}
|
||||
var nightModeEnabled by remember {
|
||||
mutableStateOf(
|
||||
prefs.getBoolean(
|
||||
"night_mode_enabled",
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
// Dynamic color is available on Android 12+, and custom 1t!
|
||||
var dynamicColor by remember {
|
||||
mutableStateOf(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) prefs.getBoolean(
|
||||
"use_system_color_theme",
|
||||
true
|
||||
) else false
|
||||
)
|
||||
}
|
||||
var customColorScheme by remember { mutableStateOf(prefs.getString("custom_color", "blue")) }
|
||||
|
||||
val refreshThemeObserver by refreshTheme.observeAsState(false)
|
||||
if (refreshThemeObserver == true) {
|
||||
darkThemeFollowSys = prefs.getBoolean("night_mode_follow_sys", true)
|
||||
nightModeEnabled = prefs.getBoolean("night_mode_enabled", false)
|
||||
dynamicColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) prefs.getBoolean(
|
||||
"use_system_color_theme",
|
||||
true
|
||||
) else false
|
||||
customColorScheme = prefs.getString("custom_color", "blue")
|
||||
refreshTheme.postValue(false)
|
||||
}
|
||||
|
||||
val darkTheme = if (darkThemeFollowSys) {
|
||||
isSystemInDarkTheme()
|
||||
} else {
|
||||
nightModeEnabled
|
||||
}
|
||||
|
||||
val colorScheme = if (!dynamicColor) {
|
||||
if (darkTheme) {
|
||||
when (customColorScheme) {
|
||||
"amber" -> DarkAmberTheme
|
||||
"blue_grey" -> DarkBlueGreyTheme
|
||||
"blue" -> DarkBlueTheme
|
||||
"brown" -> DarkBrownTheme
|
||||
"cyan" -> DarkCyanTheme
|
||||
"deep_orange" -> DarkDeepOrangeTheme
|
||||
"deep_purple" -> DarkDeepPurpleTheme
|
||||
"green" -> DarkGreenTheme
|
||||
"indigo" -> DarkIndigoTheme
|
||||
"light_blue" -> DarkLightBlueTheme
|
||||
"light_green" -> DarkLightGreenTheme
|
||||
"lime" -> DarkLimeTheme
|
||||
"orange" -> DarkOrangeTheme
|
||||
"pink" -> DarkPinkTheme
|
||||
"purple" -> DarkPurpleTheme
|
||||
"red" -> DarkRedTheme
|
||||
"sakura" -> DarkSakuraTheme
|
||||
"teal" -> DarkTealTheme
|
||||
"yellow" -> DarkYellowTheme
|
||||
else -> DarkBlueTheme
|
||||
}
|
||||
} else {
|
||||
when (customColorScheme) {
|
||||
"amber" -> LightAmberTheme
|
||||
"blue_grey" -> LightBlueGreyTheme
|
||||
"blue" -> LightBlueTheme
|
||||
"brown" -> LightBrownTheme
|
||||
"cyan" -> LightCyanTheme
|
||||
"deep_orange" -> LightDeepOrangeTheme
|
||||
"deep_purple" -> LightDeepPurpleTheme
|
||||
"green" -> LightGreenTheme
|
||||
"indigo" -> LightIndigoTheme
|
||||
"light_blue" -> LightLightBlueTheme
|
||||
"light_green" -> LightLightGreenTheme
|
||||
"lime" -> LightLimeTheme
|
||||
"orange" -> LightOrangeTheme
|
||||
"pink" -> LightPinkTheme
|
||||
"purple" -> LightPurpleTheme
|
||||
"red" -> LightRedTheme
|
||||
"sakura" -> LightSakuraTheme
|
||||
"teal" -> LightTealTheme
|
||||
"yellow" -> LightYellowTheme
|
||||
else -> LightBlueTheme
|
||||
}
|
||||
}
|
||||
} else {
|
||||
when {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkBlueTheme
|
||||
else -> LightBlueTheme
|
||||
}
|
||||
}
|
||||
|
||||
SystemBarStyle(
|
||||
darkMode = darkTheme
|
||||
)
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme, typography = Typography, content = content
|
||||
)
|
||||
}
|
||||
33
app/src/main/java/me/bmax/apatch/ui/theme/Type.kt
Normal file
33
app/src/main/java/me/bmax/apatch/ui/theme/Type.kt
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
package me.bmax.apatch.ui.theme
|
||||
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = androidx.compose.material3.Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
*/
|
||||
)
|
||||
131
app/src/main/java/me/bmax/apatch/ui/theme/YellowTheme.kt
Normal file
131
app/src/main/java/me/bmax/apatch/ui/theme/YellowTheme.kt
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package me.bmax.apatch.ui.theme
|
||||
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
private val md_theme_light_primary = Color(0xFF695F00)
|
||||
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_primaryContainer = Color(0xFFF9E534)
|
||||
private val md_theme_light_onPrimaryContainer = Color(0xFF201C00)
|
||||
private val md_theme_light_secondary = Color(0xFF645F41)
|
||||
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_secondaryContainer = Color(0xFFEBE3BD)
|
||||
private val md_theme_light_onSecondaryContainer = Color(0xFF1F1C05)
|
||||
private val md_theme_light_tertiary = Color(0xFF406652)
|
||||
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_tertiaryContainer = Color(0xFFC2ECD3)
|
||||
private val md_theme_light_onTertiaryContainer = Color(0xFF002113)
|
||||
private val md_theme_light_error = Color(0xFFBA1A1A)
|
||||
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_onErrorContainer = Color(0xFF410002)
|
||||
private val md_theme_light_background = Color(0xFFFFFBFF)
|
||||
private val md_theme_light_onBackground = Color(0xFF1D1C16)
|
||||
private val md_theme_light_surface = Color(0xFFFFFBFF)
|
||||
private val md_theme_light_onSurface = Color(0xFF1D1C16)
|
||||
private val md_theme_light_surfaceVariant = Color(0xFFE8E2D0)
|
||||
private val md_theme_light_onSurfaceVariant = Color(0xFF4A473A)
|
||||
private val md_theme_light_outline = Color(0xFF7B7768)
|
||||
private val md_theme_light_inverseOnSurface = Color(0xFFF5F0E7)
|
||||
private val md_theme_light_inverseSurface = Color(0xFF32302A)
|
||||
private val md_theme_light_inversePrimary = Color(0xFFDBC90A)
|
||||
private val md_theme_light_shadow = Color(0xFF000000)
|
||||
private val md_theme_light_surfaceTint = Color(0xFF695F00)
|
||||
private val md_theme_light_outlineVariant = Color(0xFFCBC6B5)
|
||||
private val md_theme_light_scrim = Color(0xFF000000)
|
||||
|
||||
private val md_theme_dark_primary = Color(0xFFDBC90A)
|
||||
private val md_theme_dark_onPrimary = Color(0xFF363100)
|
||||
private val md_theme_dark_primaryContainer = Color(0xFF4F4800)
|
||||
private val md_theme_dark_onPrimaryContainer = Color(0xFFF9E534)
|
||||
private val md_theme_dark_secondary = Color(0xFFCEC7A3)
|
||||
private val md_theme_dark_onSecondary = Color(0xFF343117)
|
||||
private val md_theme_dark_secondaryContainer = Color(0xFF4B472B)
|
||||
private val md_theme_dark_onSecondaryContainer = Color(0xFFEBE3BD)
|
||||
private val md_theme_dark_tertiary = Color(0xFFA7D0B7)
|
||||
private val md_theme_dark_onTertiary = Color(0xFF103726)
|
||||
private val md_theme_dark_tertiaryContainer = Color(0xFF294E3B)
|
||||
private val md_theme_dark_onTertiaryContainer = Color(0xFFC2ECD3)
|
||||
private val md_theme_dark_error = Color(0xFFFFB4AB)
|
||||
private val md_theme_dark_errorContainer = Color(0xFF93000A)
|
||||
private val md_theme_dark_onError = Color(0xFF690005)
|
||||
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_dark_background = Color(0xFF1D1C16)
|
||||
private val md_theme_dark_onBackground = Color(0xFFE7E2D9)
|
||||
private val md_theme_dark_surface = Color(0xFF1D1C16)
|
||||
private val md_theme_dark_onSurface = Color(0xFFE7E2D9)
|
||||
private val md_theme_dark_surfaceVariant = Color(0xFF4A473A)
|
||||
private val md_theme_dark_onSurfaceVariant = Color(0xFFCBC6B5)
|
||||
private val md_theme_dark_outline = Color(0xFF959181)
|
||||
private val md_theme_dark_inverseOnSurface = Color(0xFF1D1C16)
|
||||
private val md_theme_dark_inverseSurface = Color(0xFFE7E2D9)
|
||||
private val md_theme_dark_inversePrimary = Color(0xFF695F00)
|
||||
private val md_theme_dark_shadow = Color(0xFF000000)
|
||||
private val md_theme_dark_surfaceTint = Color(0xFFDBC90A)
|
||||
private val md_theme_dark_outlineVariant = Color(0xFF4A473A)
|
||||
private val md_theme_dark_scrim = Color(0xFF000000)
|
||||
|
||||
val LightYellowTheme = lightColorScheme(
|
||||
primary = md_theme_light_primary,
|
||||
onPrimary = md_theme_light_onPrimary,
|
||||
primaryContainer = md_theme_light_primaryContainer,
|
||||
onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
||||
secondary = md_theme_light_secondary,
|
||||
onSecondary = md_theme_light_onSecondary,
|
||||
secondaryContainer = md_theme_light_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
||||
tertiary = md_theme_light_tertiary,
|
||||
onTertiary = md_theme_light_onTertiary,
|
||||
tertiaryContainer = md_theme_light_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
||||
error = md_theme_light_error,
|
||||
errorContainer = md_theme_light_errorContainer,
|
||||
onError = md_theme_light_onError,
|
||||
onErrorContainer = md_theme_light_onErrorContainer,
|
||||
background = md_theme_light_background,
|
||||
onBackground = md_theme_light_onBackground,
|
||||
surface = md_theme_light_surface,
|
||||
onSurface = md_theme_light_onSurface,
|
||||
surfaceVariant = md_theme_light_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
||||
outline = md_theme_light_outline,
|
||||
inverseOnSurface = md_theme_light_inverseOnSurface,
|
||||
inverseSurface = md_theme_light_inverseSurface,
|
||||
inversePrimary = md_theme_light_inversePrimary,
|
||||
surfaceTint = md_theme_light_surfaceTint,
|
||||
outlineVariant = md_theme_light_outlineVariant,
|
||||
scrim = md_theme_light_scrim,
|
||||
)
|
||||
|
||||
val DarkYellowTheme = darkColorScheme(
|
||||
primary = md_theme_dark_primary,
|
||||
onPrimary = md_theme_dark_onPrimary,
|
||||
primaryContainer = md_theme_dark_primaryContainer,
|
||||
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
|
||||
secondary = md_theme_dark_secondary,
|
||||
onSecondary = md_theme_dark_onSecondary,
|
||||
secondaryContainer = md_theme_dark_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
|
||||
tertiary = md_theme_dark_tertiary,
|
||||
onTertiary = md_theme_dark_onTertiary,
|
||||
tertiaryContainer = md_theme_dark_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
|
||||
error = md_theme_dark_error,
|
||||
errorContainer = md_theme_dark_errorContainer,
|
||||
onError = md_theme_dark_onError,
|
||||
onErrorContainer = md_theme_dark_onErrorContainer,
|
||||
background = md_theme_dark_background,
|
||||
onBackground = md_theme_dark_onBackground,
|
||||
surface = md_theme_dark_surface,
|
||||
onSurface = md_theme_dark_onSurface,
|
||||
surfaceVariant = md_theme_dark_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
|
||||
outline = md_theme_dark_outline,
|
||||
inverseOnSurface = md_theme_dark_inverseOnSurface,
|
||||
inverseSurface = md_theme_dark_inverseSurface,
|
||||
inversePrimary = md_theme_dark_inversePrimary,
|
||||
surfaceTint = md_theme_dark_surfaceTint,
|
||||
outlineVariant = md_theme_dark_outlineVariant,
|
||||
scrim = md_theme_dark_scrim,
|
||||
)
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
package me.bmax.apatch.ui.viewmodel
|
||||
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import me.bmax.apatch.apApp
|
||||
import me.bmax.apatch.util.listModules
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.text.Collator
|
||||
import java.util.Locale
|
||||
|
||||
class APModuleViewModel : ViewModel() {
|
||||
companion object {
|
||||
private const val TAG = "ModuleViewModel"
|
||||
private var modules by mutableStateOf<List<ModuleInfo>>(emptyList())
|
||||
}
|
||||
|
||||
class ModuleInfo(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val author: String,
|
||||
val version: String,
|
||||
val versionCode: Int,
|
||||
val description: String,
|
||||
val enabled: Boolean,
|
||||
val update: Boolean,
|
||||
val remove: Boolean,
|
||||
val updateJson: String,
|
||||
val hasWebUi: Boolean,
|
||||
val hasActionScript: Boolean,
|
||||
)
|
||||
|
||||
data class ModuleUpdateInfo(
|
||||
val version: String,
|
||||
val versionCode: Int,
|
||||
val zipUrl: String,
|
||||
val changelog: String,
|
||||
)
|
||||
|
||||
var isRefreshing by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
val moduleList by derivedStateOf {
|
||||
val comparator = compareBy(Collator.getInstance(Locale.getDefault()), ModuleInfo::id)
|
||||
modules.sortedWith(comparator).also {
|
||||
isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
var isNeedRefresh by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
fun markNeedRefresh() {
|
||||
isNeedRefresh = true
|
||||
}
|
||||
|
||||
fun fetchModuleList() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
isRefreshing = true
|
||||
|
||||
val oldModuleList = modules
|
||||
|
||||
val start = SystemClock.elapsedRealtime()
|
||||
|
||||
kotlin.runCatching {
|
||||
|
||||
val result = listModules()
|
||||
|
||||
Log.i(TAG, "result: $result")
|
||||
|
||||
val array = JSONArray(result)
|
||||
modules = (0 until array.length())
|
||||
.asSequence()
|
||||
.map { array.getJSONObject(it) }
|
||||
.map { obj ->
|
||||
ModuleInfo(
|
||||
obj.getString("id"),
|
||||
|
||||
obj.optString("name"),
|
||||
obj.optString("author", "Unknown"),
|
||||
obj.optString("version", "Unknown"),
|
||||
obj.optInt("versionCode", 0),
|
||||
obj.optString("description"),
|
||||
obj.getBoolean("enabled"),
|
||||
obj.getBoolean("update"),
|
||||
obj.getBoolean("remove"),
|
||||
obj.optString("updateJson"),
|
||||
obj.optBoolean("web"),
|
||||
obj.optBoolean("action")
|
||||
)
|
||||
}.toList()
|
||||
isNeedRefresh = false
|
||||
}.onFailure { e ->
|
||||
Log.e(TAG, "fetchModuleList: ", e)
|
||||
isRefreshing = false
|
||||
}
|
||||
|
||||
// when both old and new is kotlin.collections.EmptyList
|
||||
// moduleList update will don't trigger
|
||||
if (oldModuleList === modules) {
|
||||
isRefreshing = false
|
||||
}
|
||||
|
||||
Log.i(TAG, "load cost: ${SystemClock.elapsedRealtime() - start}, modules: $modules")
|
||||
}
|
||||
}
|
||||
|
||||
private fun sanitizeVersionString(version: String): String {
|
||||
return version.replace(Regex("[^a-zA-Z0-9.\\-_]"), "_")
|
||||
}
|
||||
|
||||
fun checkUpdate(m: ModuleInfo): Triple<String, String, String> {
|
||||
val empty = Triple("", "", "")
|
||||
if (m.updateJson.isEmpty() || m.remove || m.update || !m.enabled) {
|
||||
return empty
|
||||
}
|
||||
// download updateJson
|
||||
val result = kotlin.runCatching {
|
||||
val url = m.updateJson
|
||||
Log.i(TAG, "checkUpdate url: $url")
|
||||
val response = apApp.okhttpClient
|
||||
.newCall(
|
||||
okhttp3.Request.Builder()
|
||||
.url(url)
|
||||
.build()
|
||||
).execute()
|
||||
Log.d(TAG, "checkUpdate code: ${response.code}")
|
||||
if (response.isSuccessful) {
|
||||
response.body?.string() ?: ""
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}.getOrDefault("")
|
||||
Log.i(TAG, "checkUpdate result: $result")
|
||||
|
||||
if (result.isEmpty()) {
|
||||
return empty
|
||||
}
|
||||
|
||||
val updateJson = kotlin.runCatching {
|
||||
JSONObject(result)
|
||||
}.getOrNull() ?: return empty
|
||||
|
||||
val version = sanitizeVersionString(updateJson.optString("version", ""))
|
||||
val versionCode = updateJson.optInt("versionCode", 0)
|
||||
val zipUrl = updateJson.optString("zipUrl", "")
|
||||
val changelog = updateJson.optString("changelog", "")
|
||||
if (versionCode <= m.versionCode || zipUrl.isEmpty()) {
|
||||
return empty
|
||||
}
|
||||
|
||||
return Triple(zipUrl, version, changelog)
|
||||
}
|
||||
}
|
||||
66
app/src/main/java/me/bmax/apatch/ui/viewmodel/KPModel.kt
Normal file
66
app/src/main/java/me/bmax/apatch/ui/viewmodel/KPModel.kt
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
package me.bmax.apatch.ui.viewmodel
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.Keep
|
||||
import androidx.compose.runtime.Immutable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
object KPModel {
|
||||
|
||||
enum class TriggerEvent(val event: String) {
|
||||
PAGING_INIT("paging-init"),
|
||||
PRE_KERNEL_INIT("pre-kernel-init"),
|
||||
POST_KERNEL_INIT("post-kernel-init"),
|
||||
}
|
||||
|
||||
|
||||
enum class ExtraType(val desc: String) {
|
||||
NONE("none"),
|
||||
KPM("kpm"),
|
||||
SHELL("shell"),
|
||||
EXEC("exec"),
|
||||
RAW("raw"),
|
||||
ANDROID_RC("android_rc");
|
||||
}
|
||||
|
||||
interface IExtraInfo : Parcelable {
|
||||
var type: ExtraType
|
||||
var name: String
|
||||
var event: String
|
||||
var args: String
|
||||
}
|
||||
|
||||
@Immutable
|
||||
@Parcelize
|
||||
@Keep
|
||||
data class KPMInfo(
|
||||
override var type: ExtraType,
|
||||
override var name: String,
|
||||
override var event: String,
|
||||
override var args: String,
|
||||
var version: String,
|
||||
var license: String,
|
||||
var author: String,
|
||||
var description: String,
|
||||
) : IExtraInfo
|
||||
|
||||
@Immutable
|
||||
@Parcelize
|
||||
@Keep
|
||||
data class KPImgInfo(
|
||||
var version: String,
|
||||
var compileTime: String,
|
||||
var config: String,
|
||||
var superKey: String,
|
||||
var rootSuperkey: String
|
||||
) : Parcelable
|
||||
|
||||
@Immutable
|
||||
@Parcelize
|
||||
@Keep
|
||||
data class KImgInfo(
|
||||
var banner: String,
|
||||
var patched: Boolean,
|
||||
) : Parcelable
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
package me.bmax.apatch.ui.viewmodel
|
||||
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import me.bmax.apatch.Natives
|
||||
import java.text.Collator
|
||||
import java.util.Locale
|
||||
|
||||
class KPModuleViewModel : ViewModel() {
|
||||
companion object {
|
||||
private const val TAG = "KPModuleViewModel"
|
||||
private var modules by mutableStateOf<List<KPModel.KPMInfo>>(emptyList())
|
||||
}
|
||||
|
||||
var isRefreshing by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
val moduleList by derivedStateOf {
|
||||
val comparator = compareBy(Collator.getInstance(Locale.getDefault()), KPModel.KPMInfo::name)
|
||||
modules.sortedWith(comparator).also {
|
||||
isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
var isNeedRefresh by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
fun markNeedRefresh() {
|
||||
isNeedRefresh = true
|
||||
}
|
||||
|
||||
fun fetchModuleList() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
isRefreshing = true
|
||||
val oldModuleList = modules
|
||||
val start = SystemClock.elapsedRealtime()
|
||||
|
||||
kotlin.runCatching {
|
||||
var names = Natives.kernelPatchModuleList()
|
||||
if (Natives.kernelPatchModuleNum() <= 0)
|
||||
names = ""
|
||||
val nameList = names.split('\n').toList()
|
||||
Log.d(TAG, "kpm list: $nameList")
|
||||
modules = nameList.filter { it.isNotEmpty() }.map {
|
||||
val infoline = Natives.kernelPatchModuleInfo(it)
|
||||
val spi = infoline.split('\n')
|
||||
val name = spi.find { it.startsWith("name=") }?.removePrefix("name=")
|
||||
val version = spi.find { it.startsWith("version=") }?.removePrefix("version=")
|
||||
val license = spi.find { it.startsWith("license=") }?.removePrefix("license=")
|
||||
val author = spi.find { it.startsWith("author=") }?.removePrefix("author=")
|
||||
val description =
|
||||
spi.find { it.startsWith("description=") }?.removePrefix("description=")
|
||||
val args = spi.find { it.startsWith("args=") }?.removePrefix("args=")
|
||||
val info = KPModel.KPMInfo(
|
||||
KPModel.ExtraType.KPM,
|
||||
name ?: "",
|
||||
"",
|
||||
args ?: "",
|
||||
version ?: "",
|
||||
license ?: "",
|
||||
author ?: "",
|
||||
description ?: ""
|
||||
)
|
||||
info
|
||||
}
|
||||
isNeedRefresh = false
|
||||
}.onFailure { e ->
|
||||
Log.e(TAG, "fetchModuleList: ", e)
|
||||
isRefreshing = false
|
||||
}
|
||||
|
||||
// when both old and new is kotlin.collections.EmptyList
|
||||
// moduleList update will don't trigger
|
||||
if (oldModuleList === modules) {
|
||||
isRefreshing = false
|
||||
}
|
||||
|
||||
Log.i(TAG, "load cost: ${SystemClock.elapsedRealtime() - start}, modules: $modules")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,574 @@
|
|||
package me.bmax.apatch.ui.viewmodel
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.system.Os
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.superuser.CallbackList
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.nio.ExtendedFile
|
||||
import com.topjohnwu.superuser.nio.FileSystemManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import me.bmax.apatch.APApplication
|
||||
import me.bmax.apatch.BuildConfig
|
||||
import me.bmax.apatch.R
|
||||
import me.bmax.apatch.apApp
|
||||
import me.bmax.apatch.util.Version
|
||||
import me.bmax.apatch.util.copyAndClose
|
||||
import me.bmax.apatch.util.copyAndCloseOut
|
||||
import me.bmax.apatch.util.createRootShell
|
||||
import me.bmax.apatch.util.inputStream
|
||||
import me.bmax.apatch.util.shellForResult
|
||||
import me.bmax.apatch.util.writeTo
|
||||
import org.ini4j.Ini
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.io.InputStreamReader
|
||||
import java.io.StringReader
|
||||
|
||||
private const val TAG = "PatchViewModel"
|
||||
|
||||
class PatchesViewModel : ViewModel() {
|
||||
|
||||
enum class PatchMode(val sId: Int) {
|
||||
PATCH_ONLY(R.string.patch_mode_bootimg_patch),
|
||||
PATCH_AND_INSTALL(R.string.patch_mode_patch_and_install),
|
||||
INSTALL_TO_NEXT_SLOT(R.string.patch_mode_install_to_next_slot),
|
||||
UNPATCH(R.string.patch_mode_uninstall_patch)
|
||||
}
|
||||
|
||||
var bootSlot by mutableStateOf("")
|
||||
var bootDev by mutableStateOf("")
|
||||
var kimgInfo by mutableStateOf(KPModel.KImgInfo("", false))
|
||||
var kpimgInfo by mutableStateOf(KPModel.KPImgInfo("", "", "", "", ""))
|
||||
var superkey by mutableStateOf(APApplication.superKey)
|
||||
var existedExtras = mutableStateListOf<KPModel.IExtraInfo>()
|
||||
var newExtras = mutableStateListOf<KPModel.IExtraInfo>()
|
||||
var newExtrasFileName = mutableListOf<String>()
|
||||
|
||||
var running by mutableStateOf(false)
|
||||
var patching by mutableStateOf(false)
|
||||
var patchdone by mutableStateOf(false)
|
||||
var needReboot by mutableStateOf(false)
|
||||
|
||||
var error by mutableStateOf("")
|
||||
var patchLog by mutableStateOf("")
|
||||
|
||||
private val patchDir: ExtendedFile = FileSystemManager.getLocal().getFile(apApp.filesDir.parent, "patch")
|
||||
private var srcBoot: ExtendedFile = patchDir.getChildFile("boot.img")
|
||||
private var shell: Shell = createRootShell()
|
||||
private var prepared: Boolean = false
|
||||
|
||||
private fun prepare() {
|
||||
patchDir.deleteRecursively()
|
||||
patchDir.mkdirs()
|
||||
val execs = listOf(
|
||||
"libkptools.so", "libmagiskboot.so", "libbusybox.so", "libkpatch.so", "libbootctl.so"
|
||||
)
|
||||
error = ""
|
||||
|
||||
val info = apApp.applicationInfo
|
||||
val libs = File(info.nativeLibraryDir).listFiles { _, name ->
|
||||
execs.contains(name)
|
||||
} ?: emptyArray()
|
||||
|
||||
for (lib in libs) {
|
||||
val name = lib.name.substring(3, lib.name.length - 3)
|
||||
Os.symlink(lib.path, "$patchDir/$name")
|
||||
}
|
||||
|
||||
// Extract scripts
|
||||
for (script in listOf(
|
||||
"boot_patch.sh", "boot_unpatch.sh", "boot_extract.sh", "util_functions.sh", "kpimg"
|
||||
)) {
|
||||
val dest = File(patchDir, script)
|
||||
apApp.assets.open(script).writeTo(dest)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun parseKpimg() {
|
||||
val result = shellForResult(
|
||||
shell, "cd $patchDir", "./kptools -l -k kpimg"
|
||||
)
|
||||
|
||||
if (result.isSuccess) {
|
||||
val ini = Ini(StringReader(result.out.joinToString("\n")))
|
||||
val kpimg = ini["kpimg"]
|
||||
if (kpimg != null) {
|
||||
kpimgInfo = KPModel.KPImgInfo(
|
||||
kpimg["version"].toString(),
|
||||
kpimg["compile_time"].toString(),
|
||||
kpimg["config"].toString(),
|
||||
APApplication.superKey, // current key
|
||||
kpimg["root_superkey"].toString(), // empty
|
||||
)
|
||||
} else {
|
||||
error += "parse kpimg error\n"
|
||||
}
|
||||
} else {
|
||||
error = result.err.joinToString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseBootimg(bootimg: String) {
|
||||
val result = shellForResult(
|
||||
shell,
|
||||
"cd $patchDir",
|
||||
"./magiskboot unpack $bootimg",
|
||||
"./kptools -l -i kernel",
|
||||
)
|
||||
if (result.isSuccess) {
|
||||
val ini = Ini(StringReader(result.out.joinToString("\n")))
|
||||
Log.d(TAG, "kernel image info: $ini")
|
||||
|
||||
val kernel = ini["kernel"]
|
||||
if (kernel == null) {
|
||||
error += "empty kernel section"
|
||||
Log.d(TAG, error)
|
||||
return
|
||||
}
|
||||
kimgInfo = KPModel.KImgInfo(kernel["banner"].toString(), kernel["patched"].toBoolean())
|
||||
if (kimgInfo.patched) {
|
||||
val superkey = ini["kpimg"]?.getOrDefault("superkey", "") ?: ""
|
||||
kpimgInfo.superKey = superkey
|
||||
if (checkSuperKeyValidation(superkey)) {
|
||||
this.superkey = superkey
|
||||
}
|
||||
var kpmNum = kernel["extra_num"]?.toInt()
|
||||
if (kpmNum == null) {
|
||||
val extras = ini["extras"]
|
||||
kpmNum = extras?.get("num")?.toInt()
|
||||
}
|
||||
if (kpmNum != null && kpmNum > 0) {
|
||||
for (i in 0..<kpmNum) {
|
||||
val extra = ini["extra $i"]
|
||||
if (extra == null) {
|
||||
error += "empty extra section"
|
||||
break
|
||||
}
|
||||
val type = KPModel.ExtraType.valueOf(extra["type"]!!.uppercase())
|
||||
val name = extra["name"].toString()
|
||||
val args = extra["args"].toString()
|
||||
var event = extra["event"].toString()
|
||||
if (event.isEmpty()) {
|
||||
event = KPModel.TriggerEvent.PRE_KERNEL_INIT.event
|
||||
}
|
||||
if (type == KPModel.ExtraType.KPM) {
|
||||
val kpmInfo = KPModel.KPMInfo(
|
||||
type, name, event, args,
|
||||
extra["version"].toString(),
|
||||
extra["license"].toString(),
|
||||
extra["author"].toString(),
|
||||
extra["description"].toString(),
|
||||
)
|
||||
existedExtras.add(kpmInfo)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error += result.err.joinToString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
val checkSuperKeyValidation: (superKey: String) -> Boolean = { superKey ->
|
||||
superKey.length in 8..63 && superKey.any { it.isDigit() } && superKey.any { it.isLetter() }
|
||||
}
|
||||
|
||||
fun copyAndParseBootimg(uri: Uri) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
if (running) return@launch
|
||||
running = true
|
||||
try {
|
||||
uri.inputStream().buffered().use { src ->
|
||||
srcBoot.also {
|
||||
src.copyAndCloseOut(it.newOutputStream())
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "copy boot image error: $e")
|
||||
}
|
||||
parseBootimg(srcBoot.path)
|
||||
running = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractAndParseBootimg(mode: PatchMode) {
|
||||
var cmdBuilder = "./boot_extract.sh"
|
||||
|
||||
if (mode == PatchMode.INSTALL_TO_NEXT_SLOT) {
|
||||
cmdBuilder += " true"
|
||||
}
|
||||
|
||||
val result = shellForResult(
|
||||
shell,
|
||||
"export ASH_STANDALONE=1",
|
||||
"cd $patchDir",
|
||||
"./busybox sh $cmdBuilder",
|
||||
)
|
||||
|
||||
if (result.isSuccess) {
|
||||
bootSlot = if (!result.out.toString().contains("SLOT=")) {
|
||||
""
|
||||
} else {
|
||||
result.out.filter { it.startsWith("SLOT=") }[0].removePrefix("SLOT=")
|
||||
}
|
||||
bootDev =
|
||||
result.out.filter { it.startsWith("BOOTIMAGE=") }[0].removePrefix("BOOTIMAGE=")
|
||||
Log.i(TAG, "current slot: $bootSlot")
|
||||
Log.i(TAG, "current bootimg: $bootDev")
|
||||
srcBoot = FileSystemManager.getLocal().getFile(bootDev)
|
||||
parseBootimg(bootDev)
|
||||
} else {
|
||||
error = result.err.joinToString("\n")
|
||||
}
|
||||
running = false
|
||||
}
|
||||
|
||||
fun prepare(mode: PatchMode) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
if (prepared) return@launch
|
||||
prepared = true
|
||||
|
||||
running = true
|
||||
prepare()
|
||||
if (mode != PatchMode.UNPATCH) {
|
||||
parseKpimg()
|
||||
}
|
||||
if (mode == PatchMode.PATCH_AND_INSTALL || mode == PatchMode.UNPATCH || mode == PatchMode.INSTALL_TO_NEXT_SLOT) {
|
||||
extractAndParseBootimg(mode)
|
||||
}
|
||||
running = false
|
||||
}
|
||||
}
|
||||
|
||||
fun embedKPM(uri: Uri) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
if (running) return@launch
|
||||
running = true
|
||||
error = ""
|
||||
|
||||
val rand = (1..4).map { ('a'..'z').random() }.joinToString("")
|
||||
val kpmFileName = "${rand}.kpm"
|
||||
val kpmFile: ExtendedFile = patchDir.getChildFile(kpmFileName)
|
||||
|
||||
Log.i(TAG, "copy kpm to: " + kpmFile.path)
|
||||
try {
|
||||
uri.inputStream().buffered().use { src ->
|
||||
kpmFile.also {
|
||||
src.copyAndCloseOut(it.newOutputStream())
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Copy kpm error: $e")
|
||||
}
|
||||
|
||||
val result = shellForResult(
|
||||
shell, "cd $patchDir", "./kptools -l -M ${kpmFile.path}"
|
||||
)
|
||||
|
||||
if (result.isSuccess) {
|
||||
val ini = Ini(StringReader(result.out.joinToString("\n")))
|
||||
val kpm = ini["kpm"]
|
||||
if (kpm != null) {
|
||||
val kpmInfo = KPModel.KPMInfo(
|
||||
KPModel.ExtraType.KPM,
|
||||
kpm["name"].toString(),
|
||||
KPModel.TriggerEvent.PRE_KERNEL_INIT.event,
|
||||
"",
|
||||
kpm["version"].toString(),
|
||||
kpm["license"].toString(),
|
||||
kpm["author"].toString(),
|
||||
kpm["description"].toString(),
|
||||
)
|
||||
newExtras.add(kpmInfo)
|
||||
newExtrasFileName.add(kpmFileName)
|
||||
}
|
||||
} else {
|
||||
error = "Invalid KPM\n"
|
||||
}
|
||||
running = false
|
||||
}
|
||||
}
|
||||
|
||||
fun doUnpatch() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
patching = true
|
||||
patchLog = ""
|
||||
Log.i(TAG, "starting unpatching...")
|
||||
|
||||
val logs = object : CallbackList<String>() {
|
||||
override fun onAddElement(e: String?) {
|
||||
patchLog += e
|
||||
Log.i(TAG, "" + e)
|
||||
patchLog += "\n"
|
||||
}
|
||||
}
|
||||
|
||||
val result = shell.newJob().add(
|
||||
"export ASH_STANDALONE=1",
|
||||
"cd $patchDir",
|
||||
"cp /data/adb/ap/ori.img new-boot.img",
|
||||
"./busybox sh ./boot_unpatch.sh $bootDev",
|
||||
"rm -f ${APApplication.APD_PATH}",
|
||||
"rm -rf ${APApplication.APATCH_FOLDER}",
|
||||
).to(logs, logs).exec()
|
||||
|
||||
if (result.isSuccess) {
|
||||
logs.add(" Unpatch successful")
|
||||
needReboot = true
|
||||
APApplication.markNeedReboot()
|
||||
} else {
|
||||
logs.add(" Unpatched failed")
|
||||
error = result.err.joinToString("\n")
|
||||
}
|
||||
logs.add("****************************")
|
||||
|
||||
patchdone = true
|
||||
patching = false
|
||||
}
|
||||
}
|
||||
fun isSuExecutable(): Boolean {
|
||||
val suFile = File("/system/bin/su")
|
||||
return suFile.exists() && suFile.canExecute()
|
||||
}
|
||||
fun doPatch(mode: PatchMode) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
patching = true
|
||||
Log.d(TAG, "starting patching...")
|
||||
|
||||
val apVer = Version.getManagerVersion().second
|
||||
val rand = (1..4).map { ('a'..'z').random() }.joinToString("")
|
||||
val outFilename = "apatch_patched_${apVer}_${BuildConfig.buildKPV}_${rand}.img"
|
||||
|
||||
val logs = object : CallbackList<String>() {
|
||||
override fun onAddElement(e: String?) {
|
||||
patchLog += e
|
||||
Log.d(TAG, "" + e)
|
||||
patchLog += "\n"
|
||||
}
|
||||
}
|
||||
logs.add("****************************")
|
||||
|
||||
var patchCommand = mutableListOf("./busybox sh boot_patch.sh \"$0\" \"$@\"")
|
||||
|
||||
// adapt for 0.10.7 and lower KP
|
||||
var isKpOld = false
|
||||
|
||||
if (mode == PatchMode.PATCH_AND_INSTALL || mode == PatchMode.INSTALL_TO_NEXT_SLOT) {
|
||||
|
||||
val KPCheck = shell.newJob().add("truncate $superkey -Z u:r:magisk:s0 -c whoami").exec()
|
||||
|
||||
if (KPCheck.isSuccess && !isSuExecutable()) {
|
||||
patchCommand.addAll(0, listOf("truncate", APApplication.superKey, "-Z", APApplication.MAGISK_SCONTEXT, "-c"))
|
||||
patchCommand.addAll(listOf(superkey, srcBoot.path, "true"))
|
||||
} else {
|
||||
patchCommand = mutableListOf("./busybox", "sh", "boot_patch.sh")
|
||||
patchCommand.addAll(listOf(superkey, srcBoot.path, "true"))
|
||||
isKpOld = true
|
||||
}
|
||||
|
||||
} else {
|
||||
patchCommand.addAll(0, listOf("sh", "-c"))
|
||||
patchCommand.addAll(listOf(superkey, srcBoot.path))
|
||||
}
|
||||
|
||||
for (i in 0..<newExtrasFileName.size) {
|
||||
patchCommand.addAll(listOf("-M", newExtrasFileName[i]))
|
||||
val extra = newExtras[i]
|
||||
if (extra.args.isNotEmpty()) {
|
||||
patchCommand.addAll(listOf("-A", extra.args))
|
||||
}
|
||||
if (extra.event.isNotEmpty()) {
|
||||
patchCommand.addAll(listOf("-V", extra.event))
|
||||
}
|
||||
patchCommand.addAll(listOf("-T", extra.type.desc))
|
||||
}
|
||||
for (i in 0..<existedExtras.size) {
|
||||
val extra = existedExtras[i]
|
||||
patchCommand.addAll(listOf("-E", extra.name))
|
||||
if (extra.args.isNotEmpty()) {
|
||||
patchCommand.addAll(listOf("-A", extra.args))
|
||||
}
|
||||
if (extra.event.isNotEmpty()) {
|
||||
patchCommand.addAll(listOf("-V", extra.event))
|
||||
}
|
||||
patchCommand.addAll(listOf("-T", extra.type.desc))
|
||||
}
|
||||
|
||||
val builder = ProcessBuilder(patchCommand)
|
||||
|
||||
Log.i(TAG, "patchCommand: $patchCommand")
|
||||
|
||||
var succ = false
|
||||
|
||||
if (isKpOld) {
|
||||
val resultString = "\"" + patchCommand.joinToString(separator = "\" \"") + "\""
|
||||
val result = shell.newJob().add(
|
||||
"export ASH_STANDALONE=1",
|
||||
"cd $patchDir",
|
||||
resultString,
|
||||
).to(logs, logs).exec()
|
||||
succ = result.isSuccess
|
||||
} else {
|
||||
builder.environment().put("ASH_STANDALONE", "1")
|
||||
builder.directory(patchDir)
|
||||
builder.redirectErrorStream(true)
|
||||
|
||||
val process = builder.start()
|
||||
|
||||
Thread {
|
||||
BufferedReader(InputStreamReader(process.inputStream)).use { reader ->
|
||||
var line: String?
|
||||
while (reader.readLine().also { line = it } != null) {
|
||||
patchLog += line
|
||||
Log.i(TAG, "" + line)
|
||||
patchLog += "\n"
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
succ = process.waitFor() == 0
|
||||
}
|
||||
|
||||
if (!succ) {
|
||||
val msg = " Patch failed."
|
||||
error = msg
|
||||
// error += result.err.joinToString("\n")
|
||||
logs.add(error)
|
||||
logs.add("****************************")
|
||||
patching = false
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (mode == PatchMode.PATCH_AND_INSTALL) {
|
||||
logs.add("- Reboot to finish the installation...")
|
||||
needReboot = true
|
||||
APApplication.markNeedReboot()
|
||||
} else if (mode == PatchMode.INSTALL_TO_NEXT_SLOT) {
|
||||
logs.add("- Connecting boot hal...")
|
||||
val bootctlStatus = shell.newJob().add(
|
||||
"cd $patchDir", "chmod 0777 $patchDir/bootctl", "./bootctl hal-info"
|
||||
).to(logs, logs).exec()
|
||||
if (!bootctlStatus.isSuccess) {
|
||||
logs.add("[X] Failed to connect to boot hal, you may need switch slot manually")
|
||||
} else {
|
||||
val currSlot = shellForResult(
|
||||
shell, "cd $patchDir", "./bootctl get-current-slot"
|
||||
).out.toString()
|
||||
val targetSlot = if (currSlot.contains("0")) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
logs.add("- Switching to next slot: $targetSlot...")
|
||||
val setNextActiveSlot = shell.newJob().add(
|
||||
"cd $patchDir", "./bootctl set-active-boot-slot $targetSlot"
|
||||
).exec()
|
||||
if (setNextActiveSlot.isSuccess) {
|
||||
logs.add("- Switch done")
|
||||
logs.add("- Writing boot marker script...")
|
||||
val markBootableScript = shell.newJob().add(
|
||||
"mkdir -p /data/adb/post-fs-data.d && rm -rf /data/adb/post-fs-data.d/post_ota.sh && touch /data/adb/post-fs-data.d/post_ota.sh",
|
||||
"echo \"chmod 0777 $patchDir/bootctl\" > /data/adb/post-fs-data.d/post_ota.sh",
|
||||
"echo \"chown root:root 0777 $patchDir/bootctl\" > /data/adb/post-fs-data.d/post_ota.sh",
|
||||
"echo \"$patchDir/bootctl mark-boot-successful\" > /data/adb/post-fs-data.d/post_ota.sh",
|
||||
"echo >> /data/adb/post-fs-data.d/post_ota.sh",
|
||||
"echo \"rm -rf $patchDir\" >> /data/adb/post-fs-data.d/post_ota.sh",
|
||||
"echo >> /data/adb/post-fs-data.d/post_ota.sh",
|
||||
"echo \"rm -f /data/adb/post-fs-data.d/post_ota.sh\" >> /data/adb/post-fs-data.d/post_ota.sh",
|
||||
"chmod 0777 /data/adb/post-fs-data.d/post_ota.sh",
|
||||
"chown root:root /data/adb/post-fs-data.d/post_ota.sh",
|
||||
).to(logs, logs).exec()
|
||||
if (markBootableScript.isSuccess) {
|
||||
logs.add("- Boot marker script write done")
|
||||
} else {
|
||||
logs.add("[X] Boot marker scripts write failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
logs.add("- Reboot to finish the installation...")
|
||||
needReboot = true
|
||||
APApplication.markNeedReboot()
|
||||
} else if (mode == PatchMode.PATCH_ONLY) {
|
||||
val newBootFile = patchDir.getChildFile("new-boot.img")
|
||||
val outDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||
if (!outDir.exists()) outDir.mkdirs()
|
||||
val outPath = File(outDir, outFilename)
|
||||
val inputUri = newBootFile.getUri(apApp)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val outUri = createDownloadUri(apApp, outFilename)
|
||||
succ = insertDownload(apApp, outUri, inputUri)
|
||||
} else {
|
||||
newBootFile.inputStream().copyAndClose(outPath.outputStream())
|
||||
}
|
||||
if (succ) {
|
||||
logs.add(" Output file is written to ")
|
||||
logs.add(" ${outPath.path}")
|
||||
} else {
|
||||
logs.add(" Write patched boot.img failed")
|
||||
}
|
||||
}
|
||||
logs.add("****************************")
|
||||
patchdone = true
|
||||
patching = false
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
fun createDownloadUri(context: Context, outFilename: String): Uri? {
|
||||
val contentValues = ContentValues().apply {
|
||||
put(MediaStore.Downloads.DISPLAY_NAME, outFilename)
|
||||
put(MediaStore.Downloads.MIME_TYPE, "application/octet-stream")
|
||||
put(MediaStore.Downloads.IS_PENDING, 1)
|
||||
}
|
||||
|
||||
val resolver = context.contentResolver
|
||||
return resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
fun insertDownload(context: Context, outUri: Uri?, inputUri: Uri): Boolean {
|
||||
if (outUri == null) return false
|
||||
|
||||
try {
|
||||
val resolver = context.contentResolver
|
||||
resolver.openInputStream(inputUri)?.use { inputStream ->
|
||||
resolver.openOutputStream(outUri)?.use { outputStream ->
|
||||
inputStream.copyTo(outputStream)
|
||||
}
|
||||
}
|
||||
val contentValues = ContentValues().apply {
|
||||
put(MediaStore.Downloads.IS_PENDING, 0)
|
||||
}
|
||||
resolver.update(outUri, contentValues, null, null)
|
||||
|
||||
return true
|
||||
} catch (_: FileNotFoundException) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
fun File.getUri(context: Context): Uri {
|
||||
val authority = "${context.packageName}.fileprovider"
|
||||
return FileProvider.getUriForFile(context, authority, this)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
package me.bmax.apatch.ui.viewmodel
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageInfo
|
||||
import android.os.IBinder
|
||||
import android.os.Parcelable
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import me.bmax.apatch.IAPRootService
|
||||
import me.bmax.apatch.Natives
|
||||
import me.bmax.apatch.apApp
|
||||
import me.bmax.apatch.services.RootServices
|
||||
import me.bmax.apatch.util.APatchCli
|
||||
import me.bmax.apatch.util.HanziToPinyin
|
||||
import me.bmax.apatch.util.PkgConfig
|
||||
import java.text.Collator
|
||||
import java.util.Locale
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
|
||||
class SuperUserViewModel : ViewModel() {
|
||||
companion object {
|
||||
private const val TAG = "SuperUserViewModel"
|
||||
private var apps by mutableStateOf<List<AppInfo>>(emptyList())
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class AppInfo(
|
||||
val label: String, val packageInfo: PackageInfo, val config: PkgConfig.Config
|
||||
) : Parcelable {
|
||||
val packageName: String
|
||||
get() = packageInfo.packageName
|
||||
val uid: Int
|
||||
get() = packageInfo.applicationInfo!!.uid
|
||||
}
|
||||
|
||||
var search by mutableStateOf("")
|
||||
var showSystemApps by mutableStateOf(false)
|
||||
var isRefreshing by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
private val sortedList by derivedStateOf {
|
||||
val comparator = compareBy<AppInfo> {
|
||||
when {
|
||||
it.config.allow != 0 -> 0
|
||||
it.config.exclude == 1 -> 1
|
||||
else -> 2
|
||||
}
|
||||
}.then(compareBy(Collator.getInstance(Locale.getDefault()), AppInfo::label))
|
||||
apps.sortedWith(comparator).also {
|
||||
isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
val appList by derivedStateOf {
|
||||
sortedList.filter {
|
||||
it.label.lowercase().contains(search.lowercase()) || it.packageName.lowercase()
|
||||
.contains(search.lowercase()) || HanziToPinyin.getInstance()
|
||||
.toPinyinString(it.label).contains(search.lowercase())
|
||||
}.filter {
|
||||
it.uid == 2000 // Always show shell
|
||||
|| showSystemApps || it.packageInfo.applicationInfo!!.flags.and(ApplicationInfo.FLAG_SYSTEM) == 0
|
||||
}
|
||||
}
|
||||
|
||||
private suspend inline fun connectRootService(
|
||||
crossinline onDisconnect: () -> Unit = {}
|
||||
): Pair<IBinder, ServiceConnection> = suspendCoroutine {
|
||||
val connection = object : ServiceConnection {
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
onDisconnect()
|
||||
}
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
|
||||
it.resume(binder as IBinder to this)
|
||||
}
|
||||
}
|
||||
val intent = Intent(apApp, RootServices::class.java)
|
||||
val task = RootServices.bindOrTask(
|
||||
intent,
|
||||
Shell.EXECUTOR,
|
||||
connection,
|
||||
)
|
||||
val shell = APatchCli.SHELL
|
||||
task?.let { it1 -> shell.execTask(it1) }
|
||||
}
|
||||
|
||||
private fun stopRootService() {
|
||||
val intent = Intent(apApp, RootServices::class.java)
|
||||
RootServices.stop(intent)
|
||||
}
|
||||
|
||||
suspend fun fetchAppList() {
|
||||
isRefreshing = true
|
||||
|
||||
val result = connectRootService {
|
||||
Log.w(TAG, "RootService disconnected")
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val binder = result.first
|
||||
val allPackages = IAPRootService.Stub.asInterface(binder).getPackages(0)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
stopRootService()
|
||||
}
|
||||
val uids = Natives.suUids().toList()
|
||||
Log.d(TAG, "all allows: $uids")
|
||||
|
||||
var configs: HashMap<Int, PkgConfig.Config> = HashMap()
|
||||
thread {
|
||||
Natives.su()
|
||||
configs = PkgConfig.readConfigs()
|
||||
}.join()
|
||||
|
||||
Log.d(TAG, "all configs: $configs")
|
||||
|
||||
apps = allPackages.list.map {
|
||||
val appInfo = it.applicationInfo
|
||||
val uid = appInfo!!.uid
|
||||
val actProfile = if (uids.contains(uid)) Natives.suProfile(uid) else null
|
||||
val config = configs.getOrDefault(
|
||||
uid, PkgConfig.Config(appInfo.packageName, Natives.isUidExcluded(uid), 0, Natives.Profile(uid = uid))
|
||||
)
|
||||
config.allow = 0
|
||||
|
||||
// from kernel
|
||||
if (actProfile != null) {
|
||||
config.allow = 1
|
||||
config.profile = actProfile
|
||||
}
|
||||
AppInfo(
|
||||
label = appInfo.loadLabel(apApp.packageManager).toString(),
|
||||
packageInfo = it,
|
||||
config = config
|
||||
)
|
||||
}.filter { it.packageName != apApp.packageName }
|
||||
}
|
||||
}
|
||||
}
|
||||
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