Repo cloned

This commit is contained in:
Fr4nz D13trich 2026-01-04 20:10:16 +01:00
commit 11ea8025b0
214 changed files with 33943 additions and 0 deletions

674
LICENSE Normal file
View file

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <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
View 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>
[![Latest Release](https://img.shields.io/github/v/release/bmax121/APatch?label=Release&logo=github)](https://github.com/bmax121/APatch/releases/latest)
[![Nightly Release](https://img.shields.io/badge/Nightly%20release-gray?logo=hackthebox&logoColor=fff)](https://nightly.link/bmax121/APatch/workflows/build/main/APatch)
[![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/APatch)
[![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/APatchGroup)
[![GitHub License](https://img.shields.io/github/license/bmax121/APatch?logo=gnu)](/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">
[![Translation Status](https://hosted.weblate.org/widget/APatch/open-graph.png)](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
View file

@ -0,0 +1,2 @@
/target
.cargo/

1766
apd/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

61
apd/Cargo.toml Normal file
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,5 @@
_ ____ _ _
/ \ | _ \ __ _| |_ ___| |__
/ _ \ | |_) / _` | __/ __| '_ \
/ ___ \| __/ (_| | || (__| | | |
/_/ \_\_| \__,_|\__\___|_| |_|

162
apd/src/cli.rs Normal file
View 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
View 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
View 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
View 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
View 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
View 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(&current.name);
let work_dir_path = work_dir_path.as_ref().join(&current.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) = &current.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) = &current.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) = &current.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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,2 @@
/build
/release/

294
app/build.gradle.kts Normal file
View 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
View file

@ -0,0 +1,4 @@
libkptools.so
libapjni.so
libkpatch.so
libapd.so

BIN
app/libs/arm64-v8a/libbootctl.so Executable file

Binary file not shown.

BIN
app/libs/arm64-v8a/libbusybox.so Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

26
app/proguard-rules.pro vendored Normal file
View 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

View 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>

View 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
View file

@ -0,0 +1,2 @@
kpimg
*.kpm

View 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

View 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

View 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

View 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

View 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

View 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
}

View 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
View 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;
}

View 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

File diff suppressed because it is too large Load diff

View 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

View 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

View 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
View file

@ -0,0 +1,3 @@
#define MAJOR 0
#define MINOR 12
#define PATCH 0

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View 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)
}
}

View 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)
}
}

View 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);
}
}
}

View 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")
}
}

View 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
)
}
}
}
}

View 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)
}
}

View 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())
}
)
}

View file

@ -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
)

View file

@ -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()
}
}

View file

@ -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)
)
}

View 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 = "" }
)
}

View file

@ -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)
},
)
}

View 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)
}
}
}
}

View 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) }
},
)
}

View file

@ -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
)
}

View 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("")) { // 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"
)
}
}
)
}

View 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
)
}
}
}
}

View 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("")) { // clear command
text = tempText.substring(6)
} else {
text += tempText
}
logContent.append(it).append("\n")
}, onStderr = {
tempText = "$it\n"
if (tempText.startsWith("")) { // 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)
}

View 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) }
},
)
}

View 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) })
}
}
}
}
}

View 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)) })
}

View 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)
}
}
}

View 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,
)
)
}
}

View 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,
)

View 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,
)

View 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,
)

View 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,
)

View 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,
)

View 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,
)

View 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,
)

View 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,
)

View 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,
)

View 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,
)

View 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,
)

View 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,
)

View 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,
)

View 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,
)

View 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,
)

View 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,
)

View 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,
)

View 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,
)

View 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
)
}

View 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
)
*/
)

View 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,
)

View file

@ -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)
}
}

View 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
}

View file

@ -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")
}
}
}

View file

@ -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)
}
}

View file

@ -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