mirror of
https://github.com/BobTheBlinker/android_kernel_motorola_sm6375.git
synced 2026-10-05 19:31:57 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e050241d1 | ||
|
|
6ce8ac07b0 | ||
|
|
8634caf0d9 | ||
|
|
607c935a70 |
335 changed files with 40865 additions and 0 deletions
|
|
@ -0,0 +1 @@
|
|||
TARGET_KERNEL_ADDITIONAL_FLAGS += LOCALVERSION=
|
||||
1
KernelSU/.gitattributes
vendored
Normal file
1
KernelSU/.gitattributes
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
*.bat eol=crlf
|
||||
2
KernelSU/.gitignore
vendored
Normal file
2
KernelSU/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
.idea
|
||||
.vscode
|
||||
674
KernelSU/LICENSE
Normal file
674
KernelSU/LICENSE
Normal file
|
|
@ -0,0 +1,674 @@
|
|||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
7
KernelSU/SECURITY.md
Normal file
7
KernelSU/SECURITY.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Reporting Security Issues
|
||||
|
||||
The KernelSU team and community take security bugs in KernelSU seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.
|
||||
|
||||
To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/tiann/KernelSU/security/advisories/new) tab, or you can mailto [weishu](mailto:twsxtd@gmail.com) directly.
|
||||
|
||||
The KernelSU team will send a response indicating the next steps in handling your report. After the initial reply to your report, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance.
|
||||
57
KernelSU/docs/README.md
Normal file
57
KernelSU/docs/README.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
**English** | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md)
|
||||
|
||||
# KernelSU
|
||||
|
||||
<img src="https://kernelsu.org/logo.png" style="width: 96px;" alt="logo">
|
||||
|
||||
A Kernel-based root solution for Android devices.
|
||||
|
||||
[](https://github.com/tiann/KernelSU/releases/latest)
|
||||
[](https://hosted.weblate.org/engage/kernelsu)
|
||||
[](https://t.me/KernelSU)
|
||||
[](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)
|
||||
[](/LICENSE)
|
||||
|
||||
## Features
|
||||
|
||||
1. Kernel-based `su` and root access management.
|
||||
2. Module system based on [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS).
|
||||
3. [App Profile](https://kernelsu.org/guide/app-profile.html): Lock up the root power in a cage.
|
||||
|
||||
## Compatibility State
|
||||
|
||||
KernelSU officially supports Android GKI 2.0 devices (kernel 5.10+). Older kernels (4.14+) are also compatible, but the kernel will have to be built manually.
|
||||
|
||||
With this, WSA, ChromeOS, and container-based Android are all supported.
|
||||
|
||||
Currently, only `arm64-v8a` and `x86_64` are supported.
|
||||
|
||||
## Usage
|
||||
|
||||
- [Installation Instruction](https://kernelsu.org/guide/installation.html)
|
||||
- [How to build?](https://kernelsu.org/guide/how-to-build.html)
|
||||
- [Official Website](https://kernelsu.org/)
|
||||
|
||||
## Translation
|
||||
|
||||
To help translate KernelSU or improve existing translations, please use [Weblate](https://hosted.weblate.org/engage/kernelsu/). PR of Manager's translation is no longer accepted, because it will conflict with Weblate.
|
||||
|
||||
## Discussion
|
||||
|
||||
- Telegram: [@KernelSU](https://t.me/KernelSU)
|
||||
|
||||
## Security
|
||||
|
||||
For information on reporting security vulnerabilities in KernelSU, see [SECURITY.md](/SECURITY.md).
|
||||
|
||||
## License
|
||||
|
||||
- Files under the `kernel` directory are [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html).
|
||||
- All other parts except the `kernel` directory are [GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html).
|
||||
|
||||
## Credits
|
||||
|
||||
- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): the KernelSU idea.
|
||||
- [Magisk](https://github.com/topjohnwu/Magisk): the powerful root tool.
|
||||
- [genuine](https://github.com/brevent/genuine/): apk v2 signature validation.
|
||||
- [Diamorphine](https://github.com/m0nad/Diamorphine): some rootkit skills.
|
||||
57
KernelSU/docs/README_CN.md
Normal file
57
KernelSU/docs/README_CN.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
[English](README.md) | [Español](README_ES.md) | **简体中文** | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md)
|
||||
|
||||
# KernelSU
|
||||
|
||||
<img src="https://kernelsu.org/logo.png" style="width: 96px;" alt="logo">
|
||||
|
||||
一个 Android 上基于内核的 root 方案。
|
||||
|
||||
[](https://github.com/tiann/KernelSU/releases/latest)
|
||||
[](https://hosted.weblate.org/engage/kernelsu)
|
||||
[](https://t.me/KernelSU)
|
||||
[](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)
|
||||
[](/LICENSE)
|
||||
|
||||
## 特性
|
||||
|
||||
- 基于内核的 `su` 和权限管理。
|
||||
- 基于 [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS) 的模块系统。
|
||||
- [App Profile](https://kernelsu.org/zh_CN/guide/app-profile.html): 把 Root 权限关进笼子里。
|
||||
|
||||
## 兼容状态
|
||||
|
||||
KernelSU 官方支持 GKI 2.0 的设备(内核版本5.10以上);旧内核也是兼容的(最低4.14+),不过需要自己编译内核。
|
||||
|
||||
WSA, ChromeOS 和运行在容器上的 Android 也可以与 KernelSU 一起工作。
|
||||
|
||||
目前支持架构 : `arm64-v8a` 和 `x86_64`。
|
||||
|
||||
## 使用方法
|
||||
|
||||
- [安装教程](https://kernelsu.org/zh_CN/guide/installation.html)
|
||||
- [如何构建?](https://kernelsu.org/zh_CN/guide/how-to-build.html)
|
||||
- [官方网站](https://kernelsu.org/zh_CN/)
|
||||
|
||||
## 参与翻译
|
||||
|
||||
要将 KernelSU 翻译成您的语言,或完善现有的翻译,请使用 [Weblate](https://hosted.weblate.org/engage/kernelsu/)。现已不再接受有关管理器翻译的PR,因为这会与Weblate冲突。
|
||||
|
||||
## 讨论
|
||||
|
||||
- Telegram: [@KernelSU](https://t.me/KernelSU)
|
||||
|
||||
## 安全性
|
||||
|
||||
有关报告 KernelSU 安全漏洞的信息,请参阅 [SECURITY.md](/SECURITY.md)。
|
||||
|
||||
## 许可证
|
||||
|
||||
- 目录 `kernel` 下所有文件为 [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)。
|
||||
- 除 `kernel` 目录的其他部分均为 [GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html)。
|
||||
|
||||
## 鸣谢
|
||||
|
||||
- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/):KernelSU 的灵感。
|
||||
- [Magisk](https://github.com/topjohnwu/Magisk):强大的 root 工具箱。
|
||||
- [genuine](https://github.com/brevent/genuine/):apk v2 签名验证。
|
||||
- [Diamorphine](https://github.com/m0nad/Diamorphine):一些 rootkit 技巧。
|
||||
56
KernelSU/docs/README_ES.md
Normal file
56
KernelSU/docs/README_ES.md
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
[English](README.md) | **Español** | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md)
|
||||
|
||||
# KernelSU
|
||||
|
||||
<img src="https://kernelsu.org/logo.png" style="width: 96px;" alt="logo">
|
||||
|
||||
Una solución root basada en el kernel para dispositivos Android.
|
||||
|
||||
[](https://github.com/tiann/KernelSU/releases/latest)
|
||||
[](https://hosted.weblate.org/engage/kernelsu)
|
||||
[](https://t.me/KernelSU)
|
||||
[](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)
|
||||
[](/LICENSE)
|
||||
|
||||
## Características
|
||||
|
||||
1. Binario `su` basado en el kernel y gestión de acceso root.
|
||||
2. Sistema de módulos basado en [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS).
|
||||
|
||||
## Estado de compatibilidad
|
||||
|
||||
**KernelSU** soporta de forma oficial dispositivos Android con **GKI 2.0** (a partir de la versión **5.10** del kernel). Los kernels antiguos (a partir de la versión **4.14**) también son compatibles, pero necesitas compilarlos por tu cuenta.
|
||||
|
||||
Con esto, WSA, ChromeOS y Android basado en contenedores están todos compatibles.
|
||||
|
||||
Actualmente, solo se admiten las arquitecturas `arm64-v8a` y `x86_64`.
|
||||
|
||||
## Uso
|
||||
|
||||
- [¿Cómo instalarlo?](https://kernelsu.org/guide/installation.html)
|
||||
- [¿Cómo compilarlo?](https://kernelsu.org/guide/how-to-build.html)
|
||||
- [Site oficial](https://kernelsu.org/)
|
||||
|
||||
## Traducción
|
||||
|
||||
Para ayudar a traducir KernelSU o mejorar las traducciones existentes, utilice [Weblate](https://hosted.weblate.org/engage/kernelsu/). Ya no se aceptan PR de la traducción de Manager porque entrará en conflicto con Weblate.
|
||||
|
||||
## Discusión
|
||||
|
||||
- Telegram: [@KernelSU](https://t.me/KernelSU)
|
||||
|
||||
## Seguridad
|
||||
|
||||
Para obtener información sobre cómo informar vulnerabilidades de seguridad en KernelSU, consulte [SECURITY.md](/SECURITY.md).
|
||||
|
||||
## Licencia
|
||||
|
||||
- Los archivos bajo el directorio `kernel` están licenciados bajo [GPL-2-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html).
|
||||
- Todas las demás partes, a excepción del directorio `kernel`, están licenciados bajo [GPL-3-or-later](https://www.gnu.org/licenses/gpl-3.0.html).
|
||||
|
||||
## Créditos
|
||||
|
||||
- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): la idea de KernelSU.
|
||||
- [Magisk](https://github.com/topjohnwu/Magisk): la poderosa herramienta root.
|
||||
- [genuine](https://github.com/brevent/genuine/): validación de firma apk v2.
|
||||
- [Diamorphine](https://github.com/m0nad/Diamorphine): algunas habilidades de rootkit.
|
||||
53
KernelSU/docs/README_ID.md
Normal file
53
KernelSU/docs/README_ID.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | **Indonesia** | [עברית](README_IW.md) | [हिंदी](README_IN.md)
|
||||
|
||||
# KernelSU
|
||||
|
||||
<img src="https://kernelsu.org/logo.png" style="width: 96px;" alt="logo">
|
||||
|
||||
Solusi root berbasis Kernel untuk perangkat Android.
|
||||
|
||||
[](https://github.com/tiann/KernelSU/releases/latest)
|
||||
[](https://hosted.weblate.org/engage/kernelsu)
|
||||
[](https://t.me/KernelSU)
|
||||
[](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)
|
||||
[](/LICENSE)
|
||||
|
||||
## Fitur
|
||||
|
||||
1. Manajemen akses root dan `su` berbasis kernel.
|
||||
2. Sistem modul berdasarkan [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS).
|
||||
3. [Profil Aplikasi](https://kernelsu.org/guide/app-profile.html): Kunci daya root di dalam sangkar.
|
||||
|
||||
## Status Kompatibilitas
|
||||
|
||||
KernelSU secara resmi mendukung perangkat Android GKI 2.0 (dengan kernel 5.10+), kernel lama (4.14+) juga kompatibel, tetapi Anda perlu membuat kernel sendiri.
|
||||
|
||||
WSA, ChromeOS, dan Android berbasis wadah juga dapat bekerja dengan KernelSU terintegrasi.
|
||||
|
||||
Dan ABI yang didukung saat ini adalah: `arm64-v8a` dan `x86_64`
|
||||
|
||||
## Penggunaan
|
||||
|
||||
- [Petunjuk Instalasi](https://kernelsu.org/id_ID/guide/installation.html)
|
||||
- [Bagaimana cara membuat?](https://kernelsu.org/id_ID/guide/how-to-build.html)
|
||||
- [Situs Web Resmi](https://kernelsu.org/id_ID/)
|
||||
|
||||
## Terjemahan
|
||||
|
||||
Untuk menerjemahkan KernelSU ke dalam bahasa Anda atau menyempurnakan terjemahan yang sudah ada, harap gunakan [Weblat](https://hosted.weblate.org/engage/kernelsu/).
|
||||
|
||||
## Diskusi
|
||||
|
||||
- Telegram: [@KernelSU](https://t.me/KernelSU)
|
||||
|
||||
## Lisensi
|
||||
|
||||
- File di bawah direktori `kernel` adalah [GPL-2-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html).
|
||||
- Semua bagian lain kecuali direktori `kernel` adalah [GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html).
|
||||
|
||||
## Kredit
|
||||
|
||||
- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): ide KernelSU.
|
||||
- [Magisk](https://github.com/topjohnwu/Magisk): alat root yang ampuh.
|
||||
- [genuine](https://github.com/brevent/genuine/): validasi tanda tangan apk v2.
|
||||
- [Diamorphine](https://github.com/m0nad/Diamorphine): beberapa keterampilan rootkit.
|
||||
53
KernelSU/docs/README_IN.md
Normal file
53
KernelSU/docs/README_IN.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | **हिंदी**
|
||||
|
||||
# KernelSU
|
||||
|
||||
<img src="https://kernelsu.org/logo.png" style="width: 96px;" alt="logo">
|
||||
|
||||
Android उपकरणों के लिए कर्नेल-आधारित रूट समाधान।
|
||||
|
||||
[](https://github.com/tiann/KernelSU/releases/latest)
|
||||
[](https://hosted.weblate.org/engage/kernelsu)
|
||||
[](https://t.me/KernelSU)
|
||||
[](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)
|
||||
[](/LICENSE)
|
||||
|
||||
## विशेषताएँ
|
||||
|
||||
1. कर्नेल-आधारित `su` और रूट एक्सेस प्रबंधन।
|
||||
2. [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS) पर आधारित मॉड्यूल प्रणाली।
|
||||
3. [App Profile](https://kernelsu.org/guide/app-profile.html): Root शक्ति को पिंजरे में बंद कर दो।
|
||||
|
||||
## अनुकूलता अवस्था
|
||||
|
||||
KernelSU आधिकारिक तौर पर Android GKI 2.0 डिवाइस (कर्नेल 5.10+) का समर्थन करता है। पुराने कर्नेल (4.14+) भी संगत हैं, लेकिन कर्नेल को मैन्युअल रूप से बनाना होगा।
|
||||
|
||||
इसके साथ, WSA, ChromeOS और कंटेनर-आधारित Android सभी समर्थित हैं।
|
||||
|
||||
वर्तमान में, केवल `arm64-v8a` और `x86_64` समर्थित हैं।
|
||||
|
||||
## प्रयोग
|
||||
|
||||
- [स्थापना निर्देश](https://kernelsu.org/guide/installation.html)
|
||||
- [कैसे बनाना है ?](https://kernelsu.org/guide/how-to-build.html)
|
||||
- [आधिकारिक वेबसाइट](https://kernelsu.org/)
|
||||
|
||||
## अनुवाद करना
|
||||
|
||||
KernelSU का अनुवाद करने या मौजूदा अनुवादों को बेहतर बनाने में सहायता के लिए, कृपया इसका उपयोग करें [Weblate](https://hosted.weblate.org/engage/kernelsu/).
|
||||
|
||||
## बहस
|
||||
|
||||
- Telegram: [@KernelSU](https://t.me/KernelSU)
|
||||
|
||||
## लाइसेंस
|
||||
|
||||
- `Kernel` निर्देशिका के अंतर्गत फ़ाइलें हैं [GPL-2-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)
|
||||
- `Kernel` निर्देशिका को छोड़कर अन्य सभी भाग हैं [GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html)
|
||||
|
||||
## आभार सूची
|
||||
|
||||
- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): KernelSU विचार।
|
||||
- [Magisk](https://github.com/topjohnwu/Magisk): शक्तिशाली root उपकरण।
|
||||
- [genuine](https://github.com/brevent/genuine/): apk v2 हस्ताक्षर सत्यापन।
|
||||
- [Diamorphine](https://github.com/m0nad/Diamorphine): कुछ रूटकिट कौशल।
|
||||
53
KernelSU/docs/README_IW.md
Normal file
53
KernelSU/docs/README_IW.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | **עברית** | [हिंदी](README_IN.md)
|
||||
|
||||
# KernelSU
|
||||
|
||||
<img src="https://kernelsu.org/logo.png" style="width: 96px;" alt="logo">
|
||||
|
||||
פתרון לניהול root מבוסס על Kernel עבור מכשירי Android.
|
||||
|
||||
[](https://github.com/tiann/KernelSU/releases/latest)
|
||||
[](https://hosted.weblate.org/engage/kernelsu)
|
||||
[](https://t.me/KernelSU)
|
||||
[](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)
|
||||
[](/LICENSE)
|
||||
|
||||
## תכונות
|
||||
|
||||
1. ניהול root ו־`su` מבוססים על Kernel.
|
||||
2. מערכת מודולים מבוססת [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS).
|
||||
3. [פרופיל אפליקציה](https://kernelsu.org/guide/app-profile.html): נעילת גישת root בכלוב.
|
||||
|
||||
## מצב תאימות
|
||||
|
||||
KernelSU תומך במכשירי Android GKI 2.0 (kernel 5.10+) באופן רשמי. לליבות ישנות (4.14+) יש גם תאימות, אך יידרש לבנות את הליבה באופן ידני.
|
||||
|
||||
באמצעות זה, תמיכה זמינה גם ל-WSA, ChromeOS ומכשירי Android המבוססים על מיכלים.
|
||||
|
||||
כרגע, רק `arm64-v8a` ו־`x86_64` נתמכים.
|
||||
|
||||
## שימוש
|
||||
|
||||
- [הוראות התקנה](https://kernelsu.org/guide/installation.html)
|
||||
- [איך לבנות?](https://kernelsu.org/guide/how-to-build.html)
|
||||
- [האתר רשמי](https://kernelsu.org/)
|
||||
|
||||
## תרגום
|
||||
|
||||
כדי לעזור בתרגום של KernelSU או לשפר תרגומים קיימים, יש להשתמש ב-[Weblate](https://hosted.weblate.org/engage/kernelsu/).
|
||||
|
||||
## דיון
|
||||
|
||||
- Telegram: [@KernelSU](https://t.me/KernelSU)
|
||||
|
||||
## רשיון
|
||||
|
||||
- קבצים תחת הספרייה `kernel` מוגנים על פי [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html).
|
||||
- כל החלקים האחרים, למעט הספרייה `kernel`, מוגנים על פי [GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html).
|
||||
|
||||
## קרדיטים
|
||||
|
||||
- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): הרעיון של KernelSU.
|
||||
- [Magisk](https://github.com/topjohnwu/Magisk): הכלי הסופר חזק לניהול root.
|
||||
- [genuine](https://github.com/brevent/genuine/): אימות חתימת apk v2.
|
||||
- [Diamorphine](https://github.com/m0nad/Diamorphine): כמה יכולות רוט.
|
||||
53
KernelSU/docs/README_JP.md
Normal file
53
KernelSU/docs/README_JP.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | **日本語** | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md)
|
||||
|
||||
# KernelSU
|
||||
|
||||
<img src="https://kernelsu.org/logo.png" style="width: 96px;" alt="logo">
|
||||
|
||||
Android におけるカーネルベースの root ソリューションです。
|
||||
|
||||
[](https://github.com/tiann/KernelSU/releases/latest)
|
||||
[](https://hosted.weblate.org/engage/kernelsu)
|
||||
[](https://t.me/KernelSU)
|
||||
[](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)
|
||||
[](/LICENSE)
|
||||
|
||||
## 特徴
|
||||
|
||||
1. カーネルベースの `su` と権限管理。
|
||||
2. [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS) に基づくモジュールシステム。
|
||||
3. [アプリのプロファイル](https://kernelsu.org/guide/app-profile.html): root の権限をケージ内に閉じ込めます。
|
||||
|
||||
## 対応状況
|
||||
|
||||
KernelSU は GKI 2.0 デバイス(カーネルバージョン 5.10 以上)を公式にサポートしています。古いカーネル(4.14以上)とも互換性がありますが、自分でカーネルをビルドする必要があります。
|
||||
|
||||
WSA 、ChromeOS とコンテナ上で動作する Android でも KernelSU を統合して動かせます。
|
||||
|
||||
現在サポートしているアーキテクチャは `arm64-v8a` および `x86_64` です。
|
||||
|
||||
## 使用方法
|
||||
|
||||
- [インストール方法はこちら](https://kernelsu.org/ja_JP/guide/installation.html)
|
||||
- [ビルド方法はこちら](https://kernelsu.org/guide/how-to-build.html)
|
||||
- [公式サイト](https://kernelsu.org/ja_JP/)
|
||||
|
||||
## 翻訳
|
||||
|
||||
KernelSU をあなたの言語に翻訳するか、既存の翻訳を改善するには、[Weblate](https://hosted.weblate.org/engage/kernelsu/) を使用してください。Manager翻訳した PR は、Weblate と競合するため受け入れられなくなりました。
|
||||
|
||||
## ディスカッション
|
||||
|
||||
- Telegram: [@KernelSU](https://t.me/KernelSU)
|
||||
|
||||
## ライセンス
|
||||
|
||||
- `kernel` ディレクトリの下にあるすべてのファイル: [GPL-2-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)。
|
||||
- `kernel` ディレクトリ以外のすべてのファイル: [GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html)。
|
||||
|
||||
## クレジット
|
||||
|
||||
- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/):KernelSU のアイデア元。
|
||||
- [Magisk](https://github.com/topjohnwu/Magisk):強力な root ツール。
|
||||
- [genuine](https://github.com/brevent/genuine/):apk v2 の署名検証。
|
||||
- [Diamorphine](https://github.com/m0nad/Diamorphine): rootkit のスキル。
|
||||
57
KernelSU/docs/README_KR.md
Normal file
57
KernelSU/docs/README_KR.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | **한국어** | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md)
|
||||
|
||||
# KernelSU
|
||||
|
||||
<img src="https://kernelsu.org/logo.png" style="width: 96px;" alt="logo">
|
||||
|
||||
안드로이드 기기에서 사용되는 커널 기반 루팅 솔루션입니다.
|
||||
|
||||
[](https://github.com/tiann/KernelSU/releases/latest)
|
||||
[](https://hosted.weblate.org/engage/kernelsu)
|
||||
[](https://t.me/KernelSU)
|
||||
[](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)
|
||||
[](/LICENSE)
|
||||
|
||||
## 기능들
|
||||
|
||||
1. 커널 기반 `su` 및 루트 액세스 관리.
|
||||
2. [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS) 기반 모듈 시스템.
|
||||
3. [App Profile](https://kernelsu.org/guide/app-profile.html): 루트 권한을 케이지에 가둡니다.
|
||||
|
||||
## 호환 상태
|
||||
|
||||
KernelSU는 공식적으로 안드로이드 GKI 2.0 디바이스(커널 5.10 이상)를 지원합니다. 오래된 커널(4.14 이상)도 사용할 수 있지만, 커널을 수동으로 빌드해야 합니다.
|
||||
|
||||
KernelSU는 WSA, ChromeOS, 컨테이너 기반 안드로이드 모두를 지원합니다.
|
||||
|
||||
현재는 `arm64-v8a`와 `x86_64`만 지원됩니다.
|
||||
|
||||
## 사용 방법
|
||||
|
||||
- [설치 방법](https://kernelsu.org/guide/installation.html)
|
||||
- [어떻게 빌드하나요?](https://kernelsu.org/guide/how-to-build.html)
|
||||
- [공식 웹사이트](https://kernelsu.org/)
|
||||
|
||||
## 번역
|
||||
|
||||
KernelSU 번역을 돕거나 기존 번역을 개선하려면 [Weblate](https://hosted.weblate.org/engage/kernelsu/)를 이용해 주세요. 매니저의 번역은 Weblate와 충돌할 수 있으므로 더 이상 허용되지 않습니다.
|
||||
|
||||
## 토론
|
||||
|
||||
- 텔레그램: [@KernelSU](https://t.me/KernelSU)
|
||||
|
||||
## 보안
|
||||
|
||||
KernelSU의 보안 취약점 보고에 대한 자세한 내용은 [SECURITY.md](/SECURITY.md)를 참조하세요.
|
||||
|
||||
## 저작권
|
||||
|
||||
- `kernel` 디렉터리 아래의 파일은 [GPL-2.0 전용](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)입니다.
|
||||
- `kernel` 디렉토리를 제외한 다른 모든 부분은 [GPL-3.0-이상](https://www.gnu.org/licenses/gpl-3.0.html)입니다.
|
||||
|
||||
## 크래딧
|
||||
|
||||
- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): KernelSU의 아이디어.
|
||||
- [Magisk](https://github.com/topjohnwu/Magisk): 강력한 루팅 도구.
|
||||
- [genuine](https://github.com/brevent/genuine/): apk v2 서명 유효성 검사.
|
||||
- [Diamorphine](https://github.com/m0nad/Diamorphine): 일부 rootkit 스킬.
|
||||
55
KernelSU/docs/README_PL.md
Normal file
55
KernelSU/docs/README_PL.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | **Polski** | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md)
|
||||
|
||||
# KernelSU
|
||||
|
||||
<img src="https://kernelsu.org/logo.png" style="width: 96px;" alt="logo">
|
||||
|
||||
Rozwiązanie root oparte na jądrze dla urządzeń z systemem Android.
|
||||
|
||||
[](https://github.com/tiann/KernelSU/releases/latest)
|
||||
[](https://hosted.weblate.org/engage/kernelsu)
|
||||
[](https://t.me/KernelSU)
|
||||
[](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)
|
||||
[](/LICENSE)
|
||||
|
||||
## Cechy
|
||||
|
||||
1. Oparte na jądrze `su` i zarządzanie dostępem roota.
|
||||
2. System modułów oparty na [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS).
|
||||
|
||||
## Kompatybilność
|
||||
|
||||
KernelSU oficjalnie obsługuje urządzenia z Androidem GKI 2.0 (z jądrem 5.10+), starsze jądra (4.14+) są również kompatybilne, ale musisz sam skompilować jądro.
|
||||
|
||||
WSA i Android oparty na kontenerach również powinny działać ze zintegrowanym KernelSU.
|
||||
|
||||
Aktualnie obsługiwane ABI to : `arm64-v8a` i `x86_64`.
|
||||
|
||||
## Użycie
|
||||
|
||||
- [Instalacja](https://kernelsu.org/guide/installation.html)
|
||||
- [Jak skompilować?](https://kernelsu.org/guide/how-to-build.html)
|
||||
|
||||
## Tłumaczenie
|
||||
|
||||
Aby pomóc w tłumaczeniu KernelSU lub ulepszyć istniejące tłumaczenia, użyj [Weblate](https://hosted.weblate.org/engage/kernelsu/). PR tłumaczenia Managera nie jest już akceptowany, ponieważ będzie kolidował z Weblate.
|
||||
|
||||
## Dyskusja
|
||||
|
||||
- Telegram: [@KernelSU](https://t.me/KernelSU)
|
||||
|
||||
## Bezpieczeństwo
|
||||
|
||||
Informacje na temat zgłaszania luk w zabezpieczeniach w KernelSU można znaleźć w pliku [SECURITY.md](/SECURITY.md).
|
||||
|
||||
## Licencja
|
||||
|
||||
- Pliki w katalogu `kernel` są na licencji [GPL-2-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html).
|
||||
- Wszystkie inne części poza katalogiem `kernel` są na licencji [GPL-3-or-later](https://www.gnu.org/licenses/gpl-3.0.html).
|
||||
|
||||
## Podziękowania
|
||||
|
||||
- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): pomysłodawca KernelSU.
|
||||
- [Magisk](https://github.com/topjohnwu/Magisk): implementacja sepolicy.
|
||||
- [genuine](https://github.com/brevent/genuine/): walidacja podpisu apk v2.
|
||||
- [Diamorphine](https://github.com/m0nad/Diamorphine): cenna znajomość rootkitów.
|
||||
57
KernelSU/docs/README_PT-BR.md
Normal file
57
KernelSU/docs/README_PT-BR.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | **Português (Brasil)** | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md)
|
||||
|
||||
# KernelSU
|
||||
|
||||
<img src="https://kernelsu.org/logo.png" style="width: 96px;" alt="logo">
|
||||
|
||||
Uma solução root baseada em kernel para dispositivos Android.
|
||||
|
||||
[](https://github.com/tiann/KernelSU/releases/latest)
|
||||
[](https://hosted.weblate.org/engage/kernelsu)
|
||||
[](https://t.me/KernelSU)
|
||||
[](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)
|
||||
[](/LICENSE)
|
||||
|
||||
## Características
|
||||
|
||||
1. `su` e gerenciamento de acesso root baseado em kernel.
|
||||
2. Sistema modular baseado em [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS).
|
||||
3. [Perfil do Aplicativo](https://kernelsu.org/pt_BR/guide/app-profile.html): Tranque o poder root em uma gaiola.
|
||||
|
||||
## Estado de compatibilidade
|
||||
|
||||
O KernelSU oferece suporte oficial a dispositivos Android GKI 2.0 (kernel 5.10+). Kernels mais antigos (4.14+) também são compatíveis, mas o kernel terá que ser construído manualmente.
|
||||
|
||||
Com isso, WSA, ChromeOS e Android baseado em contêiner são todos suportados.
|
||||
|
||||
Atualmente, apenas `arm64-v8a` e `x86_64` são suportados.
|
||||
|
||||
## Uso
|
||||
|
||||
- [Instalação](https://kernelsu.org/pt_BR/guide/installation.html)
|
||||
- [Como compilar o KernelSU?](https://kernelsu.org/pt_BR/guide/how-to-build.html)
|
||||
- [Site oficial](https://kernelsu.org/pt_BR/)
|
||||
|
||||
## Tradução
|
||||
|
||||
Para contribuir com a tradução do KernelSU ou aprimorar traduções existentes, por favor, utilize o [Weblate](https://hosted.weblate.org/engage/kernelsu/). PR para a tradução do Gerenciador não são mais aceitas, pois podem entrar em conflito com o Weblate.
|
||||
|
||||
## Discussão
|
||||
|
||||
- Telegram: [@KernelSU](https://t.me/KernelSU)
|
||||
|
||||
## Segurança
|
||||
|
||||
Para obter informações sobre como relatar vulnerabilidades de segurança do KernelSU, consulte [SECURITY.md](/SECURITY.md).
|
||||
|
||||
## Licença
|
||||
|
||||
- Os arquivos no diretório `kernel` são [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html).
|
||||
- Todas as outras partes, exceto o diretório `kernel` são [GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html).
|
||||
|
||||
## Créditos
|
||||
|
||||
- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): a ideia do KernelSU.
|
||||
- [Magisk](https://github.com/topjohnwu/Magisk): a poderosa ferramenta root.
|
||||
- [genuine](https://github.com/brevent/genuine/): validação de assinatura apk v2.
|
||||
- [Diamorphine](https://github.com/m0nad/Diamorphine): algumas habilidades de rootkit.
|
||||
49
KernelSU/docs/README_RU.md
Normal file
49
KernelSU/docs/README_RU.md
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | **Русский** | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md)
|
||||
|
||||
# KernelSU
|
||||
|
||||
<img src="https://kernelsu.org/logo.png" style="width: 96px;" alt="logo">
|
||||
|
||||
Решение на основе ядра root для Android-устройств.
|
||||
|
||||
[](https://github.com/tiann/KernelSU/releases/latest)
|
||||
[](https://hosted.weblate.org/engage/kernelsu)
|
||||
[](https://t.me/KernelSU)
|
||||
[](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)
|
||||
[](/LICENSE)
|
||||
|
||||
## Особенности
|
||||
|
||||
1. Управление `su` и root-доступом на основе ядра.
|
||||
2. Система модулей на основе [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS).
|
||||
3. [Профиль приложений](https://kernelsu.org/ru_RU/guide/app-profile.html): Запри корневую силу в клетке.
|
||||
|
||||
## Совместимость
|
||||
|
||||
KernelSU официально поддерживает устройства на базе Android GKI 2.0 (с ядром 5.10+), старые ядра (4.14+) также совместимы, но для этого необходимо собрать ядро самостоятельно.
|
||||
|
||||
WSA и Android на основе контейнеров также должны работать с интегрированным KernelSU.
|
||||
|
||||
В настоящее время поддерживаются следующие ABI: `arm64-v8a` и `x86_64`.
|
||||
|
||||
## Использование
|
||||
|
||||
- [Установка](https://kernelsu.org/ru_RU/guide/installation.html)
|
||||
- [Как собрать?](https://kernelsu.org/ru_RU/guide/how-to-build.html)
|
||||
- [официальный сайт](https://kernelsu.org/ru_RU/)
|
||||
|
||||
## Обсуждение
|
||||
|
||||
- Telegram: [@KernelSU](https://t.me/KernelSU)
|
||||
|
||||
## Лицензия
|
||||
|
||||
- Файлы в директории `kernel` [GPL-2-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html).
|
||||
- Все остальные части, кроме директории `kernel` [GPL-3-or-later](https://www.gnu.org/licenses/gpl-3.0.html).
|
||||
|
||||
## Благодарности
|
||||
|
||||
- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): идея KernelSU.
|
||||
- [Magisk](https://github.com/topjohnwu/Magisk): реализация sepolicy.
|
||||
- [genuine](https://github.com/brevent/genuine/): проверка подписи apk v2.
|
||||
- [Diamorphine](https://github.com/m0nad/Diamorphine): некоторые навыки руткита.
|
||||
57
KernelSU/docs/README_TR.md
Normal file
57
KernelSU/docs/README_TR.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | **Türkçe** | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md)
|
||||
|
||||
# KernelSU
|
||||
|
||||
<img src="https://kernelsu.org/logo.png" style="width: 96px;" alt="logo">
|
||||
|
||||
Android cihazlar için kernel tabanlı root çözümü.
|
||||
|
||||
[](https://github.com/tiann/KernelSU/releases/latest)
|
||||
[](https://hosted.weblate.org/engage/kernelsu)
|
||||
[](https://t.me/KernelSU)
|
||||
[](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)
|
||||
[](/LICENSE)
|
||||
|
||||
## Özellikler
|
||||
|
||||
1. Kernel-tabanlı `su` ve root erişimi yönetimi.
|
||||
2. [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS)'ye dayalı modül sistemi.
|
||||
3. [Uygulama profili](https://kernelsu.org/guide/app-profile.html): Root gücünü bir kafese kapatın.
|
||||
|
||||
## Uyumluluk Durumu
|
||||
|
||||
KernelSU resmi olarak Android GKI 2.0 cihazlarını (5.10+ kernelli) destekler, eski kernellerle de (4.14+) uyumludur, ancak kerneli kendinizin derlemeniz gerekir.
|
||||
|
||||
Bununla birlikte; WSA, ChromeOS ve konteyner tabanlı Android'in tamamı desteklenmektedir.
|
||||
|
||||
Şimdilik sadece `arm64-v8a` ve `x86_64` desteklenmektedir.
|
||||
|
||||
## Kullanım
|
||||
|
||||
- [Yükleme yönergeleri](https://kernelsu.org/guide/installation.html)
|
||||
- [Nasıl derlenir?](https://kernelsu.org/guide/how-to-build.html)
|
||||
- [Resmi WEB sitesi](https://kernelsu.org/)
|
||||
|
||||
## Çeviri
|
||||
|
||||
KernelSU'nun başka dillere çevrilmesine veya mevcut çevirilerin iyileştirilmesine yardımcı olmak için lütfen [Weblate](https://hosted.weblate.org/engage/kernelsu/) kullanın. Yönetici uygulamasının PR ile çevirisi, Weblate ile çakışacağından artık kabul edilmeyecektir.
|
||||
|
||||
## Tartışma
|
||||
|
||||
- Telegram: [@KernelSU](https://t.me/KernelSU)
|
||||
|
||||
## Güvenlik
|
||||
|
||||
KernelSU'daki güvenlik açıklarını bildirme hakkında bilgi için, bkz [SECURITY.md](/SECURITY.md).
|
||||
|
||||
## Lisans
|
||||
|
||||
- `kernel` klasöründeki dosyalar [GPL-2-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) lisansı altındadır.
|
||||
- `kernel` klasörü dışındaki bütün diğer bölümler [GPL-3-veya-sonraki](https://www.gnu.org/licenses/gpl-3.0.html) lisansı altındadır.
|
||||
|
||||
## Krediler
|
||||
|
||||
- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): KernelSU fikri.
|
||||
- [Magisk](https://github.com/topjohnwu/Magisk): güçlü root aracı.
|
||||
- [genuine](https://github.com/brevent/genuine/): apk v2 imza doğrulaması.
|
||||
- [Diamorphine](https://github.com/m0nad/Diamorphine): bazı rootkit becerileri.
|
||||
56
KernelSU/docs/README_TW.md
Normal file
56
KernelSU/docs/README_TW.md
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | **繁體中文** | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md)
|
||||
|
||||
# KernelSU
|
||||
|
||||
<img src="https://kernelsu.org/logo.png" style="width: 96px;" alt="logo">
|
||||
|
||||
一個基於核心的 Android 裝置 Root 解決方案
|
||||
|
||||
[](https://github.com/tiann/KernelSU/releases/latest)
|
||||
[](https://hosted.weblate.org/engage/kernelsu)
|
||||
[](https://t.me/KernelSU)
|
||||
[](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)
|
||||
[](/LICENSE)
|
||||
|
||||
## 功能
|
||||
|
||||
- 基於核心的 `su` 和 Root 存取權管理。
|
||||
- 基於 [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS) 的模組系統。
|
||||
- [App Profile](https://kernelsu.org/zh_TW/guide/app-profile.html): 將 Root 的權限鎖在牢籠中.
|
||||
|
||||
## 相容性狀態
|
||||
|
||||
KernelSU 官方支援 Android GKI 2.0 的裝置 (核心版本 5.10+ );舊版核心同樣相容 (最低 4.14+ ),但需要自行編譯核心。
|
||||
|
||||
WSA和ChromeOS和執行在容器中的 Android 也可以與 KernelSU 一同運作。
|
||||
|
||||
目前支援架構:`arm64-v8a` 和 `x86_64`。
|
||||
|
||||
## 使用方法
|
||||
|
||||
- [安裝教學](https://kernelsu.org/zh_TW/guide/installation.html)
|
||||
- [如何建置?](https://kernelsu.org/zh_TW/guide/how-to-build.html)
|
||||
- [官方網站](https://kernelsu.org/zh_TW/)
|
||||
|
||||
## 翻譯
|
||||
|
||||
若要協助翻譯 KernelSU 或改進現有翻譯,請使用 [Weblate](https://hosted.weblate.org/engage/kernelsu/)。 翻譯管理器的PR不再被接受,因為它會與Weblate衝突。
|
||||
|
||||
### 討論
|
||||
|
||||
- Telegram:[@KernelSU](https://t.me/KernelSU)
|
||||
|
||||
## 安全
|
||||
有關報告 KernelSU 中的安全漏洞的資訊,請參閱 [SECURITY.md](/SECURITY.md)。
|
||||
|
||||
## 授權
|
||||
|
||||
- 目錄 `kernel` 下所有檔案為 [GPL-2-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)。
|
||||
- 除 `kernel` 目錄的其他部分均為 [GPL-3-or-later](https://www.gnu.org/licenses/gpl-3.0.html)。
|
||||
|
||||
## 致謝
|
||||
|
||||
- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/):KernelSU 的靈感。
|
||||
- [Magisk](https://github.com/topjohnwu/Magisk):sepolicy 實作。
|
||||
- [genuine](https://github.com/brevent/genuine/):apk v2 簽章驗證。
|
||||
- [Diamorphine](https://github.com/m0nad/Diamorphine):一些 rootkit 技巧。
|
||||
53
KernelSU/docs/README_VI.md
Normal file
53
KernelSU/docs/README_VI.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | **Tiếng Việt** | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md)
|
||||
|
||||
# KernelSU
|
||||
|
||||
<img src="https://kernelsu.org/logo.png" style="width: 96px;" alt="logo">
|
||||
|
||||
Giải pháp root thông qua thay đổi trên Kernel hệ điều hành cho các thiết bị Android.
|
||||
|
||||
[](https://github.com/tiann/KernelSU/releases/latest)
|
||||
[](https://hosted.weblate.org/engage/kernelsu)
|
||||
[](https://t.me/KernelSU)
|
||||
[](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)
|
||||
[](/LICENSE)
|
||||
|
||||
## Tính năng
|
||||
|
||||
1. Hỗ trợ gói thực thi `su` và quản lý quyền root.
|
||||
2. Hệ thống mô-đun thông qua [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS).
|
||||
3. [App Profile](https://kernelsu.org/guide/app-profile.html): Hạn chế quyền root của ứng dụng.
|
||||
|
||||
## Tình trạng tương thích
|
||||
|
||||
KernelSU chính thức hỗ trợ các thiết bị Android với kernel GKI 2.0 (phiên bản kernel 5.10+), các phiên bản kernel cũ hơn (4.14+) cũng tương thích, nhưng bạn cần phải tự biên dịch.
|
||||
|
||||
WSA, ChromeOS và Android dựa trên container(container-based) cũng được hỗ trợ bởi KernelSU.
|
||||
|
||||
Hiên tại Giao diện nhị phân của ứng dụng (ABI) được hỗ trợ bao gồm `arm64-v8a` và `x86_64`.
|
||||
|
||||
## Sử dụng
|
||||
|
||||
- [Hướng dẫn cài đặt](https://kernelsu.org/vi_VN/guide/installation.html)
|
||||
- [Cách để build?](https://kernelsu.org/vi_VN/guide/how-to-build.html)
|
||||
- [Website Chính Thức](https://kernelsu.org/vi_VN/)
|
||||
|
||||
## Hỗ trợ dịch
|
||||
|
||||
Nếu bạn muốn hỗ trợ dịch KernelSU sang một ngôn ngữ khác hoặc cải thiện các bản dịch trước, vui lòng sử dụng [Weblate](https://hosted.weblate.org/engage/kernelsu/).
|
||||
|
||||
## Thảo luận
|
||||
|
||||
- Telegram: [@KernelSU](https://t.me/KernelSU)
|
||||
|
||||
## Giấy phép
|
||||
|
||||
- Tất cả các file trong thư mục `kernel` dùng giấy phép [GPL-2-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html).
|
||||
- Tất cả các thành phần khác ngoại trừ thư mục `kernel` dùng giấy phép [GPL-3-or-later](https://www.gnu.org/licenses/gpl-3.0.html).
|
||||
|
||||
## Lời cảm ơn
|
||||
|
||||
- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): ý tưởng cho KernelSU.
|
||||
- [Magisk](https://github.com/topjohnwu/Magisk): công cụ root mạnh mẽ.
|
||||
- [genuine](https://github.com/brevent/genuine/): phương pháp xác thực apk v2.
|
||||
- [Diamorphine](https://github.com/m0nad/Diamorphine): các phương pháp ẩn của rootkit.
|
||||
111
KernelSU/js/README.md
Normal file
111
KernelSU/js/README.md
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
# Library for KernelSU's module WebUI
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
yarn add kernelsu
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### exec
|
||||
|
||||
Spawns a **root** shell and runs a command within that shell, passing the `stdout` and `stderr` to a Promise when complete.
|
||||
|
||||
- `command` `<string>` The command to run, with space-separated arguments.
|
||||
- `options` `<Object>`
|
||||
- `cwd` - Current working directory of the child process
|
||||
- `env` - Environment key-value pairs
|
||||
|
||||
```javascript
|
||||
import { exec } from 'kernelsu';
|
||||
|
||||
const { errno, stdout, stderr } = await exec('ls -l', { cwd: '/tmp' });
|
||||
if (errno === 0) {
|
||||
// success
|
||||
console.log(stdout);
|
||||
}
|
||||
```
|
||||
|
||||
### spawn
|
||||
|
||||
Spawns a new process using the given `command` in **root** shell, with command-line arguments in `args`. If omitted, `args` defaults to an empty array.
|
||||
|
||||
Returns a `ChildProcess`, Instances of the ChildProcess represent spawned child processes.
|
||||
|
||||
- `command` `<string>` The command to run.
|
||||
- `args` `<string[]>` List of string arguments.
|
||||
- `options` `<Object>`:
|
||||
- `cwd` `<string>` - Current working directory of the child process
|
||||
- `env` `<Object>` - Environment key-value pairs
|
||||
|
||||
Example of running `ls -lh /data`, capturing `stdout`, `stderr`, and the exit code:
|
||||
|
||||
```javascript
|
||||
import { spawn } from 'kernelsu';
|
||||
|
||||
const ls = spawn('ls', ['-lh', '/data']);
|
||||
|
||||
ls.stdout.on('data', (data) => {
|
||||
console.log(`stdout: ${data}`);
|
||||
});
|
||||
|
||||
ls.stderr.on('data', (data) => {
|
||||
console.log(`stderr: ${data}`);
|
||||
});
|
||||
|
||||
ls.on('exit', (code) => {
|
||||
console.log(`child process exited with code ${code}`);
|
||||
});
|
||||
```
|
||||
|
||||
#### ChildProcess
|
||||
|
||||
##### Event 'exit'
|
||||
|
||||
- `code` `<number>` The exit code if the child exited on its own.
|
||||
|
||||
The `'exit'` event is emitted after the child process ends. If the process exited, `code` is the final exit code of the process, otherwise null
|
||||
|
||||
##### Event 'error'
|
||||
|
||||
- `err` `<Error>` The error.
|
||||
|
||||
The `'error'` event is emitted whenever:
|
||||
|
||||
- The process could not be spawned.
|
||||
- The process could not be killed.
|
||||
|
||||
##### `stdout`
|
||||
|
||||
A `Readable Stream` that represents the child process's `stdout`.
|
||||
|
||||
```javascript
|
||||
const subprocess = spawn('ls');
|
||||
|
||||
subprocess.stdout.on('data', (data) => {
|
||||
console.log(`Received chunk ${data}`);
|
||||
});
|
||||
```
|
||||
|
||||
#### `stderr`
|
||||
|
||||
A `Readable Stream` that represents the child process's `stderr`.
|
||||
|
||||
### fullScreen
|
||||
|
||||
Request the WebView enter/exit full screen.
|
||||
|
||||
```javascript
|
||||
import { fullScreen } from 'kernelsu';
|
||||
fullScreen(true);
|
||||
```
|
||||
|
||||
### toast
|
||||
|
||||
Show a toast message.
|
||||
|
||||
```javascript
|
||||
import { toast } from 'kernelsu';
|
||||
toast('Hello, world!');
|
||||
```
|
||||
45
KernelSU/js/index.d.ts
vendored
Normal file
45
KernelSU/js/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
interface ExecOptions {
|
||||
cwd?: string,
|
||||
env?: { [key: string]: string }
|
||||
}
|
||||
|
||||
interface ExecResults {
|
||||
errno: number,
|
||||
stdout: string,
|
||||
stderr: string
|
||||
}
|
||||
|
||||
declare function exec(command: string): Promise<ExecResults>;
|
||||
declare function exec(command: string, options: ExecOptions): Promise<ExecResults>;
|
||||
|
||||
interface SpawnOptions {
|
||||
cwd?: string,
|
||||
env?: { [key: string]: string }
|
||||
}
|
||||
|
||||
interface Stdio {
|
||||
on(event: 'data', callback: (data: string) => void)
|
||||
}
|
||||
|
||||
interface ChildProcess {
|
||||
stdout: Stdio,
|
||||
stderr: Stdio,
|
||||
on(event: 'exit', callback: (code: number) => void)
|
||||
on(event: 'error', callback: (err: any) => void)
|
||||
}
|
||||
|
||||
declare function spawn(command: string): ChildProcess;
|
||||
declare function spawn(command: string, args: string[]): ChildProcess;
|
||||
declare function spawn(command: string, options: SpawnOptions): ChildProcess;
|
||||
declare function spawn(command: string, args: string[], options: SpawnOptions): ChildProcess;
|
||||
|
||||
declare function fullScreen(isFullScreen: boolean);
|
||||
|
||||
declare function toast(message: string);
|
||||
|
||||
export {
|
||||
exec,
|
||||
spawn,
|
||||
fullScreen,
|
||||
toast
|
||||
}
|
||||
115
KernelSU/js/index.js
Normal file
115
KernelSU/js/index.js
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
let callbackCounter = 0;
|
||||
function getUniqueCallbackName(prefix) {
|
||||
return `${prefix}_callback_${Date.now()}_${callbackCounter++}`;
|
||||
}
|
||||
|
||||
export function exec(command, options) {
|
||||
if (typeof options === "undefined") {
|
||||
options = {};
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// Generate a unique callback function name
|
||||
const callbackFuncName = getUniqueCallbackName("exec");
|
||||
|
||||
// Define the success callback function
|
||||
window[callbackFuncName] = (errno, stdout, stderr) => {
|
||||
resolve({ errno, stdout, stderr });
|
||||
cleanup(callbackFuncName);
|
||||
};
|
||||
|
||||
function cleanup(successName) {
|
||||
delete window[successName];
|
||||
}
|
||||
|
||||
try {
|
||||
ksu.exec(command, JSON.stringify(options), callbackFuncName);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
cleanup(callbackFuncName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function Stdio() {
|
||||
this.listeners = {};
|
||||
}
|
||||
|
||||
Stdio.prototype.on = function (event, listener) {
|
||||
if (!this.listeners[event]) {
|
||||
this.listeners[event] = [];
|
||||
}
|
||||
this.listeners[event].push(listener);
|
||||
};
|
||||
|
||||
Stdio.prototype.emit = function (event, ...args) {
|
||||
if (this.listeners[event]) {
|
||||
this.listeners[event].forEach((listener) => listener(...args));
|
||||
}
|
||||
};
|
||||
|
||||
function ChildProcess() {
|
||||
this.listeners = {};
|
||||
this.stdin = new Stdio();
|
||||
this.stdout = new Stdio();
|
||||
this.stderr = new Stdio();
|
||||
}
|
||||
|
||||
ChildProcess.prototype.on = function (event, listener) {
|
||||
if (!this.listeners[event]) {
|
||||
this.listeners[event] = [];
|
||||
}
|
||||
this.listeners[event].push(listener);
|
||||
};
|
||||
|
||||
ChildProcess.prototype.emit = function (event, ...args) {
|
||||
if (this.listeners[event]) {
|
||||
this.listeners[event].forEach((listener) => listener(...args));
|
||||
}
|
||||
};
|
||||
|
||||
export function spawn(command, args, options) {
|
||||
if (typeof args === "undefined") {
|
||||
args = [];
|
||||
} else if (!(args instanceof Array)) {
|
||||
// allow for (command, options) signature
|
||||
options = args;
|
||||
}
|
||||
|
||||
if (typeof options === "undefined") {
|
||||
options = {};
|
||||
}
|
||||
|
||||
const child = new ChildProcess();
|
||||
const childCallbackName = getUniqueCallbackName("spawn");
|
||||
window[childCallbackName] = child;
|
||||
|
||||
function cleanup(name) {
|
||||
delete window[name];
|
||||
}
|
||||
|
||||
child.on("exit", code => {
|
||||
cleanup(childCallbackName);
|
||||
});
|
||||
|
||||
try {
|
||||
ksu.spawn(
|
||||
command,
|
||||
JSON.stringify(args),
|
||||
JSON.stringify(options),
|
||||
childCallbackName
|
||||
);
|
||||
} catch (error) {
|
||||
child.emit("error", error);
|
||||
cleanup(childCallbackName);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
export function fullScreen(isFullScreen) {
|
||||
ksu.fullScreen(isFullScreen);
|
||||
}
|
||||
|
||||
export function toast(message) {
|
||||
ksu.toast(message);
|
||||
}
|
||||
26
KernelSU/js/package.json
Normal file
26
KernelSU/js/package.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "kernelsu",
|
||||
"version": "1.0.6",
|
||||
"description": "Library for KernelSU's module WebUI",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"scripts": {
|
||||
"test": "npm run test"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tiann/KernelSU.git"
|
||||
},
|
||||
"keywords": [
|
||||
"su",
|
||||
"kernelsu",
|
||||
"module",
|
||||
"webui"
|
||||
],
|
||||
"author": "weishu",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tiann/KernelSU/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tiann/KernelSU#readme"
|
||||
}
|
||||
14
KernelSU/justfile
Normal file
14
KernelSU/justfile
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
alias bk := build_ksud
|
||||
alias bm := build_manager
|
||||
|
||||
build_ksud:
|
||||
cross build --target aarch64-linux-android --release --manifest-path ./userspace/ksud/Cargo.toml
|
||||
|
||||
build_manager: build_ksud
|
||||
cp userspace/ksud/target/aarch64-linux-android/release/ksud manager/app/src/main/jniLibs/arm64-v8a/libksud.so
|
||||
cd manager && ./gradlew aDebug
|
||||
|
||||
clippy:
|
||||
cargo fmt --manifest-path ./userspace/ksud/Cargo.toml
|
||||
cross clippy --target x86_64-pc-windows-gnu --release --manifest-path ./userspace/ksud/Cargo.toml
|
||||
cross clippy --target aarch64-linux-android --release --manifest-path ./userspace/ksud/Cargo.toml
|
||||
548
KernelSU/kernel/.clang-format
Normal file
548
KernelSU/kernel/.clang-format
Normal file
|
|
@ -0,0 +1,548 @@
|
|||
# SPDX-License-Identifier: GPL-2.0
|
||||
#
|
||||
# clang-format configuration file. Intended for clang-format >= 4.
|
||||
#
|
||||
# For more information, see:
|
||||
#
|
||||
# Documentation/process/clang-format.rst
|
||||
# https://clang.llvm.org/docs/ClangFormat.html
|
||||
# https://clang.llvm.org/docs/ClangFormatStyleOptions.html
|
||||
#
|
||||
---
|
||||
AccessModifierOffset: -4
|
||||
AlignAfterOpenBracket: Align
|
||||
AlignConsecutiveAssignments: false
|
||||
AlignConsecutiveDeclarations: false
|
||||
#AlignEscapedNewlines: Left # Unknown to clang-format-4.0
|
||||
AlignOperands: true
|
||||
AlignTrailingComments: false
|
||||
AllowAllParametersOfDeclarationOnNextLine: false
|
||||
AllowShortBlocksOnASingleLine: false
|
||||
AllowShortCaseLabelsOnASingleLine: false
|
||||
AllowShortFunctionsOnASingleLine: None
|
||||
AllowShortIfStatementsOnASingleLine: false
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
AlwaysBreakAfterDefinitionReturnType: None
|
||||
AlwaysBreakAfterReturnType: None
|
||||
AlwaysBreakBeforeMultilineStrings: false
|
||||
AlwaysBreakTemplateDeclarations: false
|
||||
BinPackArguments: true
|
||||
BinPackParameters: true
|
||||
BraceWrapping:
|
||||
AfterClass: false
|
||||
AfterControlStatement: false
|
||||
AfterEnum: false
|
||||
AfterFunction: true
|
||||
AfterNamespace: true
|
||||
AfterObjCDeclaration: false
|
||||
AfterStruct: false
|
||||
AfterUnion: false
|
||||
#AfterExternBlock: false # Unknown to clang-format-5.0
|
||||
BeforeCatch: false
|
||||
BeforeElse: false
|
||||
IndentBraces: false
|
||||
#SplitEmptyFunction: true # Unknown to clang-format-4.0
|
||||
#SplitEmptyRecord: true # Unknown to clang-format-4.0
|
||||
#SplitEmptyNamespace: true # Unknown to clang-format-4.0
|
||||
BreakBeforeBinaryOperators: None
|
||||
BreakBeforeBraces: Custom
|
||||
#BreakBeforeInheritanceComma: false # Unknown to clang-format-4.0
|
||||
BreakBeforeTernaryOperators: false
|
||||
BreakConstructorInitializersBeforeComma: false
|
||||
#BreakConstructorInitializers: BeforeComma # Unknown to clang-format-4.0
|
||||
BreakAfterJavaFieldAnnotations: false
|
||||
BreakStringLiterals: false
|
||||
ColumnLimit: 80
|
||||
CommentPragmas: '^ IWYU pragma:'
|
||||
#CompactNamespaces: false # Unknown to clang-format-4.0
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: false
|
||||
ConstructorInitializerIndentWidth: 8
|
||||
ContinuationIndentWidth: 8
|
||||
Cpp11BracedListStyle: false
|
||||
DerivePointerAlignment: false
|
||||
DisableFormat: false
|
||||
ExperimentalAutoDetectBinPacking: false
|
||||
#FixNamespaceComments: false # Unknown to clang-format-4.0
|
||||
|
||||
# Taken from:
|
||||
# git grep -h '^#define [^[:space:]]*for_each[^[:space:]]*(' include/ \
|
||||
# | sed "s,^#define \([^[:space:]]*for_each[^[:space:]]*\)(.*$, - '\1'," \
|
||||
# | sort | uniq
|
||||
ForEachMacros:
|
||||
- 'apei_estatus_for_each_section'
|
||||
- 'ata_for_each_dev'
|
||||
- 'ata_for_each_link'
|
||||
- '__ata_qc_for_each'
|
||||
- 'ata_qc_for_each'
|
||||
- 'ata_qc_for_each_raw'
|
||||
- 'ata_qc_for_each_with_internal'
|
||||
- 'ax25_for_each'
|
||||
- 'ax25_uid_for_each'
|
||||
- '__bio_for_each_bvec'
|
||||
- 'bio_for_each_bvec'
|
||||
- 'bio_for_each_bvec_all'
|
||||
- 'bio_for_each_integrity_vec'
|
||||
- '__bio_for_each_segment'
|
||||
- 'bio_for_each_segment'
|
||||
- 'bio_for_each_segment_all'
|
||||
- 'bio_list_for_each'
|
||||
- 'bip_for_each_vec'
|
||||
- 'bitmap_for_each_clear_region'
|
||||
- 'bitmap_for_each_set_region'
|
||||
- 'blkg_for_each_descendant_post'
|
||||
- 'blkg_for_each_descendant_pre'
|
||||
- 'blk_queue_for_each_rl'
|
||||
- 'bond_for_each_slave'
|
||||
- 'bond_for_each_slave_rcu'
|
||||
- 'bpf_for_each_spilled_reg'
|
||||
- 'btree_for_each_safe128'
|
||||
- 'btree_for_each_safe32'
|
||||
- 'btree_for_each_safe64'
|
||||
- 'btree_for_each_safel'
|
||||
- 'card_for_each_dev'
|
||||
- 'cgroup_taskset_for_each'
|
||||
- 'cgroup_taskset_for_each_leader'
|
||||
- 'cpufreq_for_each_entry'
|
||||
- 'cpufreq_for_each_entry_idx'
|
||||
- 'cpufreq_for_each_valid_entry'
|
||||
- 'cpufreq_for_each_valid_entry_idx'
|
||||
- 'css_for_each_child'
|
||||
- 'css_for_each_descendant_post'
|
||||
- 'css_for_each_descendant_pre'
|
||||
- 'device_for_each_child_node'
|
||||
- 'dma_fence_chain_for_each'
|
||||
- 'do_for_each_ftrace_op'
|
||||
- 'drm_atomic_crtc_for_each_plane'
|
||||
- 'drm_atomic_crtc_state_for_each_plane'
|
||||
- 'drm_atomic_crtc_state_for_each_plane_state'
|
||||
- 'drm_atomic_for_each_plane_damage'
|
||||
- 'drm_client_for_each_connector_iter'
|
||||
- 'drm_client_for_each_modeset'
|
||||
- 'drm_connector_for_each_possible_encoder'
|
||||
- 'drm_for_each_bridge_in_chain'
|
||||
- 'drm_for_each_connector_iter'
|
||||
- 'drm_for_each_crtc'
|
||||
- 'drm_for_each_encoder'
|
||||
- 'drm_for_each_encoder_mask'
|
||||
- 'drm_for_each_fb'
|
||||
- 'drm_for_each_legacy_plane'
|
||||
- 'drm_for_each_plane'
|
||||
- 'drm_for_each_plane_mask'
|
||||
- 'drm_for_each_privobj'
|
||||
- 'drm_mm_for_each_hole'
|
||||
- 'drm_mm_for_each_node'
|
||||
- 'drm_mm_for_each_node_in_range'
|
||||
- 'drm_mm_for_each_node_safe'
|
||||
- 'flow_action_for_each'
|
||||
- 'for_each_active_dev_scope'
|
||||
- 'for_each_active_drhd_unit'
|
||||
- 'for_each_active_iommu'
|
||||
- 'for_each_aggr_pgid'
|
||||
- 'for_each_available_child_of_node'
|
||||
- 'for_each_bio'
|
||||
- 'for_each_board_func_rsrc'
|
||||
- 'for_each_bvec'
|
||||
- 'for_each_card_auxs'
|
||||
- 'for_each_card_auxs_safe'
|
||||
- 'for_each_card_components'
|
||||
- 'for_each_card_dapms'
|
||||
- 'for_each_card_pre_auxs'
|
||||
- 'for_each_card_prelinks'
|
||||
- 'for_each_card_rtds'
|
||||
- 'for_each_card_rtds_safe'
|
||||
- 'for_each_card_widgets'
|
||||
- 'for_each_card_widgets_safe'
|
||||
- 'for_each_cgroup_storage_type'
|
||||
- 'for_each_child_of_node'
|
||||
- 'for_each_clear_bit'
|
||||
- 'for_each_clear_bit_from'
|
||||
- 'for_each_cmsghdr'
|
||||
- 'for_each_compatible_node'
|
||||
- 'for_each_component_dais'
|
||||
- 'for_each_component_dais_safe'
|
||||
- 'for_each_comp_order'
|
||||
- 'for_each_console'
|
||||
- 'for_each_cpu'
|
||||
- 'for_each_cpu_and'
|
||||
- 'for_each_cpu_not'
|
||||
- 'for_each_cpu_wrap'
|
||||
- 'for_each_dapm_widgets'
|
||||
- 'for_each_dev_addr'
|
||||
- 'for_each_dev_scope'
|
||||
- 'for_each_displayid_db'
|
||||
- 'for_each_dma_cap_mask'
|
||||
- 'for_each_dpcm_be'
|
||||
- 'for_each_dpcm_be_rollback'
|
||||
- 'for_each_dpcm_be_safe'
|
||||
- 'for_each_dpcm_fe'
|
||||
- 'for_each_drhd_unit'
|
||||
- 'for_each_dss_dev'
|
||||
- 'for_each_efi_memory_desc'
|
||||
- 'for_each_efi_memory_desc_in_map'
|
||||
- 'for_each_element'
|
||||
- 'for_each_element_extid'
|
||||
- 'for_each_element_id'
|
||||
- 'for_each_endpoint_of_node'
|
||||
- 'for_each_evictable_lru'
|
||||
- 'for_each_fib6_node_rt_rcu'
|
||||
- 'for_each_fib6_walker_rt'
|
||||
- 'for_each_free_mem_pfn_range_in_zone'
|
||||
- 'for_each_free_mem_pfn_range_in_zone_from'
|
||||
- 'for_each_free_mem_range'
|
||||
- 'for_each_free_mem_range_reverse'
|
||||
- 'for_each_func_rsrc'
|
||||
- 'for_each_hstate'
|
||||
- 'for_each_if'
|
||||
- 'for_each_iommu'
|
||||
- 'for_each_ip_tunnel_rcu'
|
||||
- 'for_each_irq_nr'
|
||||
- 'for_each_link_codecs'
|
||||
- 'for_each_link_cpus'
|
||||
- 'for_each_link_platforms'
|
||||
- 'for_each_lru'
|
||||
- 'for_each_matching_node'
|
||||
- 'for_each_matching_node_and_match'
|
||||
- 'for_each_member'
|
||||
- 'for_each_mem_region'
|
||||
- 'for_each_memblock_type'
|
||||
- 'for_each_memcg_cache_index'
|
||||
- 'for_each_mem_pfn_range'
|
||||
- '__for_each_mem_range'
|
||||
- 'for_each_mem_range'
|
||||
- '__for_each_mem_range_rev'
|
||||
- 'for_each_mem_range_rev'
|
||||
- 'for_each_migratetype_order'
|
||||
- 'for_each_msi_entry'
|
||||
- 'for_each_msi_entry_safe'
|
||||
- 'for_each_net'
|
||||
- 'for_each_net_continue_reverse'
|
||||
- 'for_each_netdev'
|
||||
- 'for_each_netdev_continue'
|
||||
- 'for_each_netdev_continue_rcu'
|
||||
- 'for_each_netdev_continue_reverse'
|
||||
- 'for_each_netdev_feature'
|
||||
- 'for_each_netdev_in_bond_rcu'
|
||||
- 'for_each_netdev_rcu'
|
||||
- 'for_each_netdev_reverse'
|
||||
- 'for_each_netdev_safe'
|
||||
- 'for_each_net_rcu'
|
||||
- 'for_each_new_connector_in_state'
|
||||
- 'for_each_new_crtc_in_state'
|
||||
- 'for_each_new_mst_mgr_in_state'
|
||||
- 'for_each_new_plane_in_state'
|
||||
- 'for_each_new_private_obj_in_state'
|
||||
- 'for_each_node'
|
||||
- 'for_each_node_by_name'
|
||||
- 'for_each_node_by_type'
|
||||
- 'for_each_node_mask'
|
||||
- 'for_each_node_state'
|
||||
- 'for_each_node_with_cpus'
|
||||
- 'for_each_node_with_property'
|
||||
- 'for_each_nonreserved_multicast_dest_pgid'
|
||||
- 'for_each_of_allnodes'
|
||||
- 'for_each_of_allnodes_from'
|
||||
- 'for_each_of_cpu_node'
|
||||
- 'for_each_of_pci_range'
|
||||
- 'for_each_old_connector_in_state'
|
||||
- 'for_each_old_crtc_in_state'
|
||||
- 'for_each_old_mst_mgr_in_state'
|
||||
- 'for_each_oldnew_connector_in_state'
|
||||
- 'for_each_oldnew_crtc_in_state'
|
||||
- 'for_each_oldnew_mst_mgr_in_state'
|
||||
- 'for_each_oldnew_plane_in_state'
|
||||
- 'for_each_oldnew_plane_in_state_reverse'
|
||||
- 'for_each_oldnew_private_obj_in_state'
|
||||
- 'for_each_old_plane_in_state'
|
||||
- 'for_each_old_private_obj_in_state'
|
||||
- 'for_each_online_cpu'
|
||||
- 'for_each_online_node'
|
||||
- 'for_each_online_pgdat'
|
||||
- 'for_each_pci_bridge'
|
||||
- 'for_each_pci_dev'
|
||||
- 'for_each_pci_msi_entry'
|
||||
- 'for_each_pcm_streams'
|
||||
- 'for_each_physmem_range'
|
||||
- 'for_each_populated_zone'
|
||||
- 'for_each_possible_cpu'
|
||||
- 'for_each_present_cpu'
|
||||
- 'for_each_prime_number'
|
||||
- 'for_each_prime_number_from'
|
||||
- 'for_each_process'
|
||||
- 'for_each_process_thread'
|
||||
- 'for_each_property_of_node'
|
||||
- 'for_each_registered_fb'
|
||||
- 'for_each_requested_gpio'
|
||||
- 'for_each_requested_gpio_in_range'
|
||||
- 'for_each_reserved_mem_range'
|
||||
- 'for_each_reserved_mem_region'
|
||||
- 'for_each_rtd_codec_dais'
|
||||
- 'for_each_rtd_codec_dais_rollback'
|
||||
- 'for_each_rtd_components'
|
||||
- 'for_each_rtd_cpu_dais'
|
||||
- 'for_each_rtd_cpu_dais_rollback'
|
||||
- 'for_each_rtd_dais'
|
||||
- 'for_each_set_bit'
|
||||
- 'for_each_set_bit_from'
|
||||
- 'for_each_set_clump8'
|
||||
- 'for_each_sg'
|
||||
- 'for_each_sg_dma_page'
|
||||
- 'for_each_sg_page'
|
||||
- 'for_each_sgtable_dma_page'
|
||||
- 'for_each_sgtable_dma_sg'
|
||||
- 'for_each_sgtable_page'
|
||||
- 'for_each_sgtable_sg'
|
||||
- 'for_each_sibling_event'
|
||||
- 'for_each_subelement'
|
||||
- 'for_each_subelement_extid'
|
||||
- 'for_each_subelement_id'
|
||||
- '__for_each_thread'
|
||||
- 'for_each_thread'
|
||||
- 'for_each_unicast_dest_pgid'
|
||||
- 'for_each_wakeup_source'
|
||||
- 'for_each_zone'
|
||||
- 'for_each_zone_zonelist'
|
||||
- 'for_each_zone_zonelist_nodemask'
|
||||
- 'fwnode_for_each_available_child_node'
|
||||
- 'fwnode_for_each_child_node'
|
||||
- 'fwnode_graph_for_each_endpoint'
|
||||
- 'gadget_for_each_ep'
|
||||
- 'genradix_for_each'
|
||||
- 'genradix_for_each_from'
|
||||
- 'hash_for_each'
|
||||
- 'hash_for_each_possible'
|
||||
- 'hash_for_each_possible_rcu'
|
||||
- 'hash_for_each_possible_rcu_notrace'
|
||||
- 'hash_for_each_possible_safe'
|
||||
- 'hash_for_each_rcu'
|
||||
- 'hash_for_each_safe'
|
||||
- 'hctx_for_each_ctx'
|
||||
- 'hlist_bl_for_each_entry'
|
||||
- 'hlist_bl_for_each_entry_rcu'
|
||||
- 'hlist_bl_for_each_entry_safe'
|
||||
- 'hlist_for_each'
|
||||
- 'hlist_for_each_entry'
|
||||
- 'hlist_for_each_entry_continue'
|
||||
- 'hlist_for_each_entry_continue_rcu'
|
||||
- 'hlist_for_each_entry_continue_rcu_bh'
|
||||
- 'hlist_for_each_entry_from'
|
||||
- 'hlist_for_each_entry_from_rcu'
|
||||
- 'hlist_for_each_entry_rcu'
|
||||
- 'hlist_for_each_entry_rcu_bh'
|
||||
- 'hlist_for_each_entry_rcu_notrace'
|
||||
- 'hlist_for_each_entry_safe'
|
||||
- '__hlist_for_each_rcu'
|
||||
- 'hlist_for_each_safe'
|
||||
- 'hlist_nulls_for_each_entry'
|
||||
- 'hlist_nulls_for_each_entry_from'
|
||||
- 'hlist_nulls_for_each_entry_rcu'
|
||||
- 'hlist_nulls_for_each_entry_safe'
|
||||
- 'i3c_bus_for_each_i2cdev'
|
||||
- 'i3c_bus_for_each_i3cdev'
|
||||
- 'ide_host_for_each_port'
|
||||
- 'ide_port_for_each_dev'
|
||||
- 'ide_port_for_each_present_dev'
|
||||
- 'idr_for_each_entry'
|
||||
- 'idr_for_each_entry_continue'
|
||||
- 'idr_for_each_entry_continue_ul'
|
||||
- 'idr_for_each_entry_ul'
|
||||
- 'in_dev_for_each_ifa_rcu'
|
||||
- 'in_dev_for_each_ifa_rtnl'
|
||||
- 'inet_bind_bucket_for_each'
|
||||
- 'inet_lhash2_for_each_icsk_rcu'
|
||||
- 'key_for_each'
|
||||
- 'key_for_each_safe'
|
||||
- 'klp_for_each_func'
|
||||
- 'klp_for_each_func_safe'
|
||||
- 'klp_for_each_func_static'
|
||||
- 'klp_for_each_object'
|
||||
- 'klp_for_each_object_safe'
|
||||
- 'klp_for_each_object_static'
|
||||
- 'kunit_suite_for_each_test_case'
|
||||
- 'kvm_for_each_memslot'
|
||||
- 'kvm_for_each_vcpu'
|
||||
- 'list_for_each'
|
||||
- 'list_for_each_codec'
|
||||
- 'list_for_each_codec_safe'
|
||||
- 'list_for_each_continue'
|
||||
- 'list_for_each_entry'
|
||||
- 'list_for_each_entry_continue'
|
||||
- 'list_for_each_entry_continue_rcu'
|
||||
- 'list_for_each_entry_continue_reverse'
|
||||
- 'list_for_each_entry_from'
|
||||
- 'list_for_each_entry_from_rcu'
|
||||
- 'list_for_each_entry_from_reverse'
|
||||
- 'list_for_each_entry_lockless'
|
||||
- 'list_for_each_entry_rcu'
|
||||
- 'list_for_each_entry_reverse'
|
||||
- 'list_for_each_entry_safe'
|
||||
- 'list_for_each_entry_safe_continue'
|
||||
- 'list_for_each_entry_safe_from'
|
||||
- 'list_for_each_entry_safe_reverse'
|
||||
- 'list_for_each_prev'
|
||||
- 'list_for_each_prev_safe'
|
||||
- 'list_for_each_safe'
|
||||
- 'llist_for_each'
|
||||
- 'llist_for_each_entry'
|
||||
- 'llist_for_each_entry_safe'
|
||||
- 'llist_for_each_safe'
|
||||
- 'mci_for_each_dimm'
|
||||
- 'media_device_for_each_entity'
|
||||
- 'media_device_for_each_intf'
|
||||
- 'media_device_for_each_link'
|
||||
- 'media_device_for_each_pad'
|
||||
- 'nanddev_io_for_each_page'
|
||||
- 'netdev_for_each_lower_dev'
|
||||
- 'netdev_for_each_lower_private'
|
||||
- 'netdev_for_each_lower_private_rcu'
|
||||
- 'netdev_for_each_mc_addr'
|
||||
- 'netdev_for_each_uc_addr'
|
||||
- 'netdev_for_each_upper_dev_rcu'
|
||||
- 'netdev_hw_addr_list_for_each'
|
||||
- 'nft_rule_for_each_expr'
|
||||
- 'nla_for_each_attr'
|
||||
- 'nla_for_each_nested'
|
||||
- 'nlmsg_for_each_attr'
|
||||
- 'nlmsg_for_each_msg'
|
||||
- 'nr_neigh_for_each'
|
||||
- 'nr_neigh_for_each_safe'
|
||||
- 'nr_node_for_each'
|
||||
- 'nr_node_for_each_safe'
|
||||
- 'of_for_each_phandle'
|
||||
- 'of_property_for_each_string'
|
||||
- 'of_property_for_each_u32'
|
||||
- 'pci_bus_for_each_resource'
|
||||
- 'pcm_for_each_format'
|
||||
- 'ping_portaddr_for_each_entry'
|
||||
- 'plist_for_each'
|
||||
- 'plist_for_each_continue'
|
||||
- 'plist_for_each_entry'
|
||||
- 'plist_for_each_entry_continue'
|
||||
- 'plist_for_each_entry_safe'
|
||||
- 'plist_for_each_safe'
|
||||
- 'pnp_for_each_card'
|
||||
- 'pnp_for_each_dev'
|
||||
- 'protocol_for_each_card'
|
||||
- 'protocol_for_each_dev'
|
||||
- 'queue_for_each_hw_ctx'
|
||||
- 'radix_tree_for_each_slot'
|
||||
- 'radix_tree_for_each_tagged'
|
||||
- 'rbtree_postorder_for_each_entry_safe'
|
||||
- 'rdma_for_each_block'
|
||||
- 'rdma_for_each_port'
|
||||
- 'rdma_umem_for_each_dma_block'
|
||||
- 'resource_list_for_each_entry'
|
||||
- 'resource_list_for_each_entry_safe'
|
||||
- 'rhl_for_each_entry_rcu'
|
||||
- 'rhl_for_each_rcu'
|
||||
- 'rht_for_each'
|
||||
- 'rht_for_each_entry'
|
||||
- 'rht_for_each_entry_from'
|
||||
- 'rht_for_each_entry_rcu'
|
||||
- 'rht_for_each_entry_rcu_from'
|
||||
- 'rht_for_each_entry_safe'
|
||||
- 'rht_for_each_from'
|
||||
- 'rht_for_each_rcu'
|
||||
- 'rht_for_each_rcu_from'
|
||||
- '__rq_for_each_bio'
|
||||
- 'rq_for_each_bvec'
|
||||
- 'rq_for_each_segment'
|
||||
- 'scsi_for_each_prot_sg'
|
||||
- 'scsi_for_each_sg'
|
||||
- 'sctp_for_each_hentry'
|
||||
- 'sctp_skb_for_each'
|
||||
- 'shdma_for_each_chan'
|
||||
- '__shost_for_each_device'
|
||||
- 'shost_for_each_device'
|
||||
- 'sk_for_each'
|
||||
- 'sk_for_each_bound'
|
||||
- 'sk_for_each_entry_offset_rcu'
|
||||
- 'sk_for_each_from'
|
||||
- 'sk_for_each_rcu'
|
||||
- 'sk_for_each_safe'
|
||||
- 'sk_nulls_for_each'
|
||||
- 'sk_nulls_for_each_from'
|
||||
- 'sk_nulls_for_each_rcu'
|
||||
- 'snd_array_for_each'
|
||||
- 'snd_pcm_group_for_each_entry'
|
||||
- 'snd_soc_dapm_widget_for_each_path'
|
||||
- 'snd_soc_dapm_widget_for_each_path_safe'
|
||||
- 'snd_soc_dapm_widget_for_each_sink_path'
|
||||
- 'snd_soc_dapm_widget_for_each_source_path'
|
||||
- 'tb_property_for_each'
|
||||
- 'tcf_exts_for_each_action'
|
||||
- 'udp_portaddr_for_each_entry'
|
||||
- 'udp_portaddr_for_each_entry_rcu'
|
||||
- 'usb_hub_for_each_child'
|
||||
- 'v4l2_device_for_each_subdev'
|
||||
- 'v4l2_m2m_for_each_dst_buf'
|
||||
- 'v4l2_m2m_for_each_dst_buf_safe'
|
||||
- 'v4l2_m2m_for_each_src_buf'
|
||||
- 'v4l2_m2m_for_each_src_buf_safe'
|
||||
- 'virtio_device_for_each_vq'
|
||||
- 'while_for_each_ftrace_op'
|
||||
- 'xa_for_each'
|
||||
- 'xa_for_each_marked'
|
||||
- 'xa_for_each_range'
|
||||
- 'xa_for_each_start'
|
||||
- 'xas_for_each'
|
||||
- 'xas_for_each_conflict'
|
||||
- 'xas_for_each_marked'
|
||||
- 'xbc_array_for_each_value'
|
||||
- 'xbc_for_each_key_value'
|
||||
- 'xbc_node_for_each_array_value'
|
||||
- 'xbc_node_for_each_child'
|
||||
- 'xbc_node_for_each_key_value'
|
||||
- 'zorro_for_each_dev'
|
||||
|
||||
#IncludeBlocks: Preserve # Unknown to clang-format-5.0
|
||||
IncludeCategories:
|
||||
- Regex: '.*'
|
||||
Priority: 1
|
||||
IncludeIsMainRegex: '(Test)?$'
|
||||
IndentCaseLabels: false
|
||||
#IndentPPDirectives: None # Unknown to clang-format-5.0
|
||||
IndentWidth: 8
|
||||
IndentWrappedFunctionNames: false
|
||||
JavaScriptQuotes: Leave
|
||||
JavaScriptWrapImports: true
|
||||
KeepEmptyLinesAtTheStartOfBlocks: false
|
||||
MacroBlockBegin: ''
|
||||
MacroBlockEnd: ''
|
||||
MaxEmptyLinesToKeep: 1
|
||||
NamespaceIndentation: None
|
||||
#ObjCBinPackProtocolList: Auto # Unknown to clang-format-5.0
|
||||
ObjCBlockIndentWidth: 8
|
||||
ObjCSpaceAfterProperty: true
|
||||
ObjCSpaceBeforeProtocolList: true
|
||||
|
||||
# Taken from git's rules
|
||||
#PenaltyBreakAssignment: 10 # Unknown to clang-format-4.0
|
||||
PenaltyBreakBeforeFirstCallParameter: 30
|
||||
PenaltyBreakComment: 10
|
||||
PenaltyBreakFirstLessLess: 0
|
||||
PenaltyBreakString: 10
|
||||
PenaltyExcessCharacter: 100
|
||||
PenaltyReturnTypeOnItsOwnLine: 60
|
||||
|
||||
PointerAlignment: Right
|
||||
ReflowComments: false
|
||||
SortIncludes: false
|
||||
#SortUsingDeclarations: false # Unknown to clang-format-4.0
|
||||
SpaceAfterCStyleCast: false
|
||||
SpaceAfterTemplateKeyword: true
|
||||
SpaceBeforeAssignmentOperators: true
|
||||
#SpaceBeforeCtorInitializerColon: true # Unknown to clang-format-5.0
|
||||
#SpaceBeforeInheritanceColon: true # Unknown to clang-format-5.0
|
||||
SpaceBeforeParens: ControlStatements
|
||||
#SpaceBeforeRangeBasedForLoopColon: true # Unknown to clang-format-5.0
|
||||
SpaceInEmptyParentheses: false
|
||||
SpacesBeforeTrailingComments: 1
|
||||
SpacesInAngles: false
|
||||
SpacesInContainerLiterals: false
|
||||
SpacesInCStyleCastParentheses: false
|
||||
SpacesInParentheses: false
|
||||
SpacesInSquareBrackets: false
|
||||
Standard: Cpp03
|
||||
TabWidth: 8
|
||||
UseTab: Always
|
||||
...
|
||||
19
KernelSU/kernel/Kconfig
Normal file
19
KernelSU/kernel/Kconfig
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
menu "KernelSU"
|
||||
|
||||
config KSU
|
||||
tristate "KernelSU function support"
|
||||
depends on OVERLAY_FS
|
||||
default y
|
||||
help
|
||||
Enable kernel-level root privileges on Android System.
|
||||
To compile as a module, choose M here: the
|
||||
module will be called kernelsu.
|
||||
|
||||
config KSU_DEBUG
|
||||
bool "KernelSU debug mode"
|
||||
depends on KSU
|
||||
default n
|
||||
help
|
||||
Enable KernelSU debug mode.
|
||||
|
||||
endmenu
|
||||
339
KernelSU/kernel/LICENSE
Normal file
339
KernelSU/kernel/LICENSE
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) 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
|
||||
this service 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 make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. 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.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
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
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the 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 a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE 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.
|
||||
|
||||
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
|
||||
convey 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 2 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, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision 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, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This 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.
|
||||
70
KernelSU/kernel/Makefile
Normal file
70
KernelSU/kernel/Makefile
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
kernelsu-objs := ksu.o
|
||||
kernelsu-objs += allowlist.o
|
||||
kernelsu-objs += apk_sign.o
|
||||
kernelsu-objs += sucompat.o
|
||||
kernelsu-objs += throne_tracker.o
|
||||
kernelsu-objs += core_hook.o
|
||||
kernelsu-objs += ksud.o
|
||||
kernelsu-objs += embed_ksud.o
|
||||
kernelsu-objs += kernel_compat.o
|
||||
|
||||
kernelsu-objs += selinux/selinux.o
|
||||
kernelsu-objs += selinux/sepolicy.o
|
||||
kernelsu-objs += selinux/rules.o
|
||||
ccflags-y += -I$(srctree)/security/selinux -I$(srctree)/security/selinux/include
|
||||
ccflags-y += -I$(objtree)/security/selinux -include $(srctree)/include/uapi/asm-generic/errno.h
|
||||
|
||||
obj-$(CONFIG_KSU) += kernelsu.o
|
||||
|
||||
# .git is a text file while the module is imported by 'git submodule add'.
|
||||
ifeq ($(shell test -e $(srctree)/$(src)/../.git; echo $$?),0)
|
||||
$(shell cd $(srctree)/$(src); /usr/bin/env PATH="$$PATH":/usr/bin:/usr/local/bin [ -f ../.git/shallow ] && git fetch --unshallow)
|
||||
KSU_GIT_VERSION := $(shell cd $(srctree)/$(src); /usr/bin/env PATH="$$PATH":/usr/bin:/usr/local/bin git rev-list --count HEAD)
|
||||
# ksu_version: major * 10000 + git version + 200 for historical reasons
|
||||
$(eval KSU_VERSION=$(shell expr 10000 + $(KSU_GIT_VERSION) + 200))
|
||||
$(info -- KernelSU version: $(KSU_VERSION))
|
||||
ccflags-y += -DKSU_VERSION=$(KSU_VERSION)
|
||||
else # Preserve the pinned v0.9.5 version when KernelSU is vendored.
|
||||
KSU_VERSION := 11872
|
||||
$(info -- KernelSU version: $(KSU_VERSION) (vendored v0.9.5))
|
||||
ccflags-y += -DKSU_VERSION=$(KSU_VERSION)
|
||||
endif
|
||||
|
||||
ifeq ($(shell grep -q " current_sid(void)" $(srctree)/security/selinux/include/objsec.h; echo $$?),0)
|
||||
ccflags-y += -DKSU_COMPAT_HAS_CURRENT_SID
|
||||
endif
|
||||
|
||||
ifeq ($(shell grep -q "struct selinux_state " $(srctree)/security/selinux/include/security.h; echo $$?),0)
|
||||
ccflags-y += -DKSU_COMPAT_HAS_SELINUX_STATE
|
||||
endif
|
||||
|
||||
ifndef KSU_EXPECTED_SIZE
|
||||
KSU_EXPECTED_SIZE := 0x033b
|
||||
endif
|
||||
|
||||
ifndef KSU_EXPECTED_HASH
|
||||
KSU_EXPECTED_HASH := c371061b19d8c7d7d6133c6a9bafe198fa944e50c1b31c9d8daa8d7f1fc2d2d6
|
||||
endif
|
||||
|
||||
ifdef KSU_MANAGER_PACKAGE
|
||||
ccflags-y += -DKSU_MANAGER_PACKAGE=\"$(KSU_MANAGER_PACKAGE)\"
|
||||
$(info -- KernelSU Manager package name: $(KSU_MANAGER_PACKAGE))
|
||||
endif
|
||||
|
||||
$(info -- KernelSU Manager signature size: $(KSU_EXPECTED_SIZE))
|
||||
$(info -- KernelSU Manager signature hash: $(KSU_EXPECTED_HASH))
|
||||
|
||||
ccflags-y += -DEXPECTED_SIZE=$(KSU_EXPECTED_SIZE)
|
||||
ccflags-y += -DEXPECTED_HASH=\"$(KSU_EXPECTED_HASH)\"
|
||||
|
||||
ifeq ($(shell grep -q "int path_umount" $(srctree)/fs/namespace.c; echo $$?),0)
|
||||
ccflags-y += -DKSU_UMOUNT
|
||||
else
|
||||
$(info -- Did you know you can backport path_umount to fs/namespace.c from 5.9?)
|
||||
$(info -- Read: https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#how-to-backport-path-umount)
|
||||
endif
|
||||
|
||||
ccflags-y += -Wno-implicit-function-declaration -Wno-strict-prototypes -Wno-int-conversion -Wno-gcc-compat
|
||||
ccflags-y += -Wno-declaration-after-statement -Wno-unused-function
|
||||
|
||||
# Keep a new line here!! Because someone may append config
|
||||
529
KernelSU/kernel/allowlist.c
Normal file
529
KernelSU/kernel/allowlist.c
Normal file
|
|
@ -0,0 +1,529 @@
|
|||
#include <linux/compiler.h>
|
||||
#include <linux/fs.h>
|
||||
#include <linux/gfp.h>
|
||||
#include <linux/kernel.h>
|
||||
#include <linux/list.h>
|
||||
#include <linux/printk.h>
|
||||
#include <linux/slab.h>
|
||||
#include <linux/types.h>
|
||||
#include <linux/version.h>
|
||||
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 14, 0)
|
||||
#include <linux/compiler_types.h>
|
||||
#endif
|
||||
|
||||
#include "ksu.h"
|
||||
#include "klog.h" // IWYU pragma: keep
|
||||
#include "selinux/selinux.h"
|
||||
#include "kernel_compat.h"
|
||||
#include "allowlist.h"
|
||||
#include "manager.h"
|
||||
|
||||
#define FILE_MAGIC 0x7f4b5355 // ' KSU', u32
|
||||
#define FILE_FORMAT_VERSION 3 // u32
|
||||
|
||||
#define KSU_APP_PROFILE_PRESERVE_UID 9999 // NOBODY_UID
|
||||
#define KSU_DEFAULT_SELINUX_DOMAIN "u:r:su:s0"
|
||||
|
||||
static DEFINE_MUTEX(allowlist_mutex);
|
||||
|
||||
// default profiles, these may be used frequently, so we cache it
|
||||
static struct root_profile default_root_profile;
|
||||
static struct non_root_profile default_non_root_profile;
|
||||
|
||||
static int allow_list_arr[PAGE_SIZE / sizeof(int)] __read_mostly __aligned(PAGE_SIZE);
|
||||
static int allow_list_pointer __read_mostly = 0;
|
||||
|
||||
static void remove_uid_from_arr(uid_t uid)
|
||||
{
|
||||
int *temp_arr;
|
||||
int i, j;
|
||||
|
||||
if (allow_list_pointer == 0)
|
||||
return;
|
||||
|
||||
temp_arr = kmalloc(sizeof(allow_list_arr), GFP_KERNEL);
|
||||
if (temp_arr == NULL) {
|
||||
pr_err("%s: unable to allocate memory\n", __func__);
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = j = 0; i < allow_list_pointer; i++) {
|
||||
if (allow_list_arr[i] == uid)
|
||||
continue;
|
||||
temp_arr[j++] = allow_list_arr[i];
|
||||
}
|
||||
|
||||
allow_list_pointer = j;
|
||||
|
||||
for (; j < ARRAY_SIZE(allow_list_arr); j++)
|
||||
temp_arr[j] = -1;
|
||||
|
||||
memcpy(&allow_list_arr, temp_arr, PAGE_SIZE);
|
||||
kfree(temp_arr);
|
||||
}
|
||||
|
||||
static void init_default_profiles()
|
||||
{
|
||||
default_root_profile.uid = 0;
|
||||
default_root_profile.gid = 0;
|
||||
default_root_profile.groups_count = 1;
|
||||
default_root_profile.groups[0] = 0;
|
||||
memset(&default_root_profile.capabilities, 0xff,
|
||||
sizeof(default_root_profile.capabilities));
|
||||
default_root_profile.namespaces = 0;
|
||||
strcpy(default_root_profile.selinux_domain, KSU_DEFAULT_SELINUX_DOMAIN);
|
||||
|
||||
// This means that we will umount modules by default!
|
||||
default_non_root_profile.umount_modules = true;
|
||||
}
|
||||
|
||||
struct perm_data {
|
||||
struct list_head list;
|
||||
struct app_profile profile;
|
||||
};
|
||||
|
||||
static struct list_head allow_list;
|
||||
|
||||
static uint8_t allow_list_bitmap[PAGE_SIZE] __read_mostly __aligned(PAGE_SIZE);
|
||||
#define BITMAP_UID_MAX ((sizeof(allow_list_bitmap) * BITS_PER_BYTE) - 1)
|
||||
|
||||
#define KERNEL_SU_ALLOWLIST "/data/adb/ksu/.allowlist"
|
||||
|
||||
static struct work_struct ksu_save_work;
|
||||
static struct work_struct ksu_load_work;
|
||||
|
||||
bool persistent_allow_list(void);
|
||||
|
||||
void ksu_show_allow_list(void)
|
||||
{
|
||||
struct perm_data *p = NULL;
|
||||
struct list_head *pos = NULL;
|
||||
pr_info("ksu_show_allow_list\n");
|
||||
list_for_each (pos, &allow_list) {
|
||||
p = list_entry(pos, struct perm_data, list);
|
||||
pr_info("uid :%d, allow: %d\n", p->profile.current_uid,
|
||||
p->profile.allow_su);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef CONFIG_KSU_DEBUG
|
||||
static void ksu_grant_root_to_shell()
|
||||
{
|
||||
struct app_profile profile = {
|
||||
.allow_su = true,
|
||||
.current_uid = 2000,
|
||||
};
|
||||
strcpy(profile.key, "com.android.shell");
|
||||
strcpy(profile.rp_config.profile.selinux_domain, KSU_DEFAULT_SELINUX_DOMAIN);
|
||||
ksu_set_app_profile(&profile, false);
|
||||
}
|
||||
#endif
|
||||
|
||||
bool ksu_get_app_profile(struct app_profile *profile)
|
||||
{
|
||||
struct perm_data *p = NULL;
|
||||
struct list_head *pos = NULL;
|
||||
bool found = false;
|
||||
|
||||
list_for_each (pos, &allow_list) {
|
||||
p = list_entry(pos, struct perm_data, list);
|
||||
bool uid_match = profile->current_uid == p->profile.current_uid;
|
||||
if (uid_match) {
|
||||
// found it, override it with ours
|
||||
memcpy(profile, &p->profile, sizeof(*profile));
|
||||
found = true;
|
||||
goto exit;
|
||||
}
|
||||
}
|
||||
|
||||
exit:
|
||||
return found;
|
||||
}
|
||||
|
||||
static inline bool forbid_system_uid(uid_t uid) {
|
||||
#define SHELL_UID 2000
|
||||
#define SYSTEM_UID 1000
|
||||
return uid < SHELL_UID && uid != SYSTEM_UID;
|
||||
}
|
||||
|
||||
static bool profile_valid(struct app_profile *profile)
|
||||
{
|
||||
if (!profile) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (forbid_system_uid(profile->current_uid)) {
|
||||
pr_err("uid lower than 2000 is unsupported: %d\n", profile->current_uid);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (profile->version < KSU_APP_PROFILE_VER) {
|
||||
pr_info("Unsupported profile version: %d\n", profile->version);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (profile->allow_su) {
|
||||
if (profile->rp_config.profile.groups_count > KSU_MAX_GROUPS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strlen(profile->rp_config.profile.selinux_domain) == 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ksu_set_app_profile(struct app_profile *profile, bool persist)
|
||||
{
|
||||
struct perm_data *p = NULL;
|
||||
struct list_head *pos = NULL;
|
||||
bool result = false;
|
||||
|
||||
if (!profile_valid(profile)) {
|
||||
pr_err("Failed to set app profile: invalid profile!\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
list_for_each (pos, &allow_list) {
|
||||
p = list_entry(pos, struct perm_data, list);
|
||||
// both uid and package must match, otherwise it will break multiple package with different user id
|
||||
if (profile->current_uid == p->profile.current_uid &&
|
||||
!strcmp(profile->key, p->profile.key)) {
|
||||
// found it, just override it all!
|
||||
memcpy(&p->profile, profile, sizeof(*profile));
|
||||
result = true;
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
|
||||
// not found, alloc a new node!
|
||||
p = (struct perm_data *)kmalloc(sizeof(struct perm_data), GFP_KERNEL);
|
||||
if (!p) {
|
||||
pr_err("ksu_set_app_profile alloc failed\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
memcpy(&p->profile, profile, sizeof(*profile));
|
||||
if (profile->allow_su) {
|
||||
pr_info("set root profile, key: %s, uid: %d, gid: %d, context: %s\n",
|
||||
profile->key, profile->current_uid,
|
||||
profile->rp_config.profile.gid,
|
||||
profile->rp_config.profile.selinux_domain);
|
||||
} else {
|
||||
pr_info("set app profile, key: %s, uid: %d, umount modules: %d\n",
|
||||
profile->key, profile->current_uid,
|
||||
profile->nrp_config.profile.umount_modules);
|
||||
}
|
||||
list_add_tail(&p->list, &allow_list);
|
||||
|
||||
out:
|
||||
if (profile->current_uid <= BITMAP_UID_MAX) {
|
||||
if (profile->allow_su)
|
||||
allow_list_bitmap[profile->current_uid / BITS_PER_BYTE] |= 1 << (profile->current_uid % BITS_PER_BYTE);
|
||||
else
|
||||
allow_list_bitmap[profile->current_uid / BITS_PER_BYTE] &= ~(1 << (profile->current_uid % BITS_PER_BYTE));
|
||||
} else {
|
||||
if (profile->allow_su) {
|
||||
/*
|
||||
* 1024 apps with uid higher than BITMAP_UID_MAX
|
||||
* registered to request superuser?
|
||||
*/
|
||||
if (allow_list_pointer >= ARRAY_SIZE(allow_list_arr)) {
|
||||
pr_err("too many apps registered\n");
|
||||
WARN_ON(1);
|
||||
return false;
|
||||
}
|
||||
allow_list_arr[allow_list_pointer++] = profile->current_uid;
|
||||
} else {
|
||||
remove_uid_from_arr(profile->current_uid);
|
||||
}
|
||||
}
|
||||
result = true;
|
||||
|
||||
// check if the default profiles is changed, cache it to a single struct to accelerate access.
|
||||
if (unlikely(!strcmp(profile->key, "$"))) {
|
||||
// set default non root profile
|
||||
memcpy(&default_non_root_profile, &profile->nrp_config.profile,
|
||||
sizeof(default_non_root_profile));
|
||||
}
|
||||
|
||||
if (unlikely(!strcmp(profile->key, "#"))) {
|
||||
// set default root profile
|
||||
memcpy(&default_root_profile, &profile->rp_config.profile,
|
||||
sizeof(default_root_profile));
|
||||
}
|
||||
|
||||
if (persist)
|
||||
persistent_allow_list();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool __ksu_is_allow_uid(uid_t uid)
|
||||
{
|
||||
int i;
|
||||
|
||||
if (unlikely(uid == 0)) {
|
||||
// already root, but only allow our domain.
|
||||
return is_ksu_domain();
|
||||
}
|
||||
|
||||
if (forbid_system_uid(uid)) {
|
||||
// do not bother going through the list if it's system
|
||||
return false;
|
||||
}
|
||||
|
||||
if (likely(ksu_is_manager_uid_valid()) && unlikely(ksu_get_manager_uid() == uid)) {
|
||||
// manager is always allowed!
|
||||
return true;
|
||||
}
|
||||
|
||||
if (likely(uid <= BITMAP_UID_MAX)) {
|
||||
return !!(allow_list_bitmap[uid / BITS_PER_BYTE] & (1 << (uid % BITS_PER_BYTE)));
|
||||
} else {
|
||||
for (i = 0; i < allow_list_pointer; i++) {
|
||||
if (allow_list_arr[i] == uid)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ksu_uid_should_umount(uid_t uid)
|
||||
{
|
||||
struct app_profile profile = { .current_uid = uid };
|
||||
if (likely(ksu_is_manager_uid_valid()) && unlikely(ksu_get_manager_uid() == uid)) {
|
||||
// we should not umount on manager!
|
||||
return false;
|
||||
}
|
||||
bool found = ksu_get_app_profile(&profile);
|
||||
if (!found) {
|
||||
// no app profile found, it must be non root app
|
||||
return default_non_root_profile.umount_modules;
|
||||
}
|
||||
if (profile.allow_su) {
|
||||
// if found and it is granted to su, we shouldn't umount for it
|
||||
return false;
|
||||
} else {
|
||||
// found an app profile
|
||||
if (profile.nrp_config.use_default) {
|
||||
return default_non_root_profile.umount_modules;
|
||||
} else {
|
||||
return profile.nrp_config.profile.umount_modules;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct root_profile *ksu_get_root_profile(uid_t uid)
|
||||
{
|
||||
struct perm_data *p = NULL;
|
||||
struct list_head *pos = NULL;
|
||||
|
||||
list_for_each (pos, &allow_list) {
|
||||
p = list_entry(pos, struct perm_data, list);
|
||||
if (uid == p->profile.current_uid && p->profile.allow_su) {
|
||||
if (!p->profile.rp_config.use_default) {
|
||||
return &p->profile.rp_config.profile;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// use default profile
|
||||
return &default_root_profile;
|
||||
}
|
||||
|
||||
bool ksu_get_allow_list(int *array, int *length, bool allow)
|
||||
{
|
||||
struct perm_data *p = NULL;
|
||||
struct list_head *pos = NULL;
|
||||
int i = 0;
|
||||
list_for_each (pos, &allow_list) {
|
||||
p = list_entry(pos, struct perm_data, list);
|
||||
// pr_info("get_allow_list uid: %d allow: %d\n", p->uid, p->allow);
|
||||
if (p->profile.allow_su == allow) {
|
||||
array[i++] = p->profile.current_uid;
|
||||
}
|
||||
}
|
||||
*length = i;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void do_save_allow_list(struct work_struct *work)
|
||||
{
|
||||
u32 magic = FILE_MAGIC;
|
||||
u32 version = FILE_FORMAT_VERSION;
|
||||
struct perm_data *p = NULL;
|
||||
struct list_head *pos = NULL;
|
||||
loff_t off = 0;
|
||||
|
||||
struct file *fp =
|
||||
ksu_filp_open_compat(KERNEL_SU_ALLOWLIST, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||
if (IS_ERR(fp)) {
|
||||
pr_err("save_allow_list create file failed: %ld\n", PTR_ERR(fp));
|
||||
return;
|
||||
}
|
||||
|
||||
// store magic and version
|
||||
if (ksu_kernel_write_compat(fp, &magic, sizeof(magic), &off) !=
|
||||
sizeof(magic)) {
|
||||
pr_err("save_allow_list write magic failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
|
||||
if (ksu_kernel_write_compat(fp, &version, sizeof(version), &off) !=
|
||||
sizeof(version)) {
|
||||
pr_err("save_allow_list write version failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
|
||||
list_for_each (pos, &allow_list) {
|
||||
p = list_entry(pos, struct perm_data, list);
|
||||
pr_info("save allow list, name: %s uid :%d, allow: %d\n",
|
||||
p->profile.key, p->profile.current_uid,
|
||||
p->profile.allow_su);
|
||||
|
||||
ksu_kernel_write_compat(fp, &p->profile, sizeof(p->profile),
|
||||
&off);
|
||||
}
|
||||
|
||||
exit:
|
||||
filp_close(fp, 0);
|
||||
}
|
||||
|
||||
void do_load_allow_list(struct work_struct *work)
|
||||
{
|
||||
loff_t off = 0;
|
||||
ssize_t ret = 0;
|
||||
struct file *fp = NULL;
|
||||
u32 magic;
|
||||
u32 version;
|
||||
|
||||
#ifdef CONFIG_KSU_DEBUG
|
||||
// always allow adb shell by default
|
||||
ksu_grant_root_to_shell();
|
||||
#endif
|
||||
|
||||
// load allowlist now!
|
||||
fp = ksu_filp_open_compat(KERNEL_SU_ALLOWLIST, O_RDONLY, 0);
|
||||
if (IS_ERR(fp)) {
|
||||
pr_err("load_allow_list open file failed: %ld\n", PTR_ERR(fp));
|
||||
return;
|
||||
}
|
||||
|
||||
// verify magic
|
||||
if (ksu_kernel_read_compat(fp, &magic, sizeof(magic), &off) !=
|
||||
sizeof(magic) ||
|
||||
magic != FILE_MAGIC) {
|
||||
pr_err("allowlist file invalid: %d!\n", magic);
|
||||
goto exit;
|
||||
}
|
||||
|
||||
if (ksu_kernel_read_compat(fp, &version, sizeof(version), &off) !=
|
||||
sizeof(version)) {
|
||||
pr_err("allowlist read version: %d failed\n", version);
|
||||
goto exit;
|
||||
}
|
||||
|
||||
pr_info("allowlist version: %d\n", version);
|
||||
|
||||
while (true) {
|
||||
struct app_profile profile;
|
||||
|
||||
ret = ksu_kernel_read_compat(fp, &profile, sizeof(profile),
|
||||
&off);
|
||||
|
||||
if (ret <= 0) {
|
||||
pr_info("load_allow_list read err: %zd\n", ret);
|
||||
break;
|
||||
}
|
||||
|
||||
pr_info("load_allow_uid, name: %s, uid: %d, allow: %d\n",
|
||||
profile.key, profile.current_uid, profile.allow_su);
|
||||
ksu_set_app_profile(&profile, false);
|
||||
}
|
||||
|
||||
exit:
|
||||
ksu_show_allow_list();
|
||||
filp_close(fp, 0);
|
||||
}
|
||||
|
||||
void ksu_prune_allowlist(bool (*is_uid_valid)(uid_t, char *, void *), void *data)
|
||||
{
|
||||
struct perm_data *np = NULL;
|
||||
struct perm_data *n = NULL;
|
||||
|
||||
bool modified = false;
|
||||
// TODO: use RCU!
|
||||
mutex_lock(&allowlist_mutex);
|
||||
list_for_each_entry_safe (np, n, &allow_list, list) {
|
||||
uid_t uid = np->profile.current_uid;
|
||||
char *package = np->profile.key;
|
||||
// we use this uid for special cases, don't prune it!
|
||||
bool is_preserved_uid = uid == KSU_APP_PROFILE_PRESERVE_UID;
|
||||
if (!is_preserved_uid && !is_uid_valid(uid, package, data)) {
|
||||
modified = true;
|
||||
pr_info("prune uid: %d, package: %s\n", uid, package);
|
||||
list_del(&np->list);
|
||||
if (likely(uid <= BITMAP_UID_MAX)) {
|
||||
allow_list_bitmap[uid / BITS_PER_BYTE] &= ~(1 << (uid % BITS_PER_BYTE));
|
||||
}
|
||||
remove_uid_from_arr(uid);
|
||||
smp_mb();
|
||||
kfree(np);
|
||||
}
|
||||
}
|
||||
mutex_unlock(&allowlist_mutex);
|
||||
|
||||
if (modified) {
|
||||
persistent_allow_list();
|
||||
}
|
||||
}
|
||||
|
||||
// make sure allow list works cross boot
|
||||
bool persistent_allow_list(void)
|
||||
{
|
||||
return ksu_queue_work(&ksu_save_work);
|
||||
}
|
||||
|
||||
bool ksu_load_allow_list(void)
|
||||
{
|
||||
return ksu_queue_work(&ksu_load_work);
|
||||
}
|
||||
|
||||
void ksu_allowlist_init(void)
|
||||
{
|
||||
int i;
|
||||
|
||||
BUILD_BUG_ON(sizeof(allow_list_bitmap) != PAGE_SIZE);
|
||||
BUILD_BUG_ON(sizeof(allow_list_arr) != PAGE_SIZE);
|
||||
|
||||
for (i = 0; i < ARRAY_SIZE(allow_list_arr); i++)
|
||||
allow_list_arr[i] = -1;
|
||||
|
||||
INIT_LIST_HEAD(&allow_list);
|
||||
|
||||
INIT_WORK(&ksu_save_work, do_save_allow_list);
|
||||
INIT_WORK(&ksu_load_work, do_load_allow_list);
|
||||
|
||||
init_default_profiles();
|
||||
}
|
||||
|
||||
void ksu_allowlist_exit(void)
|
||||
{
|
||||
struct perm_data *np = NULL;
|
||||
struct perm_data *n = NULL;
|
||||
|
||||
do_save_allow_list(NULL);
|
||||
|
||||
// free allowlist
|
||||
mutex_lock(&allowlist_mutex);
|
||||
list_for_each_entry_safe (np, n, &allow_list, list) {
|
||||
list_del(&np->list);
|
||||
kfree(np);
|
||||
}
|
||||
mutex_unlock(&allowlist_mutex);
|
||||
}
|
||||
27
KernelSU/kernel/allowlist.h
Normal file
27
KernelSU/kernel/allowlist.h
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
#ifndef __KSU_H_ALLOWLIST
|
||||
#define __KSU_H_ALLOWLIST
|
||||
|
||||
#include <linux/types.h>
|
||||
#include "ksu.h"
|
||||
|
||||
void ksu_allowlist_init(void);
|
||||
|
||||
void ksu_allowlist_exit(void);
|
||||
|
||||
bool ksu_load_allow_list(void);
|
||||
|
||||
void ksu_show_allow_list(void);
|
||||
|
||||
bool __ksu_is_allow_uid(uid_t uid);
|
||||
#define ksu_is_allow_uid(uid) unlikely(__ksu_is_allow_uid(uid))
|
||||
|
||||
bool ksu_get_allow_list(int *array, int *length, bool allow);
|
||||
|
||||
void ksu_prune_allowlist(bool (*is_uid_exist)(uid_t, char *, void *), void *data);
|
||||
|
||||
bool ksu_get_app_profile(struct app_profile *);
|
||||
bool ksu_set_app_profile(struct app_profile *, bool persist);
|
||||
|
||||
bool ksu_uid_should_umount(uid_t uid);
|
||||
struct root_profile *ksu_get_root_profile(uid_t uid);
|
||||
#endif
|
||||
320
KernelSU/kernel/apk_sign.c
Normal file
320
KernelSU/kernel/apk_sign.c
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
#include <linux/err.h>
|
||||
#include <linux/fs.h>
|
||||
#include <linux/gfp.h>
|
||||
#include <linux/kernel.h>
|
||||
#include <linux/slab.h>
|
||||
#include <linux/version.h>
|
||||
#ifdef CONFIG_KSU_DEBUG
|
||||
#include <linux/moduleparam.h>
|
||||
#endif
|
||||
#include <crypto/hash.h>
|
||||
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 11, 0)
|
||||
#include <crypto/sha2.h>
|
||||
#else
|
||||
#include <crypto/sha.h>
|
||||
#endif
|
||||
|
||||
#include "apk_sign.h"
|
||||
#include "klog.h" // IWYU pragma: keep
|
||||
#include "kernel_compat.h"
|
||||
|
||||
|
||||
struct sdesc {
|
||||
struct shash_desc shash;
|
||||
char ctx[];
|
||||
};
|
||||
|
||||
static struct sdesc *init_sdesc(struct crypto_shash *alg)
|
||||
{
|
||||
struct sdesc *sdesc;
|
||||
int size;
|
||||
|
||||
size = sizeof(struct shash_desc) + crypto_shash_descsize(alg);
|
||||
sdesc = kmalloc(size, GFP_KERNEL);
|
||||
if (!sdesc)
|
||||
return ERR_PTR(-ENOMEM);
|
||||
sdesc->shash.tfm = alg;
|
||||
return sdesc;
|
||||
}
|
||||
|
||||
static int calc_hash(struct crypto_shash *alg, const unsigned char *data,
|
||||
unsigned int datalen, unsigned char *digest)
|
||||
{
|
||||
struct sdesc *sdesc;
|
||||
int ret;
|
||||
|
||||
sdesc = init_sdesc(alg);
|
||||
if (IS_ERR(sdesc)) {
|
||||
pr_info("can't alloc sdesc\n");
|
||||
return PTR_ERR(sdesc);
|
||||
}
|
||||
|
||||
ret = crypto_shash_digest(&sdesc->shash, data, datalen, digest);
|
||||
kfree(sdesc);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int ksu_sha256(const unsigned char *data, unsigned int datalen,
|
||||
unsigned char *digest)
|
||||
{
|
||||
struct crypto_shash *alg;
|
||||
char *hash_alg_name = "sha256";
|
||||
int ret;
|
||||
|
||||
alg = crypto_alloc_shash(hash_alg_name, 0, 0);
|
||||
if (IS_ERR(alg)) {
|
||||
pr_info("can't alloc alg %s\n", hash_alg_name);
|
||||
return PTR_ERR(alg);
|
||||
}
|
||||
ret = calc_hash(alg, data, datalen, digest);
|
||||
crypto_free_shash(alg);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static bool check_block(struct file *fp, u32 *size4, loff_t *pos, u32 *offset,
|
||||
unsigned expected_size, const char *expected_sha256)
|
||||
{
|
||||
ksu_kernel_read_compat(fp, size4, 0x4, pos); // signer-sequence length
|
||||
ksu_kernel_read_compat(fp, size4, 0x4, pos); // signer length
|
||||
ksu_kernel_read_compat(fp, size4, 0x4, pos); // signed data length
|
||||
|
||||
*offset += 0x4 * 3;
|
||||
|
||||
ksu_kernel_read_compat(fp, size4, 0x4, pos); // digests-sequence length
|
||||
|
||||
*pos += *size4;
|
||||
*offset += 0x4 + *size4;
|
||||
|
||||
ksu_kernel_read_compat(fp, size4, 0x4, pos); // certificates length
|
||||
ksu_kernel_read_compat(fp, size4, 0x4, pos); // certificate length
|
||||
*offset += 0x4 * 2;
|
||||
|
||||
if (*size4 == expected_size) {
|
||||
*offset += *size4;
|
||||
|
||||
#define CERT_MAX_LENGTH 1024
|
||||
char cert[CERT_MAX_LENGTH];
|
||||
if (*size4 > CERT_MAX_LENGTH) {
|
||||
pr_info("cert length overlimit\n");
|
||||
return false;
|
||||
}
|
||||
ksu_kernel_read_compat(fp, cert, *size4, pos);
|
||||
unsigned char digest[SHA256_DIGEST_SIZE];
|
||||
if (IS_ERR(ksu_sha256(cert, *size4, digest))) {
|
||||
pr_info("sha256 error\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
char hash_str[SHA256_DIGEST_SIZE * 2 + 1];
|
||||
hash_str[SHA256_DIGEST_SIZE * 2] = '\0';
|
||||
|
||||
bin2hex(hash_str, digest, SHA256_DIGEST_SIZE);
|
||||
pr_info("sha256: %s, expected: %s\n", hash_str,
|
||||
expected_sha256);
|
||||
if (strcmp(expected_sha256, hash_str) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
struct zip_entry_header {
|
||||
uint32_t signature;
|
||||
uint16_t version;
|
||||
uint16_t flags;
|
||||
uint16_t compression;
|
||||
uint16_t mod_time;
|
||||
uint16_t mod_date;
|
||||
uint32_t crc32;
|
||||
uint32_t compressed_size;
|
||||
uint32_t uncompressed_size;
|
||||
uint16_t file_name_length;
|
||||
uint16_t extra_field_length;
|
||||
} __attribute__((packed));
|
||||
|
||||
// This is a necessary but not sufficient condition, but it is enough for us
|
||||
static bool has_v1_signature_file(struct file *fp)
|
||||
{
|
||||
struct zip_entry_header header;
|
||||
const char MANIFEST[] = "META-INF/MANIFEST.MF";
|
||||
|
||||
loff_t pos = 0;
|
||||
|
||||
while (ksu_kernel_read_compat(fp, &header,
|
||||
sizeof(struct zip_entry_header), &pos) ==
|
||||
sizeof(struct zip_entry_header)) {
|
||||
if (header.signature != 0x04034b50) {
|
||||
// ZIP magic: 'PK'
|
||||
return false;
|
||||
}
|
||||
// Read the entry file name
|
||||
if (header.file_name_length == sizeof(MANIFEST) - 1) {
|
||||
char fileName[sizeof(MANIFEST)];
|
||||
ksu_kernel_read_compat(fp, fileName,
|
||||
header.file_name_length, &pos);
|
||||
fileName[header.file_name_length] = '\0';
|
||||
|
||||
// Check if the entry matches META-INF/MANIFEST.MF
|
||||
if (strncmp(MANIFEST, fileName, sizeof(MANIFEST) - 1) ==
|
||||
0) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// Skip the entry file name
|
||||
pos += header.file_name_length;
|
||||
}
|
||||
|
||||
// Skip to the next entry
|
||||
pos += header.extra_field_length + header.compressed_size;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static __always_inline bool check_v2_signature(char *path,
|
||||
unsigned expected_size,
|
||||
const char *expected_sha256)
|
||||
{
|
||||
unsigned char buffer[0x11] = { 0 };
|
||||
u32 size4;
|
||||
u64 size8, size_of_block;
|
||||
|
||||
loff_t pos;
|
||||
|
||||
bool v2_signing_valid = false;
|
||||
int v2_signing_blocks = 0;
|
||||
bool v3_signing_exist = false;
|
||||
bool v3_1_signing_exist = false;
|
||||
|
||||
int i;
|
||||
struct file *fp = ksu_filp_open_compat(path, O_RDONLY, 0);
|
||||
if (IS_ERR(fp)) {
|
||||
pr_err("open %s error.\n", path);
|
||||
return false;
|
||||
}
|
||||
|
||||
// disable inotify for this file
|
||||
fp->f_mode |= FMODE_NONOTIFY;
|
||||
|
||||
// https://en.wikipedia.org/wiki/Zip_(file_format)#End_of_central_directory_record_(EOCD)
|
||||
for (i = 0;; ++i) {
|
||||
unsigned short n;
|
||||
pos = generic_file_llseek(fp, -i - 2, SEEK_END);
|
||||
ksu_kernel_read_compat(fp, &n, 2, &pos);
|
||||
if (n == i) {
|
||||
pos -= 22;
|
||||
ksu_kernel_read_compat(fp, &size4, 4, &pos);
|
||||
if ((size4 ^ 0xcafebabeu) == 0xccfbf1eeu) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (i == 0xffff) {
|
||||
pr_info("error: cannot find eocd\n");
|
||||
goto clean;
|
||||
}
|
||||
}
|
||||
|
||||
pos += 12;
|
||||
// offset
|
||||
ksu_kernel_read_compat(fp, &size4, 0x4, &pos);
|
||||
pos = size4 - 0x18;
|
||||
|
||||
ksu_kernel_read_compat(fp, &size8, 0x8, &pos);
|
||||
ksu_kernel_read_compat(fp, buffer, 0x10, &pos);
|
||||
if (strcmp((char *)buffer, "APK Sig Block 42")) {
|
||||
goto clean;
|
||||
}
|
||||
|
||||
pos = size4 - (size8 + 0x8);
|
||||
ksu_kernel_read_compat(fp, &size_of_block, 0x8, &pos);
|
||||
if (size_of_block != size8) {
|
||||
goto clean;
|
||||
}
|
||||
|
||||
int loop_count = 0;
|
||||
while (loop_count++ < 10) {
|
||||
uint32_t id;
|
||||
uint32_t offset;
|
||||
ksu_kernel_read_compat(fp, &size8, 0x8,
|
||||
&pos); // sequence length
|
||||
if (size8 == size_of_block) {
|
||||
break;
|
||||
}
|
||||
ksu_kernel_read_compat(fp, &id, 0x4, &pos); // id
|
||||
offset = 4;
|
||||
if (id == 0x7109871au) {
|
||||
v2_signing_blocks++;
|
||||
v2_signing_valid =
|
||||
check_block(fp, &size4, &pos, &offset,
|
||||
expected_size, expected_sha256);
|
||||
} else if (id == 0xf05368c0u) {
|
||||
// http://aospxref.com/android-14.0.0_r2/xref/frameworks/base/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java#73
|
||||
v3_signing_exist = true;
|
||||
} else if (id == 0x1b93ad61u) {
|
||||
// http://aospxref.com/android-14.0.0_r2/xref/frameworks/base/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java#74
|
||||
v3_1_signing_exist = true;
|
||||
} else {
|
||||
#ifdef CONFIG_KSU_DEBUG
|
||||
pr_info("Unknown id: 0x%08x\n", id);
|
||||
#endif
|
||||
}
|
||||
pos += (size8 - offset);
|
||||
}
|
||||
|
||||
if (v2_signing_blocks != 1) {
|
||||
#ifdef CONFIG_KSU_DEBUG
|
||||
pr_err("Unexpected v2 signature count: %d\n",
|
||||
v2_signing_blocks);
|
||||
#endif
|
||||
v2_signing_valid = false;
|
||||
}
|
||||
|
||||
if (v2_signing_valid) {
|
||||
int has_v1_signing = has_v1_signature_file(fp);
|
||||
if (has_v1_signing) {
|
||||
pr_err("Unexpected v1 signature scheme found!\n");
|
||||
filp_close(fp, 0);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
clean:
|
||||
filp_close(fp, 0);
|
||||
|
||||
if (v3_signing_exist || v3_1_signing_exist) {
|
||||
#ifdef CONFIG_KSU_DEBUG
|
||||
pr_err("Unexpected v3 signature scheme found!\n");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
return v2_signing_valid;
|
||||
}
|
||||
|
||||
#ifdef CONFIG_KSU_DEBUG
|
||||
|
||||
int ksu_debug_manager_uid = -1;
|
||||
|
||||
#include "manager.h"
|
||||
|
||||
static int set_expected_size(const char *val, const struct kernel_param *kp)
|
||||
{
|
||||
int rv = param_set_uint(val, kp);
|
||||
ksu_set_manager_uid(ksu_debug_manager_uid);
|
||||
pr_info("ksu_manager_uid set to %d\n", ksu_debug_manager_uid);
|
||||
return rv;
|
||||
}
|
||||
|
||||
static struct kernel_param_ops expected_size_ops = {
|
||||
.set = set_expected_size,
|
||||
.get = param_get_uint,
|
||||
};
|
||||
|
||||
module_param_cb(ksu_debug_manager_uid, &expected_size_ops,
|
||||
&ksu_debug_manager_uid, S_IRUSR | S_IWUSR);
|
||||
|
||||
#endif
|
||||
|
||||
bool is_manager_apk(char *path)
|
||||
{
|
||||
return check_v2_signature(path, EXPECTED_SIZE, EXPECTED_HASH);
|
||||
}
|
||||
8
KernelSU/kernel/apk_sign.h
Normal file
8
KernelSU/kernel/apk_sign.h
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
#ifndef __KSU_H_APK_V2_SIGN
|
||||
#define __KSU_H_APK_V2_SIGN
|
||||
|
||||
#include <linux/types.h>
|
||||
|
||||
bool is_manager_apk(char *path);
|
||||
|
||||
#endif
|
||||
92
KernelSU/kernel/arch.h
Normal file
92
KernelSU/kernel/arch.h
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
#ifndef __KSU_H_ARCH
|
||||
#define __KSU_H_ARCH
|
||||
|
||||
#include <linux/version.h>
|
||||
|
||||
#if defined(__aarch64__)
|
||||
|
||||
#define __PT_PARM1_REG regs[0]
|
||||
#define __PT_PARM2_REG regs[1]
|
||||
#define __PT_PARM3_REG regs[2]
|
||||
#define __PT_SYSCALL_PARM4_REG regs[3]
|
||||
#define __PT_CCALL_PARM4_REG regs[3]
|
||||
#define __PT_PARM5_REG regs[4]
|
||||
#define __PT_PARM6_REG regs[5]
|
||||
#define __PT_RET_REG regs[30]
|
||||
#define __PT_FP_REG regs[29] /* Works only with CONFIG_FRAME_POINTER */
|
||||
#define __PT_RC_REG regs[0]
|
||||
#define __PT_SP_REG sp
|
||||
#define __PT_IP_REG pc
|
||||
|
||||
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 16, 0)
|
||||
#define PRCTL_SYMBOL "__arm64_sys_prctl"
|
||||
#define SYS_READ_SYMBOL "__arm64_sys_read"
|
||||
#define SYS_NEWFSTATAT_SYMBOL "__arm64_sys_newfstatat"
|
||||
#define SYS_FACCESSAT_SYMBOL "__arm64_sys_faccessat"
|
||||
#define SYS_EXECVE_SYMBOL "__arm64_sys_execve"
|
||||
#else
|
||||
#define PRCTL_SYMBOL "sys_prctl"
|
||||
#define SYS_READ_SYMBOL "sys_read"
|
||||
#define SYS_NEWFSTATAT_SYMBOL "sys_newfstatat"
|
||||
#define SYS_FACCESSAT_SYMBOL "sys_faccessat"
|
||||
#define SYS_EXECVE_SYMBOL "sys_execve"
|
||||
#endif
|
||||
|
||||
#elif defined(__x86_64__)
|
||||
|
||||
#define __PT_PARM1_REG di
|
||||
#define __PT_PARM2_REG si
|
||||
#define __PT_PARM3_REG dx
|
||||
/* syscall uses r10 for PARM4 */
|
||||
#define __PT_SYSCALL_PARM4_REG r10
|
||||
#define __PT_CCALL_PARM4_REG cx
|
||||
#define __PT_PARM5_REG r8
|
||||
#define __PT_PARM6_REG r9
|
||||
#define __PT_RET_REG sp
|
||||
#define __PT_FP_REG bp
|
||||
#define __PT_RC_REG ax
|
||||
#define __PT_SP_REG sp
|
||||
#define __PT_IP_REG ip
|
||||
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 16, 0)
|
||||
#define PRCTL_SYMBOL "__x64_sys_prctl"
|
||||
#define SYS_READ_SYMBOL "__x64_sys_read"
|
||||
#define SYS_NEWFSTATAT_SYMBOL "__x64_sys_newfstatat"
|
||||
#define SYS_FACCESSAT_SYMBOL "__x64_sys_faccessat"
|
||||
#define SYS_EXECVE_SYMBOL "__x64_sys_execve"
|
||||
#else
|
||||
#define PRCTL_SYMBOL "sys_prctl"
|
||||
#define SYS_READ_SYMBOL "sys_read"
|
||||
#define SYS_NEWFSTATAT_SYMBOL "sys_newfstatat"
|
||||
#define SYS_FACCESSAT_SYMBOL "sys_faccessat"
|
||||
#define SYS_EXECVE_SYMBOL "sys_execve"
|
||||
#endif
|
||||
|
||||
#else
|
||||
#error "Unsupported arch"
|
||||
#endif
|
||||
|
||||
/* allow some architecutres to override `struct pt_regs` */
|
||||
#ifndef __PT_REGS_CAST
|
||||
#define __PT_REGS_CAST(x) (x)
|
||||
#endif
|
||||
|
||||
#define PT_REGS_PARM1(x) (__PT_REGS_CAST(x)->__PT_PARM1_REG)
|
||||
#define PT_REGS_PARM2(x) (__PT_REGS_CAST(x)->__PT_PARM2_REG)
|
||||
#define PT_REGS_PARM3(x) (__PT_REGS_CAST(x)->__PT_PARM3_REG)
|
||||
#define PT_REGS_SYSCALL_PARM4(x) (__PT_REGS_CAST(x)->__PT_SYSCALL_PARM4_REG)
|
||||
#define PT_REGS_CCALL_PARM4(x) (__PT_REGS_CAST(x)->__PT_CCALL_PARM4_REG)
|
||||
#define PT_REGS_PARM5(x) (__PT_REGS_CAST(x)->__PT_PARM5_REG)
|
||||
#define PT_REGS_PARM6(x) (__PT_REGS_CAST(x)->__PT_PARM6_REG)
|
||||
#define PT_REGS_RET(x) (__PT_REGS_CAST(x)->__PT_RET_REG)
|
||||
#define PT_REGS_FP(x) (__PT_REGS_CAST(x)->__PT_FP_REG)
|
||||
#define PT_REGS_RC(x) (__PT_REGS_CAST(x)->__PT_RC_REG)
|
||||
#define PT_REGS_SP(x) (__PT_REGS_CAST(x)->__PT_SP_REG)
|
||||
#define PT_REGS_IP(x) (__PT_REGS_CAST(x)->__PT_IP_REG)
|
||||
|
||||
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 16, 0)
|
||||
#define PT_REAL_REGS(regs) ((struct pt_regs *)PT_REGS_PARM1(regs))
|
||||
#else
|
||||
#define PT_REAL_REGS(regs) ((regs))
|
||||
#endif
|
||||
|
||||
#endif
|
||||
856
KernelSU/kernel/core_hook.c
Normal file
856
KernelSU/kernel/core_hook.c
Normal file
|
|
@ -0,0 +1,856 @@
|
|||
#include <linux/capability.h>
|
||||
#include <linux/cred.h>
|
||||
#include <linux/dcache.h>
|
||||
#include <linux/err.h>
|
||||
#include <linux/init.h>
|
||||
#include <linux/init_task.h>
|
||||
#include <linux/kallsyms.h>
|
||||
#include <linux/kernel.h>
|
||||
#include <linux/kprobes.h>
|
||||
#include <linux/lsm_hooks.h>
|
||||
#include <linux/mm.h>
|
||||
#include <linux/nsproxy.h>
|
||||
#include <linux/path.h>
|
||||
#include <linux/printk.h>
|
||||
#include <linux/sched.h>
|
||||
#include <linux/security.h>
|
||||
#include <linux/stddef.h>
|
||||
#include <linux/types.h>
|
||||
#include <linux/uaccess.h>
|
||||
#include <linux/uidgid.h>
|
||||
#include <linux/version.h>
|
||||
#include <linux/mount.h>
|
||||
|
||||
#include <linux/fs.h>
|
||||
#include <linux/namei.h>
|
||||
|
||||
#ifdef MODULE
|
||||
#include <linux/list.h>
|
||||
#include <linux/irqflags.h>
|
||||
#include <linux/mm_types.h>
|
||||
#include <linux/rcupdate.h>
|
||||
#include <linux/vmalloc.h>
|
||||
#endif
|
||||
|
||||
#include "allowlist.h"
|
||||
#include "arch.h"
|
||||
#include "core_hook.h"
|
||||
#include "klog.h" // IWYU pragma: keep
|
||||
#include "ksu.h"
|
||||
#include "ksud.h"
|
||||
#include "manager.h"
|
||||
#include "selinux/selinux.h"
|
||||
#include "throne_tracker.h"
|
||||
#include "throne_tracker.h"
|
||||
#include "kernel_compat.h"
|
||||
|
||||
static bool ksu_module_mounted = false;
|
||||
|
||||
extern int handle_sepolicy(unsigned long arg3, void __user *arg4);
|
||||
|
||||
static inline bool is_allow_su()
|
||||
{
|
||||
if (is_manager()) {
|
||||
// we are manager, allow!
|
||||
return true;
|
||||
}
|
||||
return ksu_is_allow_uid(current_uid().val);
|
||||
}
|
||||
|
||||
static inline bool is_unsupported_uid(uid_t uid)
|
||||
{
|
||||
#define LAST_APPLICATION_UID 19999
|
||||
uid_t appid = uid % 100000;
|
||||
return appid > LAST_APPLICATION_UID;
|
||||
}
|
||||
|
||||
static struct group_info root_groups = { .usage = ATOMIC_INIT(2) };
|
||||
|
||||
static void setup_groups(struct root_profile *profile, struct cred *cred)
|
||||
{
|
||||
if (profile->groups_count > KSU_MAX_GROUPS) {
|
||||
pr_warn("Failed to setgroups, too large group: %d!\n",
|
||||
profile->uid);
|
||||
return;
|
||||
}
|
||||
|
||||
if (profile->groups_count == 1 && profile->groups[0] == 0) {
|
||||
// setgroup to root and return early.
|
||||
if (cred->group_info)
|
||||
put_group_info(cred->group_info);
|
||||
cred->group_info = get_group_info(&root_groups);
|
||||
return;
|
||||
}
|
||||
|
||||
u32 ngroups = profile->groups_count;
|
||||
struct group_info *group_info = groups_alloc(ngroups);
|
||||
if (!group_info) {
|
||||
pr_warn("Failed to setgroups, ENOMEM for: %d\n", profile->uid);
|
||||
return;
|
||||
}
|
||||
|
||||
int i;
|
||||
for (i = 0; i < ngroups; i++) {
|
||||
gid_t gid = profile->groups[i];
|
||||
kgid_t kgid = make_kgid(current_user_ns(), gid);
|
||||
if (!gid_valid(kgid)) {
|
||||
pr_warn("Failed to setgroups, invalid gid: %d\n", gid);
|
||||
put_group_info(group_info);
|
||||
return;
|
||||
}
|
||||
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 9, 0)
|
||||
group_info->gid[i] = kgid;
|
||||
#else
|
||||
GROUP_AT(group_info, i) = kgid;
|
||||
#endif
|
||||
}
|
||||
|
||||
groups_sort(group_info);
|
||||
set_groups(cred, group_info);
|
||||
}
|
||||
|
||||
void escape_to_root(void)
|
||||
{
|
||||
struct cred *cred;
|
||||
|
||||
cred = (struct cred *)__task_cred(current);
|
||||
|
||||
if (cred->euid.val == 0) {
|
||||
pr_warn("Already root, don't escape!\n");
|
||||
return;
|
||||
}
|
||||
struct root_profile *profile = ksu_get_root_profile(cred->uid.val);
|
||||
|
||||
cred->uid.val = profile->uid;
|
||||
cred->suid.val = profile->uid;
|
||||
cred->euid.val = profile->uid;
|
||||
cred->fsuid.val = profile->uid;
|
||||
|
||||
cred->gid.val = profile->gid;
|
||||
cred->fsgid.val = profile->gid;
|
||||
cred->sgid.val = profile->gid;
|
||||
cred->egid.val = profile->gid;
|
||||
|
||||
BUILD_BUG_ON(sizeof(profile->capabilities.effective) !=
|
||||
sizeof(kernel_cap_t));
|
||||
|
||||
// setup capabilities
|
||||
// we need CAP_DAC_READ_SEARCH becuase `/data/adb/ksud` is not accessible for non root process
|
||||
// we add it here but don't add it to cap_inhertiable, it would be dropped automaticly after exec!
|
||||
u64 cap_for_ksud =
|
||||
profile->capabilities.effective | CAP_DAC_READ_SEARCH;
|
||||
memcpy(&cred->cap_effective, &cap_for_ksud,
|
||||
sizeof(cred->cap_effective));
|
||||
memcpy(&cred->cap_inheritable, &profile->capabilities.effective,
|
||||
sizeof(cred->cap_inheritable));
|
||||
memcpy(&cred->cap_permitted, &profile->capabilities.effective,
|
||||
sizeof(cred->cap_permitted));
|
||||
memcpy(&cred->cap_bset, &profile->capabilities.effective,
|
||||
sizeof(cred->cap_bset));
|
||||
memcpy(&cred->cap_ambient, &profile->capabilities.effective,
|
||||
sizeof(cred->cap_ambient));
|
||||
|
||||
// disable seccomp
|
||||
#if defined(CONFIG_GENERIC_ENTRY) && \
|
||||
LINUX_VERSION_CODE >= KERNEL_VERSION(5, 11, 0)
|
||||
current_thread_info()->syscall_work &= ~SYSCALL_WORK_SECCOMP;
|
||||
#else
|
||||
current_thread_info()->flags &= ~(TIF_SECCOMP | _TIF_SECCOMP);
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_SECCOMP
|
||||
current->seccomp.mode = 0;
|
||||
current->seccomp.filter = NULL;
|
||||
#else
|
||||
#endif
|
||||
|
||||
setup_groups(profile, cred);
|
||||
|
||||
setup_selinux(profile->selinux_domain);
|
||||
}
|
||||
|
||||
int ksu_handle_rename(struct dentry *old_dentry, struct dentry *new_dentry)
|
||||
{
|
||||
if (!current->mm) {
|
||||
// skip kernel threads
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (current_uid().val != 1000) {
|
||||
// skip non system uid
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!old_dentry || !new_dentry) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// /data/system/packages.list.tmp -> /data/system/packages.list
|
||||
if (strcmp(new_dentry->d_iname, "packages.list")) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
char path[128];
|
||||
char *buf = dentry_path_raw(new_dentry, path, sizeof(path));
|
||||
if (IS_ERR(buf)) {
|
||||
pr_err("dentry_path_raw failed.\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (strcmp(buf, "/system/packages.list")) {
|
||||
return 0;
|
||||
}
|
||||
pr_info("renameat: %s -> %s, new path: %s\n", old_dentry->d_iname,
|
||||
new_dentry->d_iname, buf);
|
||||
|
||||
track_throne();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
|
||||
unsigned long arg4, unsigned long arg5)
|
||||
{
|
||||
// if success, we modify the arg5 as result!
|
||||
u32 *result = (u32 *)arg5;
|
||||
u32 reply_ok = KERNEL_SU_OPTION;
|
||||
|
||||
if (KERNEL_SU_OPTION != option) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// TODO: find it in throne tracker!
|
||||
uid_t current_uid_val = current_uid().val;
|
||||
uid_t manager_uid = ksu_get_manager_uid();
|
||||
if (current_uid_val != manager_uid &&
|
||||
current_uid_val % 100000 == manager_uid) {
|
||||
ksu_set_manager_uid(current_uid_val);
|
||||
}
|
||||
|
||||
bool from_root = 0 == current_uid().val;
|
||||
bool from_manager = is_manager();
|
||||
|
||||
if (!from_root && !from_manager) {
|
||||
// only root or manager can access this interface
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef CONFIG_KSU_DEBUG
|
||||
pr_info("option: 0x%x, cmd: %ld\n", option, arg2);
|
||||
#endif
|
||||
|
||||
if (arg2 == CMD_BECOME_MANAGER) {
|
||||
if (from_manager) {
|
||||
if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) {
|
||||
pr_err("become_manager: prctl reply error\n");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (arg2 == CMD_GRANT_ROOT) {
|
||||
if (is_allow_su()) {
|
||||
pr_info("allow root for: %d\n", current_uid().val);
|
||||
escape_to_root();
|
||||
if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) {
|
||||
pr_err("grant_root: prctl reply error\n");
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Both root manager and root processes should be allowed to get version
|
||||
if (arg2 == CMD_GET_VERSION) {
|
||||
u32 version = KERNEL_SU_VERSION;
|
||||
if (copy_to_user(arg3, &version, sizeof(version))) {
|
||||
pr_err("prctl reply error, cmd: %lu\n", arg2);
|
||||
}
|
||||
#ifdef MODULE
|
||||
u32 is_lkm = 0x1;
|
||||
#else
|
||||
u32 is_lkm = 0x0;
|
||||
#endif
|
||||
if (arg4 && copy_to_user(arg4, &is_lkm, sizeof(is_lkm))) {
|
||||
pr_err("prctl reply error, cmd: %lu\n", arg2);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (arg2 == CMD_REPORT_EVENT) {
|
||||
if (!from_root) {
|
||||
return 0;
|
||||
}
|
||||
switch (arg3) {
|
||||
case EVENT_POST_FS_DATA: {
|
||||
static bool post_fs_data_lock = false;
|
||||
if (!post_fs_data_lock) {
|
||||
post_fs_data_lock = true;
|
||||
pr_info("post-fs-data triggered\n");
|
||||
on_post_fs_data();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case EVENT_BOOT_COMPLETED: {
|
||||
static bool boot_complete_lock = false;
|
||||
if (!boot_complete_lock) {
|
||||
boot_complete_lock = true;
|
||||
pr_info("boot_complete triggered\n");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case EVENT_MODULE_MOUNTED: {
|
||||
ksu_module_mounted = true;
|
||||
pr_info("module mounted!\n");
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (arg2 == CMD_SET_SEPOLICY) {
|
||||
if (!from_root) {
|
||||
return 0;
|
||||
}
|
||||
if (!handle_sepolicy(arg3, arg4)) {
|
||||
if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) {
|
||||
pr_err("sepolicy: prctl reply error\n");
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (arg2 == CMD_CHECK_SAFEMODE) {
|
||||
if (ksu_is_safe_mode()) {
|
||||
pr_warn("safemode enabled!\n");
|
||||
if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) {
|
||||
pr_err("safemode: prctl reply error\n");
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (arg2 == CMD_GET_ALLOW_LIST || arg2 == CMD_GET_DENY_LIST) {
|
||||
u32 array[128];
|
||||
u32 array_length;
|
||||
bool success = ksu_get_allow_list(array, &array_length,
|
||||
arg2 == CMD_GET_ALLOW_LIST);
|
||||
if (success) {
|
||||
if (!copy_to_user(arg4, &array_length,
|
||||
sizeof(array_length)) &&
|
||||
!copy_to_user(arg3, array,
|
||||
sizeof(u32) * array_length)) {
|
||||
if (copy_to_user(result, &reply_ok,
|
||||
sizeof(reply_ok))) {
|
||||
pr_err("prctl reply error, cmd: %lu\n",
|
||||
arg2);
|
||||
}
|
||||
} else {
|
||||
pr_err("prctl copy allowlist error\n");
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (arg2 == CMD_UID_GRANTED_ROOT || arg2 == CMD_UID_SHOULD_UMOUNT) {
|
||||
uid_t target_uid = (uid_t)arg3;
|
||||
bool allow = false;
|
||||
if (arg2 == CMD_UID_GRANTED_ROOT) {
|
||||
allow = ksu_is_allow_uid(target_uid);
|
||||
} else if (arg2 == CMD_UID_SHOULD_UMOUNT) {
|
||||
allow = ksu_uid_should_umount(target_uid);
|
||||
} else {
|
||||
pr_err("unknown cmd: %lu\n", arg2);
|
||||
}
|
||||
if (!copy_to_user(arg4, &allow, sizeof(allow))) {
|
||||
if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) {
|
||||
pr_err("prctl reply error, cmd: %lu\n", arg2);
|
||||
}
|
||||
} else {
|
||||
pr_err("prctl copy err, cmd: %lu\n", arg2);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// all other cmds are for 'root manager'
|
||||
if (!from_manager) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// we are already manager
|
||||
if (arg2 == CMD_GET_APP_PROFILE) {
|
||||
struct app_profile profile;
|
||||
if (copy_from_user(&profile, arg3, sizeof(profile))) {
|
||||
pr_err("copy profile failed\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool success = ksu_get_app_profile(&profile);
|
||||
if (success) {
|
||||
if (copy_to_user(arg3, &profile, sizeof(profile))) {
|
||||
pr_err("copy profile failed\n");
|
||||
return 0;
|
||||
}
|
||||
if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) {
|
||||
pr_err("prctl reply error, cmd: %lu\n", arg2);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (arg2 == CMD_SET_APP_PROFILE) {
|
||||
struct app_profile profile;
|
||||
if (copy_from_user(&profile, arg3, sizeof(profile))) {
|
||||
pr_err("copy profile failed\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// todo: validate the params
|
||||
if (ksu_set_app_profile(&profile, true)) {
|
||||
if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) {
|
||||
pr_err("prctl reply error, cmd: %lu\n", arg2);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool is_appuid(kuid_t uid)
|
||||
{
|
||||
#define PER_USER_RANGE 100000
|
||||
#define FIRST_APPLICATION_UID 10000
|
||||
#define LAST_APPLICATION_UID 19999
|
||||
|
||||
uid_t appid = uid.val % PER_USER_RANGE;
|
||||
return appid >= FIRST_APPLICATION_UID && appid <= LAST_APPLICATION_UID;
|
||||
}
|
||||
|
||||
static bool should_umount(struct path *path)
|
||||
{
|
||||
if (!path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (current->nsproxy->mnt_ns == init_nsproxy.mnt_ns) {
|
||||
pr_info("ignore global mnt namespace process: %d\n",
|
||||
current_uid().val);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (path->mnt && path->mnt->mnt_sb && path->mnt->mnt_sb->s_type) {
|
||||
const char *fstype = path->mnt->mnt_sb->s_type->name;
|
||||
return strcmp(fstype, "overlay") == 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static int ksu_umount_mnt(struct path *path, int flags)
|
||||
{
|
||||
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 9, 0) || defined(KSU_UMOUNT)
|
||||
return path_umount(path, flags);
|
||||
#else
|
||||
// TODO: umount for non GKI kernel
|
||||
return -ENOSYS;
|
||||
#endif
|
||||
}
|
||||
|
||||
static void try_umount(const char *mnt, bool check_mnt, int flags)
|
||||
{
|
||||
struct path path;
|
||||
int err = kern_path(mnt, 0, &path);
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.dentry != path.mnt->mnt_root) {
|
||||
// it is not root mountpoint, maybe umounted by others already.
|
||||
return;
|
||||
}
|
||||
|
||||
// we are only interest in some specific mounts
|
||||
if (check_mnt && !should_umount(&path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
err = ksu_umount_mnt(&path, flags);
|
||||
if (err) {
|
||||
pr_warn("umount %s failed: %d\n", mnt, err);
|
||||
}
|
||||
}
|
||||
|
||||
int ksu_handle_setuid(struct cred *new, const struct cred *old)
|
||||
{
|
||||
// this hook is used for umounting overlayfs for some uid, if there isn't any module mounted, just ignore it!
|
||||
if (!ksu_module_mounted) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!new || !old) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
kuid_t new_uid = new->uid;
|
||||
kuid_t old_uid = old->uid;
|
||||
|
||||
if (0 != old_uid.val) {
|
||||
// old process is not root, ignore it.
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!is_appuid(new_uid) || is_unsupported_uid(new_uid.val)) {
|
||||
// pr_info("handle setuid ignore non application or isolated uid: %d\n", new_uid.val);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (ksu_is_allow_uid(new_uid.val)) {
|
||||
// pr_info("handle setuid ignore allowed application: %d\n", new_uid.val);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!ksu_uid_should_umount(new_uid.val)) {
|
||||
return 0;
|
||||
} else {
|
||||
#ifdef CONFIG_KSU_DEBUG
|
||||
pr_info("uid: %d should not umount!\n", current_uid().val);
|
||||
#endif
|
||||
}
|
||||
|
||||
// check old process's selinux context, if it is not zygote, ignore it!
|
||||
// because some su apps may setuid to untrusted_app but they are in global mount namespace
|
||||
// when we umount for such process, that is a disaster!
|
||||
bool is_zygote_child = is_zygote(old->security);
|
||||
if (!is_zygote_child) {
|
||||
pr_info("handle umount ignore non zygote child: %d\n",
|
||||
current->pid);
|
||||
return 0;
|
||||
}
|
||||
#ifdef CONFIG_KSU_DEBUG
|
||||
// umount the target mnt
|
||||
pr_info("handle umount for uid: %d, pid: %d\n", new_uid.val,
|
||||
current->pid);
|
||||
#endif
|
||||
|
||||
// fixme: use `collect_mounts` and `iterate_mount` to iterate all mountpoint and
|
||||
// filter the mountpoint whose target is `/data/adb`
|
||||
try_umount("/system", true, 0);
|
||||
try_umount("/vendor", true, 0);
|
||||
try_umount("/product", true, 0);
|
||||
try_umount("/data/adb/modules", false, MNT_DETACH);
|
||||
|
||||
// try umount ksu temp path
|
||||
try_umount("/debug_ramdisk", false, MNT_DETACH);
|
||||
try_umount("/sbin", false, MNT_DETACH);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Init functons
|
||||
|
||||
static int handler_pre(struct kprobe *p, struct pt_regs *regs)
|
||||
{
|
||||
struct pt_regs *real_regs = PT_REAL_REGS(regs);
|
||||
int option = (int)PT_REGS_PARM1(real_regs);
|
||||
unsigned long arg2 = (unsigned long)PT_REGS_PARM2(real_regs);
|
||||
unsigned long arg3 = (unsigned long)PT_REGS_PARM3(real_regs);
|
||||
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 16, 0)
|
||||
// PRCTL_SYMBOL is the arch-specificed one, which receive raw pt_regs from syscall
|
||||
unsigned long arg4 = (unsigned long)PT_REGS_SYSCALL_PARM4(real_regs);
|
||||
#else
|
||||
// PRCTL_SYMBOL is the common one, called by C convention in do_syscall_64
|
||||
// https://elixir.bootlin.com/linux/v4.15.18/source/arch/x86/entry/common.c#L287
|
||||
unsigned long arg4 = (unsigned long)PT_REGS_CCALL_PARM4(real_regs);
|
||||
#endif
|
||||
unsigned long arg5 = (unsigned long)PT_REGS_PARM5(real_regs);
|
||||
|
||||
return ksu_handle_prctl(option, arg2, arg3, arg4, arg5);
|
||||
}
|
||||
|
||||
static struct kprobe prctl_kp = {
|
||||
.symbol_name = PRCTL_SYMBOL,
|
||||
.pre_handler = handler_pre,
|
||||
};
|
||||
|
||||
static int renameat_handler_pre(struct kprobe *p, struct pt_regs *regs)
|
||||
{
|
||||
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 12, 0)
|
||||
// https://elixir.bootlin.com/linux/v5.12-rc1/source/include/linux/fs.h
|
||||
struct renamedata *rd = PT_REGS_PARM1(regs);
|
||||
struct dentry *old_entry = rd->old_dentry;
|
||||
struct dentry *new_entry = rd->new_dentry;
|
||||
#else
|
||||
struct dentry *old_entry = (struct dentry *)PT_REGS_PARM2(regs);
|
||||
struct dentry *new_entry = (struct dentry *)PT_REGS_CCALL_PARM4(regs);
|
||||
#endif
|
||||
|
||||
return ksu_handle_rename(old_entry, new_entry);
|
||||
}
|
||||
|
||||
static struct kprobe renameat_kp = {
|
||||
.symbol_name = "vfs_rename",
|
||||
.pre_handler = renameat_handler_pre,
|
||||
};
|
||||
|
||||
__maybe_unused int ksu_kprobe_init(void)
|
||||
{
|
||||
int rc = 0;
|
||||
rc = register_kprobe(&prctl_kp);
|
||||
|
||||
if (rc) {
|
||||
pr_info("prctl kprobe failed: %d.\n", rc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
rc = register_kprobe(&renameat_kp);
|
||||
pr_info("renameat kp: %d\n", rc);
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
__maybe_unused int ksu_kprobe_exit(void)
|
||||
{
|
||||
unregister_kprobe(&prctl_kp);
|
||||
unregister_kprobe(&renameat_kp);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int ksu_task_prctl(int option, unsigned long arg2, unsigned long arg3,
|
||||
unsigned long arg4, unsigned long arg5)
|
||||
{
|
||||
ksu_handle_prctl(option, arg2, arg3, arg4, arg5);
|
||||
return -ENOSYS;
|
||||
}
|
||||
// kernel 4.4 and 4.9
|
||||
#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 10, 0) || defined(CONFIG_IS_HW_HISI)
|
||||
static int ksu_key_permission(key_ref_t key_ref, const struct cred *cred,
|
||||
unsigned perm)
|
||||
{
|
||||
if (init_session_keyring != NULL) {
|
||||
return 0;
|
||||
}
|
||||
if (strcmp(current->comm, "init")) {
|
||||
// we are only interested in `init` process
|
||||
return 0;
|
||||
}
|
||||
init_session_keyring = cred->session_keyring;
|
||||
pr_info("kernel_compat: got init_session_keyring\n");
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
static int ksu_inode_rename(struct inode *old_inode, struct dentry *old_dentry,
|
||||
struct inode *new_inode, struct dentry *new_dentry)
|
||||
{
|
||||
return ksu_handle_rename(old_dentry, new_dentry);
|
||||
}
|
||||
|
||||
static int ksu_task_fix_setuid(struct cred *new, const struct cred *old,
|
||||
int flags)
|
||||
{
|
||||
return ksu_handle_setuid(new, old);
|
||||
}
|
||||
|
||||
#ifndef MODULE
|
||||
static struct security_hook_list ksu_hooks[] = {
|
||||
LSM_HOOK_INIT(task_prctl, ksu_task_prctl),
|
||||
LSM_HOOK_INIT(inode_rename, ksu_inode_rename),
|
||||
LSM_HOOK_INIT(task_fix_setuid, ksu_task_fix_setuid),
|
||||
#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 10, 0) || defined(CONFIG_IS_HW_HISI)
|
||||
LSM_HOOK_INIT(key_permission, ksu_key_permission)
|
||||
#endif
|
||||
};
|
||||
|
||||
void __init ksu_lsm_hook_init(void)
|
||||
{
|
||||
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 11, 0)
|
||||
security_add_hooks(ksu_hooks, ARRAY_SIZE(ksu_hooks), "ksu");
|
||||
#else
|
||||
// https://elixir.bootlin.com/linux/v4.10.17/source/include/linux/lsm_hooks.h#L1892
|
||||
security_add_hooks(ksu_hooks, ARRAY_SIZE(ksu_hooks));
|
||||
#endif
|
||||
}
|
||||
|
||||
#else
|
||||
static int override_security_head(void *head, const void *new_head, size_t len)
|
||||
{
|
||||
unsigned long base = (unsigned long)head & PAGE_MASK;
|
||||
unsigned long offset = offset_in_page(head);
|
||||
|
||||
// this is impossible for our case because the page alignment
|
||||
// but be careful for other cases!
|
||||
BUG_ON(offset + len > PAGE_SIZE);
|
||||
struct page *page = phys_to_page(__pa(base));
|
||||
if (!page) {
|
||||
return -EFAULT;
|
||||
}
|
||||
|
||||
void *addr = vmap(&page, 1, VM_MAP, PAGE_KERNEL);
|
||||
if (!addr) {
|
||||
return -ENOMEM;
|
||||
}
|
||||
local_irq_disable();
|
||||
memcpy(addr + offset, new_head, len);
|
||||
local_irq_enable();
|
||||
vunmap(addr);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void free_security_hook_list(struct hlist_head *head)
|
||||
{
|
||||
struct hlist_node *temp;
|
||||
struct security_hook_list *entry;
|
||||
|
||||
if (!head)
|
||||
return;
|
||||
|
||||
hlist_for_each_entry_safe (entry, temp, head, list) {
|
||||
hlist_del(&entry->list);
|
||||
kfree(entry);
|
||||
}
|
||||
|
||||
kfree(head);
|
||||
}
|
||||
|
||||
struct hlist_head *copy_security_hlist(struct hlist_head *orig)
|
||||
{
|
||||
struct hlist_head *new_head = kmalloc(sizeof(*new_head), GFP_KERNEL);
|
||||
if (!new_head)
|
||||
return NULL;
|
||||
|
||||
INIT_HLIST_HEAD(new_head);
|
||||
|
||||
struct security_hook_list *entry;
|
||||
struct security_hook_list *new_entry;
|
||||
|
||||
hlist_for_each_entry (entry, orig, list) {
|
||||
new_entry = kmalloc(sizeof(*new_entry), GFP_KERNEL);
|
||||
if (!new_entry) {
|
||||
free_security_hook_list(new_head);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
*new_entry = *entry;
|
||||
|
||||
hlist_add_tail_rcu(&new_entry->list, new_head);
|
||||
}
|
||||
|
||||
return new_head;
|
||||
}
|
||||
|
||||
#define LSM_SEARCH_MAX 180 // This should be enough to iterate
|
||||
static void *find_head_addr(void *security_ptr, int *index)
|
||||
{
|
||||
if (!security_ptr) {
|
||||
return NULL;
|
||||
}
|
||||
struct hlist_head *head_start =
|
||||
(struct hlist_head *)&security_hook_heads;
|
||||
|
||||
for (int i = 0; i < LSM_SEARCH_MAX; i++) {
|
||||
struct hlist_head *head = head_start + i;
|
||||
struct security_hook_list *pos;
|
||||
hlist_for_each_entry (pos, head, list) {
|
||||
if (pos->hook.capget == security_ptr) {
|
||||
if (index) {
|
||||
*index = i;
|
||||
}
|
||||
return head;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
#define GET_SYMBOL_ADDR(sym) \
|
||||
({ \
|
||||
void *addr = kallsyms_lookup_name(#sym ".cfi_jt"); \
|
||||
if (!addr) { \
|
||||
addr = kallsyms_lookup_name(#sym); \
|
||||
} \
|
||||
addr; \
|
||||
})
|
||||
|
||||
#define KSU_LSM_HOOK_HACK_INIT(head_ptr, name, func) \
|
||||
do { \
|
||||
static struct security_hook_list hook = { \
|
||||
.hook = { .name = func } \
|
||||
}; \
|
||||
hook.head = head_ptr; \
|
||||
hook.lsm = "ksu"; \
|
||||
struct hlist_head *new_head = copy_security_hlist(hook.head); \
|
||||
if (!new_head) { \
|
||||
pr_err("Failed to copy security list: %s\n", #name); \
|
||||
break; \
|
||||
} \
|
||||
hlist_add_tail_rcu(&hook.list, new_head); \
|
||||
if (override_security_head(hook.head, new_head, \
|
||||
sizeof(*new_head))) { \
|
||||
free_security_hook_list(new_head); \
|
||||
pr_err("Failed to hack lsm for: %s\n", #name); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
void __init ksu_lsm_hook_init(void)
|
||||
{
|
||||
void *cap_prctl = GET_SYMBOL_ADDR(cap_task_prctl);
|
||||
void *prctl_head = find_head_addr(cap_prctl, NULL);
|
||||
if (prctl_head) {
|
||||
if (prctl_head != &security_hook_heads.task_prctl) {
|
||||
pr_warn("prctl's address has shifted!\n");
|
||||
}
|
||||
KSU_LSM_HOOK_HACK_INIT(prctl_head, task_prctl, ksu_task_prctl);
|
||||
} else {
|
||||
pr_warn("Failed to find task_prctl!\n");
|
||||
}
|
||||
|
||||
int inode_killpriv_index = -1;
|
||||
void *cap_killpriv = GET_SYMBOL_ADDR(cap_inode_killpriv);
|
||||
find_head_addr(cap_killpriv, &inode_killpriv_index);
|
||||
if (inode_killpriv_index < 0) {
|
||||
pr_warn("Failed to find inode_rename, use kprobe instead!\n");
|
||||
register_kprobe(&renameat_kp);
|
||||
} else {
|
||||
int inode_rename_index = inode_killpriv_index +
|
||||
&security_hook_heads.inode_rename -
|
||||
&security_hook_heads.inode_killpriv;
|
||||
struct hlist_head *head_start =
|
||||
(struct hlist_head *)&security_hook_heads;
|
||||
void *inode_rename_head = head_start + inode_rename_index;
|
||||
if (inode_rename_head != &security_hook_heads.inode_rename) {
|
||||
pr_warn("inode_rename's address has shifted!\n");
|
||||
}
|
||||
KSU_LSM_HOOK_HACK_INIT(inode_rename_head, inode_rename,
|
||||
ksu_inode_rename);
|
||||
}
|
||||
void *cap_setuid = GET_SYMBOL_ADDR(cap_task_fix_setuid);
|
||||
void *setuid_head = find_head_addr(cap_setuid, NULL);
|
||||
if (setuid_head) {
|
||||
if (setuid_head != &security_hook_heads.task_fix_setuid) {
|
||||
pr_warn("setuid's address has shifted!\n");
|
||||
}
|
||||
KSU_LSM_HOOK_HACK_INIT(setuid_head, task_fix_setuid,
|
||||
ksu_task_fix_setuid);
|
||||
} else {
|
||||
pr_warn("Failed to find task_fix_setuid!\n");
|
||||
}
|
||||
smp_mb();
|
||||
}
|
||||
#endif
|
||||
|
||||
void __init ksu_core_init(void)
|
||||
{
|
||||
ksu_lsm_hook_init();
|
||||
}
|
||||
|
||||
void ksu_core_exit(void)
|
||||
{
|
||||
#ifdef CONFIG_KPROBES
|
||||
pr_info("ksu_core_kprobe_exit\n");
|
||||
// we dont use this now
|
||||
// ksu_kprobe_exit();
|
||||
#endif
|
||||
}
|
||||
9
KernelSU/kernel/core_hook.h
Normal file
9
KernelSU/kernel/core_hook.h
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
#ifndef __KSU_H_KSU_CORE
|
||||
#define __KSU_H_KSU_CORE
|
||||
|
||||
#include <linux/init.h>
|
||||
|
||||
void __init ksu_core_init(void);
|
||||
void ksu_core_exit(void);
|
||||
|
||||
#endif
|
||||
5
KernelSU/kernel/embed_ksud.c
Normal file
5
KernelSU/kernel/embed_ksud.c
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// WARNING: THIS IS A STUB FILE
|
||||
// This file will be regenerated by CI
|
||||
|
||||
unsigned int ksud_size = 0;
|
||||
const char ksud[0] = {};
|
||||
2
KernelSU/kernel/export_symbol.txt
Normal file
2
KernelSU/kernel/export_symbol.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
register_kprobe
|
||||
unregister_kprobe
|
||||
28
KernelSU/kernel/include/ksu_hook.h
Normal file
28
KernelSU/kernel/include/ksu_hook.h
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#ifndef __KSU_H_KSHOOK
|
||||
#define __KSU_H_KSHOOK
|
||||
|
||||
#include <linux/fs.h>
|
||||
#include <linux/types.h>
|
||||
|
||||
// For sucompat
|
||||
|
||||
int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode,
|
||||
int *flags);
|
||||
|
||||
int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags);
|
||||
|
||||
// For ksud
|
||||
|
||||
int ksu_handle_vfs_read(struct file **file_ptr, char __user **buf_ptr,
|
||||
size_t *count_ptr, loff_t **pos);
|
||||
|
||||
// For ksud and sucompat
|
||||
|
||||
int ksu_handle_execveat(int *fd, struct filename **filename_ptr, void *argv,
|
||||
void *envp, int *flags);
|
||||
|
||||
// For volume button
|
||||
int ksu_handle_input_handle_event(unsigned int *type, unsigned int *code,
|
||||
int *value);
|
||||
|
||||
#endif
|
||||
176
KernelSU/kernel/kernel_compat.c
Normal file
176
KernelSU/kernel/kernel_compat.c
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
#include <linux/version.h>
|
||||
#include <linux/fs.h>
|
||||
#include <linux/nsproxy.h>
|
||||
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 10, 0)
|
||||
#include <linux/sched/task.h>
|
||||
#else
|
||||
#include <linux/sched.h>
|
||||
#endif
|
||||
#include <linux/uaccess.h>
|
||||
#include "klog.h" // IWYU pragma: keep
|
||||
#include "kernel_compat.h" // Add check Huawei Device
|
||||
|
||||
#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 10, 0) || defined(CONFIG_IS_HW_HISI)
|
||||
#include <linux/key.h>
|
||||
#include <linux/errno.h>
|
||||
#include <linux/cred.h>
|
||||
struct key *init_session_keyring = NULL;
|
||||
|
||||
static inline int install_session_keyring(struct key *keyring)
|
||||
{
|
||||
struct cred *new;
|
||||
int ret;
|
||||
|
||||
new = prepare_creds();
|
||||
if (!new)
|
||||
return -ENOMEM;
|
||||
|
||||
ret = install_session_keyring_to_cred(new, keyring);
|
||||
if (ret < 0) {
|
||||
abort_creds(new);
|
||||
return ret;
|
||||
}
|
||||
|
||||
return commit_creds(new);
|
||||
}
|
||||
#endif
|
||||
|
||||
extern struct task_struct init_task;
|
||||
|
||||
// mnt_ns context switch for environment that android_init->nsproxy->mnt_ns != init_task.nsproxy->mnt_ns, such as WSA
|
||||
struct ksu_ns_fs_saved {
|
||||
struct nsproxy *ns;
|
||||
struct fs_struct *fs;
|
||||
};
|
||||
|
||||
static void ksu_save_ns_fs(struct ksu_ns_fs_saved *ns_fs_saved)
|
||||
{
|
||||
ns_fs_saved->ns = current->nsproxy;
|
||||
ns_fs_saved->fs = current->fs;
|
||||
}
|
||||
|
||||
static void ksu_load_ns_fs(struct ksu_ns_fs_saved *ns_fs_saved)
|
||||
{
|
||||
current->nsproxy = ns_fs_saved->ns;
|
||||
current->fs = ns_fs_saved->fs;
|
||||
}
|
||||
|
||||
static bool android_context_saved_checked = false;
|
||||
static bool android_context_saved_enabled = false;
|
||||
static struct ksu_ns_fs_saved android_context_saved;
|
||||
|
||||
void ksu_android_ns_fs_check()
|
||||
{
|
||||
if (android_context_saved_checked)
|
||||
return;
|
||||
android_context_saved_checked = true;
|
||||
task_lock(current);
|
||||
if (current->nsproxy && current->fs &&
|
||||
current->nsproxy->mnt_ns != init_task.nsproxy->mnt_ns) {
|
||||
android_context_saved_enabled = true;
|
||||
pr_info("android context saved enabled due to init mnt_ns(%p) != android mnt_ns(%p)\n",
|
||||
current->nsproxy->mnt_ns, init_task.nsproxy->mnt_ns);
|
||||
ksu_save_ns_fs(&android_context_saved);
|
||||
} else {
|
||||
pr_info("android context saved disabled\n");
|
||||
}
|
||||
task_unlock(current);
|
||||
}
|
||||
|
||||
struct file *ksu_filp_open_compat(const char *filename, int flags, umode_t mode)
|
||||
{
|
||||
#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 10, 0) || defined(CONFIG_IS_HW_HISI)
|
||||
if (init_session_keyring != NULL && !current_cred()->session_keyring &&
|
||||
(current->flags & PF_WQ_WORKER)) {
|
||||
pr_info("installing init session keyring for older kernel\n");
|
||||
install_session_keyring(init_session_keyring);
|
||||
}
|
||||
#endif
|
||||
// switch mnt_ns even if current is not wq_worker, to ensure what we open is the correct file in android mnt_ns, rather than user created mnt_ns
|
||||
struct ksu_ns_fs_saved saved;
|
||||
if (android_context_saved_enabled) {
|
||||
pr_info("start switch current nsproxy and fs to android context\n");
|
||||
task_lock(current);
|
||||
ksu_save_ns_fs(&saved);
|
||||
ksu_load_ns_fs(&android_context_saved);
|
||||
task_unlock(current);
|
||||
}
|
||||
struct file *fp = filp_open(filename, flags, mode);
|
||||
if (android_context_saved_enabled) {
|
||||
task_lock(current);
|
||||
ksu_load_ns_fs(&saved);
|
||||
task_unlock(current);
|
||||
pr_info("switch current nsproxy and fs back to saved successfully\n");
|
||||
}
|
||||
return fp;
|
||||
}
|
||||
|
||||
ssize_t ksu_kernel_read_compat(struct file *p, void *buf, size_t count,
|
||||
loff_t *pos)
|
||||
{
|
||||
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 14, 0)
|
||||
return kernel_read(p, buf, count, pos);
|
||||
#else
|
||||
loff_t offset = pos ? *pos : 0;
|
||||
ssize_t result = kernel_read(p, offset, (char *)buf, count);
|
||||
if (pos && result > 0) {
|
||||
*pos = offset + result;
|
||||
}
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
|
||||
ssize_t ksu_kernel_write_compat(struct file *p, const void *buf, size_t count,
|
||||
loff_t *pos)
|
||||
{
|
||||
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 14, 0)
|
||||
return kernel_write(p, buf, count, pos);
|
||||
#else
|
||||
loff_t offset = pos ? *pos : 0;
|
||||
ssize_t result = kernel_write(p, buf, count, offset);
|
||||
if (pos && result > 0) {
|
||||
*pos = offset + result;
|
||||
}
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 8, 0)
|
||||
long ksu_strncpy_from_user_nofault(char *dst, const void __user *unsafe_addr,
|
||||
long count)
|
||||
{
|
||||
return strncpy_from_user_nofault(dst, unsafe_addr, count);
|
||||
}
|
||||
#elif LINUX_VERSION_CODE >= KERNEL_VERSION(5, 3, 0)
|
||||
long ksu_strncpy_from_user_nofault(char *dst, const void __user *unsafe_addr,
|
||||
long count)
|
||||
{
|
||||
return strncpy_from_user_nofault(dst, unsafe_addr, count);
|
||||
}
|
||||
#else
|
||||
// Copied from: https://elixir.bootlin.com/linux/v4.9.337/source/mm/maccess.c#L201
|
||||
long ksu_strncpy_from_user_nofault(char *dst, const void __user *unsafe_addr,
|
||||
long count)
|
||||
{
|
||||
mm_segment_t old_fs = get_fs();
|
||||
long ret;
|
||||
|
||||
if (unlikely(count <= 0))
|
||||
return 0;
|
||||
|
||||
set_fs(USER_DS);
|
||||
pagefault_disable();
|
||||
ret = strncpy_from_user(dst, unsafe_addr, count);
|
||||
pagefault_enable();
|
||||
set_fs(old_fs);
|
||||
|
||||
if (ret >= count) {
|
||||
ret = count;
|
||||
dst[ret - 1] = '\0';
|
||||
} else if (ret > 0) {
|
||||
ret++;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
#endif
|
||||
39
KernelSU/kernel/kernel_compat.h
Normal file
39
KernelSU/kernel/kernel_compat.h
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
#ifndef __KSU_H_KERNEL_COMPAT
|
||||
#define __KSU_H_KERNEL_COMPAT
|
||||
|
||||
#include <linux/fs.h>
|
||||
#include <linux/version.h>
|
||||
#include "ss/policydb.h"
|
||||
#include "linux/key.h"
|
||||
|
||||
/*
|
||||
* Adapt to Huawei HISI kernel without affecting other kernels ,
|
||||
* Huawei Hisi Kernel EBITMAP Enable or Disable Flag ,
|
||||
* From ss/ebitmap.h
|
||||
*/
|
||||
#if (LINUX_VERSION_CODE >= KERNEL_VERSION(4, 9, 0)) && \
|
||||
(LINUX_VERSION_CODE < KERNEL_VERSION(4, 10, 0)) || \
|
||||
(LINUX_VERSION_CODE >= KERNEL_VERSION(4, 14, 0)) && \
|
||||
(LINUX_VERSION_CODE < KERNEL_VERSION(4, 15, 0))
|
||||
#ifdef HISI_SELINUX_EBITMAP_RO
|
||||
#define CONFIG_IS_HW_HISI
|
||||
#endif
|
||||
#endif
|
||||
|
||||
extern long ksu_strncpy_from_user_nofault(char *dst,
|
||||
const void __user *unsafe_addr,
|
||||
long count);
|
||||
|
||||
#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 10, 0) || defined(CONFIG_IS_HW_HISI)
|
||||
extern struct key *init_session_keyring;
|
||||
#endif
|
||||
|
||||
extern void ksu_android_ns_fs_check();
|
||||
extern struct file *ksu_filp_open_compat(const char *filename, int flags,
|
||||
umode_t mode);
|
||||
extern ssize_t ksu_kernel_read_compat(struct file *p, void *buf, size_t count,
|
||||
loff_t *pos);
|
||||
extern ssize_t ksu_kernel_write_compat(struct file *p, const void *buf,
|
||||
size_t count, loff_t *pos);
|
||||
|
||||
#endif
|
||||
11
KernelSU/kernel/klog.h
Normal file
11
KernelSU/kernel/klog.h
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#ifndef __KSU_H_KLOG
|
||||
#define __KSU_H_KLOG
|
||||
|
||||
#include <linux/printk.h>
|
||||
|
||||
#ifdef pr_fmt
|
||||
#undef pr_fmt
|
||||
#define pr_fmt(fmt) "KernelSU: " fmt
|
||||
#endif
|
||||
|
||||
#endif
|
||||
100
KernelSU/kernel/ksu.c
Normal file
100
KernelSU/kernel/ksu.c
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
#include <linux/export.h>
|
||||
#include <linux/fs.h>
|
||||
#include <linux/kobject.h>
|
||||
#include <linux/module.h>
|
||||
#include <linux/workqueue.h>
|
||||
|
||||
#include "allowlist.h"
|
||||
#include "arch.h"
|
||||
#include "core_hook.h"
|
||||
#include "klog.h" // IWYU pragma: keep
|
||||
#include "ksu.h"
|
||||
#include "throne_tracker.h"
|
||||
|
||||
static struct workqueue_struct *ksu_workqueue;
|
||||
|
||||
bool ksu_queue_work(struct work_struct *work)
|
||||
{
|
||||
return queue_work(ksu_workqueue, work);
|
||||
}
|
||||
|
||||
extern int ksu_handle_execveat_sucompat(int *fd, struct filename **filename_ptr,
|
||||
void *argv, void *envp, int *flags);
|
||||
|
||||
extern int ksu_handle_execveat_ksud(int *fd, struct filename **filename_ptr,
|
||||
void *argv, void *envp, int *flags);
|
||||
|
||||
int ksu_handle_execveat(int *fd, struct filename **filename_ptr, void *argv,
|
||||
void *envp, int *flags)
|
||||
{
|
||||
ksu_handle_execveat_ksud(fd, filename_ptr, argv, envp, flags);
|
||||
return ksu_handle_execveat_sucompat(fd, filename_ptr, argv, envp,
|
||||
flags);
|
||||
}
|
||||
|
||||
extern void ksu_sucompat_init();
|
||||
extern void ksu_sucompat_exit();
|
||||
extern void ksu_ksud_init();
|
||||
extern void ksu_ksud_exit();
|
||||
|
||||
int __init kernelsu_init(void)
|
||||
{
|
||||
#ifdef CONFIG_KSU_DEBUG
|
||||
pr_alert("*************************************************************");
|
||||
pr_alert("** NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE **");
|
||||
pr_alert("** **");
|
||||
pr_alert("** You are running KernelSU in DEBUG mode **");
|
||||
pr_alert("** **");
|
||||
pr_alert("** NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE **");
|
||||
pr_alert("*************************************************************");
|
||||
#endif
|
||||
|
||||
ksu_core_init();
|
||||
|
||||
ksu_workqueue = alloc_ordered_workqueue("kernelsu_work_queue", 0);
|
||||
|
||||
ksu_allowlist_init();
|
||||
|
||||
ksu_throne_tracker_init();
|
||||
|
||||
#ifdef CONFIG_KPROBES
|
||||
ksu_sucompat_init();
|
||||
ksu_ksud_init();
|
||||
#else
|
||||
pr_alert("KPROBES is disabled, KernelSU may not work, please check https://kernelsu.org/guide/how-to-integrate-for-non-gki.html");
|
||||
#endif
|
||||
|
||||
#ifdef MODULE
|
||||
#ifndef CONFIG_KSU_DEBUG
|
||||
kobject_del(&THIS_MODULE->mkobj.kobj);
|
||||
#endif
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
void kernelsu_exit(void)
|
||||
{
|
||||
ksu_allowlist_exit();
|
||||
|
||||
ksu_throne_tracker_exit();
|
||||
|
||||
destroy_workqueue(ksu_workqueue);
|
||||
|
||||
#ifdef CONFIG_KPROBES
|
||||
ksu_ksud_exit();
|
||||
ksu_sucompat_exit();
|
||||
#endif
|
||||
|
||||
ksu_core_exit();
|
||||
}
|
||||
|
||||
module_init(kernelsu_init);
|
||||
module_exit(kernelsu_exit);
|
||||
|
||||
MODULE_LICENSE("GPL");
|
||||
MODULE_AUTHOR("weishu");
|
||||
MODULE_DESCRIPTION("Android KernelSU");
|
||||
|
||||
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 0, 0)
|
||||
MODULE_IMPORT_NS(VFS_internal_I_am_really_a_filesystem_and_am_NOT_a_driver);
|
||||
#endif
|
||||
99
KernelSU/kernel/ksu.h
Normal file
99
KernelSU/kernel/ksu.h
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
#ifndef __KSU_H_KSU
|
||||
#define __KSU_H_KSU
|
||||
|
||||
#include <linux/types.h>
|
||||
#include <linux/workqueue.h>
|
||||
|
||||
#define KERNEL_SU_VERSION KSU_VERSION
|
||||
#define KERNEL_SU_OPTION 0xDEADBEEF
|
||||
|
||||
#define CMD_GRANT_ROOT 0
|
||||
#define CMD_BECOME_MANAGER 1
|
||||
#define CMD_GET_VERSION 2
|
||||
#define CMD_ALLOW_SU 3
|
||||
#define CMD_DENY_SU 4
|
||||
#define CMD_GET_ALLOW_LIST 5
|
||||
#define CMD_GET_DENY_LIST 6
|
||||
#define CMD_REPORT_EVENT 7
|
||||
#define CMD_SET_SEPOLICY 8
|
||||
#define CMD_CHECK_SAFEMODE 9
|
||||
#define CMD_GET_APP_PROFILE 10
|
||||
#define CMD_SET_APP_PROFILE 11
|
||||
#define CMD_UID_GRANTED_ROOT 12
|
||||
#define CMD_UID_SHOULD_UMOUNT 13
|
||||
|
||||
#define EVENT_POST_FS_DATA 1
|
||||
#define EVENT_BOOT_COMPLETED 2
|
||||
#define EVENT_MODULE_MOUNTED 3
|
||||
|
||||
#define KSU_APP_PROFILE_VER 2
|
||||
#define KSU_MAX_PACKAGE_NAME 256
|
||||
// NGROUPS_MAX for Linux is 65535 generally, but we only supports 32 groups.
|
||||
#define KSU_MAX_GROUPS 32
|
||||
#define KSU_SELINUX_DOMAIN 64
|
||||
|
||||
struct root_profile {
|
||||
int32_t uid;
|
||||
int32_t gid;
|
||||
|
||||
int32_t groups_count;
|
||||
int32_t groups[KSU_MAX_GROUPS];
|
||||
|
||||
// kernel_cap_t is u32[2] for capabilities v3
|
||||
struct {
|
||||
u64 effective;
|
||||
u64 permitted;
|
||||
u64 inheritable;
|
||||
} capabilities;
|
||||
|
||||
char selinux_domain[KSU_SELINUX_DOMAIN];
|
||||
|
||||
int32_t namespaces;
|
||||
};
|
||||
|
||||
struct non_root_profile {
|
||||
bool umount_modules;
|
||||
};
|
||||
|
||||
struct app_profile {
|
||||
// It may be utilized for backward compatibility, although we have never explicitly made any promises regarding this.
|
||||
u32 version;
|
||||
|
||||
// this is usually the package of the app, but can be other value for special apps
|
||||
char key[KSU_MAX_PACKAGE_NAME];
|
||||
int32_t current_uid;
|
||||
bool allow_su;
|
||||
|
||||
union {
|
||||
struct {
|
||||
bool use_default;
|
||||
char template_name[KSU_MAX_PACKAGE_NAME];
|
||||
|
||||
struct root_profile profile;
|
||||
} rp_config;
|
||||
|
||||
struct {
|
||||
bool use_default;
|
||||
|
||||
struct non_root_profile profile;
|
||||
} nrp_config;
|
||||
};
|
||||
};
|
||||
|
||||
bool ksu_queue_work(struct work_struct *work);
|
||||
|
||||
static inline int startswith(char *s, char *prefix)
|
||||
{
|
||||
return strncmp(s, prefix, strlen(prefix));
|
||||
}
|
||||
|
||||
static inline int endswith(const char *s, const char *t)
|
||||
{
|
||||
size_t slen = strlen(s);
|
||||
size_t tlen = strlen(t);
|
||||
if (tlen > slen)
|
||||
return 1;
|
||||
return strcmp(s + slen - tlen, t);
|
||||
}
|
||||
|
||||
#endif
|
||||
666
KernelSU/kernel/ksud.c
Normal file
666
KernelSU/kernel/ksud.c
Normal file
|
|
@ -0,0 +1,666 @@
|
|||
#include <asm/current.h>
|
||||
#include <linux/compat.h>
|
||||
#include <linux/cred.h>
|
||||
#include <linux/dcache.h>
|
||||
#include <linux/err.h>
|
||||
#include <linux/file.h>
|
||||
#include <linux/fs.h>
|
||||
#include <linux/version.h>
|
||||
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 4, 0)
|
||||
#include <linux/input-event-codes.h>
|
||||
#else
|
||||
#include <uapi/linux/input.h>
|
||||
#endif
|
||||
#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 1, 0)
|
||||
#include <linux/aio.h>
|
||||
#endif
|
||||
#include <linux/kprobes.h>
|
||||
#include <linux/printk.h>
|
||||
#include <linux/types.h>
|
||||
#include <linux/uaccess.h>
|
||||
#include <linux/workqueue.h>
|
||||
|
||||
#include "allowlist.h"
|
||||
#include "arch.h"
|
||||
#include "klog.h" // IWYU pragma: keep
|
||||
#include "ksud.h"
|
||||
#include "kernel_compat.h"
|
||||
#include "selinux/selinux.h"
|
||||
|
||||
static const char KERNEL_SU_RC[] =
|
||||
"\n"
|
||||
|
||||
"on post-fs-data\n"
|
||||
" start logd\n"
|
||||
// We should wait for the post-fs-data finish
|
||||
" exec u:r:su:s0 root -- " KSUD_PATH " post-fs-data\n"
|
||||
"\n"
|
||||
|
||||
"on nonencrypted\n"
|
||||
" exec u:r:su:s0 root -- " KSUD_PATH " services\n"
|
||||
"\n"
|
||||
|
||||
"on property:vold.decrypt=trigger_restart_framework\n"
|
||||
" exec u:r:su:s0 root -- " KSUD_PATH " services\n"
|
||||
"\n"
|
||||
|
||||
"on property:sys.boot_completed=1\n"
|
||||
" exec u:r:su:s0 root -- " KSUD_PATH " boot-completed\n"
|
||||
"\n"
|
||||
|
||||
"\n";
|
||||
|
||||
static void stop_vfs_read_hook();
|
||||
static void stop_execve_hook();
|
||||
static void stop_input_hook();
|
||||
|
||||
#ifdef CONFIG_KPROBES
|
||||
static struct work_struct stop_vfs_read_work;
|
||||
static struct work_struct stop_execve_hook_work;
|
||||
static struct work_struct stop_input_hook_work;
|
||||
#else
|
||||
bool ksu_vfs_read_hook __read_mostly = true;
|
||||
bool ksu_execveat_hook __read_mostly = true;
|
||||
bool ksu_input_hook __read_mostly = true;
|
||||
#endif
|
||||
|
||||
u32 ksu_devpts_sid;
|
||||
|
||||
void on_post_fs_data(void)
|
||||
{
|
||||
static bool done = false;
|
||||
if (done) {
|
||||
pr_info("on_post_fs_data already done\n");
|
||||
return;
|
||||
}
|
||||
done = true;
|
||||
pr_info("on_post_fs_data!\n");
|
||||
ksu_load_allow_list();
|
||||
// sanity check, this may influence the performance
|
||||
stop_input_hook();
|
||||
|
||||
ksu_devpts_sid = ksu_get_devpts_sid();
|
||||
pr_info("devpts sid: %d\n", ksu_devpts_sid);
|
||||
}
|
||||
|
||||
#define MAX_ARG_STRINGS 0x7FFFFFFF
|
||||
struct user_arg_ptr {
|
||||
#ifdef CONFIG_COMPAT
|
||||
bool is_compat;
|
||||
#endif
|
||||
union {
|
||||
const char __user *const __user *native;
|
||||
#ifdef CONFIG_COMPAT
|
||||
const compat_uptr_t __user *compat;
|
||||
#endif
|
||||
} ptr;
|
||||
};
|
||||
|
||||
static const char __user *get_user_arg_ptr(struct user_arg_ptr argv, int nr)
|
||||
{
|
||||
const char __user *native;
|
||||
|
||||
#ifdef CONFIG_COMPAT
|
||||
if (unlikely(argv.is_compat)) {
|
||||
compat_uptr_t compat;
|
||||
|
||||
if (get_user(compat, argv.ptr.compat + nr))
|
||||
return ERR_PTR(-EFAULT);
|
||||
|
||||
return compat_ptr(compat);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (get_user(native, argv.ptr.native + nr))
|
||||
return ERR_PTR(-EFAULT);
|
||||
|
||||
return native;
|
||||
}
|
||||
|
||||
/*
|
||||
* count() counts the number of strings in array ARGV.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Make sure old GCC compiler can use __maybe_unused,
|
||||
* Test passed in 4.4.x ~ 4.9.x when use GCC.
|
||||
*/
|
||||
|
||||
static int __maybe_unused count(struct user_arg_ptr argv, int max)
|
||||
{
|
||||
int i = 0;
|
||||
|
||||
if (argv.ptr.native != NULL) {
|
||||
for (;;) {
|
||||
const char __user *p = get_user_arg_ptr(argv, i);
|
||||
|
||||
if (!p)
|
||||
break;
|
||||
|
||||
if (IS_ERR(p))
|
||||
return -EFAULT;
|
||||
|
||||
if (i >= max)
|
||||
return -E2BIG;
|
||||
++i;
|
||||
|
||||
if (fatal_signal_pending(current))
|
||||
return -ERESTARTNOHAND;
|
||||
cond_resched();
|
||||
}
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
// IMPORTANT NOTE: the call from execve_handler_pre WON'T provided correct value for envp and flags in GKI version
|
||||
int ksu_handle_execveat_ksud(int *fd, struct filename **filename_ptr,
|
||||
struct user_arg_ptr *argv,
|
||||
struct user_arg_ptr *envp, int *flags)
|
||||
{
|
||||
#ifndef CONFIG_KPROBES
|
||||
if (!ksu_execveat_hook) {
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
struct filename *filename;
|
||||
|
||||
static const char app_process[] = "/system/bin/app_process";
|
||||
static bool first_app_process = true;
|
||||
|
||||
/* This applies to versions Android 10+ */
|
||||
static const char system_bin_init[] = "/system/bin/init";
|
||||
/* This applies to versions between Android 6 ~ 9 */
|
||||
static const char old_system_init[] = "/init";
|
||||
static bool init_second_stage_executed = false;
|
||||
|
||||
if (!filename_ptr)
|
||||
return 0;
|
||||
|
||||
filename = *filename_ptr;
|
||||
if (IS_ERR(filename)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (unlikely(!memcmp(filename->name, system_bin_init,
|
||||
sizeof(system_bin_init) - 1) &&
|
||||
argv)) {
|
||||
// /system/bin/init executed
|
||||
int argc = count(*argv, MAX_ARG_STRINGS);
|
||||
pr_info("/system/bin/init argc: %d\n", argc);
|
||||
if (argc > 1 && !init_second_stage_executed) {
|
||||
const char __user *p = get_user_arg_ptr(*argv, 1);
|
||||
if (p && !IS_ERR(p)) {
|
||||
char first_arg[16];
|
||||
ksu_strncpy_from_user_nofault(
|
||||
first_arg, p, sizeof(first_arg));
|
||||
pr_info("/system/bin/init first arg: %s\n",
|
||||
first_arg);
|
||||
if (!strcmp(first_arg, "second_stage")) {
|
||||
pr_info("/system/bin/init second_stage executed\n");
|
||||
apply_kernelsu_rules();
|
||||
init_second_stage_executed = true;
|
||||
ksu_android_ns_fs_check();
|
||||
}
|
||||
} else {
|
||||
pr_err("/system/bin/init parse args err!\n");
|
||||
}
|
||||
}
|
||||
} else if (unlikely(!memcmp(filename->name, old_system_init,
|
||||
sizeof(old_system_init) - 1) &&
|
||||
argv)) {
|
||||
// /init executed
|
||||
int argc = count(*argv, MAX_ARG_STRINGS);
|
||||
pr_info("/init argc: %d\n", argc);
|
||||
if (argc > 1 && !init_second_stage_executed) {
|
||||
/* This applies to versions between Android 6 ~ 7 */
|
||||
const char __user *p = get_user_arg_ptr(*argv, 1);
|
||||
if (p && !IS_ERR(p)) {
|
||||
char first_arg[16];
|
||||
ksu_strncpy_from_user_nofault(
|
||||
first_arg, p, sizeof(first_arg));
|
||||
pr_info("/init first arg: %s\n", first_arg);
|
||||
if (!strcmp(first_arg, "--second-stage")) {
|
||||
pr_info("/init second_stage executed\n");
|
||||
apply_kernelsu_rules();
|
||||
init_second_stage_executed = true;
|
||||
ksu_android_ns_fs_check();
|
||||
}
|
||||
} else {
|
||||
pr_err("/init parse args err!\n");
|
||||
}
|
||||
} else if (argc == 1 && !init_second_stage_executed && envp) {
|
||||
/* This applies to versions between Android 8 ~ 9 */
|
||||
int envc = count(*envp, MAX_ARG_STRINGS);
|
||||
if (envc > 0) {
|
||||
int n;
|
||||
for (n = 1; n <= envc; n++) {
|
||||
const char __user *p =
|
||||
get_user_arg_ptr(*envp, n);
|
||||
if (!p || IS_ERR(p)) {
|
||||
continue;
|
||||
}
|
||||
char env[256];
|
||||
// Reading environment variable strings from user space
|
||||
if (ksu_strncpy_from_user_nofault(
|
||||
env, p, sizeof(env)) < 0)
|
||||
continue;
|
||||
// Parsing environment variable names and values
|
||||
char *env_name = env;
|
||||
char *env_value = strchr(env, '=');
|
||||
if (env_value == NULL)
|
||||
continue;
|
||||
// Replace equal sign with string terminator
|
||||
*env_value = '\0';
|
||||
env_value++;
|
||||
// Check if the environment variable name and value are matching
|
||||
if (!strcmp(env_name,
|
||||
"INIT_SECOND_STAGE") &&
|
||||
(!strcmp(env_value, "1") ||
|
||||
!strcmp(env_value, "true"))) {
|
||||
pr_info("/init second_stage executed\n");
|
||||
apply_kernelsu_rules();
|
||||
init_second_stage_executed =
|
||||
true;
|
||||
ksu_android_ns_fs_check();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (unlikely(first_app_process && !memcmp(filename->name, app_process,
|
||||
sizeof(app_process) - 1))) {
|
||||
first_app_process = false;
|
||||
pr_info("exec app_process, /data prepared, second_stage: %d\n",
|
||||
init_second_stage_executed);
|
||||
on_post_fs_data(); // we keep this for old ksud
|
||||
stop_execve_hook();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static ssize_t (*orig_read)(struct file *, char __user *, size_t, loff_t *);
|
||||
static ssize_t (*orig_read_iter)(struct kiocb *, struct iov_iter *);
|
||||
static struct file_operations fops_proxy;
|
||||
static ssize_t read_count_append = 0;
|
||||
|
||||
static ssize_t read_proxy(struct file *file, char __user *buf, size_t count,
|
||||
loff_t *pos)
|
||||
{
|
||||
bool first_read = file->f_pos == 0;
|
||||
ssize_t ret = orig_read(file, buf, count, pos);
|
||||
if (first_read) {
|
||||
pr_info("read_proxy append %ld + %ld\n", ret,
|
||||
read_count_append);
|
||||
ret += read_count_append;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static ssize_t read_iter_proxy(struct kiocb *iocb, struct iov_iter *to)
|
||||
{
|
||||
bool first_read = iocb->ki_pos == 0;
|
||||
ssize_t ret = orig_read_iter(iocb, to);
|
||||
if (first_read) {
|
||||
pr_info("read_iter_proxy append %ld + %ld\n", ret,
|
||||
read_count_append);
|
||||
ret += read_count_append;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
int ksu_handle_vfs_read(struct file **file_ptr, char __user **buf_ptr,
|
||||
size_t *count_ptr, loff_t **pos)
|
||||
{
|
||||
#ifndef CONFIG_KPROBES
|
||||
if (!ksu_vfs_read_hook) {
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
struct file *file;
|
||||
char __user *buf;
|
||||
size_t count;
|
||||
|
||||
if (strcmp(current->comm, "init")) {
|
||||
// we are only interest in `init` process
|
||||
return 0;
|
||||
}
|
||||
|
||||
file = *file_ptr;
|
||||
if (IS_ERR(file)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!d_is_reg(file->f_path.dentry)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char *short_name = file->f_path.dentry->d_name.name;
|
||||
if (strcmp(short_name, "atrace.rc")) {
|
||||
// we are only interest `atrace.rc` file name file
|
||||
return 0;
|
||||
}
|
||||
char path[256];
|
||||
char *dpath = d_path(&file->f_path, path, sizeof(path));
|
||||
|
||||
if (IS_ERR(dpath)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (strcmp(dpath, "/system/etc/init/atrace.rc")) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// we only process the first read
|
||||
static bool rc_inserted = false;
|
||||
if (rc_inserted) {
|
||||
// we don't need this kprobe, unregister it!
|
||||
stop_vfs_read_hook();
|
||||
return 0;
|
||||
}
|
||||
rc_inserted = true;
|
||||
|
||||
// now we can sure that the init process is reading
|
||||
// `/system/etc/init/atrace.rc`
|
||||
buf = *buf_ptr;
|
||||
count = *count_ptr;
|
||||
|
||||
size_t rc_count = strlen(KERNEL_SU_RC);
|
||||
|
||||
pr_info("vfs_read: %s, comm: %s, count: %zu, rc_count: %zu\n", dpath,
|
||||
current->comm, count, rc_count);
|
||||
|
||||
if (count < rc_count) {
|
||||
pr_err("count: %zu < rc_count: %zu\n", count, rc_count);
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t ret = copy_to_user(buf, KERNEL_SU_RC, rc_count);
|
||||
if (ret) {
|
||||
pr_err("copy ksud.rc failed: %zu\n", ret);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// we've succeed to insert ksud.rc, now we need to proxy the read and modify the result!
|
||||
// But, we can not modify the file_operations directly, because it's in read-only memory.
|
||||
// We just replace the whole file_operations with a proxy one.
|
||||
memcpy(&fops_proxy, file->f_op, sizeof(struct file_operations));
|
||||
orig_read = file->f_op->read;
|
||||
if (orig_read) {
|
||||
fops_proxy.read = read_proxy;
|
||||
}
|
||||
orig_read_iter = file->f_op->read_iter;
|
||||
if (orig_read_iter) {
|
||||
fops_proxy.read_iter = read_iter_proxy;
|
||||
}
|
||||
// replace the file_operations
|
||||
file->f_op = &fops_proxy;
|
||||
read_count_append = rc_count;
|
||||
|
||||
*buf_ptr = buf + rc_count;
|
||||
*count_ptr = count - rc_count;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ksu_handle_sys_read(unsigned int fd, char __user **buf_ptr,
|
||||
size_t *count_ptr)
|
||||
{
|
||||
struct file *file = fget(fd);
|
||||
if (!file) {
|
||||
return 0;
|
||||
}
|
||||
int result = ksu_handle_vfs_read(&file, buf_ptr, count_ptr, NULL);
|
||||
fput(file);
|
||||
return result;
|
||||
}
|
||||
|
||||
static unsigned int volumedown_pressed_count = 0;
|
||||
|
||||
static bool is_volumedown_enough(unsigned int count)
|
||||
{
|
||||
return count >= 3;
|
||||
}
|
||||
|
||||
int ksu_handle_input_handle_event(unsigned int *type, unsigned int *code,
|
||||
int *value)
|
||||
{
|
||||
#ifndef CONFIG_KPROBES
|
||||
if (!ksu_input_hook) {
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
if (*type == EV_KEY && *code == KEY_VOLUMEDOWN) {
|
||||
int val = *value;
|
||||
pr_info("KEY_VOLUMEDOWN val: %d\n", val);
|
||||
if (val) {
|
||||
// key pressed, count it
|
||||
volumedown_pressed_count += 1;
|
||||
if (is_volumedown_enough(volumedown_pressed_count)) {
|
||||
stop_input_hook();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool ksu_is_safe_mode()
|
||||
{
|
||||
static bool safe_mode = false;
|
||||
if (safe_mode) {
|
||||
// don't need to check again, userspace may call multiple times
|
||||
return true;
|
||||
}
|
||||
|
||||
// stop hook first!
|
||||
stop_input_hook();
|
||||
|
||||
pr_info("volumedown_pressed_count: %d\n", volumedown_pressed_count);
|
||||
if (is_volumedown_enough(volumedown_pressed_count)) {
|
||||
// pressed over 3 times
|
||||
pr_info("KEY_VOLUMEDOWN pressed max times, safe mode detected!\n");
|
||||
safe_mode = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef CONFIG_KPROBES
|
||||
|
||||
// https://elixir.bootlin.com/linux/v5.10.158/source/fs/exec.c#L1864
|
||||
static int execve_handler_pre(struct kprobe *p, struct pt_regs *regs)
|
||||
{
|
||||
int *fd = (int *)&PT_REGS_PARM1(regs);
|
||||
struct filename **filename_ptr =
|
||||
(struct filename **)&PT_REGS_PARM2(regs);
|
||||
struct user_arg_ptr argv;
|
||||
#ifdef CONFIG_COMPAT
|
||||
argv.is_compat = PT_REGS_PARM3(regs);
|
||||
if (unlikely(argv.is_compat)) {
|
||||
argv.ptr.compat = PT_REGS_CCALL_PARM4(regs);
|
||||
} else {
|
||||
argv.ptr.native = PT_REGS_CCALL_PARM4(regs);
|
||||
}
|
||||
#else
|
||||
argv.ptr.native = PT_REGS_PARM3(regs);
|
||||
#endif
|
||||
|
||||
return ksu_handle_execveat_ksud(fd, filename_ptr, &argv, NULL, NULL);
|
||||
}
|
||||
|
||||
static int sys_execve_handler_pre(struct kprobe *p, struct pt_regs *regs)
|
||||
{
|
||||
struct pt_regs *real_regs = PT_REAL_REGS(regs);
|
||||
const char __user **filename_user =
|
||||
(const char **)&PT_REGS_PARM1(real_regs);
|
||||
const char __user *const __user *__argv =
|
||||
(const char __user *const __user *)PT_REGS_PARM2(real_regs);
|
||||
struct user_arg_ptr argv = { .ptr.native = __argv };
|
||||
struct filename filename_in, *filename_p;
|
||||
char path[32];
|
||||
|
||||
if (!filename_user)
|
||||
return 0;
|
||||
|
||||
memset(path, 0, sizeof(path));
|
||||
ksu_strncpy_from_user_nofault(path, *filename_user, 32);
|
||||
filename_in.name = path;
|
||||
|
||||
filename_p = &filename_in;
|
||||
return ksu_handle_execveat_ksud(AT_FDCWD, &filename_p, &argv, NULL,
|
||||
NULL);
|
||||
}
|
||||
|
||||
// remove this later!
|
||||
__maybe_unused static int vfs_read_handler_pre(struct kprobe *p,
|
||||
struct pt_regs *regs)
|
||||
{
|
||||
struct file **file_ptr = (struct file **)&PT_REGS_PARM1(regs);
|
||||
char __user **buf_ptr = (char **)&PT_REGS_PARM2(regs);
|
||||
size_t *count_ptr = (size_t *)&PT_REGS_PARM3(regs);
|
||||
loff_t **pos_ptr = (loff_t **)&PT_REGS_CCALL_PARM4(regs);
|
||||
|
||||
return ksu_handle_vfs_read(file_ptr, buf_ptr, count_ptr, pos_ptr);
|
||||
}
|
||||
|
||||
static int sys_read_handler_pre(struct kprobe *p, struct pt_regs *regs)
|
||||
{
|
||||
struct pt_regs *real_regs = PT_REAL_REGS(regs);
|
||||
unsigned int fd = PT_REGS_PARM1(real_regs);
|
||||
char __user **buf_ptr = (char __user **)&PT_REGS_PARM2(real_regs);
|
||||
size_t count_ptr = (size_t *)&PT_REGS_PARM3(real_regs);
|
||||
|
||||
return ksu_handle_sys_read(fd, buf_ptr, count_ptr);
|
||||
}
|
||||
|
||||
static int input_handle_event_handler_pre(struct kprobe *p,
|
||||
struct pt_regs *regs)
|
||||
{
|
||||
unsigned int *type = (unsigned int *)&PT_REGS_PARM2(regs);
|
||||
unsigned int *code = (unsigned int *)&PT_REGS_PARM3(regs);
|
||||
int *value = (int *)&PT_REGS_CCALL_PARM4(regs);
|
||||
return ksu_handle_input_handle_event(type, code, value);
|
||||
}
|
||||
|
||||
#if 1
|
||||
static struct kprobe execve_kp = {
|
||||
.symbol_name = SYS_EXECVE_SYMBOL,
|
||||
.pre_handler = sys_execve_handler_pre,
|
||||
};
|
||||
#else
|
||||
static struct kprobe execve_kp = {
|
||||
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 9, 0)
|
||||
.symbol_name = "do_execveat_common",
|
||||
#elif LINUX_VERSION_CODE >= KERNEL_VERSION(4, 19, 0)
|
||||
.symbol_name = "__do_execve_file",
|
||||
#elif LINUX_VERSION_CODE >= KERNEL_VERSION(3, 19, 0)
|
||||
.symbol_name = "do_execveat_common",
|
||||
#endif
|
||||
.pre_handler = execve_handler_pre,
|
||||
};
|
||||
#endif
|
||||
|
||||
#if 1
|
||||
static struct kprobe vfs_read_kp = {
|
||||
.symbol_name = SYS_READ_SYMBOL,
|
||||
.pre_handler = sys_read_handler_pre,
|
||||
};
|
||||
#else
|
||||
static struct kprobe vfs_read_kp = {
|
||||
.symbol_name = "vfs_read",
|
||||
.pre_handler = vfs_read_handler_pre,
|
||||
};
|
||||
#endif
|
||||
|
||||
static struct kprobe input_event_kp = {
|
||||
.symbol_name = "input_event",
|
||||
.pre_handler = input_handle_event_handler_pre,
|
||||
};
|
||||
|
||||
static void do_stop_vfs_read_hook(struct work_struct *work)
|
||||
{
|
||||
unregister_kprobe(&vfs_read_kp);
|
||||
}
|
||||
|
||||
static void do_stop_execve_hook(struct work_struct *work)
|
||||
{
|
||||
unregister_kprobe(&execve_kp);
|
||||
}
|
||||
|
||||
static void do_stop_input_hook(struct work_struct *work)
|
||||
{
|
||||
unregister_kprobe(&input_event_kp);
|
||||
}
|
||||
#endif
|
||||
|
||||
static void stop_vfs_read_hook()
|
||||
{
|
||||
#ifdef CONFIG_KPROBES
|
||||
bool ret = schedule_work(&stop_vfs_read_work);
|
||||
pr_info("unregister vfs_read kprobe: %d!\n", ret);
|
||||
#else
|
||||
ksu_vfs_read_hook = false;
|
||||
pr_info("stop vfs_read_hook\n");
|
||||
#endif
|
||||
}
|
||||
|
||||
static void stop_execve_hook()
|
||||
{
|
||||
#ifdef CONFIG_KPROBES
|
||||
bool ret = schedule_work(&stop_execve_hook_work);
|
||||
pr_info("unregister execve kprobe: %d!\n", ret);
|
||||
#else
|
||||
ksu_execveat_hook = false;
|
||||
pr_info("stop execve_hook\n");
|
||||
#endif
|
||||
}
|
||||
|
||||
static void stop_input_hook()
|
||||
{
|
||||
static bool input_hook_stopped = false;
|
||||
if (input_hook_stopped) {
|
||||
return;
|
||||
}
|
||||
input_hook_stopped = true;
|
||||
#ifdef CONFIG_KPROBES
|
||||
bool ret = schedule_work(&stop_input_hook_work);
|
||||
pr_info("unregister input kprobe: %d!\n", ret);
|
||||
#else
|
||||
ksu_input_hook = false;
|
||||
pr_info("stop input_hook\n");
|
||||
#endif
|
||||
}
|
||||
|
||||
// ksud: module support
|
||||
void ksu_ksud_init()
|
||||
{
|
||||
#ifdef CONFIG_KPROBES
|
||||
int ret;
|
||||
|
||||
ret = register_kprobe(&execve_kp);
|
||||
pr_info("ksud: execve_kp: %d\n", ret);
|
||||
|
||||
ret = register_kprobe(&vfs_read_kp);
|
||||
pr_info("ksud: vfs_read_kp: %d\n", ret);
|
||||
|
||||
ret = register_kprobe(&input_event_kp);
|
||||
pr_info("ksud: input_event_kp: %d\n", ret);
|
||||
|
||||
INIT_WORK(&stop_vfs_read_work, do_stop_vfs_read_hook);
|
||||
INIT_WORK(&stop_execve_hook_work, do_stop_execve_hook);
|
||||
INIT_WORK(&stop_input_hook_work, do_stop_input_hook);
|
||||
#endif
|
||||
}
|
||||
|
||||
void ksu_ksud_exit()
|
||||
{
|
||||
#ifdef CONFIG_KPROBES
|
||||
unregister_kprobe(&execve_kp);
|
||||
// this should be done before unregister vfs_read_kp
|
||||
// unregister_kprobe(&vfs_read_kp);
|
||||
unregister_kprobe(&input_event_kp);
|
||||
#endif
|
||||
}
|
||||
14
KernelSU/kernel/ksud.h
Normal file
14
KernelSU/kernel/ksud.h
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
#ifndef __KSU_H_KSUD
|
||||
#define __KSU_H_KSUD
|
||||
|
||||
#include <linux/types.h>
|
||||
|
||||
#define KSUD_PATH "/data/adb/ksud"
|
||||
|
||||
void on_post_fs_data(void);
|
||||
|
||||
bool ksu_is_safe_mode(void);
|
||||
|
||||
extern u32 ksu_devpts_sid;
|
||||
|
||||
#endif
|
||||
36
KernelSU/kernel/manager.h
Normal file
36
KernelSU/kernel/manager.h
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
#ifndef __KSU_H_KSU_MANAGER
|
||||
#define __KSU_H_KSU_MANAGER
|
||||
|
||||
#include <linux/cred.h>
|
||||
#include <linux/types.h>
|
||||
|
||||
#define KSU_INVALID_UID -1
|
||||
|
||||
extern uid_t ksu_manager_uid; // DO NOT DIRECT USE
|
||||
|
||||
static inline bool ksu_is_manager_uid_valid()
|
||||
{
|
||||
return ksu_manager_uid != KSU_INVALID_UID;
|
||||
}
|
||||
|
||||
static inline bool is_manager()
|
||||
{
|
||||
return unlikely(ksu_manager_uid == current_uid().val);
|
||||
}
|
||||
|
||||
static inline uid_t ksu_get_manager_uid()
|
||||
{
|
||||
return ksu_manager_uid;
|
||||
}
|
||||
|
||||
static inline void ksu_set_manager_uid(uid_t uid)
|
||||
{
|
||||
ksu_manager_uid = uid;
|
||||
}
|
||||
|
||||
static inline void ksu_invalidate_manager_uid()
|
||||
{
|
||||
ksu_manager_uid = KSU_INVALID_UID;
|
||||
}
|
||||
|
||||
#endif
|
||||
16
KernelSU/kernel/selinux/Makefile
Normal file
16
KernelSU/kernel/selinux/Makefile
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
obj-y += selinux.o
|
||||
obj-y += sepolicy.o
|
||||
obj-y += rules.o
|
||||
|
||||
ifeq ($(shell grep -q " current_sid(void)" $(srctree)/security/selinux/include/objsec.h; echo $$?),0)
|
||||
ccflags-y += -DKSU_COMPAT_HAS_CURRENT_SID
|
||||
endif
|
||||
|
||||
ifeq ($(shell grep -q "struct selinux_state " $(srctree)/security/selinux/include/security.h; echo $$?),0)
|
||||
ccflags-y += -DKSU_COMPAT_HAS_SELINUX_STATE
|
||||
endif
|
||||
|
||||
ccflags-y += -Wno-implicit-function-declaration -Wno-strict-prototypes -Wno-int-conversion
|
||||
ccflags-y += -Wno-declaration-after-statement -Wno-unused-function
|
||||
ccflags-y += -I$(srctree)/security/selinux -I$(srctree)/security/selinux/include
|
||||
ccflags-y += -I$(objtree)/security/selinux -include $(srctree)/include/uapi/asm-generic/errno.h
|
||||
476
KernelSU/kernel/selinux/rules.c
Normal file
476
KernelSU/kernel/selinux/rules.c
Normal file
|
|
@ -0,0 +1,476 @@
|
|||
#include <linux/uaccess.h>
|
||||
#include <linux/types.h>
|
||||
#include <linux/version.h>
|
||||
|
||||
#include "../klog.h" // IWYU pragma: keep
|
||||
#include "selinux.h"
|
||||
#include "sepolicy.h"
|
||||
#include "ss/services.h"
|
||||
#include "linux/lsm_audit.h"
|
||||
#include "xfrm.h"
|
||||
|
||||
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 10, 0)
|
||||
#define SELINUX_POLICY_INSTEAD_SELINUX_SS
|
||||
#endif
|
||||
|
||||
#define KERNEL_SU_DOMAIN "su"
|
||||
#define KERNEL_SU_FILE "ksu_file"
|
||||
#define KERNEL_EXEC_TYPE "ksu_exec"
|
||||
#define ALL NULL
|
||||
|
||||
static struct policydb *get_policydb(void)
|
||||
{
|
||||
struct policydb *db;
|
||||
// selinux_state does not exists before 4.19
|
||||
#ifdef KSU_COMPAT_USE_SELINUX_STATE
|
||||
#ifdef SELINUX_POLICY_INSTEAD_SELINUX_SS
|
||||
struct selinux_policy *policy = rcu_dereference(selinux_state.policy);
|
||||
db = &policy->policydb;
|
||||
#else
|
||||
struct selinux_ss *ss = rcu_dereference(selinux_state.ss);
|
||||
db = &ss->policydb;
|
||||
#endif
|
||||
#else
|
||||
db = &policydb;
|
||||
#endif
|
||||
return db;
|
||||
}
|
||||
|
||||
void apply_kernelsu_rules()
|
||||
{
|
||||
if (!getenforce()) {
|
||||
pr_info("SELinux permissive or disabled, apply rules!\n");
|
||||
}
|
||||
|
||||
rcu_read_lock();
|
||||
struct policydb *db = get_policydb();
|
||||
|
||||
ksu_permissive(db, KERNEL_SU_DOMAIN);
|
||||
ksu_typeattribute(db, KERNEL_SU_DOMAIN, "mlstrustedsubject");
|
||||
ksu_typeattribute(db, KERNEL_SU_DOMAIN, "netdomain");
|
||||
ksu_typeattribute(db, KERNEL_SU_DOMAIN, "bluetoothdomain");
|
||||
|
||||
// Create unconstrained file type
|
||||
ksu_type(db, KERNEL_SU_FILE, "file_type");
|
||||
ksu_typeattribute(db, KERNEL_SU_FILE, "mlstrustedobject");
|
||||
ksu_allow(db, ALL, KERNEL_SU_FILE, ALL, ALL);
|
||||
|
||||
// allow all!
|
||||
ksu_allow(db, KERNEL_SU_DOMAIN, ALL, ALL, ALL);
|
||||
|
||||
// allow us do any ioctl
|
||||
if (db->policyvers >= POLICYDB_VERSION_XPERMS_IOCTL) {
|
||||
ksu_allowxperm(db, KERNEL_SU_DOMAIN, ALL, "blk_file", ALL);
|
||||
ksu_allowxperm(db, KERNEL_SU_DOMAIN, ALL, "fifo_file", ALL);
|
||||
ksu_allowxperm(db, KERNEL_SU_DOMAIN, ALL, "chr_file", ALL);
|
||||
ksu_allowxperm(db, KERNEL_SU_DOMAIN, ALL, "file", ALL);
|
||||
}
|
||||
|
||||
// we need to save allowlist in /data/adb/ksu
|
||||
ksu_allow(db, "kernel", "adb_data_file", "dir", ALL);
|
||||
ksu_allow(db, "kernel", "adb_data_file", "file", ALL);
|
||||
// we need to search /data/app
|
||||
ksu_allow(db, "kernel", "apk_data_file", "file", "open");
|
||||
ksu_allow(db, "kernel", "apk_data_file", "dir", "open");
|
||||
ksu_allow(db, "kernel", "apk_data_file", "dir", "read");
|
||||
ksu_allow(db, "kernel", "apk_data_file", "dir", "search");
|
||||
// we may need to do mount on shell
|
||||
ksu_allow(db, "kernel", "shell_data_file", "file", ALL);
|
||||
// we need to read /data/system/packages.list
|
||||
ksu_allow(db, "kernel", "kernel", "capability", "dac_override");
|
||||
// Android 10+:
|
||||
// http://aospxref.com/android-12.0.0_r3/xref/system/sepolicy/private/file_contexts#512
|
||||
ksu_allow(db, "kernel", "packages_list_file", "file", ALL);
|
||||
// Kernel 4.4
|
||||
ksu_allow(db, "kernel", "packages_list_file", "dir", ALL);
|
||||
// Android 9-:
|
||||
// http://aospxref.com/android-9.0.0_r61/xref/system/sepolicy/private/file_contexts#360
|
||||
ksu_allow(db, "kernel", "system_data_file", "file", ALL);
|
||||
ksu_allow(db, "kernel", "system_data_file", "dir", ALL);
|
||||
// our ksud triggered by init
|
||||
ksu_allow(db, "init", "adb_data_file", "file", ALL);
|
||||
ksu_allow(db, "init", "adb_data_file", "dir", ALL); // #1289
|
||||
ksu_allow(db, "init", KERNEL_SU_DOMAIN, ALL, ALL);
|
||||
// we need to umount modules in zygote
|
||||
ksu_allow(db, "zygote", "adb_data_file", "dir", "search");
|
||||
|
||||
// copied from Magisk rules
|
||||
// suRights
|
||||
ksu_allow(db, "servicemanager", KERNEL_SU_DOMAIN, "dir", "search");
|
||||
ksu_allow(db, "servicemanager", KERNEL_SU_DOMAIN, "dir", "read");
|
||||
ksu_allow(db, "servicemanager", KERNEL_SU_DOMAIN, "file", "open");
|
||||
ksu_allow(db, "servicemanager", KERNEL_SU_DOMAIN, "file", "read");
|
||||
ksu_allow(db, "servicemanager", KERNEL_SU_DOMAIN, "process", "getattr");
|
||||
ksu_allow(db, ALL, KERNEL_SU_DOMAIN, "process", "sigchld");
|
||||
|
||||
// allowLog
|
||||
ksu_allow(db, "logd", KERNEL_SU_DOMAIN, "dir", "search");
|
||||
ksu_allow(db, "logd", KERNEL_SU_DOMAIN, "file", "read");
|
||||
ksu_allow(db, "logd", KERNEL_SU_DOMAIN, "file", "open");
|
||||
ksu_allow(db, "logd", KERNEL_SU_DOMAIN, "file", "getattr");
|
||||
|
||||
// dumpsys
|
||||
ksu_allow(db, ALL, KERNEL_SU_DOMAIN, "fd", "use");
|
||||
ksu_allow(db, ALL, KERNEL_SU_DOMAIN, "fifo_file", "write");
|
||||
ksu_allow(db, ALL, KERNEL_SU_DOMAIN, "fifo_file", "read");
|
||||
ksu_allow(db, ALL, KERNEL_SU_DOMAIN, "fifo_file", "open");
|
||||
ksu_allow(db, ALL, KERNEL_SU_DOMAIN, "fifo_file", "getattr");
|
||||
|
||||
// bootctl
|
||||
ksu_allow(db, "hwservicemanager", KERNEL_SU_DOMAIN, "dir", "search");
|
||||
ksu_allow(db, "hwservicemanager", KERNEL_SU_DOMAIN, "file", "read");
|
||||
ksu_allow(db, "hwservicemanager", KERNEL_SU_DOMAIN, "file", "open");
|
||||
ksu_allow(db, "hwservicemanager", KERNEL_SU_DOMAIN, "process",
|
||||
"getattr");
|
||||
|
||||
// For mounting loop devices, mirrors, tmpfs
|
||||
ksu_allow(db, "kernel", ALL, "file", "read");
|
||||
ksu_allow(db, "kernel", ALL, "file", "write");
|
||||
|
||||
// Allow all binder transactions
|
||||
ksu_allow(db, ALL, KERNEL_SU_DOMAIN, "binder", ALL);
|
||||
|
||||
// Allow system server kill su process
|
||||
ksu_allow(db, "system_server", KERNEL_SU_DOMAIN, "process", "getpgid");
|
||||
ksu_allow(db, "system_server", KERNEL_SU_DOMAIN, "process", "sigkill");
|
||||
|
||||
rcu_read_unlock();
|
||||
}
|
||||
|
||||
#define MAX_SEPOL_LEN 128
|
||||
|
||||
#define CMD_NORMAL_PERM 1
|
||||
#define CMD_XPERM 2
|
||||
#define CMD_TYPE_STATE 3
|
||||
#define CMD_TYPE 4
|
||||
#define CMD_TYPE_ATTR 5
|
||||
#define CMD_ATTR 6
|
||||
#define CMD_TYPE_TRANSITION 7
|
||||
#define CMD_TYPE_CHANGE 8
|
||||
#define CMD_GENFSCON 9
|
||||
|
||||
struct sepol_data {
|
||||
u32 cmd;
|
||||
u32 subcmd;
|
||||
char __user *sepol1;
|
||||
char __user *sepol2;
|
||||
char __user *sepol3;
|
||||
char __user *sepol4;
|
||||
char __user *sepol5;
|
||||
char __user *sepol6;
|
||||
char __user *sepol7;
|
||||
};
|
||||
|
||||
static int get_object(char *buf, char __user *user_object, size_t buf_sz,
|
||||
char **object)
|
||||
{
|
||||
if (!user_object) {
|
||||
*object = ALL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (strncpy_from_user(buf, user_object, buf_sz) < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*object = buf;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// reset avc cache table, otherwise the new rules will not take effect if already denied
|
||||
static void reset_avc_cache()
|
||||
{
|
||||
#if ((!defined(KSU_COMPAT_USE_SELINUX_STATE)) || \
|
||||
LINUX_VERSION_CODE >= KERNEL_VERSION(6, 4, 0))
|
||||
avc_ss_reset(0);
|
||||
selnl_notify_policyload(0);
|
||||
selinux_status_update_policyload(0);
|
||||
#else
|
||||
struct selinux_avc *avc = selinux_state.avc;
|
||||
avc_ss_reset(avc, 0);
|
||||
selnl_notify_policyload(0);
|
||||
selinux_status_update_policyload(&selinux_state, 0);
|
||||
#endif
|
||||
selinux_xfrm_notify_policyload();
|
||||
}
|
||||
|
||||
int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
||||
{
|
||||
if (!arg4) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!getenforce()) {
|
||||
pr_info("SELinux permissive or disabled when handle policy!\n");
|
||||
}
|
||||
|
||||
struct sepol_data data;
|
||||
if (copy_from_user(&data, arg4, sizeof(struct sepol_data))) {
|
||||
pr_err("sepol: copy sepol_data failed.\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
u32 cmd = data.cmd;
|
||||
u32 subcmd = data.subcmd;
|
||||
|
||||
rcu_read_lock();
|
||||
|
||||
struct policydb *db = get_policydb();
|
||||
|
||||
int ret = -1;
|
||||
if (cmd == CMD_NORMAL_PERM) {
|
||||
char src_buf[MAX_SEPOL_LEN];
|
||||
char tgt_buf[MAX_SEPOL_LEN];
|
||||
char cls_buf[MAX_SEPOL_LEN];
|
||||
char perm_buf[MAX_SEPOL_LEN];
|
||||
|
||||
char *s, *t, *c, *p;
|
||||
if (get_object(src_buf, data.sepol1, sizeof(src_buf), &s) < 0) {
|
||||
pr_err("sepol: copy src failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
|
||||
if (get_object(tgt_buf, data.sepol2, sizeof(tgt_buf), &t) < 0) {
|
||||
pr_err("sepol: copy tgt failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
|
||||
if (get_object(cls_buf, data.sepol3, sizeof(cls_buf), &c) < 0) {
|
||||
pr_err("sepol: copy cls failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
|
||||
if (get_object(perm_buf, data.sepol4, sizeof(perm_buf), &p) <
|
||||
0) {
|
||||
pr_err("sepol: copy perm failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
if (subcmd == 1) {
|
||||
success = ksu_allow(db, s, t, c, p);
|
||||
} else if (subcmd == 2) {
|
||||
success = ksu_deny(db, s, t, c, p);
|
||||
} else if (subcmd == 3) {
|
||||
success = ksu_auditallow(db, s, t, c, p);
|
||||
} else if (subcmd == 4) {
|
||||
success = ksu_dontaudit(db, s, t, c, p);
|
||||
} else {
|
||||
pr_err("sepol: unknown subcmd: %d\n", subcmd);
|
||||
}
|
||||
ret = success ? 0 : -1;
|
||||
|
||||
} else if (cmd == CMD_XPERM) {
|
||||
char src_buf[MAX_SEPOL_LEN];
|
||||
char tgt_buf[MAX_SEPOL_LEN];
|
||||
char cls_buf[MAX_SEPOL_LEN];
|
||||
|
||||
char __maybe_unused
|
||||
operation[MAX_SEPOL_LEN]; // it is always ioctl now!
|
||||
char perm_set[MAX_SEPOL_LEN];
|
||||
|
||||
char *s, *t, *c;
|
||||
if (get_object(src_buf, data.sepol1, sizeof(src_buf), &s) < 0) {
|
||||
pr_err("sepol: copy src failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (get_object(tgt_buf, data.sepol2, sizeof(tgt_buf), &t) < 0) {
|
||||
pr_err("sepol: copy tgt failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (get_object(cls_buf, data.sepol3, sizeof(cls_buf), &c) < 0) {
|
||||
pr_err("sepol: copy cls failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(operation, data.sepol4,
|
||||
sizeof(operation)) < 0) {
|
||||
pr_err("sepol: copy operation failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(perm_set, data.sepol5, sizeof(perm_set)) <
|
||||
0) {
|
||||
pr_err("sepol: copy perm_set failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
if (subcmd == 1) {
|
||||
success = ksu_allowxperm(db, s, t, c, perm_set);
|
||||
} else if (subcmd == 2) {
|
||||
success = ksu_auditallowxperm(db, s, t, c, perm_set);
|
||||
} else if (subcmd == 3) {
|
||||
success = ksu_dontauditxperm(db, s, t, c, perm_set);
|
||||
} else {
|
||||
pr_err("sepol: unknown subcmd: %d\n", subcmd);
|
||||
}
|
||||
ret = success ? 0 : -1;
|
||||
} else if (cmd == CMD_TYPE_STATE) {
|
||||
char src[MAX_SEPOL_LEN];
|
||||
|
||||
if (strncpy_from_user(src, data.sepol1, sizeof(src)) < 0) {
|
||||
pr_err("sepol: copy src failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
if (subcmd == 1) {
|
||||
success = ksu_permissive(db, src);
|
||||
} else if (subcmd == 2) {
|
||||
success = ksu_enforce(db, src);
|
||||
} else {
|
||||
pr_err("sepol: unknown subcmd: %d\n", subcmd);
|
||||
}
|
||||
if (success)
|
||||
ret = 0;
|
||||
|
||||
} else if (cmd == CMD_TYPE || cmd == CMD_TYPE_ATTR) {
|
||||
char type[MAX_SEPOL_LEN];
|
||||
char attr[MAX_SEPOL_LEN];
|
||||
|
||||
if (strncpy_from_user(type, data.sepol1, sizeof(type)) < 0) {
|
||||
pr_err("sepol: copy type failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(attr, data.sepol2, sizeof(attr)) < 0) {
|
||||
pr_err("sepol: copy attr failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
if (cmd == CMD_TYPE) {
|
||||
success = ksu_type(db, type, attr);
|
||||
} else {
|
||||
success = ksu_typeattribute(db, type, attr);
|
||||
}
|
||||
if (!success) {
|
||||
pr_err("sepol: %d failed.\n", cmd);
|
||||
goto exit;
|
||||
}
|
||||
ret = 0;
|
||||
|
||||
} else if (cmd == CMD_ATTR) {
|
||||
char attr[MAX_SEPOL_LEN];
|
||||
|
||||
if (strncpy_from_user(attr, data.sepol1, sizeof(attr)) < 0) {
|
||||
pr_err("sepol: copy attr failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (!ksu_attribute(db, attr)) {
|
||||
pr_err("sepol: %d failed.\n", cmd);
|
||||
goto exit;
|
||||
}
|
||||
ret = 0;
|
||||
|
||||
} else if (cmd == CMD_TYPE_TRANSITION) {
|
||||
char src[MAX_SEPOL_LEN];
|
||||
char tgt[MAX_SEPOL_LEN];
|
||||
char cls[MAX_SEPOL_LEN];
|
||||
char default_type[MAX_SEPOL_LEN];
|
||||
char object[MAX_SEPOL_LEN];
|
||||
|
||||
if (strncpy_from_user(src, data.sepol1, sizeof(src)) < 0) {
|
||||
pr_err("sepol: copy src failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(tgt, data.sepol2, sizeof(tgt)) < 0) {
|
||||
pr_err("sepol: copy tgt failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(cls, data.sepol3, sizeof(cls)) < 0) {
|
||||
pr_err("sepol: copy cls failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(default_type, data.sepol4,
|
||||
sizeof(default_type)) < 0) {
|
||||
pr_err("sepol: copy default_type failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
char *real_object;
|
||||
if (data.sepol5 == NULL) {
|
||||
real_object = NULL;
|
||||
} else {
|
||||
if (strncpy_from_user(object, data.sepol5,
|
||||
sizeof(object)) < 0) {
|
||||
pr_err("sepol: copy object failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
real_object = object;
|
||||
}
|
||||
|
||||
bool success = ksu_type_transition(db, src, tgt, cls,
|
||||
default_type, real_object);
|
||||
if (success)
|
||||
ret = 0;
|
||||
|
||||
} else if (cmd == CMD_TYPE_CHANGE) {
|
||||
char src[MAX_SEPOL_LEN];
|
||||
char tgt[MAX_SEPOL_LEN];
|
||||
char cls[MAX_SEPOL_LEN];
|
||||
char default_type[MAX_SEPOL_LEN];
|
||||
|
||||
if (strncpy_from_user(src, data.sepol1, sizeof(src)) < 0) {
|
||||
pr_err("sepol: copy src failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(tgt, data.sepol2, sizeof(tgt)) < 0) {
|
||||
pr_err("sepol: copy tgt failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(cls, data.sepol3, sizeof(cls)) < 0) {
|
||||
pr_err("sepol: copy cls failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(default_type, data.sepol4,
|
||||
sizeof(default_type)) < 0) {
|
||||
pr_err("sepol: copy default_type failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
bool success = false;
|
||||
if (subcmd == 1) {
|
||||
success = ksu_type_change(db, src, tgt, cls,
|
||||
default_type);
|
||||
} else if (subcmd == 2) {
|
||||
success = ksu_type_member(db, src, tgt, cls,
|
||||
default_type);
|
||||
} else {
|
||||
pr_err("sepol: unknown subcmd: %d\n", subcmd);
|
||||
}
|
||||
if (success)
|
||||
ret = 0;
|
||||
} else if (cmd == CMD_GENFSCON) {
|
||||
char name[MAX_SEPOL_LEN];
|
||||
char path[MAX_SEPOL_LEN];
|
||||
char context[MAX_SEPOL_LEN];
|
||||
if (strncpy_from_user(name, data.sepol1, sizeof(name)) < 0) {
|
||||
pr_err("sepol: copy name failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(path, data.sepol2, sizeof(path)) < 0) {
|
||||
pr_err("sepol: copy path failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(context, data.sepol3, sizeof(context)) <
|
||||
0) {
|
||||
pr_err("sepol: copy context failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
|
||||
if (!ksu_genfscon(db, name, path, context)) {
|
||||
pr_err("sepol: %d failed.\n", cmd);
|
||||
goto exit;
|
||||
}
|
||||
ret = 0;
|
||||
} else {
|
||||
pr_err("sepol: unknown cmd: %d\n", cmd);
|
||||
}
|
||||
|
||||
exit:
|
||||
rcu_read_unlock();
|
||||
|
||||
// only allow and xallow needs to reset avc cache, but we cannot do that because
|
||||
// we are in atomic context. so we just reset it every time.
|
||||
reset_avc_cache();
|
||||
|
||||
return ret;
|
||||
}
|
||||
145
KernelSU/kernel/selinux/selinux.c
Normal file
145
KernelSU/kernel/selinux/selinux.c
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
#include "selinux.h"
|
||||
#include "objsec.h"
|
||||
#include "linux/version.h"
|
||||
#include "../klog.h" // IWYU pragma: keep
|
||||
#ifndef KSU_COMPAT_USE_SELINUX_STATE
|
||||
#include "avc.h"
|
||||
#endif
|
||||
|
||||
#define KERNEL_SU_DOMAIN "u:r:su:s0"
|
||||
|
||||
static int transive_to_domain(const char *domain)
|
||||
{
|
||||
struct cred *cred;
|
||||
struct task_security_struct *tsec;
|
||||
u32 sid;
|
||||
int error;
|
||||
|
||||
cred = (struct cred *)__task_cred(current);
|
||||
|
||||
tsec = cred->security;
|
||||
if (!tsec) {
|
||||
pr_err("tsec == NULL!\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
error = security_secctx_to_secid(domain, strlen(domain), &sid);
|
||||
if (error) {
|
||||
pr_info("security_secctx_to_secid %s -> sid: %d, error: %d\n",
|
||||
domain, sid, error);
|
||||
}
|
||||
if (!error) {
|
||||
tsec->sid = sid;
|
||||
tsec->create_sid = 0;
|
||||
tsec->keycreate_sid = 0;
|
||||
tsec->sockcreate_sid = 0;
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
void setup_selinux(const char *domain)
|
||||
{
|
||||
if (transive_to_domain(domain)) {
|
||||
pr_err("transive domain failed.\n");
|
||||
return;
|
||||
}
|
||||
|
||||
/* we didn't need this now, we have change selinux rules when boot!
|
||||
if (!is_domain_permissive) {
|
||||
if (set_domain_permissive() == 0) {
|
||||
is_domain_permissive = true;
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
void setenforce(bool enforce)
|
||||
{
|
||||
#ifdef CONFIG_SECURITY_SELINUX_DEVELOP
|
||||
#ifdef KSU_COMPAT_USE_SELINUX_STATE
|
||||
selinux_state.enforcing = enforce;
|
||||
#else
|
||||
selinux_enforcing = enforce;
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
bool getenforce()
|
||||
{
|
||||
#ifdef CONFIG_SECURITY_SELINUX_DISABLE
|
||||
#ifdef KSU_COMPAT_USE_SELINUX_STATE
|
||||
if (selinux_state.disabled) {
|
||||
#else
|
||||
if (selinux_disabled) {
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef CONFIG_SECURITY_SELINUX_DEVELOP
|
||||
#ifdef KSU_COMPAT_USE_SELINUX_STATE
|
||||
return selinux_state.enforcing;
|
||||
#else
|
||||
return selinux_enforcing;
|
||||
#endif
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if (LINUX_VERSION_CODE < KERNEL_VERSION(5, 10, 0)) && \
|
||||
!defined(KSU_COMPAT_HAS_CURRENT_SID)
|
||||
/*
|
||||
* get the subjective security ID of the current task
|
||||
*/
|
||||
static inline u32 current_sid(void)
|
||||
{
|
||||
const struct task_security_struct *tsec = current_security();
|
||||
|
||||
return tsec->sid;
|
||||
}
|
||||
#endif
|
||||
|
||||
bool is_ksu_domain()
|
||||
{
|
||||
char *domain;
|
||||
u32 seclen;
|
||||
bool result;
|
||||
int err = security_secid_to_secctx(current_sid(), &domain, &seclen);
|
||||
if (err) {
|
||||
return false;
|
||||
}
|
||||
result = strncmp(KERNEL_SU_DOMAIN, domain, seclen) == 0;
|
||||
security_release_secctx(domain, seclen);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool is_zygote(void *sec)
|
||||
{
|
||||
struct task_security_struct *tsec = (struct task_security_struct *)sec;
|
||||
if (!tsec) {
|
||||
return false;
|
||||
}
|
||||
char *domain;
|
||||
u32 seclen;
|
||||
bool result;
|
||||
int err = security_secid_to_secctx(tsec->sid, &domain, &seclen);
|
||||
if (err) {
|
||||
return false;
|
||||
}
|
||||
result = strncmp("u:r:zygote:s0", domain, seclen) == 0;
|
||||
security_release_secctx(domain, seclen);
|
||||
return result;
|
||||
}
|
||||
|
||||
#define DEVPTS_DOMAIN "u:object_r:devpts:s0"
|
||||
|
||||
u32 ksu_get_devpts_sid()
|
||||
{
|
||||
u32 devpts_sid = 0;
|
||||
int err = security_secctx_to_secid(DEVPTS_DOMAIN, strlen(DEVPTS_DOMAIN),
|
||||
&devpts_sid);
|
||||
if (err) {
|
||||
pr_info("get devpts sid err %d\n", err);
|
||||
}
|
||||
return devpts_sid;
|
||||
}
|
||||
25
KernelSU/kernel/selinux/selinux.h
Normal file
25
KernelSU/kernel/selinux/selinux.h
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
#ifndef __KSU_H_SELINUX
|
||||
#define __KSU_H_SELINUX
|
||||
|
||||
#include "linux/types.h"
|
||||
#include "linux/version.h"
|
||||
|
||||
#if (LINUX_VERSION_CODE >= KERNEL_VERSION(5, 10, 0)) || defined(KSU_COMPAT_HAS_SELINUX_STATE)
|
||||
#define KSU_COMPAT_USE_SELINUX_STATE
|
||||
#endif
|
||||
|
||||
void setup_selinux(const char *);
|
||||
|
||||
void setenforce(bool);
|
||||
|
||||
bool getenforce();
|
||||
|
||||
bool is_ksu_domain();
|
||||
|
||||
bool is_zygote(void *cred);
|
||||
|
||||
void apply_kernelsu_rules();
|
||||
|
||||
u32 ksu_get_devpts_sid();
|
||||
|
||||
#endif
|
||||
1070
KernelSU/kernel/selinux/sepolicy.c
Normal file
1070
KernelSU/kernel/selinux/sepolicy.c
Normal file
File diff suppressed because it is too large
Load diff
46
KernelSU/kernel/selinux/sepolicy.h
Normal file
46
KernelSU/kernel/selinux/sepolicy.h
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
#ifndef __KSU_H_SEPOLICY
|
||||
#define __KSU_H_SEPOLICY
|
||||
|
||||
#include <linux/types.h>
|
||||
|
||||
#include "ss/policydb.h"
|
||||
|
||||
// Operation on types
|
||||
bool ksu_type(struct policydb *db, const char *name, const char *attr);
|
||||
bool ksu_attribute(struct policydb *db, const char *name);
|
||||
bool ksu_permissive(struct policydb *db, const char *type);
|
||||
bool ksu_enforce(struct policydb *db, const char *type);
|
||||
bool ksu_typeattribute(struct policydb *db, const char *type, const char *attr);
|
||||
bool ksu_exists(struct policydb *db, const char *type);
|
||||
|
||||
// Access vector rules
|
||||
bool ksu_allow(struct policydb *db, const char *src, const char *tgt,
|
||||
const char *cls, const char *perm);
|
||||
bool ksu_deny(struct policydb *db, const char *src, const char *tgt,
|
||||
const char *cls, const char *perm);
|
||||
bool ksu_auditallow(struct policydb *db, const char *src, const char *tgt,
|
||||
const char *cls, const char *perm);
|
||||
bool ksu_dontaudit(struct policydb *db, const char *src, const char *tgt,
|
||||
const char *cls, const char *perm);
|
||||
|
||||
// Extended permissions access vector rules
|
||||
bool ksu_allowxperm(struct policydb *db, const char *src, const char *tgt,
|
||||
const char *cls, const char *range);
|
||||
bool ksu_auditallowxperm(struct policydb *db, const char *src, const char *tgt,
|
||||
const char *cls, const char *range);
|
||||
bool ksu_dontauditxperm(struct policydb *db, const char *src, const char *tgt,
|
||||
const char *cls, const char *range);
|
||||
|
||||
// Type rules
|
||||
bool ksu_type_transition(struct policydb *db, const char *src, const char *tgt,
|
||||
const char *cls, const char *def, const char *obj);
|
||||
bool ksu_type_change(struct policydb *db, const char *src, const char *tgt,
|
||||
const char *cls, const char *def);
|
||||
bool ksu_type_member(struct policydb *db, const char *src, const char *tgt,
|
||||
const char *cls, const char *def);
|
||||
|
||||
// File system labeling
|
||||
bool ksu_genfscon(struct policydb *db, const char *fs_name, const char *path,
|
||||
const char *ctx);
|
||||
|
||||
#endif
|
||||
75
KernelSU/kernel/setup.sh
Executable file
75
KernelSU/kernel/setup.sh
Executable file
|
|
@ -0,0 +1,75 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
GKI_ROOT=$(pwd)
|
||||
|
||||
display_usage() {
|
||||
echo "Usage: $0 [--cleanup | <commit-or-tag>]"
|
||||
echo " --cleanup: Cleans up previous modifications made by the script."
|
||||
echo " <commit-or-tag>: Sets up or updates the KernelSU to specified tag or commit."
|
||||
echo " -h, --help: Displays this usage information."
|
||||
echo " (no args): Sets up or updates the KernelSU environment to the latest tagged version."
|
||||
}
|
||||
|
||||
initialize_variables() {
|
||||
if test -d "$GKI_ROOT/common/drivers"; then
|
||||
DRIVER_DIR="$GKI_ROOT/common/drivers"
|
||||
elif test -d "$GKI_ROOT/drivers"; then
|
||||
DRIVER_DIR="$GKI_ROOT/drivers"
|
||||
else
|
||||
echo '[ERROR] "drivers/" directory not found.'
|
||||
exit 127
|
||||
fi
|
||||
|
||||
DRIVER_MAKEFILE=$DRIVER_DIR/Makefile
|
||||
DRIVER_KCONFIG=$DRIVER_DIR/Kconfig
|
||||
}
|
||||
|
||||
# Reverts modifications made by this script
|
||||
perform_cleanup() {
|
||||
echo "[+] Cleaning up..."
|
||||
[ -L "$DRIVER_DIR/kernelsu" ] && rm "$DRIVER_DIR/kernelsu" && echo "[-] Symlink removed."
|
||||
grep -q "kernelsu" "$DRIVER_MAKEFILE" && sed -i '/kernelsu/d' "$DRIVER_MAKEFILE" && echo "[-] Makefile reverted."
|
||||
grep -q "drivers/kernelsu/Kconfig" "$DRIVER_KCONFIG" && sed -i '/drivers\/kernelsu\/Kconfig/d' "$DRIVER_KCONFIG" && echo "[-] Kconfig reverted."
|
||||
if [ -d "$GKI_ROOT/KernelSU" ]; then
|
||||
rm -rf "$GKI_ROOT/KernelSU" && echo "[-] KernelSU directory deleted."
|
||||
fi
|
||||
}
|
||||
|
||||
# Sets up or update KernelSU environment
|
||||
setup_kernelsu() {
|
||||
echo "[+] Setting up KernelSU..."
|
||||
test -d "$GKI_ROOT/KernelSU" || git clone https://github.com/tiann/KernelSU && echo "[+] Repository cloned."
|
||||
cd "$GKI_ROOT/KernelSU"
|
||||
git stash && echo "[-] Stashed current changes."
|
||||
if [ "$(git status | grep -Po 'v\d+(\.\d+)*' | head -n1)" ]; then
|
||||
git checkout main && echo "[-] Switched to main branch."
|
||||
fi
|
||||
git pull && echo "[+] Repository updated."
|
||||
if [ -z "${1-}" ]; then
|
||||
git checkout "$(git describe --abbrev=0 --tags)" && echo "[-] Checked out latest tag."
|
||||
else
|
||||
git checkout "$1" && echo "[-] Checked out $1." || echo "[-] Checkout default branch"
|
||||
fi
|
||||
cd "$DRIVER_DIR"
|
||||
ln -sf "$(realpath --relative-to="$DRIVER_DIR" "$GKI_ROOT/KernelSU/kernel")" "kernelsu" && echo "[+] Symlink created."
|
||||
|
||||
# Add entries in Makefile and Kconfig if not already existing
|
||||
grep -q "kernelsu" "$DRIVER_MAKEFILE" || printf "\nobj-\$(CONFIG_KSU) += kernelsu/\n" >> "$DRIVER_MAKEFILE" && echo "[+] Modified Makefile."
|
||||
grep -q "source \"drivers/kernelsu/Kconfig\"" "$DRIVER_KCONFIG" || sed -i "/endmenu/i\source \"drivers/kernelsu/Kconfig\"" "$DRIVER_KCONFIG" && echo "[+] Modified Kconfig."
|
||||
echo '[+] Done.'
|
||||
}
|
||||
|
||||
# Process command-line arguments
|
||||
if [ "$#" -eq 0 ]; then
|
||||
initialize_variables
|
||||
setup_kernelsu
|
||||
elif [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
|
||||
display_usage
|
||||
elif [ "$1" = "--cleanup" ]; then
|
||||
initialize_variables
|
||||
perform_cleanup
|
||||
else
|
||||
initialize_variables
|
||||
setup_kernelsu "$@"
|
||||
fi
|
||||
367
KernelSU/kernel/sucompat.c
Normal file
367
KernelSU/kernel/sucompat.c
Normal file
|
|
@ -0,0 +1,367 @@
|
|||
#include <linux/dcache.h>
|
||||
#include <linux/security.h>
|
||||
#include <asm/current.h>
|
||||
#include <linux/cred.h>
|
||||
#include <linux/err.h>
|
||||
#include <linux/fs.h>
|
||||
#include <linux/kprobes.h>
|
||||
#include <linux/types.h>
|
||||
#include <linux/uaccess.h>
|
||||
#include <linux/version.h>
|
||||
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 11, 0)
|
||||
#include <linux/sched/task_stack.h>
|
||||
#else
|
||||
#include <linux/sched.h>
|
||||
#endif
|
||||
|
||||
#include "objsec.h"
|
||||
#include "allowlist.h"
|
||||
#include "arch.h"
|
||||
#include "klog.h" // IWYU pragma: keep
|
||||
#include "ksud.h"
|
||||
#include "kernel_compat.h"
|
||||
|
||||
#define SU_PATH "/system/bin/su"
|
||||
#define SH_PATH "/system/bin/sh"
|
||||
|
||||
extern void escape_to_root();
|
||||
|
||||
static void __user *userspace_stack_buffer(const void *d, size_t len)
|
||||
{
|
||||
/* To avoid having to mmap a page in userspace, just write below the stack
|
||||
* pointer. */
|
||||
char __user *p = (void __user *)current_user_stack_pointer() - len;
|
||||
|
||||
return copy_to_user(p, d, len) ? NULL : p;
|
||||
}
|
||||
|
||||
static char __user *sh_user_path(void)
|
||||
{
|
||||
static const char sh_path[] = "/system/bin/sh";
|
||||
|
||||
return userspace_stack_buffer(sh_path, sizeof(sh_path));
|
||||
}
|
||||
|
||||
static char __user *ksud_user_path(void)
|
||||
{
|
||||
static const char ksud_path[] = KSUD_PATH;
|
||||
|
||||
return userspace_stack_buffer(ksud_path, sizeof(ksud_path));
|
||||
}
|
||||
|
||||
int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode,
|
||||
int *__unused_flags)
|
||||
{
|
||||
const char su[] = SU_PATH;
|
||||
|
||||
if (!ksu_is_allow_uid(current_uid().val)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
char path[sizeof(su) + 1];
|
||||
memset(path, 0, sizeof(path));
|
||||
ksu_strncpy_from_user_nofault(path, *filename_user, sizeof(path));
|
||||
|
||||
if (unlikely(!memcmp(path, su, sizeof(su)))) {
|
||||
pr_info("faccessat su->sh!\n");
|
||||
*filename_user = sh_user_path();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags)
|
||||
{
|
||||
// const char sh[] = SH_PATH;
|
||||
const char su[] = SU_PATH;
|
||||
|
||||
if (!ksu_is_allow_uid(current_uid().val)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (unlikely(!filename_user)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
char path[sizeof(su) + 1];
|
||||
memset(path, 0, sizeof(path));
|
||||
// Remove this later!! we use syscall hook, so this will never happen!!!!!
|
||||
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 18, 0) && 0
|
||||
// it becomes a `struct filename *` after 5.18
|
||||
// https://elixir.bootlin.com/linux/v5.18/source/fs/stat.c#L216
|
||||
const char sh[] = SH_PATH;
|
||||
struct filename *filename = *((struct filename **)filename_user);
|
||||
if (IS_ERR(filename)) {
|
||||
return 0;
|
||||
}
|
||||
if (likely(memcmp(filename->name, su, sizeof(su))))
|
||||
return 0;
|
||||
pr_info("vfs_statx su->sh!\n");
|
||||
memcpy((void *)filename->name, sh, sizeof(sh));
|
||||
#else
|
||||
ksu_strncpy_from_user_nofault(path, *filename_user, sizeof(path));
|
||||
|
||||
if (unlikely(!memcmp(path, su, sizeof(su)))) {
|
||||
pr_info("newfstatat su->sh!\n");
|
||||
*filename_user = sh_user_path();
|
||||
}
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// the call from execve_handler_pre won't provided correct value for __never_use_argument, use them after fix execve_handler_pre, keeping them for consistence for manually patched code
|
||||
int ksu_handle_execveat_sucompat(int *fd, struct filename **filename_ptr,
|
||||
void *__never_use_argv, void *__never_use_envp,
|
||||
int *__never_use_flags)
|
||||
{
|
||||
struct filename *filename;
|
||||
const char sh[] = KSUD_PATH;
|
||||
const char su[] = SU_PATH;
|
||||
|
||||
if (unlikely(!filename_ptr))
|
||||
return 0;
|
||||
|
||||
filename = *filename_ptr;
|
||||
if (IS_ERR(filename)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (likely(memcmp(filename->name, su, sizeof(su))))
|
||||
return 0;
|
||||
|
||||
if (!ksu_is_allow_uid(current_uid().val))
|
||||
return 0;
|
||||
|
||||
pr_info("do_execveat_common su found\n");
|
||||
memcpy((void *)filename->name, sh, sizeof(sh));
|
||||
|
||||
escape_to_root();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ksu_handle_execve_sucompat(int *fd, const char __user **filename_user,
|
||||
void *__never_use_argv, void *__never_use_envp,
|
||||
int *__never_use_flags)
|
||||
{
|
||||
const char su[] = SU_PATH;
|
||||
char path[sizeof(su) + 1];
|
||||
|
||||
if (unlikely(!filename_user))
|
||||
return 0;
|
||||
|
||||
memset(path, 0, sizeof(path));
|
||||
ksu_strncpy_from_user_nofault(path, *filename_user, sizeof(path));
|
||||
|
||||
if (likely(memcmp(path, su, sizeof(su))))
|
||||
return 0;
|
||||
|
||||
if (!ksu_is_allow_uid(current_uid().val))
|
||||
return 0;
|
||||
|
||||
pr_info("sys_execve su found\n");
|
||||
*filename_user = ksud_user_path();
|
||||
|
||||
escape_to_root();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ksu_handle_devpts(struct inode *inode)
|
||||
{
|
||||
if (!current->mm) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uid_t uid = current_uid().val;
|
||||
if (uid % 100000 < 10000) {
|
||||
// not untrusted_app, ignore it
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!ksu_is_allow_uid(uid))
|
||||
return 0;
|
||||
|
||||
if (ksu_devpts_sid) {
|
||||
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 1, 0)
|
||||
struct inode_security_struct *sec = selinux_inode(inode);
|
||||
#else
|
||||
struct inode_security_struct *sec =
|
||||
(struct inode_security_struct *)inode->i_security;
|
||||
#endif
|
||||
if (sec) {
|
||||
sec->sid = ksu_devpts_sid;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef CONFIG_KPROBES
|
||||
|
||||
__maybe_unused static int faccessat_handler_pre(struct kprobe *p,
|
||||
struct pt_regs *regs)
|
||||
{
|
||||
int *dfd = (int *)&PT_REGS_PARM1(regs);
|
||||
const char __user **filename_user = (const char **)&PT_REGS_PARM2(regs);
|
||||
int *mode = (int *)&PT_REGS_PARM3(regs);
|
||||
// Both sys_ and do_ is C function
|
||||
int *flags = (int *)&PT_REGS_CCALL_PARM4(regs);
|
||||
|
||||
return ksu_handle_faccessat(dfd, filename_user, mode, flags);
|
||||
}
|
||||
|
||||
static int sys_faccessat_handler_pre(struct kprobe *p, struct pt_regs *regs)
|
||||
{
|
||||
struct pt_regs *real_regs = PT_REAL_REGS(regs);
|
||||
int *dfd = (int *)&PT_REGS_PARM1(real_regs);
|
||||
const char __user **filename_user =
|
||||
(const char **)&PT_REGS_PARM2(real_regs);
|
||||
int *mode = (int *)&PT_REGS_PARM3(real_regs);
|
||||
|
||||
return ksu_handle_faccessat(dfd, filename_user, mode, NULL);
|
||||
}
|
||||
|
||||
__maybe_unused static int newfstatat_handler_pre(struct kprobe *p,
|
||||
struct pt_regs *regs)
|
||||
{
|
||||
int *dfd = (int *)&PT_REGS_PARM1(regs);
|
||||
const char __user **filename_user = (const char **)&PT_REGS_PARM2(regs);
|
||||
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 11, 0)
|
||||
// static int vfs_statx(int dfd, const char __user *filename, int flags, struct kstat *stat, u32 request_mask)
|
||||
int *flags = (int *)&PT_REGS_PARM3(regs);
|
||||
#else
|
||||
// int vfs_fstatat(int dfd, const char __user *filename, struct kstat *stat,int flag)
|
||||
int *flags = (int *)&PT_REGS_CCALL_PARM4(regs);
|
||||
#endif
|
||||
|
||||
return ksu_handle_stat(dfd, filename_user, flags);
|
||||
}
|
||||
|
||||
static int sys_newfstatat_handler_pre(struct kprobe *p, struct pt_regs *regs)
|
||||
{
|
||||
struct pt_regs *real_regs = PT_REAL_REGS(regs);
|
||||
int *dfd = (int *)&PT_REGS_PARM1(real_regs);
|
||||
const char __user **filename_user =
|
||||
(const char **)&PT_REGS_PARM2(real_regs);
|
||||
int *flags = (int *)&PT_REGS_SYSCALL_PARM4(real_regs);
|
||||
|
||||
return ksu_handle_stat(dfd, filename_user, flags);
|
||||
}
|
||||
|
||||
// https://elixir.bootlin.com/linux/v5.10.158/source/fs/exec.c#L1864
|
||||
static int execve_handler_pre(struct kprobe *p, struct pt_regs *regs)
|
||||
{
|
||||
int *fd = (int *)&PT_REGS_PARM1(regs);
|
||||
struct filename **filename_ptr =
|
||||
(struct filename **)&PT_REGS_PARM2(regs);
|
||||
|
||||
return ksu_handle_execveat_sucompat(fd, filename_ptr, NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
static int sys_execve_handler_pre(struct kprobe *p, struct pt_regs *regs)
|
||||
{
|
||||
struct pt_regs *real_regs = PT_REAL_REGS(regs);
|
||||
const char __user **filename_user =
|
||||
(const char **)&PT_REGS_PARM1(real_regs);
|
||||
|
||||
return ksu_handle_execve_sucompat(AT_FDCWD, filename_user, NULL, NULL,
|
||||
NULL);
|
||||
}
|
||||
|
||||
#if 1
|
||||
static struct kprobe faccessat_kp = {
|
||||
.symbol_name = SYS_FACCESSAT_SYMBOL,
|
||||
.pre_handler = sys_faccessat_handler_pre,
|
||||
};
|
||||
#else
|
||||
static struct kprobe faccessat_kp = {
|
||||
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 17, 0)
|
||||
.symbol_name = "do_faccessat",
|
||||
#else
|
||||
.symbol_name = "sys_faccessat",
|
||||
#endif
|
||||
.pre_handler = faccessat_handler_pre,
|
||||
};
|
||||
#endif
|
||||
|
||||
#if 1
|
||||
static struct kprobe newfstatat_kp = {
|
||||
.symbol_name = SYS_NEWFSTATAT_SYMBOL,
|
||||
.pre_handler = sys_newfstatat_handler_pre,
|
||||
};
|
||||
#else
|
||||
static struct kprobe newfstatat_kp = {
|
||||
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 11, 0)
|
||||
.symbol_name = "vfs_statx",
|
||||
#else
|
||||
.symbol_name = "vfs_fstatat",
|
||||
#endif
|
||||
.pre_handler = newfstatat_handler_pre,
|
||||
};
|
||||
#endif
|
||||
|
||||
#if 1
|
||||
static struct kprobe execve_kp = {
|
||||
.symbol_name = SYS_EXECVE_SYMBOL,
|
||||
.pre_handler = sys_execve_handler_pre,
|
||||
};
|
||||
#else
|
||||
static struct kprobe execve_kp = {
|
||||
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 9, 0)
|
||||
.symbol_name = "do_execveat_common",
|
||||
#elif LINUX_VERSION_CODE >= KERNEL_VERSION(4, 19, 0)
|
||||
.symbol_name = "__do_execve_file",
|
||||
#elif LINUX_VERSION_CODE >= KERNEL_VERSION(3, 19, 0)
|
||||
.symbol_name = "do_execveat_common",
|
||||
#endif
|
||||
.pre_handler = execve_handler_pre,
|
||||
};
|
||||
#endif
|
||||
|
||||
static int pts_unix98_lookup_pre(struct kprobe *p, struct pt_regs *regs)
|
||||
{
|
||||
struct inode *inode;
|
||||
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 6, 0)
|
||||
struct file *file = (struct file *)PT_REGS_PARM2(regs);
|
||||
inode = file->f_path.dentry->d_inode;
|
||||
#else
|
||||
inode = (struct inode *)PT_REGS_PARM2(regs);
|
||||
#endif
|
||||
|
||||
return ksu_handle_devpts(inode);
|
||||
}
|
||||
|
||||
static struct kprobe pts_unix98_lookup_kp = { .symbol_name =
|
||||
"pts_unix98_lookup",
|
||||
.pre_handler =
|
||||
pts_unix98_lookup_pre };
|
||||
|
||||
#endif
|
||||
|
||||
// sucompat: permited process can execute 'su' to gain root access.
|
||||
void ksu_sucompat_init()
|
||||
{
|
||||
#ifdef CONFIG_KPROBES
|
||||
int ret;
|
||||
ret = register_kprobe(&execve_kp);
|
||||
pr_info("sucompat: execve_kp: %d\n", ret);
|
||||
ret = register_kprobe(&newfstatat_kp);
|
||||
pr_info("sucompat: newfstatat_kp: %d\n", ret);
|
||||
ret = register_kprobe(&faccessat_kp);
|
||||
pr_info("sucompat: faccessat_kp: %d\n", ret);
|
||||
ret = register_kprobe(&pts_unix98_lookup_kp);
|
||||
pr_info("sucompat: devpts_kp: %d\n", ret);
|
||||
#endif
|
||||
}
|
||||
|
||||
void ksu_sucompat_exit()
|
||||
{
|
||||
#ifdef CONFIG_KPROBES
|
||||
unregister_kprobe(&execve_kp);
|
||||
unregister_kprobe(&newfstatat_kp);
|
||||
unregister_kprobe(&faccessat_kp);
|
||||
unregister_kprobe(&pts_unix98_lookup_kp);
|
||||
#endif
|
||||
}
|
||||
382
KernelSU/kernel/throne_tracker.c
Normal file
382
KernelSU/kernel/throne_tracker.c
Normal file
|
|
@ -0,0 +1,382 @@
|
|||
#include <linux/err.h>
|
||||
#include <linux/fs.h>
|
||||
#include <linux/list.h>
|
||||
#include <linux/slab.h>
|
||||
#include <linux/string.h>
|
||||
#include <linux/types.h>
|
||||
#include <linux/version.h>
|
||||
|
||||
#include "allowlist.h"
|
||||
#include "klog.h" // IWYU pragma: keep
|
||||
#include "ksu.h"
|
||||
#include "manager.h"
|
||||
#include "throne_tracker.h"
|
||||
#include "kernel_compat.h"
|
||||
|
||||
uid_t ksu_manager_uid = KSU_INVALID_UID;
|
||||
|
||||
#define SYSTEM_PACKAGES_LIST_PATH "/data/system/packages.list.tmp"
|
||||
|
||||
struct uid_data {
|
||||
struct list_head list;
|
||||
u32 uid;
|
||||
char package[KSU_MAX_PACKAGE_NAME];
|
||||
};
|
||||
|
||||
static int get_pkg_from_apk_path(char *pkg, const char *path)
|
||||
{
|
||||
int len = strlen(path);
|
||||
if (len >= KSU_MAX_PACKAGE_NAME || len < 1)
|
||||
return -1;
|
||||
|
||||
const char *last_slash = NULL;
|
||||
const char *second_last_slash = NULL;
|
||||
|
||||
int i;
|
||||
for (i = len - 1; i >= 0; i--) {
|
||||
if (path[i] == '/') {
|
||||
if (!last_slash) {
|
||||
last_slash = &path[i];
|
||||
} else {
|
||||
second_last_slash = &path[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!last_slash || !second_last_slash)
|
||||
return -1;
|
||||
|
||||
const char *last_hyphen = strchr(second_last_slash, '-');
|
||||
if (!last_hyphen || last_hyphen > last_slash)
|
||||
return -1;
|
||||
|
||||
int pkg_len = last_hyphen - second_last_slash - 1;
|
||||
if (pkg_len >= KSU_MAX_PACKAGE_NAME || pkg_len <= 0)
|
||||
return -1;
|
||||
|
||||
// Copying the package name
|
||||
strncpy(pkg, second_last_slash + 1, pkg_len);
|
||||
pkg[pkg_len] = '\0';
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void crown_manager(const char *apk, struct list_head *uid_data)
|
||||
{
|
||||
char pkg[KSU_MAX_PACKAGE_NAME];
|
||||
if (get_pkg_from_apk_path(pkg, apk) < 0) {
|
||||
pr_err("Failed to get package name from apk path: %s\n", apk);
|
||||
return;
|
||||
}
|
||||
|
||||
pr_info("manager pkg: %s\n", pkg);
|
||||
|
||||
#ifdef KSU_MANAGER_PACKAGE
|
||||
// pkg is `/<real package>`
|
||||
if (strncmp(pkg, KSU_MANAGER_PACKAGE, sizeof(KSU_MANAGER_PACKAGE))) {
|
||||
pr_info("manager package is inconsistent with kernel build: %s\n",
|
||||
KSU_MANAGER_PACKAGE);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
struct list_head *list = (struct list_head *)uid_data;
|
||||
struct uid_data *np;
|
||||
|
||||
list_for_each_entry (np, list, list) {
|
||||
if (strncmp(np->package, pkg, KSU_MAX_PACKAGE_NAME) == 0) {
|
||||
pr_info("Crowning manager: %s(uid=%d)\n", pkg, np->uid);
|
||||
ksu_set_manager_uid(np->uid);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#define DATA_PATH_LEN 384 // 384 is enough for /data/app/<package>/base.apk
|
||||
|
||||
struct data_path {
|
||||
char dirpath[DATA_PATH_LEN];
|
||||
int depth;
|
||||
struct list_head list;
|
||||
};
|
||||
|
||||
struct apk_path_hash {
|
||||
unsigned int hash;
|
||||
bool exists;
|
||||
struct list_head list;
|
||||
};
|
||||
|
||||
static struct list_head apk_path_hash_list = LIST_HEAD_INIT(apk_path_hash_list);
|
||||
|
||||
struct my_dir_context {
|
||||
struct dir_context ctx;
|
||||
struct list_head *data_path_list;
|
||||
char *parent_dir;
|
||||
void *private_data;
|
||||
int depth;
|
||||
int *stop;
|
||||
};
|
||||
// https://docs.kernel.org/filesystems/porting.html
|
||||
// filldir_t (readdir callbacks) calling conventions have changed. Instead of returning 0 or -E... it returns bool now. false means "no more" (as -E... used to) and true - "keep going" (as 0 in old calling conventions). Rationale: callers never looked at specific -E... values anyway. -> iterate_shared() instances require no changes at all, all filldir_t ones in the tree converted.
|
||||
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 1, 0)
|
||||
#define FILLDIR_RETURN_TYPE bool
|
||||
#define FILLDIR_ACTOR_CONTINUE true
|
||||
#define FILLDIR_ACTOR_STOP false
|
||||
#else
|
||||
#define FILLDIR_RETURN_TYPE int
|
||||
#define FILLDIR_ACTOR_CONTINUE 0
|
||||
#define FILLDIR_ACTOR_STOP -EINVAL
|
||||
#endif
|
||||
|
||||
FILLDIR_RETURN_TYPE my_actor(struct dir_context *ctx, const char *name,
|
||||
int namelen, loff_t off, u64 ino,
|
||||
unsigned int d_type)
|
||||
{
|
||||
struct my_dir_context *my_ctx =
|
||||
container_of(ctx, struct my_dir_context, ctx);
|
||||
char dirpath[DATA_PATH_LEN];
|
||||
|
||||
if (!my_ctx) {
|
||||
pr_err("Invalid context\n");
|
||||
return FILLDIR_ACTOR_STOP;
|
||||
}
|
||||
if (my_ctx->stop && *my_ctx->stop) {
|
||||
pr_info("Stop searching\n");
|
||||
return FILLDIR_ACTOR_STOP;
|
||||
}
|
||||
|
||||
if (!strncmp(name, "..", namelen) || !strncmp(name, ".", namelen))
|
||||
return FILLDIR_ACTOR_CONTINUE; // Skip "." and ".."
|
||||
|
||||
if (snprintf(dirpath, DATA_PATH_LEN, "%s/%.*s", my_ctx->parent_dir,
|
||||
namelen, name) >= DATA_PATH_LEN) {
|
||||
pr_err("Path too long: %s/%.*s\n", my_ctx->parent_dir, namelen,
|
||||
name);
|
||||
return FILLDIR_ACTOR_CONTINUE;
|
||||
}
|
||||
|
||||
if (d_type == DT_DIR && my_ctx->depth > 0 &&
|
||||
(my_ctx->stop && !*my_ctx->stop)) {
|
||||
struct data_path *data = kmalloc(sizeof(struct data_path), GFP_ATOMIC);
|
||||
|
||||
if (!data) {
|
||||
pr_err("Failed to allocate memory for %s\n", dirpath);
|
||||
return FILLDIR_ACTOR_CONTINUE;
|
||||
}
|
||||
|
||||
strscpy(data->dirpath, dirpath, DATA_PATH_LEN);
|
||||
data->depth = my_ctx->depth - 1;
|
||||
list_add_tail(&data->list, my_ctx->data_path_list);
|
||||
} else {
|
||||
if ((namelen == 8) && (strncmp(name, "base.apk", namelen) == 0)) {
|
||||
struct apk_path_hash *pos, *n;
|
||||
#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 8, 0)
|
||||
unsigned int hash = full_name_hash(dirpath, strlen(dirpath));
|
||||
#else
|
||||
unsigned int hash = full_name_hash(NULL, dirpath, strlen(dirpath));
|
||||
#endif
|
||||
list_for_each_entry(pos, &apk_path_hash_list, list) {
|
||||
if (hash == pos->hash) {
|
||||
pos->exists = true;
|
||||
return FILLDIR_ACTOR_CONTINUE;
|
||||
}
|
||||
}
|
||||
|
||||
bool is_manager = is_manager_apk(dirpath);
|
||||
pr_info("Found new base.apk at path: %s, is_manager: %d\n",
|
||||
dirpath, is_manager);
|
||||
if (is_manager) {
|
||||
crown_manager(dirpath, my_ctx->private_data);
|
||||
*my_ctx->stop = 1;
|
||||
|
||||
// Manager found, clear APK cache list
|
||||
list_for_each_entry_safe(pos, n, &apk_path_hash_list, list) {
|
||||
list_del(&pos->list);
|
||||
kfree(pos);
|
||||
}
|
||||
} else {
|
||||
struct apk_path_hash *apk_data = kmalloc(sizeof(struct apk_path_hash), GFP_ATOMIC);
|
||||
apk_data->hash = hash;
|
||||
apk_data->exists = true;
|
||||
list_add_tail(&apk_data->list, &apk_path_hash_list);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return FILLDIR_ACTOR_CONTINUE;
|
||||
}
|
||||
|
||||
void search_manager(const char *path, int depth, struct list_head *uid_data)
|
||||
{
|
||||
int i, stop = 0;
|
||||
struct list_head data_path_list;
|
||||
INIT_LIST_HEAD(&data_path_list);
|
||||
|
||||
// Initialize APK cache list
|
||||
struct apk_path_hash *pos, *n;
|
||||
list_for_each_entry(pos, &apk_path_hash_list, list) {
|
||||
pos->exists = false;
|
||||
}
|
||||
|
||||
// First depth
|
||||
struct data_path data;
|
||||
strscpy(data.dirpath, path, DATA_PATH_LEN);
|
||||
data.depth = depth;
|
||||
list_add_tail(&data.list, &data_path_list);
|
||||
|
||||
for (i = depth; i > 0; i--) {
|
||||
struct data_path *pos, *n;
|
||||
|
||||
list_for_each_entry_safe(pos, n, &data_path_list, list) {
|
||||
struct my_dir_context ctx = { .ctx.actor = my_actor,
|
||||
.data_path_list = &data_path_list,
|
||||
.parent_dir = pos->dirpath,
|
||||
.private_data = uid_data,
|
||||
.depth = pos->depth,
|
||||
.stop = &stop };
|
||||
struct file *file;
|
||||
|
||||
if (!stop) {
|
||||
file = ksu_filp_open_compat(pos->dirpath, O_RDONLY | O_NOFOLLOW, 0);
|
||||
if (IS_ERR(file)) {
|
||||
pr_err("Failed to open directory: %s, err: %ld\n", pos->dirpath, PTR_ERR(file));
|
||||
return;
|
||||
}
|
||||
|
||||
iterate_dir(file, &ctx.ctx);
|
||||
filp_close(file, NULL);
|
||||
}
|
||||
|
||||
list_del(&pos->list);
|
||||
if (pos != &data)
|
||||
kfree(pos);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove stale cached APK entries
|
||||
list_for_each_entry_safe(pos, n, &apk_path_hash_list, list) {
|
||||
if (!pos->exists) {
|
||||
list_del(&pos->list);
|
||||
kfree(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool is_uid_exist(uid_t uid, char *package, void *data)
|
||||
{
|
||||
struct list_head *list = (struct list_head *)data;
|
||||
struct uid_data *np;
|
||||
|
||||
bool exist = false;
|
||||
list_for_each_entry (np, list, list) {
|
||||
if (np->uid == uid % 100000 &&
|
||||
strncmp(np->package, package, KSU_MAX_PACKAGE_NAME) == 0) {
|
||||
exist = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return exist;
|
||||
}
|
||||
|
||||
void track_throne()
|
||||
{
|
||||
struct file *fp =
|
||||
ksu_filp_open_compat(SYSTEM_PACKAGES_LIST_PATH, O_RDONLY, 0);
|
||||
if (IS_ERR(fp)) {
|
||||
pr_err("%s: open " SYSTEM_PACKAGES_LIST_PATH " failed: %ld\n",
|
||||
__func__, PTR_ERR(fp));
|
||||
return;
|
||||
}
|
||||
|
||||
struct list_head uid_list;
|
||||
INIT_LIST_HEAD(&uid_list);
|
||||
|
||||
char chr = 0;
|
||||
loff_t pos = 0;
|
||||
loff_t line_start = 0;
|
||||
char buf[KSU_MAX_PACKAGE_NAME];
|
||||
for (;;) {
|
||||
ssize_t count =
|
||||
ksu_kernel_read_compat(fp, &chr, sizeof(chr), &pos);
|
||||
if (count != sizeof(chr))
|
||||
break;
|
||||
if (chr != '\n')
|
||||
continue;
|
||||
|
||||
count = ksu_kernel_read_compat(fp, buf, sizeof(buf),
|
||||
&line_start);
|
||||
|
||||
struct uid_data *data =
|
||||
kzalloc(sizeof(struct uid_data), GFP_ATOMIC);
|
||||
if (!data) {
|
||||
filp_close(fp, 0);
|
||||
goto out;
|
||||
}
|
||||
|
||||
char *tmp = buf;
|
||||
const char *delim = " ";
|
||||
char *package = strsep(&tmp, delim);
|
||||
char *uid = strsep(&tmp, delim);
|
||||
if (!uid || !package) {
|
||||
pr_err("update_uid: package or uid is NULL!\n");
|
||||
break;
|
||||
}
|
||||
|
||||
u32 res;
|
||||
if (kstrtou32(uid, 10, &res)) {
|
||||
pr_err("update_uid: uid parse err\n");
|
||||
break;
|
||||
}
|
||||
data->uid = res;
|
||||
strncpy(data->package, package, KSU_MAX_PACKAGE_NAME);
|
||||
list_add_tail(&data->list, &uid_list);
|
||||
// reset line start
|
||||
line_start = pos;
|
||||
}
|
||||
filp_close(fp, 0);
|
||||
|
||||
// now update uid list
|
||||
struct uid_data *np;
|
||||
struct uid_data *n;
|
||||
|
||||
// first, check if manager_uid exist!
|
||||
bool manager_exist = false;
|
||||
list_for_each_entry (np, &uid_list, list) {
|
||||
// if manager is installed in work profile, the uid in packages.list is still equals main profile
|
||||
// don't delete it in this case!
|
||||
int manager_uid = ksu_get_manager_uid() % 100000;
|
||||
if (np->uid == manager_uid) {
|
||||
manager_exist = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!manager_exist) {
|
||||
if (ksu_is_manager_uid_valid()) {
|
||||
pr_info("manager is uninstalled, invalidate it!\n");
|
||||
ksu_invalidate_manager_uid();
|
||||
}
|
||||
pr_info("Searching manager...\n");
|
||||
search_manager("/data/app", 2, &uid_list);
|
||||
pr_info("Search manager finished\n");
|
||||
}
|
||||
|
||||
// then prune the allowlist
|
||||
ksu_prune_allowlist(is_uid_exist, &uid_list);
|
||||
out:
|
||||
// free uid_list
|
||||
list_for_each_entry_safe (np, n, &uid_list, list) {
|
||||
list_del(&np->list);
|
||||
kfree(np);
|
||||
}
|
||||
}
|
||||
|
||||
void ksu_throne_tracker_init()
|
||||
{
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
void ksu_throne_tracker_exit()
|
||||
{
|
||||
// nothing to do
|
||||
}
|
||||
10
KernelSU/kernel/throne_tracker.h
Normal file
10
KernelSU/kernel/throne_tracker.h
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
#ifndef __KSU_H_UID_OBSERVER
|
||||
#define __KSU_H_UID_OBSERVER
|
||||
|
||||
void ksu_throne_tracker_init();
|
||||
|
||||
void ksu_throne_tracker_exit();
|
||||
|
||||
void track_throne();
|
||||
|
||||
#endif
|
||||
9
KernelSU/manager/.gitignore
vendored
Normal file
9
KernelSU/manager/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
*.iml
|
||||
.gradle
|
||||
local.properties
|
||||
.idea
|
||||
.DS_Store
|
||||
build
|
||||
captures
|
||||
.cxx
|
||||
key.jks
|
||||
2
KernelSU/manager/app/.gitignore
vendored
Normal file
2
KernelSU/manager/app/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/build
|
||||
/release/
|
||||
116
KernelSU/manager/app/build.gradle.kts
Normal file
116
KernelSU/manager/app/build.gradle.kts
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.agp.app)
|
||||
alias(libs.plugins.kotlin)
|
||||
alias(libs.plugins.compose.compiler)
|
||||
alias(libs.plugins.ksp)
|
||||
alias(libs.plugins.lsplugin.apksign)
|
||||
id("kotlin-parcelize")
|
||||
}
|
||||
|
||||
val managerVersionCode: Int by rootProject.extra
|
||||
val managerVersionName: String by rootProject.extra
|
||||
|
||||
apksign {
|
||||
storeFileProperty = "KEYSTORE_FILE"
|
||||
storePasswordProperty = "KEYSTORE_PASSWORD"
|
||||
keyAliasProperty = "KEY_ALIAS"
|
||||
keyPasswordProperty = "KEY_PASSWORD"
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "me.weishu.kernelsu"
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
aidl = true
|
||||
buildConfig = true
|
||||
compose = true
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "21"
|
||||
}
|
||||
|
||||
packaging {
|
||||
jniLibs {
|
||||
useLegacyPackaging = true
|
||||
}
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
}
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path("src/main/cpp/CMakeLists.txt")
|
||||
}
|
||||
}
|
||||
|
||||
applicationVariants.all {
|
||||
outputs.forEach {
|
||||
val output = it as BaseVariantOutputImpl
|
||||
output.outputFileName = "KernelSU_${managerVersionName}_${managerVersionCode}-$name.apk"
|
||||
}
|
||||
|
||||
kotlin.sourceSets {
|
||||
getByName(name) {
|
||||
kotlin.srcDir("build/generated/ksp/$name/kotlin")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
|
||||
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)
|
||||
|
||||
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.com.google.accompanist.drawablepainter)
|
||||
implementation(libs.com.google.accompanist.navigation.animation)
|
||||
implementation(libs.com.google.accompanist.systemuicontroller)
|
||||
implementation(libs.com.google.accompanist.webview)
|
||||
|
||||
implementation(libs.compose.destinations.animations.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.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.androidx.webkit)
|
||||
}
|
||||
9
KernelSU/manager/app/proguard-rules.pro
vendored
Normal file
9
KernelSU/manager/app/proguard-rules.pro
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
-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
|
||||
51
KernelSU/manager/app/src/main/AndroidManifest.xml
Normal file
51
KernelSU/manager/app/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<?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" />
|
||||
|
||||
<application
|
||||
android:name=".KernelSUApplication"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:theme="@style/Theme.KernelSU"
|
||||
tools:targetApi="34">
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.KernelSU">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.lib_name"
|
||||
android:value="" />
|
||||
</activity>
|
||||
|
||||
<activity android:name=".ui.webui.WebUIActivity"
|
||||
android:autoRemoveFromRecents="true"
|
||||
android:documentLaunchMode="intoExisting"
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.KernelSU.WebUI" />
|
||||
|
||||
<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/filepaths" />
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
// IKsuInterface.aidl
|
||||
package me.weishu.kernelsu;
|
||||
|
||||
import android.content.pm.PackageInfo;
|
||||
import rikka.parcelablelist.ParcelableListSlice;
|
||||
|
||||
interface IKsuInterface {
|
||||
ParcelableListSlice<PackageInfo> getPackages(int flags);
|
||||
}
|
||||
18
KernelSU/manager/app/src/main/cpp/CMakeLists.txt
Normal file
18
KernelSU/manager/app/src/main/cpp/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
|
||||
# For more information about using CMake with Android Studio, read the
|
||||
# documentation: https://d.android.com/studio/projects/add-native-code.html
|
||||
|
||||
# Sets the minimum version of CMake required to build the native library.
|
||||
cmake_minimum_required(VERSION 3.18.1)
|
||||
|
||||
project("kernelsu")
|
||||
|
||||
add_library(kernelsu
|
||||
SHARED
|
||||
jni.cc
|
||||
ksu.cc
|
||||
)
|
||||
|
||||
find_library(log-lib log)
|
||||
|
||||
target_link_libraries(kernelsu ${log-lib})
|
||||
298
KernelSU/manager/app/src/main/cpp/jni.cc
Normal file
298
KernelSU/manager/app/src/main/cpp/jni.cc
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
#include <jni.h>
|
||||
|
||||
#include <sys/prctl.h>
|
||||
|
||||
#include <android/log.h>
|
||||
#include <cstring>
|
||||
|
||||
#include "ksu.h"
|
||||
|
||||
#define LOG_TAG "KernelSU"
|
||||
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_me_weishu_kernelsu_Natives_becomeManager(JNIEnv *env, jobject, jstring pkg) {
|
||||
auto cpkg = env->GetStringUTFChars(pkg, nullptr);
|
||||
auto result = become_manager(cpkg);
|
||||
env->ReleaseStringUTFChars(pkg, cpkg);
|
||||
return result;
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_me_weishu_kernelsu_Natives_getVersion(JNIEnv *env, jobject) {
|
||||
return get_version();
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jintArray JNICALL
|
||||
Java_me_weishu_kernelsu_Natives_getAllowList(JNIEnv *env, jobject) {
|
||||
int uids[1024];
|
||||
int size = 0;
|
||||
bool result = get_allow_list(uids, &size);
|
||||
LOGD("getAllowList: %d, size: %d", result, size);
|
||||
if (result) {
|
||||
auto array = env->NewIntArray(size);
|
||||
env->SetIntArrayRegion(array, 0, size, uids);
|
||||
return array;
|
||||
}
|
||||
return env->NewIntArray(0);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_me_weishu_kernelsu_Natives_isSafeMode(JNIEnv *env, jclass clazz) {
|
||||
return is_safe_mode();
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_me_weishu_kernelsu_Natives_isLkmMode(JNIEnv *env, jclass clazz) {
|
||||
return is_lkm_mode();
|
||||
}
|
||||
|
||||
static void fillIntArray(JNIEnv *env, jobject list, int *data, int count) {
|
||||
auto cls = env->GetObjectClass(list);
|
||||
auto add = env->GetMethodID(cls, "add", "(Ljava/lang/Object;)Z");
|
||||
auto integerCls = env->FindClass("java/lang/Integer");
|
||||
auto constructor = env->GetMethodID(integerCls, "<init>", "(I)V");
|
||||
for (int i = 0; i < count; ++i) {
|
||||
auto integer = env->NewObject(integerCls, constructor, data[i]);
|
||||
env->CallBooleanMethod(list, add, integer);
|
||||
}
|
||||
}
|
||||
|
||||
static void addIntToList(JNIEnv *env, jobject list, int ele) {
|
||||
auto cls = env->GetObjectClass(list);
|
||||
auto add = env->GetMethodID(cls, "add", "(Ljava/lang/Object;)Z");
|
||||
auto integerCls = env->FindClass("java/lang/Integer");
|
||||
auto constructor = env->GetMethodID(integerCls, "<init>", "(I)V");
|
||||
auto integer = env->NewObject(integerCls, constructor, ele);
|
||||
env->CallBooleanMethod(list, add, integer);
|
||||
}
|
||||
|
||||
static uint64_t capListToBits(JNIEnv *env, jobject list) {
|
||||
auto cls = env->GetObjectClass(list);
|
||||
auto get = env->GetMethodID(cls, "get", "(I)Ljava/lang/Object;");
|
||||
auto size = env->GetMethodID(cls, "size", "()I");
|
||||
auto listSize = env->CallIntMethod(list, size);
|
||||
auto integerCls = env->FindClass("java/lang/Integer");
|
||||
auto intValue = env->GetMethodID(integerCls, "intValue", "()I");
|
||||
uint64_t result = 0;
|
||||
for (int i = 0; i < listSize; ++i) {
|
||||
auto integer = env->CallObjectMethod(list, get, i);
|
||||
int data = env->CallIntMethod(integer, intValue);
|
||||
|
||||
if (cap_valid(data)) {
|
||||
result |= (1ULL << data);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static int getListSize(JNIEnv *env, jobject list) {
|
||||
auto cls = env->GetObjectClass(list);
|
||||
auto size = env->GetMethodID(cls, "size", "()I");
|
||||
return env->CallIntMethod(list, size);
|
||||
}
|
||||
|
||||
static void fillArrayWithList(JNIEnv *env, jobject list, int *data, int count) {
|
||||
auto cls = env->GetObjectClass(list);
|
||||
auto get = env->GetMethodID(cls, "get", "(I)Ljava/lang/Object;");
|
||||
auto integerCls = env->FindClass("java/lang/Integer");
|
||||
auto intValue = env->GetMethodID(integerCls, "intValue", "()I");
|
||||
for (int i = 0; i < count; ++i) {
|
||||
auto integer = env->CallObjectMethod(list, get, i);
|
||||
data[i] = env->CallIntMethod(integer, intValue);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_me_weishu_kernelsu_Natives_getAppProfile(JNIEnv *env, jobject, jstring pkg, jint uid) {
|
||||
if (env->GetStringLength(pkg) > KSU_MAX_PACKAGE_NAME) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
p_key_t key = {};
|
||||
auto cpkg = env->GetStringUTFChars(pkg, nullptr);
|
||||
strcpy(key, cpkg);
|
||||
env->ReleaseStringUTFChars(pkg, cpkg);
|
||||
|
||||
app_profile profile = {};
|
||||
profile.version = KSU_APP_PROFILE_VER;
|
||||
|
||||
strcpy(profile.key, key);
|
||||
profile.current_uid = uid;
|
||||
|
||||
bool useDefaultProfile = !get_app_profile(key, &profile);
|
||||
|
||||
auto cls = env->FindClass("me/weishu/kernelsu/Natives$Profile");
|
||||
auto constructor = env->GetMethodID(cls, "<init>", "()V");
|
||||
auto obj = env->NewObject(cls, constructor);
|
||||
auto keyField = env->GetFieldID(cls, "name", "Ljava/lang/String;");
|
||||
auto currentUidField = env->GetFieldID(cls, "currentUid", "I");
|
||||
auto allowSuField = env->GetFieldID(cls, "allowSu", "Z");
|
||||
|
||||
auto rootUseDefaultField = env->GetFieldID(cls, "rootUseDefault", "Z");
|
||||
auto rootTemplateField = env->GetFieldID(cls, "rootTemplate", "Ljava/lang/String;");
|
||||
|
||||
auto uidField = env->GetFieldID(cls, "uid", "I");
|
||||
auto gidField = env->GetFieldID(cls, "gid", "I");
|
||||
auto groupsField = env->GetFieldID(cls, "groups", "Ljava/util/List;");
|
||||
auto capabilitiesField = env->GetFieldID(cls, "capabilities", "Ljava/util/List;");
|
||||
auto domainField = env->GetFieldID(cls, "context", "Ljava/lang/String;");
|
||||
auto namespacesField = env->GetFieldID(cls, "namespace", "I");
|
||||
|
||||
auto nonRootUseDefaultField = env->GetFieldID(cls, "nonRootUseDefault", "Z");
|
||||
auto umountModulesField = env->GetFieldID(cls, "umountModules", "Z");
|
||||
|
||||
env->SetObjectField(obj, keyField, env->NewStringUTF(profile.key));
|
||||
env->SetIntField(obj, currentUidField, profile.current_uid);
|
||||
|
||||
if (useDefaultProfile) {
|
||||
// no profile found, so just use default profile:
|
||||
// don't allow root and use default profile!
|
||||
LOGD("use default profile for: %s, %d", key, uid);
|
||||
|
||||
// allow_su = false
|
||||
// non root use default = true
|
||||
env->SetBooleanField(obj, allowSuField, false);
|
||||
env->SetBooleanField(obj, nonRootUseDefaultField, true);
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
auto allowSu = profile.allow_su;
|
||||
|
||||
if (allowSu) {
|
||||
env->SetBooleanField(obj, rootUseDefaultField, (jboolean) profile.rp_config.use_default);
|
||||
if (strlen(profile.rp_config.template_name) > 0) {
|
||||
env->SetObjectField(obj, rootTemplateField,
|
||||
env->NewStringUTF(profile.rp_config.template_name));
|
||||
}
|
||||
|
||||
env->SetIntField(obj, uidField, profile.rp_config.profile.uid);
|
||||
env->SetIntField(obj, gidField, profile.rp_config.profile.gid);
|
||||
|
||||
jobject groupList = env->GetObjectField(obj, groupsField);
|
||||
int groupCount = profile.rp_config.profile.groups_count;
|
||||
if (groupCount > KSU_MAX_GROUPS) {
|
||||
LOGD("kernel group count too large: %d???", groupCount);
|
||||
groupCount = KSU_MAX_GROUPS;
|
||||
}
|
||||
fillIntArray(env, groupList, profile.rp_config.profile.groups, groupCount);
|
||||
|
||||
jobject capList = env->GetObjectField(obj, capabilitiesField);
|
||||
for (int i = 0; i <= CAP_LAST_CAP; i++) {
|
||||
if (profile.rp_config.profile.capabilities.effective & (1ULL << i)) {
|
||||
addIntToList(env, capList, i);
|
||||
}
|
||||
}
|
||||
|
||||
env->SetObjectField(obj, domainField,
|
||||
env->NewStringUTF(profile.rp_config.profile.selinux_domain));
|
||||
env->SetIntField(obj, namespacesField, profile.rp_config.profile.namespaces);
|
||||
env->SetBooleanField(obj, allowSuField, profile.allow_su);
|
||||
} else {
|
||||
env->SetBooleanField(obj, nonRootUseDefaultField,
|
||||
(jboolean) profile.nrp_config.use_default);
|
||||
env->SetBooleanField(obj, umountModulesField, profile.nrp_config.profile.umount_modules);
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_me_weishu_kernelsu_Natives_setAppProfile(JNIEnv *env, jobject clazz, jobject profile) {
|
||||
auto cls = env->FindClass("me/weishu/kernelsu/Natives$Profile");
|
||||
|
||||
auto keyField = env->GetFieldID(cls, "name", "Ljava/lang/String;");
|
||||
auto currentUidField = env->GetFieldID(cls, "currentUid", "I");
|
||||
auto allowSuField = env->GetFieldID(cls, "allowSu", "Z");
|
||||
|
||||
auto rootUseDefaultField = env->GetFieldID(cls, "rootUseDefault", "Z");
|
||||
auto rootTemplateField = env->GetFieldID(cls, "rootTemplate", "Ljava/lang/String;");
|
||||
|
||||
auto uidField = env->GetFieldID(cls, "uid", "I");
|
||||
auto gidField = env->GetFieldID(cls, "gid", "I");
|
||||
auto groupsField = env->GetFieldID(cls, "groups", "Ljava/util/List;");
|
||||
auto capabilitiesField = env->GetFieldID(cls, "capabilities", "Ljava/util/List;");
|
||||
auto domainField = env->GetFieldID(cls, "context", "Ljava/lang/String;");
|
||||
auto namespacesField = env->GetFieldID(cls, "namespace", "I");
|
||||
|
||||
auto nonRootUseDefaultField = env->GetFieldID(cls, "nonRootUseDefault", "Z");
|
||||
auto umountModulesField = env->GetFieldID(cls, "umountModules", "Z");
|
||||
|
||||
auto key = env->GetObjectField(profile, keyField);
|
||||
if (!key) {
|
||||
return false;
|
||||
}
|
||||
if (env->GetStringLength((jstring) key) > KSU_MAX_PACKAGE_NAME) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto cpkg = env->GetStringUTFChars((jstring) key, nullptr);
|
||||
p_key_t p_key = {};
|
||||
strcpy(p_key, cpkg);
|
||||
env->ReleaseStringUTFChars((jstring) key, cpkg);
|
||||
|
||||
auto currentUid = env->GetIntField(profile, currentUidField);
|
||||
|
||||
auto uid = env->GetIntField(profile, uidField);
|
||||
auto gid = env->GetIntField(profile, gidField);
|
||||
auto groups = env->GetObjectField(profile, groupsField);
|
||||
auto capabilities = env->GetObjectField(profile, capabilitiesField);
|
||||
auto domain = env->GetObjectField(profile, domainField);
|
||||
auto allowSu = env->GetBooleanField(profile, allowSuField);
|
||||
auto umountModules = env->GetBooleanField(profile, umountModulesField);
|
||||
|
||||
app_profile p = {};
|
||||
p.version = KSU_APP_PROFILE_VER;
|
||||
|
||||
strcpy(p.key, p_key);
|
||||
p.allow_su = allowSu;
|
||||
p.current_uid = currentUid;
|
||||
|
||||
if (allowSu) {
|
||||
p.rp_config.use_default = env->GetBooleanField(profile, rootUseDefaultField);
|
||||
auto templateName = env->GetObjectField(profile, rootTemplateField);
|
||||
if (templateName) {
|
||||
auto ctemplateName = env->GetStringUTFChars((jstring) templateName, nullptr);
|
||||
strcpy(p.rp_config.template_name, ctemplateName);
|
||||
env->ReleaseStringUTFChars((jstring) templateName, ctemplateName);
|
||||
}
|
||||
|
||||
p.rp_config.profile.uid = uid;
|
||||
p.rp_config.profile.gid = gid;
|
||||
|
||||
int groups_count = getListSize(env, groups);
|
||||
if (groups_count > KSU_MAX_GROUPS) {
|
||||
LOGD("groups count too large: %d", groups_count);
|
||||
return false;
|
||||
}
|
||||
p.rp_config.profile.groups_count = groups_count;
|
||||
fillArrayWithList(env, groups, p.rp_config.profile.groups, groups_count);
|
||||
|
||||
p.rp_config.profile.capabilities.effective = capListToBits(env, capabilities);
|
||||
|
||||
auto cdomain = env->GetStringUTFChars((jstring) domain, nullptr);
|
||||
strcpy(p.rp_config.profile.selinux_domain, cdomain);
|
||||
env->ReleaseStringUTFChars((jstring) domain, cdomain);
|
||||
|
||||
p.rp_config.profile.namespaces = env->GetIntField(profile, namespacesField);
|
||||
} else {
|
||||
p.nrp_config.use_default = env->GetBooleanField(profile, nonRootUseDefaultField);
|
||||
p.nrp_config.profile.umount_modules = umountModules;
|
||||
}
|
||||
|
||||
return set_app_profile(&p);
|
||||
}
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_me_weishu_kernelsu_Natives_uidShouldUmount(JNIEnv *env, jobject thiz, jint uid) {
|
||||
return uid_should_umount(uid);
|
||||
}
|
||||
86
KernelSU/manager/app/src/main/cpp/ksu.cc
Normal file
86
KernelSU/manager/app/src/main/cpp/ksu.cc
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
//
|
||||
// Created by weishu on 2022/12/9.
|
||||
//
|
||||
|
||||
#include <sys/prctl.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "ksu.h"
|
||||
|
||||
#define KERNEL_SU_OPTION 0xDEADBEEF
|
||||
|
||||
#define CMD_GRANT_ROOT 0
|
||||
|
||||
#define CMD_BECOME_MANAGER 1
|
||||
#define CMD_GET_VERSION 2
|
||||
#define CMD_ALLOW_SU 3
|
||||
#define CMD_DENY_SU 4
|
||||
#define CMD_GET_SU_LIST 5
|
||||
#define CMD_GET_DENY_LIST 6
|
||||
#define CMD_CHECK_SAFEMODE 9
|
||||
|
||||
#define CMD_GET_APP_PROFILE 10
|
||||
#define CMD_SET_APP_PROFILE 11
|
||||
|
||||
#define CMD_IS_UID_GRANTED_ROOT 12
|
||||
#define CMD_IS_UID_SHOULD_UMOUNT 13
|
||||
|
||||
static bool ksuctl(int cmd, void* arg1, void* arg2) {
|
||||
int32_t result = 0;
|
||||
prctl(KERNEL_SU_OPTION, cmd, arg1, arg2, &result);
|
||||
return result == KERNEL_SU_OPTION;
|
||||
}
|
||||
|
||||
bool become_manager(const char* pkg) {
|
||||
char param[128];
|
||||
uid_t uid = getuid();
|
||||
uint32_t userId = uid / 100000;
|
||||
if (userId == 0) {
|
||||
sprintf(param, "/data/data/%s", pkg);
|
||||
} else {
|
||||
snprintf(param, sizeof(param), "/data/user/%d/%s", userId, pkg);
|
||||
}
|
||||
|
||||
return ksuctl(CMD_BECOME_MANAGER, param, nullptr);
|
||||
}
|
||||
|
||||
// cache the result to avoid unnecessary syscall
|
||||
static bool is_lkm;
|
||||
int get_version() {
|
||||
int32_t version = -1;
|
||||
int32_t lkm = 0;
|
||||
ksuctl(CMD_GET_VERSION, &version, &lkm);
|
||||
if (!is_lkm && lkm != 0) {
|
||||
is_lkm = true;
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
bool get_allow_list(int *uids, int *size) {
|
||||
return ksuctl(CMD_GET_SU_LIST, uids, size);
|
||||
}
|
||||
|
||||
bool is_safe_mode() {
|
||||
return ksuctl(CMD_CHECK_SAFEMODE, nullptr, nullptr);
|
||||
}
|
||||
|
||||
bool is_lkm_mode() {
|
||||
// you should call get_version first!
|
||||
return is_lkm;
|
||||
}
|
||||
|
||||
bool uid_should_umount(int uid) {
|
||||
bool should;
|
||||
return ksuctl(CMD_IS_UID_SHOULD_UMOUNT, reinterpret_cast<void*>(uid), &should) && should;
|
||||
}
|
||||
|
||||
bool set_app_profile(const app_profile *profile) {
|
||||
return ksuctl(CMD_SET_APP_PROFILE, (void*) profile, nullptr);
|
||||
}
|
||||
|
||||
bool get_app_profile(p_key_t key, app_profile *profile) {
|
||||
return ksuctl(CMD_GET_APP_PROFILE, (void*) profile, nullptr);
|
||||
}
|
||||
82
KernelSU/manager/app/src/main/cpp/ksu.h
Normal file
82
KernelSU/manager/app/src/main/cpp/ksu.h
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
//
|
||||
// Created by weishu on 2022/12/9.
|
||||
//
|
||||
|
||||
#ifndef KERNELSU_KSU_H
|
||||
#define KERNELSU_KSU_H
|
||||
|
||||
#include <linux/capability.h>
|
||||
|
||||
bool become_manager(const char *);
|
||||
|
||||
int get_version();
|
||||
|
||||
bool get_allow_list(int *uids, int *size);
|
||||
|
||||
bool uid_should_umount(int uid);
|
||||
|
||||
bool is_safe_mode();
|
||||
|
||||
bool is_lkm_mode();
|
||||
|
||||
#define KSU_APP_PROFILE_VER 2
|
||||
#define KSU_MAX_PACKAGE_NAME 256
|
||||
// NGROUPS_MAX for Linux is 65535 generally, but we only supports 32 groups.
|
||||
#define KSU_MAX_GROUPS 32
|
||||
#define KSU_SELINUX_DOMAIN 64
|
||||
|
||||
using p_key_t = char[KSU_MAX_PACKAGE_NAME];
|
||||
|
||||
struct root_profile {
|
||||
int32_t uid;
|
||||
int32_t gid;
|
||||
|
||||
int32_t groups_count;
|
||||
int32_t groups[KSU_MAX_GROUPS];
|
||||
|
||||
// kernel_cap_t is u32[2] for capabilities v3
|
||||
struct {
|
||||
uint64_t effective;
|
||||
uint64_t permitted;
|
||||
uint64_t inheritable;
|
||||
} capabilities;
|
||||
|
||||
char selinux_domain[KSU_SELINUX_DOMAIN];
|
||||
|
||||
int32_t namespaces;
|
||||
};
|
||||
|
||||
struct non_root_profile {
|
||||
bool umount_modules;
|
||||
};
|
||||
|
||||
struct app_profile {
|
||||
// It may be utilized for backward compatibility, although we have never explicitly made any promises regarding this.
|
||||
uint32_t version;
|
||||
|
||||
// this is usually the package of the app, but can be other value for special apps
|
||||
char key[KSU_MAX_PACKAGE_NAME];
|
||||
int32_t current_uid;
|
||||
bool allow_su;
|
||||
|
||||
union {
|
||||
struct {
|
||||
bool use_default;
|
||||
char template_name[KSU_MAX_PACKAGE_NAME];
|
||||
|
||||
struct root_profile profile;
|
||||
} rp_config;
|
||||
|
||||
struct {
|
||||
bool use_default;
|
||||
|
||||
struct non_root_profile profile;
|
||||
} nrp_config;
|
||||
};
|
||||
};
|
||||
|
||||
bool set_app_profile(const app_profile *profile);
|
||||
|
||||
bool get_app_profile(p_key_t key, app_profile *profile);
|
||||
|
||||
#endif //KERNELSU_KSU_H
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package me.weishu.kernelsu
|
||||
|
||||
import android.app.Application
|
||||
import coil.Coil
|
||||
import coil.ImageLoader
|
||||
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
|
||||
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
|
||||
import java.io.File
|
||||
|
||||
lateinit var ksuApp: KernelSUApplication
|
||||
|
||||
class KernelSUApplication : Application() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
ksuApp = this
|
||||
|
||||
val context = this
|
||||
val iconSize = resources.getDimensionPixelSize(android.R.dimen.app_icon_size)
|
||||
Coil.setImageLoader(
|
||||
ImageLoader.Builder(context)
|
||||
.components {
|
||||
add(AppIconKeyer())
|
||||
add(AppIconFetcher.Factory(iconSize, false, context))
|
||||
}
|
||||
.build()
|
||||
)
|
||||
|
||||
val webroot = File(dataDir, "webroot")
|
||||
if (!webroot.exists()) {
|
||||
webroot.mkdir()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package me.weishu.kernelsu
|
||||
|
||||
import android.system.Os
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
* @date 2022/12/10.
|
||||
*/
|
||||
|
||||
data class KernelVersion(val major: Int, val patchLevel: Int, val subLevel: Int) {
|
||||
override fun toString(): String {
|
||||
return "$major.$patchLevel.$subLevel"
|
||||
}
|
||||
|
||||
fun isGKI(): Boolean {
|
||||
|
||||
// kernel 6.x
|
||||
if (major > 5) {
|
||||
return true
|
||||
}
|
||||
|
||||
// kernel 5.10.x
|
||||
if (major == 5) {
|
||||
return patchLevel >= 10
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
fun parseKernelVersion(version: String): KernelVersion {
|
||||
val find = "(\\d+)\\.(\\d+)\\.(\\d+)".toRegex().find(version)
|
||||
return if (find != null) {
|
||||
KernelVersion(find.groupValues[1].toInt(), find.groupValues[2].toInt(), find.groupValues[3].toInt())
|
||||
} else {
|
||||
KernelVersion(-1, -1, -1)
|
||||
}
|
||||
}
|
||||
|
||||
fun getKernelVersion(): KernelVersion {
|
||||
Os.uname().release.let {
|
||||
return parseKernelVersion(it)
|
||||
}
|
||||
}
|
||||
117
KernelSU/manager/app/src/main/java/me/weishu/kernelsu/Natives.kt
Normal file
117
KernelSU/manager/app/src/main/java/me/weishu/kernelsu/Natives.kt
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
package me.weishu.kernelsu
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.Keep
|
||||
import androidx.compose.runtime.Immutable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
* @date 2022/12/8.
|
||||
*/
|
||||
object Natives {
|
||||
// minimal supported kernel version
|
||||
// 10915: allowlist breaking change, add app profile
|
||||
// 10931: app profile struct add 'version' field
|
||||
// 10946: add capabilities
|
||||
// 10977: change groups_count and groups to avoid overflow write
|
||||
// 11071: Fix the issue of failing to set a custom SELinux type.
|
||||
const val MINIMAL_SUPPORTED_KERNEL = 11071
|
||||
|
||||
// 11640: Support query working mode, LKM or GKI
|
||||
// when MINIMAL_SUPPORTED_KERNEL > 11640, we can remove this constant.
|
||||
const val MINIMAL_SUPPORTED_KERNEL_LKM = 11648
|
||||
const val KERNEL_SU_DOMAIN = "u:r:su:s0"
|
||||
|
||||
const val ROOT_UID = 0
|
||||
const val ROOT_GID = 0
|
||||
|
||||
init {
|
||||
System.loadLibrary("kernelsu")
|
||||
}
|
||||
|
||||
// become root manager, return true if success.
|
||||
external fun becomeManager(pkg: String?): Boolean
|
||||
val version: Int
|
||||
external get
|
||||
|
||||
// get the uid list of allowed su processes.
|
||||
val allowList: IntArray
|
||||
external get
|
||||
|
||||
val isSafeMode: Boolean
|
||||
external get
|
||||
|
||||
val isLkmMode: Boolean
|
||||
external get
|
||||
|
||||
external fun uidShouldUmount(uid: Int): Boolean
|
||||
|
||||
/**
|
||||
* Get the profile of the given package.
|
||||
* @param key usually the package name
|
||||
* @return return null if failed.
|
||||
*/
|
||||
external fun getAppProfile(key: String?, uid: Int): Profile
|
||||
external fun setAppProfile(profile: Profile?): Boolean
|
||||
|
||||
private const val NON_ROOT_DEFAULT_PROFILE_KEY = "$"
|
||||
private const val NOBODY_UID = 9999
|
||||
|
||||
fun setDefaultUmountModules(umountModules: Boolean): Boolean {
|
||||
Profile(
|
||||
NON_ROOT_DEFAULT_PROFILE_KEY,
|
||||
NOBODY_UID,
|
||||
false,
|
||||
umountModules = umountModules
|
||||
).let {
|
||||
return setAppProfile(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun isDefaultUmountModules(): Boolean {
|
||||
getAppProfile(NON_ROOT_DEFAULT_PROFILE_KEY, NOBODY_UID).let {
|
||||
return it.umountModules
|
||||
}
|
||||
}
|
||||
|
||||
fun requireNewKernel(): Boolean {
|
||||
return version < MINIMAL_SUPPORTED_KERNEL
|
||||
}
|
||||
|
||||
@Immutable
|
||||
@Parcelize
|
||||
@Keep
|
||||
data class Profile(
|
||||
// and there is a default profile for root and non-root
|
||||
val name: String,
|
||||
// current uid for the package, this is convivent for kernel to check
|
||||
// if the package name doesn't match uid, then it should be invalidated.
|
||||
val currentUid: Int = 0,
|
||||
|
||||
// if this is true, kernel will grant root permission to this package
|
||||
val allowSu: Boolean = false,
|
||||
|
||||
// these are used for root profile
|
||||
val rootUseDefault: Boolean = true,
|
||||
val rootTemplate: String? = null,
|
||||
val uid: Int = ROOT_UID,
|
||||
val gid: Int = ROOT_GID,
|
||||
val groups: List<Int> = mutableListOf(),
|
||||
val capabilities: List<Int> = mutableListOf(),
|
||||
val context: String = KERNEL_SU_DOMAIN,
|
||||
val namespace: Int = Namespace.INHERITED.ordinal,
|
||||
|
||||
val nonRootUseDefault: Boolean = true,
|
||||
val umountModules: Boolean = true,
|
||||
var rules: String = "", // this field is save in ksud!!
|
||||
) : Parcelable {
|
||||
enum class Namespace {
|
||||
INHERITED,
|
||||
GLOBAL,
|
||||
INDIVIDUAL,
|
||||
}
|
||||
|
||||
constructor() : this("")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package me.weishu.kernelsu.profile
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
* @date 2023/6/3.
|
||||
*/
|
||||
enum class Capabilities(val cap: Int, val display: String, val desc: String) {
|
||||
CAP_CHOWN(0, "CHOWN", "Make arbitrary changes to file UIDs and GIDs (see chown(2))"),
|
||||
CAP_DAC_OVERRIDE(1, "DAC_OVERRIDE", "Bypass file read, write, and execute permission checks"),
|
||||
CAP_DAC_READ_SEARCH(2, "DAC_READ_SEARCH", "Bypass file read permission checks and directory read and execute permission checks"),
|
||||
CAP_FOWNER(3, "FOWNER", "Bypass permission checks on operations that normally require the filesystem UID of the process to match the UID of the file (e.g., chmod(2), utime(2)), excluding those operations covered by CAP_DAC_OVERRIDE and CAP_DAC_READ_SEARCH"),
|
||||
CAP_FSETID(4, "FSETID", "Don’t clear set-user-ID and set-group-ID permission bits when a file is modified; set the set-group-ID bit for a file whose GID does not match the filesystem or any of the supplementary GIDs of the calling process"),
|
||||
CAP_KILL(5, "KILL", "Bypass permission checks for sending signals (see kill(2))."),
|
||||
CAP_SETGID(6, "SETGID", "Make arbitrary manipulations of process GIDs and supplementary GID list; allow setgid(2) manipulation of the caller’s effective and real group IDs"),
|
||||
CAP_SETUID(7, "SETUID", "Make arbitrary manipulations of process UIDs (setuid(2), setreuid(2), setresuid(2), setfsuid(2)); allow changing the current process user IDs; allow changing of the current process group ID to any value in the system’s range of legal group IDs"),
|
||||
CAP_SETPCAP(8, "SETPCAP", "If file capabilities are supported: grant or remove any capability in the caller’s permitted capability set to or from any other process. (This property supersedes the obsolete notion of giving a process all capabilities by granting all capabilities in its permitted set, and of removing all capabilities from a process by granting no capabilities in its permitted set. It does not permit any actions that were not permitted before.)"),
|
||||
CAP_LINUX_IMMUTABLE(9, "LINUX_IMMUTABLE", "Set the FS_APPEND_FL and FS_IMMUTABLE_FL inode flags (see chattr(1))."),
|
||||
CAP_NET_BIND_SERVICE(10, "NET_BIND_SERVICE", "Bind a socket to Internet domain"),
|
||||
CAP_NET_BROADCAST(11, "NET_BROADCAST", "Make socket broadcasts, and listen to multicasts"),
|
||||
CAP_NET_ADMIN(12, "NET_ADMIN", "Perform various network-related operations: interface configuration, administration of IP firewall, masquerading, and accounting, modify routing tables, bind to any address for transparent proxying, set type-of-service (TOS), clear driver statistics, set promiscuous mode, enabling multicasting, use setsockopt(2) to set the following socket options: SO_DEBUG, SO_MARK, SO_PRIORITY (for a priority outside the range 0 to 6), SO_RCVBUFFORCE, and SO_SNDBUFFORCE"),
|
||||
CAP_NET_RAW(13, "NET_RAW", "Use RAW and PACKET sockets"),
|
||||
CAP_IPC_LOCK(14, "IPC_LOCK", "Lock memory (mlock(2), mlockall(2), mmap(2), shmctl(2))"),
|
||||
CAP_IPC_OWNER(15, "IPC_OWNER", "Bypass permission checks for operations on System V IPC objects"),
|
||||
CAP_SYS_MODULE(16, "SYS_MODULE", "Load and unload kernel modules (see init_module(2) and delete_module(2)); in kernels before 2.6.25, this also granted rights for various other operations related to kernel modules"),
|
||||
CAP_SYS_RAWIO(17, "SYS_RAWIO", "Perform I/O port operations (iopl(2) and ioperm(2)); access /proc/kcore"),
|
||||
CAP_SYS_CHROOT(18, "SYS_CHROOT", "Use chroot(2)"),
|
||||
CAP_SYS_PTRACE(19, "SYS_PTRACE", "Trace arbitrary processes using ptrace(2)"),
|
||||
CAP_SYS_PACCT(20, "SYS_PACCT", "Use acct(2)"),
|
||||
CAP_SYS_ADMIN(21, "SYS_ADMIN", "Perform a range of system administration operations including: quotactl(2), mount(2), umount(2), swapon(2), swapoff(2), sethostname(2), and setdomainname(2); set and modify process resource limits (setrlimit(2)); perform various network-related operations (e.g., setting privileged socket options, enabling multicasting, interface configuration); perform various IPC operations (e.g., SysV semaphores, POSIX message queues, System V shared memory); allow reboot and kexec_load(2); override /proc/sys kernel tunables; perform ptrace(2) PTRACE_SECCOMP_GET_FILTER operation; perform some tracing and debugging operations (see ptrace(2)); administer the lifetime of kernel tracepoints (tracefs(5)); perform the KEYCTL_CHOWN and KEYCTL_SETPERM keyctl(2) operations; perform the following keyctl(2) operations: KEYCTL_CAPABILITIES, KEYCTL_CAPSQUASH, and KEYCTL_PKEY_ OPERATIONS; set state for the Extensible Authentication Protocol (EAP) kernel module; and override the RLIMIT_NPROC resource limit; allow ioperm/iopl access to I/O ports"),
|
||||
CAP_SYS_BOOT(22, "SYS_BOOT", "Use reboot(2) and kexec_load(2), reboot and load a new kernel for later execution"),
|
||||
CAP_SYS_NICE(23, "SYS_NICE", "Raise process nice value (nice(2), setpriority(2)) and change the nice value for arbitrary processes; set real-time scheduling policies for calling process, and set scheduling policies and priorities for arbitrary processes (sched_setscheduler(2), sched_setparam(2)"),
|
||||
CAP_SYS_RESOURCE(24, "SYS_RESOURCE", "Override resource Limits. Set resource limits (setrlimit(2), prlimit(2)), override quota limits (quota(2), quotactl(2)), override reserved space on ext2 filesystem (ext2_ioctl(2)), override size restrictions on IPC message queues (msg(2)) and system V shared memory segments (shmget(2)), and override the /proc/sys/fs/pipe-size-max limit"),
|
||||
CAP_SYS_TIME(25, "SYS_TIME", "Set system clock (settimeofday(2), stime(2), adjtimex(2)); set real-time (hardware) clock"),
|
||||
CAP_SYS_TTY_CONFIG(26, "SYS_TTY_CONFIG", "Use vhangup(2); employ various privileged ioctl(2) operations on virtual terminals"),
|
||||
CAP_MKNOD(27, "MKNOD", "Create special files using mknod(2)"),
|
||||
CAP_LEASE(28, "LEASE", "Establish leases on arbitrary files (see fcntl(2))"),
|
||||
CAP_AUDIT_WRITE(29, "AUDIT_WRITE", "Write records to kernel auditing log"),
|
||||
CAP_AUDIT_CONTROL(30, "AUDIT_CONTROL", "Enable and disable kernel auditing; change auditing filter rules; retrieve auditing status and filtering rules"),
|
||||
CAP_SETFCAP(31, "SETFCAP", "If file capabilities are supported: grant or remove any capability in any capability set to any file"),
|
||||
CAP_MAC_OVERRIDE(32, "MAC_OVERRIDE", "Override Mandatory Access Control (MAC). Implemented for the Smack Linux Security Module (LSM)"),
|
||||
CAP_MAC_ADMIN(33, "MAC_ADMIN", "Allow MAC configuration or state changes. Implemented for the Smack LSM"),
|
||||
CAP_SYSLOG(34, "SYSLOG", "Perform privileged syslog(2) operations. See syslog(2) for information on which operations require privilege"),
|
||||
CAP_WAKE_ALARM(35, "WAKE_ALARM", "Trigger something that will wake up the system"),
|
||||
CAP_BLOCK_SUSPEND(36, "BLOCK_SUSPEND", "Employ features that can block system suspend"),
|
||||
CAP_AUDIT_READ(37, "AUDIT_READ", "Allow reading the audit log via a multicast netlink socket"),
|
||||
CAP_PERFMON(38, "PERFMON", "Allow performance monitoring via perf_event_open(2)"),
|
||||
CAP_BPF(39, "BPF", "Allow BPF operations via bpf(2)"),
|
||||
CAP_CHECKPOINT_RESTORE(40, "CHECKPOINT_RESTORE", "Allow processes to be checkpointed via checkpoint/restore in user namespace(2)"),
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
package me.weishu.kernelsu.profile
|
||||
|
||||
/**
|
||||
* https://cs.android.com/android/platform/superproject/main/+/main:system/core/libcutils/include/private/android_filesystem_config.h
|
||||
* @author weishu
|
||||
* @date 2023/6/3.
|
||||
*/
|
||||
enum class Groups(val gid: Int, val display: String, val desc: String) {
|
||||
ROOT(0, "root", "traditional unix root user"),
|
||||
DAEMON(1, "daemon", "Traditional unix daemon owner."),
|
||||
BIN(2, "bin", "Traditional unix binaries owner."),
|
||||
SYS(3, "sys", "A group with the same gid on Linux/macOS/Android."),
|
||||
SYSTEM(1000, "system", "system server"),
|
||||
RADIO(1001, "radio", "telephony subsystem, RIL"),
|
||||
BLUETOOTH(1002, "bluetooth", "bluetooth subsystem"),
|
||||
GRAPHICS(1003, "graphics", "graphics devices"),
|
||||
INPUT(1004, "input", "input devices"),
|
||||
AUDIO(1005, "audio", "audio devices"),
|
||||
CAMERA(1006, "camera", "camera devices"),
|
||||
LOG(1007, "log", "log devices"),
|
||||
COMPASS(1008, "compass", "compass device"),
|
||||
MOUNT(1009, "mount", "mountd socket"),
|
||||
WIFI(1010, "wifi", "wifi subsystem"),
|
||||
ADB(1011, "adb", "android debug bridge (adbd)"),
|
||||
INSTALL(1012, "install", "group for installing packages"),
|
||||
MEDIA(1013, "media", "mediaserver process"),
|
||||
DHCP(1014, "dhcp", "dhcp client"),
|
||||
SDCARD_RW(1015, "sdcard_rw", "external storage write access"),
|
||||
VPN(1016, "vpn", "vpn system"),
|
||||
KEYSTORE(1017, "keystore", "keystore subsystem"),
|
||||
USB(1018, "usb", "USB devices"),
|
||||
DRM(1019, "drm", "DRM server"),
|
||||
MDNSR(1020, "mdnsr", "MulticastDNSResponder (service discovery)"),
|
||||
GPS(1021, "gps", "GPS daemon"),
|
||||
UNUSED1(1022, "unused1", "deprecated, DO NOT USE"),
|
||||
MEDIA_RW(1023, "media_rw", "internal media storage write access"),
|
||||
MTP(1024, "mtp", "MTP USB driver access"),
|
||||
UNUSED2(1025, "unused2", "deprecated, DO NOT USE"),
|
||||
DRMRPC(1026, "drmrpc", "group for drm rpc"),
|
||||
NFC(1027, "nfc", "nfc subsystem"),
|
||||
SDCARD_R(1028, "sdcard_r", "external storage read access"),
|
||||
CLAT(1029, "clat", "clat part of nat464"),
|
||||
LOOP_RADIO(1030, "loop_radio", "loop radio devices"),
|
||||
MEDIA_DRM(1031, "media_drm", "MediaDrm plugins"),
|
||||
PACKAGE_INFO(1032, "package_info", "access to installed package details"),
|
||||
SDCARD_PICS(1033, "sdcard_pics", "external storage photos access"),
|
||||
SDCARD_AV(1034, "sdcard_av", "external storage audio/video access"),
|
||||
SDCARD_ALL(1035, "sdcard_all", "access all users external storage"),
|
||||
LOGD(1036, "logd", "log daemon"),
|
||||
SHARED_RELRO(1037, "shared_relro", "creator of shared GNU RELRO files"),
|
||||
DBUS(1038, "dbus", "dbus-daemon IPC broker process"),
|
||||
TLSDATE(1039, "tlsdate", "tlsdate unprivileged user"),
|
||||
MEDIA_EX(1040, "media_ex", "mediaextractor process"),
|
||||
AUDIOSERVER(1041, "audioserver", "audioserver process"),
|
||||
METRICS_COLL(1042, "metrics_coll", "metrics_collector process"),
|
||||
METRICSD(1043, "metricsd", "metricsd process"),
|
||||
WEBSERV(1044, "webserv", "webservd process"),
|
||||
DEBUGGERD(1045, "debuggerd", "debuggerd unprivileged user"),
|
||||
MEDIA_CODEC(1046, "media_codec", "media_codec process"),
|
||||
CAMERASERVER(1047, "cameraserver", "cameraserver process"),
|
||||
FIREWALL(1048, "firewall", "firewall process"),
|
||||
TRUNKS(1049, "trunks", "trunksd process"),
|
||||
NVRAM(1050, "nvram", "nvram daemon"),
|
||||
DNS(1051, "dns", "DNS resolution daemon (system: netd)"),
|
||||
DNS_TETHER(1052, "dns_tether", "DNS resolution daemon (tether: dnsmasq)"),
|
||||
WEBVIEW_ZYGOTE(1053, "webview_zygote", "WebView zygote process"),
|
||||
VEHICLE_NETWORK(1054, "vehicle_network", "Vehicle network service"),
|
||||
MEDIA_AUDIO(1055, "media_audio", "GID for audio files on internal media storage"),
|
||||
MEDIA_VIDEO(1056, "media_video", "GID for video files on internal media storage"),
|
||||
MEDIA_IMAGE(1057, "media_image", "GID for image files on internal media storage"),
|
||||
TOMBSTONED(1058, "tombstoned", "tombstoned user"),
|
||||
MEDIA_OBB(1059, "media_obb", "GID for OBB files on internal media storage"),
|
||||
ESE(1060, "ese", "embedded secure element (eSE) subsystem"),
|
||||
OTA_UPDATE(1061, "ota_update", "resource tracking UID for OTA updates"),
|
||||
AUTOMOTIVE_EVS(1062, "automotive_evs", "Automotive rear and surround view system"),
|
||||
LOWPAN(1063, "lowpan", "LoWPAN subsystem"),
|
||||
HSM(1064, "lowpan", "hardware security module subsystem"),
|
||||
RESERVED_DISK(1065, "reserved_disk", "GID that has access to reserved disk space"),
|
||||
STATSD(1066, "statsd", "statsd daemon"),
|
||||
INCIDENTD(1067, "incidentd", "incidentd daemon"),
|
||||
SECURE_ELEMENT(1068, "secure_element", "secure element subsystem"),
|
||||
LMKD(1069, "lmkd", "low memory killer daemon"),
|
||||
LLKD(1070, "llkd", "live lock daemon"),
|
||||
IORAPD(1071, "iorapd", "input/output readahead and pin daemon"),
|
||||
GPU_SERVICE(1072, "gpu_service", "GPU service daemon"),
|
||||
NETWORK_STACK(1073, "network_stack", "network stack service"),
|
||||
GSID(1074, "GSID", "GSI service daemon"),
|
||||
FSVERITY_CERT(1075, "fsverity_cert", "fs-verity key ownership in keystore"),
|
||||
CREDSTORE(1076, "credstore", "identity credential manager service"),
|
||||
EXTERNAL_STORAGE(1077, "external_storage", "Full external storage access including USB OTG volumes"),
|
||||
EXT_DATA_RW(1078, "ext_data_rw", "GID for app-private data directories on external storage"),
|
||||
EXT_OBB_RW(1079, "ext_obb_rw", "GID for OBB directories on external storage"),
|
||||
CONTEXT_HUB(1080, "context_hub", "GID for access to the Context Hub"),
|
||||
VIRTUALIZATIONSERVICE(1081, "virtualizationservice", "VirtualizationService daemon"),
|
||||
ARTD(1082, "artd", "ART Service daemon"),
|
||||
UWB(1083, "uwb", "UWB subsystem"),
|
||||
THREAD_NETWORK(1084, "thread_network", "Thread Network subsystem"),
|
||||
DICED(1085, "diced", "Android's DICE daemon"),
|
||||
DMESGD(1086, "dmesgd", "dmesg parsing daemon for kernel report collection"),
|
||||
JC_WEAVER(1087, "jc_weaver", "Javacard Weaver HAL - to manage omapi ARA rules"),
|
||||
JC_STRONGBOX(1088, "jc_strongbox", "Javacard Strongbox HAL - to manage omapi ARA rules"),
|
||||
JC_IDENTITYCRED(1089, "jc_identitycred", "Javacard Identity Cred HAL - to manage omapi ARA rules"),
|
||||
SDK_SANDBOX(1090, "sdk_sandbox", "SDK sandbox virtual UID"),
|
||||
SECURITY_LOG_WRITER(1091, "security_log_writer", "write to security log"),
|
||||
PRNG_SEEDER(1092, "prng_seeder", "PRNG seeder daemon"),
|
||||
|
||||
SHELL(2000, "shell", "adb and debug shell user"),
|
||||
CACHE(2001, "cache", "cache access"),
|
||||
DIAG(2002, "diag", "access to diagnostic resources"),
|
||||
|
||||
/* The 3000 series are intended for use as supplemental group id's only.
|
||||
* They indicate special Android capabilities that the kernel is aware of. */
|
||||
NET_BT_ADMIN(3001, "net_bt_admin", "bluetooth: create any socket"),
|
||||
NET_BT(3002, "net_bt", "bluetooth: create sco, rfcomm or l2cap sockets"),
|
||||
INET(3003, "inet", "can create AF_INET and AF_INET6 sockets"),
|
||||
NET_RAW(3004, "net_raw", "can create raw INET sockets"),
|
||||
NET_ADMIN(3005, "net_admin", "can configure interfaces and routing tables."),
|
||||
NET_BW_STATS(3006, "net_bw_stats", "read bandwidth statistics"),
|
||||
NET_BW_ACCT(3007, "net_bw_acct", "change bandwidth statistics accounting"),
|
||||
NET_BT_STACK(3008, "net_bt_stack", "access to various bluetooth management functions"),
|
||||
READPROC(3009, "readproc", "Allow /proc read access"),
|
||||
WAKELOCK(3010, "wakelock", "Allow system wakelock read/write access"),
|
||||
UHID(3011, "uhid", "Allow read/write to /dev/uhid node"),
|
||||
READTRACEFS(3012, "readtracefs", "Allow tracefs read"),
|
||||
|
||||
EVERYBODY(9997, "everybody", "Shared external storage read/write"),
|
||||
MISC(9998, "misc", "Access to misc storage"),
|
||||
NOBODY(9999, "nobody", "Reserved"),
|
||||
APP(10000, "app", "Access to app data"),
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package me.weishu.kernelsu.ui;
|
||||
|
||||
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.weishu.kernelsu.IKsuInterface;
|
||||
import rikka.parcelablelist.ParcelableListSlice;
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
* @date 2023/4/18.
|
||||
*/
|
||||
|
||||
public class KsuService extends RootService {
|
||||
|
||||
private static final String TAG = "KsuService";
|
||||
|
||||
class Stub extends IKsuInterface.Stub {
|
||||
@Override
|
||||
public ParcelableListSlice<PackageInfo> getPackages(int flags) {
|
||||
List<PackageInfo> list = getInstalledPackagesAll(flags);
|
||||
Log.i(TAG, "getPackages: " + list.size());
|
||||
return new ParcelableListSlice<>(list);
|
||||
}
|
||||
}
|
||||
|
||||
@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<>();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package me.weishu.kernelsu.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
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.SnackbarHost
|
||||
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.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.ramcosta.composedestinations.DestinationsNavHost
|
||||
import com.ramcosta.composedestinations.navigation.popBackStack
|
||||
import com.ramcosta.composedestinations.utils.isRouteOnBackStackAsState
|
||||
import me.weishu.kernelsu.Natives
|
||||
import me.weishu.kernelsu.ksuApp
|
||||
import me.weishu.kernelsu.ui.screen.BottomBarDestination
|
||||
import me.weishu.kernelsu.ui.screen.NavGraphs
|
||||
import me.weishu.kernelsu.ui.theme.KernelSUTheme
|
||||
import me.weishu.kernelsu.ui.util.LocalSnackbarHost
|
||||
import me.weishu.kernelsu.ui.util.rootAvailable
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContent {
|
||||
KernelSUTheme {
|
||||
val navController = rememberNavController()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
Scaffold(
|
||||
bottomBar = { BottomBar(navController) },
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) }
|
||||
) { innerPadding ->
|
||||
CompositionLocalProvider(
|
||||
LocalSnackbarHost provides snackbarHostState,
|
||||
) {
|
||||
DestinationsNavHost(
|
||||
modifier = Modifier.padding(innerPadding),
|
||||
navGraph = NavGraphs.root,
|
||||
navController = navController
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BottomBar(navController: NavHostController) {
|
||||
val isManager = Natives.becomeManager(ksuApp.packageName)
|
||||
val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable()
|
||||
NavigationBar(tonalElevation = 8.dp) {
|
||||
BottomBarDestination.entries.forEach { destination ->
|
||||
if (!fullFeatured && destination.rootRequired) return@forEach
|
||||
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
|
||||
NavigationBarItem(
|
||||
selected = isCurrentDestOnBackStack,
|
||||
onClick = {
|
||||
if (isCurrentDestOnBackStack) {
|
||||
navController.popBackStack(destination.direction, false)
|
||||
}
|
||||
|
||||
navController.navigate(destination.direction.route) {
|
||||
popUpTo(NavGraphs.root.route) {
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
if (isCurrentDestOnBackStack) {
|
||||
Icon(destination.iconSelected, stringResource(destination.label))
|
||||
} else {
|
||||
Icon(destination.iconNotSelected, stringResource(destination.label))
|
||||
}
|
||||
},
|
||||
label = { Text(stringResource(destination.label)) },
|
||||
alwaysShowLabel = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
package me.weishu.kernelsu.ui.component
|
||||
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.widget.TextView
|
||||
import androidx.compose.foundation.Image
|
||||
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.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
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.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.text.HtmlCompat
|
||||
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
||||
import me.weishu.kernelsu.BuildConfig
|
||||
import me.weishu.kernelsu.R
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun AboutCard() {
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(24.dp)
|
||||
) {
|
||||
AboutCardContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AboutDialog(dismiss: () -> Unit) {
|
||||
Dialog(onDismissRequest = { dismiss() }) {
|
||||
AboutCard()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AboutCardContent() {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
val drawable = ResourcesCompat.getDrawable(
|
||||
LocalContext.current.resources,
|
||||
R.mipmap.ic_launcher,
|
||||
LocalContext.current.theme
|
||||
)
|
||||
|
||||
Row {
|
||||
Image(
|
||||
painter = rememberDrawablePainter(drawable),
|
||||
contentDescription = "icon",
|
||||
modifier = Modifier.size(40.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Column {
|
||||
|
||||
Text(
|
||||
stringResource(id = R.string.app_name),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
Text(
|
||||
BuildConfig.VERSION_NAME,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
HtmlText(
|
||||
html = stringResource(
|
||||
id = R.string.about_source_code,
|
||||
"<b><a href=\"https://github.com/tiann/KernelSU\">GitHub</a></b>",
|
||||
"<b><a href=\"https://t.me/KernelSU\">Telegram</a></b>"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HtmlText(html: String, modifier: Modifier = Modifier) {
|
||||
val contentColor = LocalContentColor.current
|
||||
AndroidView(
|
||||
modifier = modifier,
|
||||
factory = { context ->
|
||||
TextView(context).also {
|
||||
it.movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
},
|
||||
update = {
|
||||
it.text = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT)
|
||||
it.setTextColor(contentColor.toArgb())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,450 @@
|
|||
package me.weishu.kernelsu.ui.component
|
||||
|
||||
import android.graphics.text.LineBreaker
|
||||
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.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
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.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 io.noties.markwon.Markwon
|
||||
import io.noties.markwon.utils.NoCopySpannableFactory
|
||||
import kotlinx.coroutines.*
|
||||
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.parcelize.Parcelize
|
||||
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 {
|
||||
object Confirmed : ConfirmResult
|
||||
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)
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.size(100.dp), shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConfirmDialog(visuals: ConfirmDialogVisuals, confirm: () -> Unit, dismiss: () -> Unit) {
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
dismiss()
|
||||
},
|
||||
title = {
|
||||
Text(text = visuals.title)
|
||||
},
|
||||
text = {
|
||||
if (visuals.isMarkdown) {
|
||||
MarkdownContent(content = visuals.content)
|
||||
} else {
|
||||
Text(text = visuals.content)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = confirm) {
|
||||
Text(text = visuals.confirm ?: stringResource(id = android.R.string.ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = dismiss) {
|
||||
Text(text = visuals.dismiss ?: stringResource(id = android.R.string.cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MarkdownContent(content: String) {
|
||||
val contentColor = LocalContentColor.current
|
||||
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
TextView(context).apply {
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
setSpannableFactory(NoCopySpannableFactory.getInstance())
|
||||
breakStrategy = LineBreaker.BREAK_STRATEGY_SIMPLE
|
||||
hyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
update = {
|
||||
Markwon.create(it.context).setMarkdown(it, content)
|
||||
it.setTextColor(contentColor.toArgb())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package me.weishu.kernelsu.ui.component
|
||||
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.input.key.KeyEvent
|
||||
import androidx.compose.ui.input.key.onKeyEvent
|
||||
|
||||
@Composable
|
||||
fun KeyEventBlocker(predicate: (KeyEvent) -> Boolean) {
|
||||
val requester = remember { FocusRequester() }
|
||||
Box(
|
||||
Modifier
|
||||
.onKeyEvent {
|
||||
predicate(it)
|
||||
}
|
||||
.focusRequester(requester)
|
||||
.focusable()
|
||||
)
|
||||
LaunchedEffect(Unit) {
|
||||
requester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
package me.weishu.kernelsu.ui.component
|
||||
|
||||
import android.util.Log
|
||||
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.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.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() }
|
||||
}
|
||||
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,
|
||||
trailingIcon = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
onSearch = false
|
||||
keyboardController?.hide()
|
||||
onClearClick()
|
||||
},
|
||||
content = { Icon(Icons.Filled.Close, null) }
|
||||
)
|
||||
},
|
||||
maxLines = 1,
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = {
|
||||
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) }
|
||||
)
|
||||
}
|
||||
|
||||
if (dropdownContent != null) {
|
||||
dropdownContent()
|
||||
}
|
||||
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun SearchAppBarPreview() {
|
||||
var searchText by remember { mutableStateOf("") }
|
||||
SearchAppBar(
|
||||
title = { Text("Search text") },
|
||||
searchText = searchText,
|
||||
onSearchTextChange = { searchText = it },
|
||||
onClearClick = { searchText = "" }
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package me.weishu.kernelsu.ui.component
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
|
||||
@Composable
|
||||
fun SwitchItem(
|
||||
icon: ImageVector? = null,
|
||||
title: String,
|
||||
summary: String? = null,
|
||||
checked: Boolean,
|
||||
enabled: Boolean = true,
|
||||
onCheckedChange: (Boolean) -> Unit
|
||||
) {
|
||||
ListItem(
|
||||
modifier = Modifier.clickable {
|
||||
onCheckedChange.invoke(!checked)
|
||||
},
|
||||
headlineContent = {
|
||||
Text(title)
|
||||
},
|
||||
leadingContent = icon?.let {
|
||||
{ Icon(icon, title) }
|
||||
},
|
||||
trailingContent = {
|
||||
Switch(checked = checked, enabled = enabled, onCheckedChange = onCheckedChange)
|
||||
},
|
||||
supportingContent = {
|
||||
if (summary != null) {
|
||||
Text(summary)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RadioItem(
|
||||
title: String,
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(title)
|
||||
},
|
||||
leadingContent = {
|
||||
RadioButton(selected = selected, onClick = onClick)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package me.weishu.kernelsu.ui.component.profile
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
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.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import me.weishu.kernelsu.Natives
|
||||
import me.weishu.kernelsu.R
|
||||
import me.weishu.kernelsu.ui.component.SwitchItem
|
||||
|
||||
@Composable
|
||||
fun AppProfileConfig(
|
||||
modifier: Modifier = Modifier,
|
||||
fixedName: Boolean,
|
||||
enabled: Boolean,
|
||||
profile: Natives.Profile,
|
||||
onProfileChange: (Natives.Profile) -> Unit,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
if (!fixedName) {
|
||||
OutlinedTextField(
|
||||
label = { Text(stringResource(R.string.profile_name)) },
|
||||
value = profile.name,
|
||||
onValueChange = { onProfileChange(profile.copy(name = it)) }
|
||||
)
|
||||
}
|
||||
|
||||
SwitchItem(
|
||||
title = stringResource(R.string.profile_umount_modules),
|
||||
summary = stringResource(R.string.profile_umount_modules_summary),
|
||||
checked = if (enabled) {
|
||||
profile.umountModules
|
||||
} else {
|
||||
Natives.isDefaultUmountModules()
|
||||
},
|
||||
enabled = enabled,
|
||||
onCheckedChange = {
|
||||
onProfileChange(
|
||||
profile.copy(
|
||||
umountModules = it,
|
||||
nonRootUseDefault = false
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun AppProfileConfigPreview() {
|
||||
var profile by remember { mutableStateOf(Natives.Profile("")) }
|
||||
AppProfileConfig(fixedName = true, enabled = false, profile = profile) {
|
||||
profile = it
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,471 @@
|
|||
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
package me.weishu.kernelsu.ui.component.profile
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||
import androidx.compose.material.icons.filled.ArrowDropUp
|
||||
import androidx.compose.material3.AssistChip
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.Text
|
||||
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.Modifier
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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
|
||||
import androidx.core.text.isDigitsOnly
|
||||
import com.maxkeppeker.sheets.core.models.base.Header
|
||||
import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState
|
||||
import com.maxkeppeler.sheets.input.InputDialog
|
||||
import com.maxkeppeler.sheets.input.models.InputHeader
|
||||
import com.maxkeppeler.sheets.input.models.InputSelection
|
||||
import com.maxkeppeler.sheets.input.models.InputTextField
|
||||
import com.maxkeppeler.sheets.input.models.InputTextFieldType
|
||||
import com.maxkeppeler.sheets.input.models.ValidationResult
|
||||
import com.maxkeppeler.sheets.list.ListDialog
|
||||
import com.maxkeppeler.sheets.list.models.ListOption
|
||||
import com.maxkeppeler.sheets.list.models.ListSelection
|
||||
import me.weishu.kernelsu.Natives
|
||||
import me.weishu.kernelsu.R
|
||||
import me.weishu.kernelsu.profile.Capabilities
|
||||
import me.weishu.kernelsu.profile.Groups
|
||||
import me.weishu.kernelsu.ui.component.rememberCustomDialog
|
||||
import me.weishu.kernelsu.ui.util.isSepolicyValid
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun RootProfileConfig(
|
||||
modifier: Modifier = Modifier,
|
||||
fixedName: Boolean,
|
||||
profile: Natives.Profile,
|
||||
onProfileChange: (Natives.Profile) -> Unit,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
if (!fixedName) {
|
||||
OutlinedTextField(
|
||||
label = { Text(stringResource(R.string.profile_name)) },
|
||||
value = profile.name,
|
||||
onValueChange = { onProfileChange(profile.copy(name = it)) }
|
||||
)
|
||||
}
|
||||
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
val currentNamespace = when (profile.namespace) {
|
||||
Natives.Profile.Namespace.INHERITED.ordinal -> stringResource(R.string.profile_namespace_inherited)
|
||||
Natives.Profile.Namespace.GLOBAL.ordinal -> stringResource(R.string.profile_namespace_global)
|
||||
Natives.Profile.Namespace.INDIVIDUAL.ordinal -> stringResource(R.string.profile_namespace_individual)
|
||||
else -> stringResource(R.string.profile_namespace_inherited)
|
||||
}
|
||||
ListItem(headlineContent = {
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = !expanded }
|
||||
) {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.menuAnchor()
|
||||
.fillMaxWidth(),
|
||||
readOnly = true,
|
||||
label = { Text(stringResource(R.string.profile_namespace)) },
|
||||
value = currentNamespace,
|
||||
onValueChange = {},
|
||||
trailingIcon = {
|
||||
if (expanded) Icon(Icons.Filled.ArrowDropUp, null)
|
||||
else Icon(Icons.Filled.ArrowDropDown, null)
|
||||
},
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.profile_namespace_inherited)) },
|
||||
onClick = {
|
||||
onProfileChange(profile.copy(namespace = Natives.Profile.Namespace.INHERITED.ordinal))
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.profile_namespace_global)) },
|
||||
onClick = {
|
||||
onProfileChange(profile.copy(namespace = Natives.Profile.Namespace.GLOBAL.ordinal))
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.profile_namespace_individual)) },
|
||||
onClick = {
|
||||
onProfileChange(profile.copy(namespace = Natives.Profile.Namespace.INDIVIDUAL.ordinal))
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
UidPanel(uid = profile.uid, label = "uid", onUidChange = {
|
||||
onProfileChange(
|
||||
profile.copy(
|
||||
uid = it,
|
||||
rootUseDefault = false
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
UidPanel(uid = profile.gid, label = "gid", onUidChange = {
|
||||
onProfileChange(
|
||||
profile.copy(
|
||||
gid = it,
|
||||
rootUseDefault = false
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
val selectedGroups = profile.groups.ifEmpty { listOf(0) }.let { e ->
|
||||
e.mapNotNull { g ->
|
||||
Groups.entries.find { it.gid == g }
|
||||
}
|
||||
}
|
||||
GroupsPanel(selectedGroups) {
|
||||
onProfileChange(
|
||||
profile.copy(
|
||||
groups = it.map { group -> group.gid }.ifEmpty { listOf(0) },
|
||||
rootUseDefault = false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val selectedCaps = profile.capabilities.mapNotNull { e ->
|
||||
Capabilities.entries.find { it.cap == e }
|
||||
}
|
||||
|
||||
CapsPanel(selectedCaps) {
|
||||
onProfileChange(
|
||||
profile.copy(
|
||||
capabilities = it.map { cap -> cap.cap },
|
||||
rootUseDefault = false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
SELinuxPanel(profile = profile, onSELinuxChange = { domain, rules ->
|
||||
onProfileChange(
|
||||
profile.copy(
|
||||
context = domain,
|
||||
rules = rules,
|
||||
rootUseDefault = false
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun GroupsPanel(selected: List<Groups>, closeSelection: (selection: Set<Groups>) -> Unit) {
|
||||
val selectGroupsDialog = rememberCustomDialog { dismiss: () -> Unit ->
|
||||
val groups = Groups.entries.toTypedArray().sortedWith(
|
||||
compareBy<Groups> { if (selected.contains(it)) 0 else 1 }
|
||||
.then(compareBy {
|
||||
when (it) {
|
||||
Groups.ROOT -> 0
|
||||
Groups.SYSTEM -> 1
|
||||
Groups.SHELL -> 2
|
||||
else -> Int.MAX_VALUE
|
||||
}
|
||||
})
|
||||
.then(compareBy { it.name })
|
||||
|
||||
)
|
||||
val options = groups.map { value ->
|
||||
ListOption(
|
||||
titleText = value.display,
|
||||
subtitleText = value.desc,
|
||||
selected = selected.contains(value),
|
||||
)
|
||||
}
|
||||
|
||||
val selection = HashSet(selected)
|
||||
ListDialog(
|
||||
state = rememberUseCaseState(visible = true, onFinishedRequest = {
|
||||
closeSelection(selection)
|
||||
}, onCloseRequest = {
|
||||
dismiss()
|
||||
}),
|
||||
header = Header.Default(
|
||||
title = stringResource(R.string.profile_groups),
|
||||
),
|
||||
selection = ListSelection.Multiple(
|
||||
showCheckBoxes = true,
|
||||
options = options,
|
||||
maxChoices = 32, // Kernel only supports 32 groups at most
|
||||
) { indecies, _ ->
|
||||
// Handle selection
|
||||
selection.clear()
|
||||
indecies.forEach { index ->
|
||||
val group = groups[index]
|
||||
selection.add(group)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
OutlinedCard(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
.clickable {
|
||||
selectGroupsDialog.show()
|
||||
}) {
|
||||
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(stringResource(R.string.profile_groups))
|
||||
FlowRow {
|
||||
selected.forEach { group ->
|
||||
AssistChip(
|
||||
modifier = Modifier.padding(3.dp),
|
||||
onClick = { /*TODO*/ },
|
||||
label = { Text(group.display) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun CapsPanel(
|
||||
selected: Collection<Capabilities>,
|
||||
closeSelection: (selection: Set<Capabilities>) -> Unit
|
||||
) {
|
||||
val selectCapabilitiesDialog = rememberCustomDialog { dismiss ->
|
||||
val caps = Capabilities.entries.toTypedArray().sortedWith(
|
||||
compareBy<Capabilities> { if (selected.contains(it)) 0 else 1 }
|
||||
.then(compareBy { it.name })
|
||||
)
|
||||
val options = caps.map { value ->
|
||||
ListOption(
|
||||
titleText = value.display,
|
||||
subtitleText = value.desc,
|
||||
selected = selected.contains(value),
|
||||
)
|
||||
}
|
||||
|
||||
val selection = HashSet(selected)
|
||||
ListDialog(
|
||||
state = rememberUseCaseState(visible = true, onFinishedRequest = {
|
||||
closeSelection(selection)
|
||||
}, onCloseRequest = {
|
||||
dismiss()
|
||||
}),
|
||||
header = Header.Default(
|
||||
title = stringResource(R.string.profile_capabilities),
|
||||
),
|
||||
selection = ListSelection.Multiple(
|
||||
showCheckBoxes = true,
|
||||
options = options
|
||||
) { indecies, _ ->
|
||||
// Handle selection
|
||||
selection.clear()
|
||||
indecies.forEach { index ->
|
||||
val group = caps[index]
|
||||
selection.add(group)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
OutlinedCard(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
.clickable {
|
||||
selectCapabilitiesDialog.show()
|
||||
}) {
|
||||
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(stringResource(R.string.profile_capabilities))
|
||||
FlowRow {
|
||||
selected.forEach { group ->
|
||||
AssistChip(
|
||||
modifier = Modifier.padding(3.dp),
|
||||
onClick = { /*TODO*/ },
|
||||
label = { Text(group.display) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UidPanel(uid: Int, label: String, onUidChange: (Int) -> Unit) {
|
||||
|
||||
ListItem(headlineContent = {
|
||||
var isError by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
var lastValidUid by remember {
|
||||
mutableStateOf(uid)
|
||||
}
|
||||
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
label = { Text(label) },
|
||||
value = uid.toString(),
|
||||
isError = isError,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Number,
|
||||
imeAction = ImeAction.Done
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = {
|
||||
keyboardController?.hide()
|
||||
}),
|
||||
onValueChange = {
|
||||
if (it.isEmpty()) {
|
||||
onUidChange(0)
|
||||
return@OutlinedTextField
|
||||
}
|
||||
val valid = isTextValidUid(it)
|
||||
|
||||
val targetUid = if (valid) it.toInt() else lastValidUid
|
||||
if (valid) {
|
||||
lastValidUid = it.toInt()
|
||||
}
|
||||
|
||||
onUidChange(targetUid)
|
||||
|
||||
isError = !valid
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SELinuxPanel(
|
||||
profile: Natives.Profile,
|
||||
onSELinuxChange: (domain: String, rules: String) -> Unit
|
||||
) {
|
||||
val editSELinuxDialog = rememberCustomDialog { dismiss ->
|
||||
var domain by remember { mutableStateOf(profile.context) }
|
||||
var rules by remember { mutableStateOf(profile.rules) }
|
||||
|
||||
val inputOptions = listOf(
|
||||
InputTextField(
|
||||
text = domain,
|
||||
header = InputHeader(
|
||||
title = stringResource(id = R.string.profile_selinux_domain),
|
||||
),
|
||||
type = InputTextFieldType.OUTLINED,
|
||||
required = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Ascii,
|
||||
imeAction = ImeAction.Next
|
||||
),
|
||||
resultListener = {
|
||||
domain = it ?: ""
|
||||
},
|
||||
validationListener = { value ->
|
||||
// value can be a-zA-Z0-9_
|
||||
val regex = Regex("^[a-z_]+:[a-z0-9_]+:[a-z0-9_]+(:[a-z0-9_]+)?$")
|
||||
if (value?.matches(regex) == true) ValidationResult.Valid
|
||||
else ValidationResult.Invalid("Domain must be in the format of \"user:role:type:level\"")
|
||||
}
|
||||
),
|
||||
InputTextField(
|
||||
text = rules,
|
||||
header = InputHeader(
|
||||
title = stringResource(id = R.string.profile_selinux_rules),
|
||||
),
|
||||
type = InputTextFieldType.OUTLINED,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Ascii,
|
||||
),
|
||||
singleLine = false,
|
||||
resultListener = {
|
||||
rules = it ?: ""
|
||||
},
|
||||
validationListener = { value ->
|
||||
if (isSepolicyValid(value)) ValidationResult.Valid
|
||||
else ValidationResult.Invalid("SELinux rules is invalid!")
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
InputDialog(
|
||||
state = rememberUseCaseState(visible = true,
|
||||
onFinishedRequest = {
|
||||
onSELinuxChange(domain, rules)
|
||||
},
|
||||
onCloseRequest = {
|
||||
dismiss()
|
||||
}),
|
||||
header = Header.Default(
|
||||
title = stringResource(R.string.profile_selinux_context),
|
||||
),
|
||||
selection = InputSelection(
|
||||
input = inputOptions,
|
||||
onPositiveClick = { result ->
|
||||
// Handle selection
|
||||
},
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
ListItem(headlineContent = {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
editSELinuxDialog.show()
|
||||
},
|
||||
enabled = false,
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
disabledTextColor = MaterialTheme.colorScheme.onSurface,
|
||||
disabledBorderColor = MaterialTheme.colorScheme.outline,
|
||||
disabledPlaceholderColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
disabledLabelColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
),
|
||||
label = { Text(text = stringResource(R.string.profile_selinux_context)) },
|
||||
value = profile.context,
|
||||
onValueChange = { },
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun RootProfileConfigPreview() {
|
||||
var profile by remember { mutableStateOf(Natives.Profile("")) }
|
||||
RootProfileConfig(fixedName = true, profile = profile) {
|
||||
profile = it
|
||||
}
|
||||
}
|
||||
|
||||
private fun isTextValidUid(text: String): Boolean {
|
||||
return text.isNotEmpty() && text.isDigitsOnly() && text.toInt() >= 0 && text.toInt() <= Int.MAX_VALUE
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
package me.weishu.kernelsu.ui.component.profile
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ReadMore
|
||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||
import androidx.compose.material.icons.filled.ArrowDropUp
|
||||
import androidx.compose.material.icons.filled.Create
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import me.weishu.kernelsu.Natives
|
||||
import me.weishu.kernelsu.R
|
||||
import me.weishu.kernelsu.ui.util.listAppProfileTemplates
|
||||
import me.weishu.kernelsu.ui.util.setSepolicy
|
||||
import me.weishu.kernelsu.ui.viewmodel.getTemplateInfoById
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
* @date 2023/10/21.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TemplateConfig(
|
||||
profile: Natives.Profile,
|
||||
onViewTemplate: (id: String) -> Unit = {},
|
||||
onManageTemplate: () -> Unit = {},
|
||||
onProfileChange: (Natives.Profile) -> Unit
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
var template by rememberSaveable {
|
||||
mutableStateOf(profile.rootTemplate ?: "")
|
||||
}
|
||||
val profileTemplates = listAppProfileTemplates()
|
||||
val noTemplates = profileTemplates.isEmpty()
|
||||
|
||||
ListItem(headlineContent = {
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = it },
|
||||
) {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.menuAnchor()
|
||||
.fillMaxWidth(),
|
||||
readOnly = true,
|
||||
label = { Text(stringResource(R.string.profile_template)) },
|
||||
value = template.ifEmpty { "None" },
|
||||
onValueChange = {},
|
||||
trailingIcon = {
|
||||
if (noTemplates) {
|
||||
IconButton(
|
||||
onClick = onManageTemplate
|
||||
) {
|
||||
Icon(Icons.Filled.Create, null)
|
||||
}
|
||||
} else if (expanded) Icon(Icons.Filled.ArrowDropUp, null)
|
||||
else Icon(Icons.Filled.ArrowDropDown, null)
|
||||
},
|
||||
)
|
||||
if (profileTemplates.isEmpty()) {
|
||||
return@ExposedDropdownMenuBox
|
||||
}
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false }
|
||||
) {
|
||||
profileTemplates.forEach { tid ->
|
||||
val templateInfo =
|
||||
getTemplateInfoById(tid) ?: return@forEach
|
||||
DropdownMenuItem(
|
||||
text = { Text(tid) },
|
||||
onClick = {
|
||||
template = tid
|
||||
if (setSepolicy(tid, templateInfo.rules.joinToString("\n"))) {
|
||||
onProfileChange(
|
||||
profile.copy(
|
||||
rootTemplate = tid,
|
||||
rootUseDefault = false,
|
||||
uid = templateInfo.uid,
|
||||
gid = templateInfo.gid,
|
||||
groups = templateInfo.groups,
|
||||
capabilities = templateInfo.capabilities,
|
||||
context = templateInfo.context,
|
||||
namespace = templateInfo.namespace,
|
||||
)
|
||||
)
|
||||
}
|
||||
expanded = false
|
||||
},
|
||||
trailingIcon = {
|
||||
IconButton(onClick = {
|
||||
onViewTemplate(tid)
|
||||
}) {
|
||||
Icon(Icons.AutoMirrored.Filled.ReadMore, null)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,362 @@
|
|||
package me.weishu.kernelsu.ui.screen
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.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.AccountCircle
|
||||
import androidx.compose.material.icons.filled.Android
|
||||
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.FilterChip
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
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.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import kotlinx.coroutines.launch
|
||||
import me.weishu.kernelsu.Natives
|
||||
import me.weishu.kernelsu.R
|
||||
import me.weishu.kernelsu.ui.component.SwitchItem
|
||||
import me.weishu.kernelsu.ui.component.profile.AppProfileConfig
|
||||
import me.weishu.kernelsu.ui.component.profile.RootProfileConfig
|
||||
import me.weishu.kernelsu.ui.component.profile.TemplateConfig
|
||||
import me.weishu.kernelsu.ui.screen.destinations.AppProfileTemplateScreenDestination
|
||||
import me.weishu.kernelsu.ui.screen.destinations.TemplateEditorScreenDestination
|
||||
import me.weishu.kernelsu.ui.util.LocalSnackbarHost
|
||||
import me.weishu.kernelsu.ui.util.forceStopApp
|
||||
import me.weishu.kernelsu.ui.util.getSepolicy
|
||||
import me.weishu.kernelsu.ui.util.launchApp
|
||||
import me.weishu.kernelsu.ui.util.restartApp
|
||||
import me.weishu.kernelsu.ui.util.setSepolicy
|
||||
import me.weishu.kernelsu.ui.viewmodel.SuperUserViewModel
|
||||
import me.weishu.kernelsu.ui.viewmodel.getTemplateInfoById
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
* @date 2023/5/16.
|
||||
*/
|
||||
@Destination
|
||||
@Composable
|
||||
fun AppProfileScreen(
|
||||
navigator: DestinationsNavigator,
|
||||
appInfo: SuperUserViewModel.AppInfo,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val snackbarHost = LocalSnackbarHost.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val failToUpdateAppProfile =
|
||||
stringResource(R.string.failed_to_update_app_profile).format(appInfo.label)
|
||||
val failToUpdateSepolicy =
|
||||
stringResource(R.string.failed_to_update_sepolicy).format(appInfo.label)
|
||||
|
||||
val packageName = appInfo.packageName
|
||||
val initialProfile = Natives.getAppProfile(packageName, appInfo.uid)
|
||||
if (initialProfile.allowSu) {
|
||||
initialProfile.rules = getSepolicy(packageName)
|
||||
}
|
||||
var profile by rememberSaveable {
|
||||
mutableStateOf(initialProfile)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = { TopBar { navigator.popBackStack() } },
|
||||
) { paddingValues ->
|
||||
AppProfileInner(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
packageName = appInfo.packageName,
|
||||
appLabel = appInfo.label,
|
||||
appIcon = {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(context).data(appInfo.packageInfo).crossfade(true)
|
||||
.build(),
|
||||
contentDescription = appInfo.label,
|
||||
modifier = Modifier
|
||||
.padding(4.dp)
|
||||
.width(48.dp)
|
||||
.height(48.dp)
|
||||
)
|
||||
},
|
||||
profile = profile,
|
||||
onViewTemplate = {
|
||||
getTemplateInfoById(it)?.let { info ->
|
||||
navigator.navigate(TemplateEditorScreenDestination(info))
|
||||
}
|
||||
},
|
||||
onManageTemplate = {
|
||||
navigator.navigate(AppProfileTemplateScreenDestination())
|
||||
},
|
||||
onProfileChange = {
|
||||
scope.launch {
|
||||
if (it.allowSu && !it.rootUseDefault && it.rules.isNotEmpty()) {
|
||||
if (!setSepolicy(profile.name, it.rules)) {
|
||||
snackbarHost.showSnackbar(failToUpdateSepolicy)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
if (!Natives.setAppProfile(it)) {
|
||||
snackbarHost.showSnackbar(failToUpdateAppProfile.format(appInfo.uid))
|
||||
} else {
|
||||
profile = it
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AppProfileInner(
|
||||
modifier: Modifier = Modifier,
|
||||
packageName: String,
|
||||
appLabel: String,
|
||||
appIcon: @Composable () -> Unit,
|
||||
profile: Natives.Profile,
|
||||
onViewTemplate: (id: String) -> Unit = {},
|
||||
onManageTemplate: () -> Unit = {},
|
||||
onProfileChange: (Natives.Profile) -> Unit,
|
||||
) {
|
||||
val isRootGranted = profile.allowSu
|
||||
|
||||
Column(modifier = modifier) {
|
||||
AppMenuBox(packageName) {
|
||||
ListItem(
|
||||
headlineContent = { Text(appLabel) },
|
||||
supportingContent = { Text(packageName) },
|
||||
leadingContent = appIcon,
|
||||
)
|
||||
}
|
||||
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.Security,
|
||||
title = stringResource(id = R.string.superuser),
|
||||
checked = isRootGranted,
|
||||
onCheckedChange = { onProfileChange(profile.copy(allowSu = it)) },
|
||||
)
|
||||
|
||||
Crossfade(targetState = isRootGranted, label = "") { current ->
|
||||
Column {
|
||||
if (current) {
|
||||
val initialMode = if (profile.rootUseDefault) {
|
||||
Mode.Default
|
||||
} else if (profile.rootTemplate != null) {
|
||||
Mode.Template
|
||||
} else {
|
||||
Mode.Custom
|
||||
}
|
||||
var mode by rememberSaveable {
|
||||
mutableStateOf(initialMode)
|
||||
}
|
||||
ProfileBox(mode, true) {
|
||||
// template mode shouldn't change profile here!
|
||||
if (it == Mode.Default || it == Mode.Custom) {
|
||||
onProfileChange(profile.copy(rootUseDefault = it == Mode.Default))
|
||||
}
|
||||
mode = it
|
||||
}
|
||||
Crossfade(targetState = mode, label = "") { currentMode ->
|
||||
if (currentMode == Mode.Template) {
|
||||
TemplateConfig(
|
||||
profile = profile,
|
||||
onViewTemplate = onViewTemplate,
|
||||
onManageTemplate = onManageTemplate,
|
||||
onProfileChange = onProfileChange
|
||||
)
|
||||
} else if (mode == Mode.Custom) {
|
||||
RootProfileConfig(
|
||||
fixedName = true,
|
||||
profile = profile,
|
||||
onProfileChange = onProfileChange
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val mode = if (profile.nonRootUseDefault) Mode.Default else Mode.Custom
|
||||
ProfileBox(mode, false) {
|
||||
onProfileChange(profile.copy(nonRootUseDefault = (it == Mode.Default)))
|
||||
}
|
||||
Crossfade(targetState = mode, label = "") { currentMode ->
|
||||
val modifyEnabled = currentMode == Mode.Custom
|
||||
AppProfileConfig(
|
||||
fixedName = true,
|
||||
profile = profile,
|
||||
enabled = modifyEnabled,
|
||||
onProfileChange = onProfileChange
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum class Mode(@StringRes private val res: Int) {
|
||||
Default(R.string.profile_default), Template(R.string.profile_template), Custom(R.string.profile_custom);
|
||||
|
||||
val text: String
|
||||
@Composable get() = stringResource(res)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar(onBack: () -> Unit) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(stringResource(R.string.profile))
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onBack
|
||||
) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProfileBox(
|
||||
mode: Mode,
|
||||
hasTemplate: Boolean,
|
||||
onModeChange: (Mode) -> Unit,
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.profile)) },
|
||||
supportingContent = { Text(mode.text) },
|
||||
leadingContent = { Icon(Icons.Filled.AccountCircle, null) },
|
||||
)
|
||||
HorizontalDivider(thickness = Dp.Hairline)
|
||||
ListItem(headlineContent = {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
FilterChip(
|
||||
selected = mode == Mode.Default,
|
||||
label = { Text(stringResource(R.string.profile_default)) },
|
||||
onClick = { onModeChange(Mode.Default) },
|
||||
)
|
||||
if (hasTemplate) {
|
||||
FilterChip(
|
||||
selected = mode == Mode.Template,
|
||||
label = { Text(stringResource(R.string.profile_template)) },
|
||||
onClick = { onModeChange(Mode.Template) },
|
||||
)
|
||||
}
|
||||
FilterChip(
|
||||
selected = mode == Mode.Custom,
|
||||
label = { Text(stringResource(R.string.profile_custom)) },
|
||||
onClick = { onModeChange(Mode.Custom) },
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AppMenuBox(packageName: String, content: @Composable () -> Unit) {
|
||||
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
var touchPoint: Offset by remember { mutableStateOf(Offset.Zero) }
|
||||
val density = LocalDensity.current
|
||||
|
||||
BoxWithConstraints(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures {
|
||||
touchPoint = it
|
||||
expanded = true
|
||||
}
|
||||
}) {
|
||||
|
||||
content()
|
||||
|
||||
val (offsetX, offsetY) = with(density) {
|
||||
(touchPoint.x.toDp()) to (touchPoint.y.toDp())
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
offset = DpOffset(offsetX, -offsetY),
|
||||
onDismissRequest = {
|
||||
expanded = false
|
||||
},
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(id = R.string.launch_app)) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
launchApp(packageName)
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(id = R.string.force_stop_app)) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
forceStopApp(packageName)
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(id = R.string.restart_app)) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
restartApp(packageName)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun AppProfilePreview() {
|
||||
var profile by remember { mutableStateOf(Natives.Profile("")) }
|
||||
AppProfileInner(
|
||||
packageName = "icu.nullptr.test",
|
||||
appLabel = "Test",
|
||||
appIcon = { Icon(Icons.Filled.Android, null) },
|
||||
profile = profile,
|
||||
onProfileChange = {
|
||||
profile = it
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package me.weishu.kernelsu.ui.screen
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import com.ramcosta.composedestinations.spec.DirectionDestinationSpec
|
||||
import me.weishu.kernelsu.R
|
||||
import me.weishu.kernelsu.ui.screen.destinations.HomeScreenDestination
|
||||
import me.weishu.kernelsu.ui.screen.destinations.SuperUserScreenDestination
|
||||
import me.weishu.kernelsu.ui.screen.destinations.ModuleScreenDestination
|
||||
|
||||
enum class BottomBarDestination(
|
||||
val direction: DirectionDestinationSpec,
|
||||
@StringRes val label: Int,
|
||||
val iconSelected: ImageVector,
|
||||
val iconNotSelected: ImageVector,
|
||||
val rootRequired: Boolean,
|
||||
) {
|
||||
Home(HomeScreenDestination, R.string.home, Icons.Filled.Home, Icons.Outlined.Home, false),
|
||||
SuperUser(SuperUserScreenDestination, R.string.superuser, Icons.Filled.Security, Icons.Outlined.Security, true),
|
||||
Module(ModuleScreenDestination, R.string.module, Icons.Filled.Apps, Icons.Outlined.Apps, true)
|
||||
}
|
||||
|
|
@ -0,0 +1,238 @@
|
|||
package me.weishu.kernelsu.ui.screen
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import android.os.Parcelable
|
||||
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.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.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import me.weishu.kernelsu.R
|
||||
import me.weishu.kernelsu.ui.component.KeyEventBlocker
|
||||
import me.weishu.kernelsu.ui.util.LkmSelection
|
||||
import me.weishu.kernelsu.ui.util.LocalSnackbarHost
|
||||
import me.weishu.kernelsu.ui.util.flashModule
|
||||
import me.weishu.kernelsu.ui.util.installBoot
|
||||
import me.weishu.kernelsu.ui.util.reboot
|
||||
import me.weishu.kernelsu.ui.util.restoreBoot
|
||||
import me.weishu.kernelsu.ui.util.uninstallPermanently
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
enum class FlashingStatus {
|
||||
FLASHING,
|
||||
SUCCESS,
|
||||
FAILED
|
||||
}
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
* @date 2023/1/1.
|
||||
*/
|
||||
@Composable
|
||||
@Destination
|
||||
fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
||||
|
||||
var text by rememberSaveable { mutableStateOf("") }
|
||||
val logContent = rememberSaveable { StringBuilder() }
|
||||
var showFloatAction by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val snackBarHost = LocalSnackbarHost.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val scrollState = rememberScrollState()
|
||||
var flashing by rememberSaveable {
|
||||
mutableStateOf(FlashingStatus.FLASHING)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (text.isNotEmpty()) {
|
||||
return@LaunchedEffect
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
flashIt(flashIt, onFinish = { showReboot, code ->
|
||||
if (code != 0) {
|
||||
text += "Error: exit code = $code.\nPlease save and check the log.\n"
|
||||
}
|
||||
if (showReboot) {
|
||||
text += "\n\n\n"
|
||||
showFloatAction = true
|
||||
}
|
||||
flashing = if (code == 0) FlashingStatus.SUCCESS else FlashingStatus.FAILED
|
||||
}, onStdout = {
|
||||
text += "$it\n"
|
||||
logContent.append(it).append("\n")
|
||||
}, onStderr = {
|
||||
logContent.append(it).append("\n")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBar(
|
||||
flashing,
|
||||
onBack = {
|
||||
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),
|
||||
"KernelSU_install_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) },
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
) { 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
sealed class FlashIt : Parcelable {
|
||||
data class FlashBoot(val boot: Uri? = null, val lkm: LkmSelection, val ota: Boolean) :
|
||||
FlashIt()
|
||||
|
||||
data class FlashModule(val uri: Uri) : FlashIt()
|
||||
|
||||
data object FlashRestore : FlashIt()
|
||||
|
||||
data object FlashUninstall : FlashIt()
|
||||
}
|
||||
|
||||
fun flashIt(
|
||||
flashIt: FlashIt, onFinish: (Boolean, Int) -> Unit,
|
||||
onStdout: (String) -> Unit,
|
||||
onStderr: (String) -> Unit
|
||||
) {
|
||||
when (flashIt) {
|
||||
is FlashIt.FlashBoot -> installBoot(
|
||||
flashIt.boot,
|
||||
flashIt.lkm,
|
||||
flashIt.ota,
|
||||
onFinish,
|
||||
onStdout,
|
||||
onStderr
|
||||
)
|
||||
|
||||
is FlashIt.FlashModule -> flashModule(flashIt.uri, onFinish, onStdout, onStderr)
|
||||
|
||||
FlashIt.FlashRestore -> restoreBoot(onFinish, onStdout, onStderr)
|
||||
|
||||
FlashIt.FlashUninstall -> uninstallPermanently(onFinish, onStdout, onStderr)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar(status: FlashingStatus, onBack: () -> Unit = {}, onSave: () -> Unit = {}) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
stringResource(
|
||||
when (status) {
|
||||
FlashingStatus.FLASHING -> R.string.flashing
|
||||
FlashingStatus.SUCCESS -> R.string.flash_success
|
||||
FlashingStatus.FAILED -> R.string.flash_failed
|
||||
}
|
||||
)
|
||||
)
|
||||
},
|
||||
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(EmptyDestinationsNavigator)
|
||||
}
|
||||
|
|
@ -0,0 +1,442 @@
|
|||
package me.weishu.kernelsu.ui.screen
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.system.Os
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Archive
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.outlined.Block
|
||||
import androidx.compose.material.icons.outlined.CheckCircle
|
||||
import androidx.compose.material.icons.outlined.Warning
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
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.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootNavGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.weishu.kernelsu.*
|
||||
import me.weishu.kernelsu.R
|
||||
import me.weishu.kernelsu.ui.component.rememberConfirmDialog
|
||||
import me.weishu.kernelsu.ui.screen.destinations.InstallScreenDestination
|
||||
import me.weishu.kernelsu.ui.screen.destinations.SettingScreenDestination
|
||||
import me.weishu.kernelsu.ui.util.*
|
||||
import me.weishu.kernelsu.ui.util.module.LatestVersionInfo
|
||||
|
||||
@RootNavGraph(start = true)
|
||||
@Destination
|
||||
@Composable
|
||||
fun HomeScreen(navigator: DestinationsNavigator) {
|
||||
val kernelVersion = getKernelVersion()
|
||||
|
||||
Scaffold(topBar = {
|
||||
TopBar(kernelVersion, onSettingsClick = {
|
||||
navigator.navigate(SettingScreenDestination)
|
||||
}, onInstallClick = {
|
||||
navigator.navigate(InstallScreenDestination)
|
||||
})
|
||||
}) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.padding(horizontal = 16.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
val isManager = Natives.becomeManager(ksuApp.packageName)
|
||||
SideEffect {
|
||||
if (isManager) install()
|
||||
}
|
||||
val ksuVersion = if (isManager) Natives.version else null
|
||||
val lkmMode = ksuVersion?.let {
|
||||
if (it >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && kernelVersion.isGKI()) Natives.isLkmMode else null
|
||||
}
|
||||
|
||||
StatusCard(kernelVersion, ksuVersion, lkmMode) {
|
||||
navigator.navigate(InstallScreenDestination)
|
||||
}
|
||||
if (isManager && Natives.requireNewKernel()) {
|
||||
WarningCard(
|
||||
stringResource(id = R.string.require_kernel_version).format(
|
||||
ksuVersion, Natives.MINIMAL_SUPPORTED_KERNEL
|
||||
)
|
||||
)
|
||||
}
|
||||
if (ksuVersion != null && !rootAvailable()) {
|
||||
WarningCard(
|
||||
stringResource(id = R.string.grant_root_failed)
|
||||
)
|
||||
}
|
||||
val checkUpdate =
|
||||
LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
.getBoolean("check_update", true)
|
||||
if (checkUpdate) {
|
||||
UpdateCard()
|
||||
}
|
||||
InfoCard()
|
||||
DonateCard()
|
||||
LearnMoreCard()
|
||||
Spacer(Modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UpdateCard() {
|
||||
val context = LocalContext.current
|
||||
val latestVersionInfo = LatestVersionInfo()
|
||||
val newVersion by produceState(initialValue = latestVersionInfo) {
|
||||
value = withContext(Dispatchers.IO){
|
||||
checkNewVersion()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val currentVersionCode = getManagerVersion(context).second
|
||||
val newVersionCode = newVersion.versionCode
|
||||
val newVersionUrl = newVersion.downloadUrl
|
||||
val changelog = newVersion.changelog
|
||||
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val title = stringResource(id = R.string.module_changelog)
|
||||
val updateText = stringResource(id = R.string.module_update)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = newVersionCode > currentVersionCode,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = shrinkVertically() + fadeOut()
|
||||
) {
|
||||
val updateDialog = rememberConfirmDialog(onConfirm = { uriHandler.openUri(newVersionUrl) })
|
||||
WarningCard(
|
||||
message = stringResource(id = R.string.new_version_available).format(newVersionCode),
|
||||
MaterialTheme.colorScheme.outlineVariant
|
||||
) {
|
||||
if (changelog.isNotEmpty()) {
|
||||
updateDialog.showConfirm(
|
||||
title = title,
|
||||
content = changelog,
|
||||
markdown = true,
|
||||
confirm = updateText
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RebootDropdownItem(@StringRes id: Int, reason: String = "") {
|
||||
DropdownMenuItem(text = {
|
||||
Text(stringResource(id))
|
||||
}, onClick = {
|
||||
reboot(reason)
|
||||
})
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar(
|
||||
kernelVersion: KernelVersion,
|
||||
onInstallClick: () -> Unit,
|
||||
onSettingsClick: () -> Unit
|
||||
) {
|
||||
TopAppBar(title = { Text(stringResource(R.string.app_name)) }, actions = {
|
||||
if (kernelVersion.isGKI()) {
|
||||
IconButton(onClick = onInstallClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Archive,
|
||||
contentDescription = stringResource(id = R.string.install)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var showDropdown by remember { mutableStateOf(false) }
|
||||
IconButton(onClick = {
|
||||
showDropdown = true
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Refresh,
|
||||
contentDescription = stringResource(id = R.string.reboot)
|
||||
)
|
||||
|
||||
DropdownMenu(expanded = showDropdown, onDismissRequest = {
|
||||
showDropdown = false
|
||||
}) {
|
||||
|
||||
RebootDropdownItem(id = R.string.reboot)
|
||||
|
||||
val pm =
|
||||
LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager?
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && pm?.isRebootingUserspaceSupported == true) {
|
||||
RebootDropdownItem(id = R.string.reboot_userspace, reason = "userspace")
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(onClick = onSettingsClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Settings,
|
||||
contentDescription = stringResource(id = R.string.settings)
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatusCard(
|
||||
kernelVersion: KernelVersion,
|
||||
ksuVersion: Int?,
|
||||
lkmMode: Boolean?,
|
||||
onClickInstall: () -> Unit = {}
|
||||
) {
|
||||
ElevatedCard(
|
||||
colors = CardDefaults.elevatedCardColors(containerColor = run {
|
||||
if (ksuVersion != null) MaterialTheme.colorScheme.secondaryContainer
|
||||
else MaterialTheme.colorScheme.errorContainer
|
||||
})
|
||||
) {
|
||||
Row(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
if (kernelVersion.isGKI()) {
|
||||
onClickInstall()
|
||||
}
|
||||
}
|
||||
.padding(24.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
when {
|
||||
ksuVersion != null -> {
|
||||
val safeMode = when {
|
||||
Natives.isSafeMode -> " [${stringResource(id = R.string.safe_mode)}]"
|
||||
else -> ""
|
||||
}
|
||||
|
||||
val workingMode = when (lkmMode) {
|
||||
null -> ""
|
||||
true -> " <LKM>"
|
||||
else -> " <GKI>"
|
||||
}
|
||||
|
||||
val workingText =
|
||||
"${stringResource(id = R.string.home_working)}$workingMode$safeMode"
|
||||
|
||||
Icon(Icons.Outlined.CheckCircle, stringResource(R.string.home_working))
|
||||
Column(Modifier.padding(start = 20.dp)) {
|
||||
Text(
|
||||
text = workingText,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.home_working_version, ksuVersion),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.home_superuser_count, getSuperuserCount()
|
||||
), style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.home_module_count, getModuleCount()),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
kernelVersion.isGKI() -> {
|
||||
Icon(Icons.Outlined.Warning, stringResource(R.string.home_not_installed))
|
||||
Column(Modifier.padding(start = 20.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.home_not_installed),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.home_click_to_install),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
Icon(Icons.Outlined.Block, stringResource(R.string.home_unsupported))
|
||||
Column(Modifier.padding(start = 20.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.home_unsupported),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.home_unsupported_reason),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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 LearnMoreCard() {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val url = stringResource(R.string.home_learn_kernelsu_url)
|
||||
|
||||
ElevatedCard {
|
||||
|
||||
Row(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
uriHandler.openUri(url)
|
||||
}
|
||||
.padding(24.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.home_learn_kernelsu),
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.home_click_to_learn_kernelsu),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DonateCard() {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
ElevatedCard {
|
||||
|
||||
Row(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
uriHandler.openUri("https://patreon.com/weishu")
|
||||
}
|
||||
.padding(24.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.home_support_title),
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.home_support_content),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InfoCard() {
|
||||
val context = LocalContext.current
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
InfoCardItem(stringResource(R.string.home_kernel), uname.release)
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
val managerVersion = getManagerVersion(context)
|
||||
InfoCardItem(
|
||||
stringResource(R.string.home_manager_version),
|
||||
"${managerVersion.first} (${managerVersion.second})"
|
||||
)
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getManagerVersion(context: Context): Pair<String, Int> {
|
||||
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
|
||||
return Pair(packageInfo.versionName, packageInfo.versionCode)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun StatusCardPreview() {
|
||||
Column {
|
||||
StatusCard(KernelVersion(5, 10, 101), 1, null)
|
||||
StatusCard(KernelVersion(5, 10, 101), 20000, true)
|
||||
StatusCard(KernelVersion(5, 10, 101), null, true)
|
||||
StatusCard(KernelVersion(4, 10, 101), null, false)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun WarningCardPreview() {
|
||||
Column {
|
||||
WarningCard(message = "Warning message")
|
||||
WarningCard(
|
||||
message = "Warning message ",
|
||||
MaterialTheme.colorScheme.outlineVariant,
|
||||
onClick = {})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,311 @@
|
|||
package me.weishu.kernelsu.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.material.icons.filled.FileUpload
|
||||
import androidx.compose.material3.Button
|
||||
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.produceState
|
||||
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.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.maxkeppeker.sheets.core.models.base.Header
|
||||
import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState
|
||||
import com.maxkeppeler.sheets.list.ListDialog
|
||||
import com.maxkeppeler.sheets.list.models.ListOption
|
||||
import com.maxkeppeler.sheets.list.models.ListSelection
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
||||
import me.weishu.kernelsu.R
|
||||
import me.weishu.kernelsu.ui.component.DialogHandle
|
||||
import me.weishu.kernelsu.ui.component.rememberConfirmDialog
|
||||
import me.weishu.kernelsu.ui.component.rememberCustomDialog
|
||||
import me.weishu.kernelsu.ui.screen.destinations.FlashScreenDestination
|
||||
import me.weishu.kernelsu.ui.util.LkmSelection
|
||||
import me.weishu.kernelsu.ui.util.getCurrentKmi
|
||||
import me.weishu.kernelsu.ui.util.getSupportedKmis
|
||||
import me.weishu.kernelsu.ui.util.isAbDevice
|
||||
import me.weishu.kernelsu.ui.util.isInitBoot
|
||||
import me.weishu.kernelsu.ui.util.rootAvailable
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
* @date 2024/3/12.
|
||||
*/
|
||||
@Destination
|
||||
@Composable
|
||||
fun InstallScreen(navigator: DestinationsNavigator) {
|
||||
var installMethod by remember {
|
||||
mutableStateOf<InstallMethod?>(null)
|
||||
}
|
||||
|
||||
var lkmSelection by remember {
|
||||
mutableStateOf<LkmSelection>(LkmSelection.KmiNone)
|
||||
}
|
||||
|
||||
val onInstall = {
|
||||
installMethod?.let { method ->
|
||||
val flashIt = FlashIt.FlashBoot(
|
||||
boot = if (method is InstallMethod.SelectFile) method.uri else null,
|
||||
lkm = lkmSelection,
|
||||
ota = method is InstallMethod.DirectInstallToInactiveSlot
|
||||
)
|
||||
navigator.navigate(FlashScreenDestination(flashIt))
|
||||
}
|
||||
}
|
||||
|
||||
val currentKmi by produceState(initialValue = "") { value = getCurrentKmi() }
|
||||
|
||||
val selectKmiDialog = rememberSelectKmiDialog { kmi ->
|
||||
kmi?.let {
|
||||
lkmSelection = LkmSelection.KmiString(it)
|
||||
onInstall()
|
||||
}
|
||||
}
|
||||
|
||||
val onClickNext = {
|
||||
if (lkmSelection == LkmSelection.KmiNone && currentKmi.isBlank()) {
|
||||
// no lkm file selected and cannot get current kmi
|
||||
selectKmiDialog.show()
|
||||
} else {
|
||||
onInstall()
|
||||
}
|
||||
}
|
||||
|
||||
val selectLkmLauncher =
|
||||
rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == Activity.RESULT_OK) {
|
||||
it.data?.data?.let { uri ->
|
||||
lkmSelection = LkmSelection.LkmUri(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val onLkmUpload = {
|
||||
selectLkmLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
type = "application/octet-stream"
|
||||
})
|
||||
}
|
||||
|
||||
Scaffold(topBar = {
|
||||
TopBar(
|
||||
onBack = { navigator.popBackStack() }, onLkmUpload = onLkmUpload
|
||||
)
|
||||
}) {
|
||||
Column(modifier = Modifier.padding(it)) {
|
||||
SelectInstallMethod { method ->
|
||||
installMethod = method
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
(lkmSelection as? LkmSelection.LkmUri)?.let {
|
||||
Text(
|
||||
stringResource(
|
||||
id = R.string.selected_lkm,
|
||||
it.uri.lastPathSegment ?: "(file)"
|
||||
)
|
||||
)
|
||||
}
|
||||
Button(modifier = Modifier.fillMaxWidth(),
|
||||
enabled = installMethod != null,
|
||||
onClick = {
|
||||
onClickNext()
|
||||
}) {
|
||||
Text(
|
||||
stringResource(id = R.string.install_next),
|
||||
fontSize = MaterialTheme.typography.bodyMedium.fontSize
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class InstallMethod {
|
||||
data class SelectFile(
|
||||
val uri: Uri? = null,
|
||||
@StringRes override val label: Int = R.string.select_file,
|
||||
override val summary: String?
|
||||
) : InstallMethod()
|
||||
|
||||
data object DirectInstall : InstallMethod() {
|
||||
override val label: Int
|
||||
get() = R.string.direct_install
|
||||
}
|
||||
|
||||
data object DirectInstallToInactiveSlot : InstallMethod() {
|
||||
override val label: Int
|
||||
get() = R.string.install_inactive_slot
|
||||
}
|
||||
|
||||
abstract val label: Int
|
||||
open val summary: String? = null
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) {
|
||||
val rootAvailable = rootAvailable()
|
||||
val isAbDevice = isAbDevice()
|
||||
val selectFileTip = stringResource(
|
||||
id = R.string.select_file_tip, if (isInitBoot()) "init_boot" else "boot"
|
||||
)
|
||||
val radioOptions =
|
||||
mutableListOf<InstallMethod>(InstallMethod.SelectFile(summary = selectFileTip))
|
||||
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, summary = selectFileTip)
|
||||
selectedOption = option
|
||||
onSelected(option)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val confirmDialog = rememberConfirmDialog(onConfirm = {
|
||||
selectedOption = InstallMethod.DirectInstallToInactiveSlot
|
||||
onSelected(InstallMethod.DirectInstallToInactiveSlot)
|
||||
}, onDismiss = null)
|
||||
val dialogTitle = stringResource(id = android.R.string.dialog_alert_title)
|
||||
val dialogContent = stringResource(id = R.string.install_inactive_slot_warning)
|
||||
|
||||
val onClick = { option: InstallMethod ->
|
||||
|
||||
when (option) {
|
||||
is InstallMethod.SelectFile -> {
|
||||
selectImageLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
type = "application/octet-stream"
|
||||
})
|
||||
}
|
||||
|
||||
is InstallMethod.DirectInstall -> {
|
||||
selectedOption = option
|
||||
onSelected(option)
|
||||
}
|
||||
|
||||
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
|
||||
fun rememberSelectKmiDialog(onSelected: (String?) -> Unit): DialogHandle {
|
||||
return rememberCustomDialog { dismiss ->
|
||||
val supportedKmi by produceState(initialValue = emptyList<String>()) {
|
||||
value = getSupportedKmis()
|
||||
}
|
||||
val options = supportedKmi.map { value ->
|
||||
ListOption(
|
||||
titleText = value
|
||||
)
|
||||
}
|
||||
|
||||
var selection by remember { mutableStateOf<String?>(null) }
|
||||
ListDialog(state = rememberUseCaseState(visible = true, onFinishedRequest = {
|
||||
onSelected(selection)
|
||||
}, onCloseRequest = {
|
||||
dismiss()
|
||||
}), header = Header.Default(
|
||||
title = stringResource(R.string.select_kmi),
|
||||
), selection = ListSelection.Single(
|
||||
showRadioButtons = true,
|
||||
options = options,
|
||||
) { _, option ->
|
||||
selection = option.titleText
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar(onBack: () -> Unit = {}, onLkmUpload: () -> Unit = {}) {
|
||||
TopAppBar(title = { Text(stringResource(R.string.install)) }, navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onBack
|
||||
) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) }
|
||||
}, actions = {
|
||||
IconButton(onClick = onLkmUpload) {
|
||||
Icon(Icons.Filled.FileUpload, contentDescription = null)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun SelectInstall_Preview() {
|
||||
InstallScreen(EmptyDestinationsNavigator)
|
||||
}
|
||||
|
|
@ -0,0 +1,576 @@
|
|||
package me.weishu.kernelsu.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.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.defaultMinSize
|
||||
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.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.Add
|
||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||
import androidx.compose.material.pullrefresh.pullRefresh
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
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.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.platform.LocalContext
|
||||
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.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.weishu.kernelsu.Natives
|
||||
import me.weishu.kernelsu.R
|
||||
import me.weishu.kernelsu.ui.component.ConfirmResult
|
||||
import me.weishu.kernelsu.ui.component.rememberConfirmDialog
|
||||
import me.weishu.kernelsu.ui.component.rememberLoadingDialog
|
||||
import me.weishu.kernelsu.ui.screen.destinations.FlashScreenDestination
|
||||
import me.weishu.kernelsu.ui.util.DownloadListener
|
||||
import me.weishu.kernelsu.ui.util.LocalSnackbarHost
|
||||
import me.weishu.kernelsu.ui.util.download
|
||||
import me.weishu.kernelsu.ui.util.hasMagisk
|
||||
import me.weishu.kernelsu.ui.util.reboot
|
||||
import me.weishu.kernelsu.ui.util.toggleModule
|
||||
import me.weishu.kernelsu.ui.util.uninstallModule
|
||||
import me.weishu.kernelsu.ui.viewmodel.ModuleViewModel
|
||||
import me.weishu.kernelsu.ui.webui.WebUIActivity
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
@Destination
|
||||
@Composable
|
||||
fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
val viewModel = viewModel<ModuleViewModel>()
|
||||
val context = LocalContext.current
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (viewModel.moduleList.isEmpty() || viewModel.isNeedRefresh) {
|
||||
viewModel.fetchModuleList()
|
||||
}
|
||||
}
|
||||
|
||||
val isSafeMode = Natives.isSafeMode
|
||||
val hasMagisk = hasMagisk()
|
||||
|
||||
val hideInstallButton = isSafeMode || hasMagisk
|
||||
|
||||
Scaffold(topBar = {
|
||||
TopBar()
|
||||
}, floatingActionButton = if (hideInstallButton) {
|
||||
{ /* Empty */ }
|
||||
} else {
|
||||
{
|
||||
val moduleInstall = stringResource(id = R.string.module_install)
|
||||
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
|
||||
|
||||
navigator.navigate(FlashScreenDestination(FlashIt.FlashModule(uri)))
|
||||
|
||||
viewModel.markNeedRefresh()
|
||||
|
||||
Log.i("ModuleScreen", "select zip result: ${it.data}")
|
||||
}
|
||||
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = {
|
||||
// select the zip file to install
|
||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
||||
intent.type = "application/zip"
|
||||
selectZipLauncher.launch(intent)
|
||||
},
|
||||
icon = { Icon(Icons.Filled.Add, moduleInstall) },
|
||||
text = { Text(text = moduleInstall) },
|
||||
)
|
||||
}
|
||||
}) { innerPadding ->
|
||||
|
||||
when {
|
||||
hasMagisk -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.module_magisk_conflict),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
ModuleList(
|
||||
viewModel = viewModel, modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxSize(),
|
||||
onInstallModule =
|
||||
{
|
||||
navigator.navigate(FlashScreenDestination(FlashIt.FlashModule(it)))
|
||||
}, onClickModule = { id, name, hasWebUi ->
|
||||
if (hasWebUi) {
|
||||
context.startActivity(Intent(context, WebUIActivity::class.java)
|
||||
.setData(Uri.parse("kernelsu://webui/$id"))
|
||||
.putExtra("id", id)
|
||||
.putExtra("name", name)
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
private fun ModuleList(
|
||||
viewModel: ModuleViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
onInstallModule: (Uri) -> Unit,
|
||||
onClickModule: (id: String, name: String, hasWebUi: Boolean) -> Unit
|
||||
) {
|
||||
val failedEnable = stringResource(R.string.module_failed_to_enable)
|
||||
val failedDisable = stringResource(R.string.module_failed_to_disable)
|
||||
val failedUninstall = stringResource(R.string.module_uninstall_failed)
|
||||
val successUninstall = stringResource(R.string.module_uninstall_success)
|
||||
val reboot = stringResource(id = R.string.reboot)
|
||||
val rebootToApply = stringResource(id = R.string.reboot_to_apply)
|
||||
val moduleStr = stringResource(id = R.string.module)
|
||||
val uninstall = stringResource(id = R.string.uninstall)
|
||||
val cancel = stringResource(id = android.R.string.cancel)
|
||||
val moduleUninstallConfirm = stringResource(id = R.string.module_uninstall_confirm)
|
||||
val updateText = stringResource(R.string.module_update)
|
||||
val changelogText = stringResource(R.string.module_changelog)
|
||||
val downloadingText = stringResource(R.string.module_downloading)
|
||||
val startDownloadingText = stringResource(R.string.module_start_downloading)
|
||||
val fetchChangeLogFailed = stringResource(R.string.module_changelog_failed)
|
||||
|
||||
val snackBarHost = LocalSnackbarHost.current
|
||||
val context = LocalContext.current
|
||||
|
||||
val loadingDialog = rememberLoadingDialog()
|
||||
val confirmDialog = rememberConfirmDialog()
|
||||
|
||||
suspend fun onModuleUpdate(
|
||||
module: ModuleViewModel.ModuleInfo,
|
||||
changelogUrl: String,
|
||||
downloadUrl: String,
|
||||
fileName: String
|
||||
) {
|
||||
val changelogResult = loadingDialog.withLoading {
|
||||
withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
OkHttpClient().newCall(
|
||||
okhttp3.Request.Builder().url(changelogUrl).build()
|
||||
).execute().body!!.string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val showToast: suspend (String) -> Unit = { msg ->
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
msg,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
val changelog = changelogResult.getOrElse {
|
||||
showToast(fetchChangeLogFailed.format(it.message))
|
||||
return
|
||||
}.ifBlank {
|
||||
showToast(fetchChangeLogFailed.format(module.name))
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
showToast(startDownloadingText.format(module.name))
|
||||
|
||||
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: ModuleViewModel.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, actionLabel = actionLabel)
|
||||
if (result == SnackbarResult.ActionPerformed) {
|
||||
reboot()
|
||||
}
|
||||
}
|
||||
|
||||
val refreshState = rememberPullRefreshState(refreshing = viewModel.isRefreshing,
|
||||
onRefresh = { viewModel.fetchModuleList() })
|
||||
Box(modifier.pullRefresh(refreshState)) {
|
||||
val context = LocalContext.current
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
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.isOverlayAvailable -> {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier.fillParentMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.module_overlay_fs_not_available),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.moduleList.isEmpty() -> {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier.fillParentMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.module_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(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(
|
||||
rebootToApply, actionLabel = reboot
|
||||
)
|
||||
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)
|
||||
|
||||
PullRefreshIndicator(
|
||||
refreshing = viewModel.isRefreshing, state = refreshState, modifier = Modifier.align(
|
||||
Alignment.TopCenter
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar() {
|
||||
TopAppBar(title = { Text(stringResource(R.string.module)) })
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ModuleItem(
|
||||
module: ModuleViewModel.ModuleInfo,
|
||||
isChecked: Boolean,
|
||||
updateUrl: String,
|
||||
onUninstall: (ModuleViewModel.ModuleInfo) -> Unit,
|
||||
onCheckChanged: (Boolean) -> Unit,
|
||||
onUpdate: (ModuleViewModel.ModuleInfo) -> Unit,
|
||||
onClick: (ModuleViewModel.ModuleInfo) -> Unit
|
||||
) {
|
||||
ElevatedCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.elevatedCardColors(containerColor = MaterialTheme.colorScheme.surface)
|
||||
) {
|
||||
|
||||
val textDecoration = if (!module.remove) null else TextDecoration.LineThrough
|
||||
|
||||
Column(modifier = Modifier.clickable { onClick(module) }.padding(24.dp, 16.dp, 24.dp, 0.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
val moduleVersion = stringResource(id = R.string.module_version)
|
||||
val moduleAuthor = stringResource(id = R.string.module_author)
|
||||
|
||||
Column(modifier = Modifier.fillMaxWidth(0.8f)) {
|
||||
Text(
|
||||
text = module.name,
|
||||
fontSize = MaterialTheme.typography.titleMedium.fontSize,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
||||
fontFamily = MaterialTheme.typography.titleMedium.fontFamily,
|
||||
textDecoration = textDecoration,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "$moduleVersion: ${module.version}",
|
||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
||||
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
|
||||
textDecoration = textDecoration
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "$moduleAuthor: ${module.author}",
|
||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
||||
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
|
||||
textDecoration = textDecoration
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
) {
|
||||
Switch(
|
||||
enabled = !module.update,
|
||||
checked = isChecked,
|
||||
onCheckedChange = onCheckChanged
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Text(
|
||||
text = module.description,
|
||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
|
||||
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
||||
fontWeight = MaterialTheme.typography.bodySmall.fontWeight,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 4,
|
||||
textDecoration = textDecoration
|
||||
)
|
||||
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
HorizontalDivider(thickness = Dp.Hairline)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(1f, true))
|
||||
|
||||
if (updateUrl.isNotEmpty()) {
|
||||
Button(
|
||||
modifier = Modifier
|
||||
.padding(0.dp)
|
||||
.defaultMinSize(52.dp, 32.dp),
|
||||
onClick = { onUpdate(module) },
|
||||
shape = RoundedCornerShape(6.dp),
|
||||
contentPadding = PaddingValues(0.dp)
|
||||
) {
|
||||
Text(
|
||||
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||
fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
||||
text = stringResource(R.string.module_update),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
TextButton(
|
||||
enabled = !module.remove,
|
||||
onClick = { onUninstall(module) },
|
||||
) {
|
||||
Text(
|
||||
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||
fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
||||
text = stringResource(R.string.uninstall),
|
||||
)
|
||||
}
|
||||
|
||||
if (module.hasWebUi) {
|
||||
TextButton(
|
||||
onClick = { onClick(module) },
|
||||
) {
|
||||
Text(
|
||||
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||
fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
||||
text = stringResource(R.string.open),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ModuleItemPreview() {
|
||||
val module = ModuleViewModel.ModuleInfo(
|
||||
id = "id",
|
||||
name = "name",
|
||||
version = "version",
|
||||
versionCode = 1,
|
||||
author = "author",
|
||||
description = "I am a test module and i do nothing but show a very long description",
|
||||
enabled = true,
|
||||
update = true,
|
||||
remove = true,
|
||||
updateJson = "",
|
||||
hasWebUi = false,
|
||||
)
|
||||
ModuleItem(module, true, "", {}, {}, {}, {})
|
||||
}
|
||||
|
|
@ -0,0 +1,474 @@
|
|||
package me.weishu.kernelsu.ui.screen
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.automirrored.filled.Undo
|
||||
import androidx.compose.material.icons.filled.BugReport
|
||||
import androidx.compose.material.icons.filled.Compress
|
||||
import androidx.compose.material.icons.filled.ContactPage
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.DeleteForever
|
||||
import androidx.compose.material.icons.filled.DeveloperMode
|
||||
import androidx.compose.material.icons.filled.Fence
|
||||
import androidx.compose.material.icons.filled.RemoveModerator
|
||||
import androidx.compose.material.icons.filled.Save
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material.icons.filled.Update
|
||||
import androidx.compose.material3.BottomSheetScaffold
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
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.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.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.LineHeightStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.FileProvider
|
||||
import com.maxkeppeker.sheets.core.models.base.Header
|
||||
import com.maxkeppeker.sheets.core.models.base.IconSource
|
||||
import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState
|
||||
import com.maxkeppeler.sheets.list.ListDialog
|
||||
import com.maxkeppeler.sheets.list.models.ListOption
|
||||
import com.maxkeppeler.sheets.list.models.ListSelection
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.weishu.kernelsu.BuildConfig
|
||||
import me.weishu.kernelsu.Natives
|
||||
import me.weishu.kernelsu.R
|
||||
import me.weishu.kernelsu.ui.component.AboutDialog
|
||||
import me.weishu.kernelsu.ui.component.ConfirmResult
|
||||
import me.weishu.kernelsu.ui.component.DialogHandle
|
||||
import me.weishu.kernelsu.ui.component.SwitchItem
|
||||
import me.weishu.kernelsu.ui.component.rememberConfirmDialog
|
||||
import me.weishu.kernelsu.ui.component.rememberCustomDialog
|
||||
import me.weishu.kernelsu.ui.component.rememberLoadingDialog
|
||||
import me.weishu.kernelsu.ui.screen.destinations.AppProfileTemplateScreenDestination
|
||||
import me.weishu.kernelsu.ui.screen.destinations.FlashScreenDestination
|
||||
import me.weishu.kernelsu.ui.util.getBugreportFile
|
||||
import me.weishu.kernelsu.ui.util.getFileNameFromUri
|
||||
import me.weishu.kernelsu.ui.util.shrinkModules
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
* @date 2023/1/1.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination
|
||||
@Composable
|
||||
fun SettingScreen(navigator: DestinationsNavigator) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBar(onBack = {
|
||||
navigator.popBackStack()
|
||||
})
|
||||
}
|
||||
) { paddingValues ->
|
||||
val aboutDialog = rememberCustomDialog {
|
||||
AboutDialog(it)
|
||||
}
|
||||
val loadingDialog = rememberLoadingDialog()
|
||||
val shrinkDialog = rememberConfirmDialog()
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val profileTemplate = stringResource(id = R.string.settings_profile_template)
|
||||
ListItem(
|
||||
leadingContent = { Icon(Icons.Filled.Fence, profileTemplate) },
|
||||
headlineContent = { Text(profileTemplate) },
|
||||
supportingContent = { Text(stringResource(id = R.string.settings_profile_template_summary)) },
|
||||
modifier = Modifier.clickable {
|
||||
navigator.navigate(AppProfileTemplateScreenDestination)
|
||||
}
|
||||
)
|
||||
|
||||
var umountChecked by rememberSaveable {
|
||||
mutableStateOf(Natives.isDefaultUmountModules())
|
||||
}
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.RemoveModerator,
|
||||
title = stringResource(id = R.string.settings_umount_modules_default),
|
||||
summary = stringResource(id = R.string.settings_umount_modules_default_summary),
|
||||
checked = umountChecked
|
||||
) {
|
||||
if (Natives.setDefaultUmountModules(it)) {
|
||||
umountChecked = it
|
||||
}
|
||||
}
|
||||
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
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).apply()
|
||||
checkUpdate = it
|
||||
}
|
||||
|
||||
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
|
||||
) {
|
||||
prefs.edit().putBoolean("enable_web_debugging", it).apply()
|
||||
enableWebDebugging = it
|
||||
}
|
||||
|
||||
var showBottomsheet by remember { mutableStateOf(false) }
|
||||
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Filled.BugReport,
|
||||
stringResource(id = R.string.send_log)
|
||||
)
|
||||
},
|
||||
headlineContent = { Text(stringResource(id = R.string.send_log)) },
|
||||
modifier = Modifier.clickable {
|
||||
showBottomsheet = true
|
||||
}
|
||||
)
|
||||
if (showBottomsheet){
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { showBottomsheet = false },
|
||||
content = {
|
||||
Row(modifier = Modifier.padding(10.dp)
|
||||
.align(Alignment.CenterHorizontally)
|
||||
|
||||
) {
|
||||
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 filename = getFileNameFromUri(context , uri)
|
||||
val savefile = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "application/zip"
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
putExtra(Intent.EXTRA_TITLE, filename)
|
||||
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
}
|
||||
context.startActivity(
|
||||
Intent.createChooser(
|
||||
savefile,
|
||||
context.getString(R.string.save_log)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
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)
|
||||
shareIntent.putExtra(Intent.EXTRA_STREAM, uri)
|
||||
shareIntent.setDataAndType(uri, "application/zip")
|
||||
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
|
||||
context.startActivity(
|
||||
Intent.createChooser(
|
||||
shareIntent,
|
||||
context.getString(R.string.send_log)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
|
||||
val shrink = stringResource(id = R.string.shrink_sparse_image)
|
||||
val shrinkMessage = stringResource(id = R.string.shrink_sparse_image_message)
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Filled.Compress,
|
||||
shrink
|
||||
)
|
||||
},
|
||||
headlineContent = { Text(shrink) },
|
||||
modifier = Modifier.clickable {
|
||||
scope.launch {
|
||||
val result =
|
||||
shrinkDialog.awaitConfirm(title = shrink, content = shrinkMessage)
|
||||
if (result == ConfirmResult.Confirmed) {
|
||||
loadingDialog.withLoading {
|
||||
shrinkModules()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val lkmMode =
|
||||
Natives.version >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && Natives.isLkmMode
|
||||
if (lkmMode) {
|
||||
UninstallItem(navigator) {
|
||||
loadingDialog.withLoading(it)
|
||||
}
|
||||
}
|
||||
|
||||
val about = stringResource(id = R.string.about)
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Filled.ContactPage,
|
||||
stringResource(id = R.string.about)
|
||||
)
|
||||
},
|
||||
headlineContent = { Text(about) },
|
||||
modifier = Modifier.clickable {
|
||||
aboutDialog.show()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@Composable
|
||||
fun UninstallItem(
|
||||
navigator: DestinationsNavigator,
|
||||
withLoading: suspend (suspend () -> Unit) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val uninstallConfirmDialog = rememberConfirmDialog()
|
||||
val showTodo = {
|
||||
Toast.makeText(context, "TODO", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
val uninstallDialog = rememberUninstallDialog { uninstallType ->
|
||||
scope.launch {
|
||||
val result = uninstallConfirmDialog.awaitConfirm(
|
||||
title = context.getString(uninstallType.title),
|
||||
content = context.getString(uninstallType.message)
|
||||
)
|
||||
if (result == ConfirmResult.Confirmed) {
|
||||
withLoading {
|
||||
when (uninstallType) {
|
||||
UninstallType.TEMPORARY -> showTodo()
|
||||
UninstallType.PERMANENT -> navigator.navigate(
|
||||
FlashScreenDestination(FlashIt.FlashUninstall)
|
||||
)
|
||||
|
||||
UninstallType.RESTORE_STOCK_IMAGE -> navigator.navigate(
|
||||
FlashScreenDestination(FlashIt.FlashRestore)
|
||||
)
|
||||
|
||||
UninstallType.NONE -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val uninstall = stringResource(id = R.string.settings_uninstall)
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Filled.Delete,
|
||||
uninstall
|
||||
)
|
||||
},
|
||||
headlineContent = { Text(uninstall) },
|
||||
modifier = Modifier.clickable {
|
||||
uninstallDialog.show()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
enum class UninstallType(val title: Int, val message: Int, val icon: ImageVector) {
|
||||
TEMPORARY(
|
||||
R.string.settings_uninstall_temporary,
|
||||
R.string.settings_uninstall_temporary_message,
|
||||
Icons.Filled.Delete
|
||||
),
|
||||
PERMANENT(
|
||||
R.string.settings_uninstall_permanent,
|
||||
R.string.settings_uninstall_permanent_message,
|
||||
Icons.Filled.DeleteForever
|
||||
),
|
||||
RESTORE_STOCK_IMAGE(
|
||||
R.string.settings_restore_stock_image,
|
||||
R.string.settings_restore_stock_image_message,
|
||||
Icons.AutoMirrored.Filled.Undo
|
||||
),
|
||||
NONE(0, 0, Icons.Filled.Delete)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
|
||||
return rememberCustomDialog { dismiss ->
|
||||
val options = listOf(
|
||||
// UninstallType.TEMPORARY,
|
||||
UninstallType.PERMANENT,
|
||||
UninstallType.RESTORE_STOCK_IMAGE
|
||||
)
|
||||
val listOptions = options.map {
|
||||
ListOption(
|
||||
titleText = stringResource(it.title),
|
||||
subtitleText = if (it.message != 0) stringResource(it.message) else null,
|
||||
icon = IconSource(it.icon)
|
||||
)
|
||||
}
|
||||
|
||||
var selection = UninstallType.NONE
|
||||
ListDialog(state = rememberUseCaseState(visible = true, onFinishedRequest = {
|
||||
if (selection != UninstallType.NONE) {
|
||||
onSelected(selection)
|
||||
}
|
||||
}, onCloseRequest = {
|
||||
dismiss()
|
||||
}), header = Header.Default(
|
||||
title = stringResource(R.string.settings_uninstall),
|
||||
), selection = ListSelection.Single(
|
||||
showRadioButtons = false,
|
||||
options = listOptions,
|
||||
) { index, _ ->
|
||||
selection = options[index]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar(onBack: () -> Unit = {}) {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.settings)) },
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onBack
|
||||
) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun SettingsPreview() {
|
||||
SettingScreen(EmptyDestinationsNavigator)
|
||||
}
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
package me.weishu.kernelsu.ui.screen
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
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.pullrefresh.PullRefreshIndicator
|
||||
import androidx.compose.material.pullrefresh.pullRefresh
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
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.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.navigation.DestinationsNavigator
|
||||
import kotlinx.coroutines.launch
|
||||
import me.weishu.kernelsu.Natives
|
||||
import me.weishu.kernelsu.R
|
||||
import me.weishu.kernelsu.ui.component.SearchAppBar
|
||||
import me.weishu.kernelsu.ui.screen.destinations.AppProfileScreenDestination
|
||||
import me.weishu.kernelsu.ui.viewmodel.SuperUserViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Destination
|
||||
@Composable
|
||||
fun SuperUserScreen(navigator: DestinationsNavigator) {
|
||||
val viewModel = viewModel<SuperUserViewModel>()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (viewModel.appList.isEmpty()) {
|
||||
viewModel.fetchAppList()
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
SearchAppBar(
|
||||
title = { Text(stringResource(R.string.superuser)) },
|
||||
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)
|
||||
)
|
||||
|
||||
DropdownMenu(expanded = showDropdown, onDismissRequest = {
|
||||
showDropdown = false
|
||||
}) {
|
||||
DropdownMenuItem(text = {
|
||||
Text(stringResource(R.string.refresh))
|
||||
}, onClick = {
|
||||
scope.launch {
|
||||
viewModel.fetchAppList()
|
||||
}
|
||||
showDropdown = false
|
||||
})
|
||||
DropdownMenuItem(text = {
|
||||
Text(
|
||||
if (viewModel.showSystemApps) {
|
||||
stringResource(R.string.hide_system_apps)
|
||||
} else {
|
||||
stringResource(R.string.show_system_apps)
|
||||
}
|
||||
)
|
||||
}, onClick = {
|
||||
viewModel.showSystemApps = !viewModel.showSystemApps
|
||||
showDropdown = false
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
val refreshState = rememberPullRefreshState(
|
||||
refreshing = viewModel.isRefreshing,
|
||||
onRefresh = { scope.launch { viewModel.fetchAppList() } },
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.pullRefresh(refreshState)
|
||||
) {
|
||||
LazyColumn(Modifier.fillMaxSize()) {
|
||||
items(viewModel.appList, key = { it.packageName + it.uid }) { app ->
|
||||
AppItem(app) {
|
||||
navigator.navigate(AppProfileScreenDestination(app))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
PullRefreshIndicator(
|
||||
refreshing = viewModel.isRefreshing,
|
||||
state = refreshState,
|
||||
modifier = Modifier.align(Alignment.TopCenter)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun AppItem(
|
||||
app: SuperUserViewModel.AppInfo,
|
||||
onClickListener: () -> Unit,
|
||||
) {
|
||||
ListItem(
|
||||
modifier = Modifier.clickable(onClick = onClickListener),
|
||||
headlineContent = { Text(app.label) },
|
||||
supportingContent = {
|
||||
Column {
|
||||
Text(app.packageName)
|
||||
FlowRow {
|
||||
if (app.allowSu) {
|
||||
LabelText(label = "ROOT")
|
||||
} else {
|
||||
if (Natives.uidShouldUmount(app.uid)) {
|
||||
LabelText(label = "UMOUNT")
|
||||
}
|
||||
}
|
||||
if (app.hasCustomProfile) {
|
||||
LabelText(label = "CUSTOM")
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
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)
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@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,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,259 @@
|
|||
package me.weishu.kernelsu.ui.screen
|
||||
|
||||
import android.widget.Toast
|
||||
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.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.ImportExport
|
||||
import androidx.compose.material.icons.filled.Sync
|
||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||
import androidx.compose.material.pullrefresh.pullRefresh
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.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.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import com.ramcosta.composedestinations.result.ResultRecipient
|
||||
import com.ramcosta.composedestinations.result.getOr
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import me.weishu.kernelsu.R
|
||||
import me.weishu.kernelsu.ui.screen.destinations.TemplateEditorScreenDestination
|
||||
import me.weishu.kernelsu.ui.viewmodel.TemplateViewModel
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
* @date 2023/10/20.
|
||||
*/
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Destination
|
||||
@Composable
|
||||
fun AppProfileTemplateScreen(
|
||||
navigator: DestinationsNavigator,
|
||||
resultRecipient: ResultRecipient<TemplateEditorScreenDestination, Boolean>
|
||||
) {
|
||||
val viewModel = viewModel<TemplateViewModel>()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (viewModel.templateList.isEmpty()) {
|
||||
viewModel.fetchTemplates()
|
||||
}
|
||||
}
|
||||
|
||||
// handle result from TemplateEditorScreen, refresh if needed
|
||||
resultRecipient.onNavResult { result ->
|
||||
if (result.getOr { false }) {
|
||||
scope.launch { viewModel.fetchTemplates() }
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val context = LocalContext.current
|
||||
val showToast = fun(msg: String) {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
TopBar(onBack = { navigator.popBackStack() },
|
||||
onSync = {
|
||||
scope.launch { viewModel.fetchTemplates(true) }
|
||||
},
|
||||
onImport = {
|
||||
clipboardManager.getText()?.text?.let {
|
||||
if (it.isEmpty()) {
|
||||
showToast(context.getString(R.string.app_profile_template_import_empty))
|
||||
return@let
|
||||
}
|
||||
scope.launch {
|
||||
viewModel.importTemplates(
|
||||
it, {
|
||||
showToast(context.getString(R.string.app_profile_template_import_success))
|
||||
viewModel.fetchTemplates(false)
|
||||
},
|
||||
showToast
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
onExport = {
|
||||
scope.launch {
|
||||
viewModel.exportTemplates(
|
||||
{
|
||||
showToast(context.getString(R.string.app_profile_template_export_empty))
|
||||
}
|
||||
) {
|
||||
clipboardManager.setText(AnnotatedString(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = {
|
||||
navigator.navigate(
|
||||
TemplateEditorScreenDestination(
|
||||
TemplateViewModel.TemplateInfo(),
|
||||
false
|
||||
)
|
||||
)
|
||||
},
|
||||
icon = { Icon(Icons.Filled.Add, null) },
|
||||
text = { Text(stringResource(id = R.string.app_profile_template_create)) },
|
||||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
val refreshState = rememberPullRefreshState(
|
||||
refreshing = viewModel.isRefreshing,
|
||||
onRefresh = { scope.launch { viewModel.fetchTemplates() } },
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.pullRefresh(refreshState)
|
||||
) {
|
||||
LazyColumn(Modifier.fillMaxSize(), contentPadding = remember {
|
||||
PaddingValues(bottom = 16.dp + 16.dp + 56.dp /* Scaffold Fab Spacing + Fab container height */)
|
||||
}) {
|
||||
items(viewModel.templateList, key = { it.id }) { app ->
|
||||
TemplateItem(navigator, app)
|
||||
}
|
||||
}
|
||||
|
||||
PullRefreshIndicator(
|
||||
refreshing = viewModel.isRefreshing,
|
||||
state = refreshState,
|
||||
modifier = Modifier.align(Alignment.TopCenter)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun TemplateItem(
|
||||
navigator: DestinationsNavigator,
|
||||
template: TemplateViewModel.TemplateInfo
|
||||
) {
|
||||
ListItem(
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
navigator.navigate(TemplateEditorScreenDestination(template, !template.local))
|
||||
},
|
||||
headlineContent = { Text(template.name) },
|
||||
supportingContent = {
|
||||
Column {
|
||||
Text(
|
||||
text = "${template.id}${if (template.author.isEmpty()) "" else "@${template.author}"}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||
)
|
||||
Text(template.description)
|
||||
FlowRow {
|
||||
LabelText(label = "UID: ${template.uid}")
|
||||
LabelText(label = "GID: ${template.gid}")
|
||||
LabelText(label = template.context)
|
||||
if (template.local) {
|
||||
LabelText(label = "local")
|
||||
} else {
|
||||
LabelText(label = "remote")
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar(
|
||||
onBack: () -> Unit,
|
||||
onSync: () -> Unit = {},
|
||||
onImport: () -> Unit = {},
|
||||
onExport: () -> Unit = {}
|
||||
) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(stringResource(R.string.settings_profile_template))
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onBack
|
||||
) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) }
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = onSync) {
|
||||
Icon(
|
||||
Icons.Filled.Sync,
|
||||
contentDescription = stringResource(id = R.string.app_profile_template_sync)
|
||||
)
|
||||
}
|
||||
|
||||
var showDropdown by remember { mutableStateOf(false) }
|
||||
IconButton(onClick = {
|
||||
showDropdown = true
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ImportExport,
|
||||
contentDescription = stringResource(id = R.string.app_profile_import_export)
|
||||
)
|
||||
|
||||
DropdownMenu(expanded = showDropdown, onDismissRequest = {
|
||||
showDropdown = false
|
||||
}) {
|
||||
DropdownMenuItem(text = {
|
||||
Text(stringResource(id = R.string.app_profile_import_from_clipboard))
|
||||
}, onClick = {
|
||||
onImport()
|
||||
showDropdown = false
|
||||
})
|
||||
DropdownMenuItem(text = {
|
||||
Text(stringResource(id = R.string.app_profile_export_to_clipboard))
|
||||
}, onClick = {
|
||||
onExport()
|
||||
showDropdown = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,322 @@
|
|||
package me.weishu.kernelsu.ui.screen
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
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.DeleteForever
|
||||
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.ListItem
|
||||
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.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.pointer.pointerInteropFilter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.result.ResultBackNavigator
|
||||
import me.weishu.kernelsu.Natives
|
||||
import me.weishu.kernelsu.R
|
||||
import me.weishu.kernelsu.ui.component.profile.RootProfileConfig
|
||||
import me.weishu.kernelsu.ui.util.deleteAppProfileTemplate
|
||||
import me.weishu.kernelsu.ui.util.getAppProfileTemplate
|
||||
import me.weishu.kernelsu.ui.util.setAppProfileTemplate
|
||||
import me.weishu.kernelsu.ui.viewmodel.TemplateViewModel
|
||||
import me.weishu.kernelsu.ui.viewmodel.toJSON
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
* @date 2023/10/20.
|
||||
*/
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Destination
|
||||
@Composable
|
||||
fun TemplateEditorScreen(
|
||||
navigator: ResultBackNavigator<Boolean>,
|
||||
initialTemplate: TemplateViewModel.TemplateInfo,
|
||||
readOnly: Boolean = true,
|
||||
) {
|
||||
|
||||
val isCreation = initialTemplate.id.isBlank()
|
||||
val autoSave = !isCreation
|
||||
|
||||
var template by rememberSaveable {
|
||||
mutableStateOf(initialTemplate)
|
||||
}
|
||||
|
||||
BackHandler {
|
||||
navigator.navigateBack(result = !readOnly)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
val author =
|
||||
if (initialTemplate.author.isNotEmpty()) "@${initialTemplate.author}" else ""
|
||||
val readOnlyHint = if (readOnly) {
|
||||
" - ${stringResource(id = R.string.app_profile_template_readonly)}"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
val titleSummary = "${initialTemplate.id}$author$readOnlyHint"
|
||||
val saveTemplateFailed = stringResource(id = R.string.app_profile_template_save_failed)
|
||||
val context = LocalContext.current
|
||||
|
||||
TopBar(
|
||||
title = if (isCreation) {
|
||||
stringResource(R.string.app_profile_template_create)
|
||||
} else if (readOnly) {
|
||||
stringResource(R.string.app_profile_template_view)
|
||||
} else {
|
||||
stringResource(R.string.app_profile_template_edit)
|
||||
},
|
||||
readOnly = readOnly,
|
||||
summary = titleSummary,
|
||||
onBack = { navigator.navigateBack(result = !readOnly) },
|
||||
onDelete = {
|
||||
if (deleteAppProfileTemplate(template.id)) {
|
||||
navigator.navigateBack(result = true)
|
||||
}
|
||||
},
|
||||
onSave = {
|
||||
if (saveTemplate(template, isCreation)) {
|
||||
navigator.navigateBack(result = true)
|
||||
} else {
|
||||
Toast.makeText(context, saveTemplateFailed, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
})
|
||||
},
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.pointerInteropFilter {
|
||||
// disable click and ripple if readOnly
|
||||
readOnly
|
||||
}
|
||||
) {
|
||||
if (isCreation) {
|
||||
var errorHint by remember {
|
||||
mutableStateOf("")
|
||||
}
|
||||
val idConflictError = stringResource(id = R.string.app_profile_template_id_exist)
|
||||
val idInvalidError = stringResource(id = R.string.app_profile_template_id_invalid)
|
||||
TextEdit(
|
||||
label = stringResource(id = R.string.app_profile_template_id),
|
||||
text = template.id,
|
||||
errorHint = errorHint,
|
||||
isError = errorHint.isNotEmpty()
|
||||
) { value ->
|
||||
errorHint = if (isTemplateExist(value)) {
|
||||
idConflictError
|
||||
} else if (!isValidTemplateId(value)) {
|
||||
idInvalidError
|
||||
} else {
|
||||
""
|
||||
}
|
||||
template = template.copy(id = value)
|
||||
}
|
||||
}
|
||||
|
||||
TextEdit(
|
||||
label = stringResource(id = R.string.app_profile_template_name),
|
||||
text = template.name
|
||||
) { value ->
|
||||
template.copy(name = value).run {
|
||||
if (autoSave) {
|
||||
if (!saveTemplate(this)) {
|
||||
// failed
|
||||
return@run
|
||||
}
|
||||
}
|
||||
template = this
|
||||
}
|
||||
}
|
||||
TextEdit(
|
||||
label = stringResource(id = R.string.app_profile_template_description),
|
||||
text = template.description
|
||||
) { value ->
|
||||
template.copy(description = value).run {
|
||||
if (autoSave) {
|
||||
if (!saveTemplate(this)) {
|
||||
// failed
|
||||
return@run
|
||||
}
|
||||
}
|
||||
template = this
|
||||
}
|
||||
}
|
||||
|
||||
RootProfileConfig(fixedName = true,
|
||||
profile = toNativeProfile(template),
|
||||
onProfileChange = {
|
||||
template.copy(
|
||||
uid = it.uid,
|
||||
gid = it.gid,
|
||||
groups = it.groups,
|
||||
capabilities = it.capabilities,
|
||||
context = it.context,
|
||||
namespace = it.namespace,
|
||||
rules = it.rules.split("\n")
|
||||
).run {
|
||||
if (autoSave) {
|
||||
if (!saveTemplate(this)) {
|
||||
// failed
|
||||
return@run
|
||||
}
|
||||
}
|
||||
template = this
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toNativeProfile(templateInfo: TemplateViewModel.TemplateInfo): Natives.Profile {
|
||||
return Natives.Profile().copy(rootTemplate = templateInfo.id,
|
||||
uid = templateInfo.uid,
|
||||
gid = templateInfo.gid,
|
||||
groups = templateInfo.groups,
|
||||
capabilities = templateInfo.capabilities,
|
||||
context = templateInfo.context,
|
||||
namespace = templateInfo.namespace,
|
||||
rules = templateInfo.rules.joinToString("\n").ifBlank { "" })
|
||||
}
|
||||
|
||||
fun isTemplateValid(template: TemplateViewModel.TemplateInfo): Boolean {
|
||||
if (template.id.isBlank()) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!isValidTemplateId(template.id)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun saveTemplate(template: TemplateViewModel.TemplateInfo, isCreation: Boolean = false): Boolean {
|
||||
if (!isTemplateValid(template)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (isCreation && isTemplateExist(template.id)) {
|
||||
return false
|
||||
}
|
||||
|
||||
val json = template.toJSON()
|
||||
json.put("local", true)
|
||||
return setAppProfileTemplate(template.id, json.toString())
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar(
|
||||
title: String,
|
||||
readOnly: Boolean,
|
||||
summary: String = "",
|
||||
onBack: () -> Unit,
|
||||
onDelete: () -> Unit = {},
|
||||
onSave: () -> Unit = {}
|
||||
) {
|
||||
TopAppBar(title = {
|
||||
Column {
|
||||
Text(title)
|
||||
if (summary.isNotBlank()) {
|
||||
Text(
|
||||
text = summary,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
}, navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onBack
|
||||
) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) }
|
||||
}, actions = {
|
||||
if (readOnly) {
|
||||
return@TopAppBar
|
||||
}
|
||||
IconButton(onClick = onDelete) {
|
||||
Icon(
|
||||
Icons.Filled.DeleteForever,
|
||||
contentDescription = stringResource(id = R.string.app_profile_template_delete)
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onSave) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Save,
|
||||
contentDescription = stringResource(id = R.string.app_profile_template_save)
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TextEdit(
|
||||
label: String,
|
||||
text: String,
|
||||
errorHint: String = "",
|
||||
isError: Boolean = false,
|
||||
onValueChange: (String) -> Unit = {}
|
||||
) {
|
||||
ListItem(headlineContent = {
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
OutlinedTextField(
|
||||
value = text,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
label = { Text(label) },
|
||||
suffix =
|
||||
if (errorHint.isNotBlank()) {
|
||||
{
|
||||
Text(
|
||||
text = if (isError) errorHint else "",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
isError = isError,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Next
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = {
|
||||
keyboardController?.hide()
|
||||
}),
|
||||
onValueChange = onValueChange
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private fun isValidTemplateId(id: String): Boolean {
|
||||
return Regex("""^([A-Za-z]{1}[A-Za-z\d_]*\.)*[A-Za-z][A-Za-z\d_]*$""").matches(id)
|
||||
}
|
||||
|
||||
private fun isTemplateExist(id: String): Boolean {
|
||||
return getAppProfileTemplate(id).isNotBlank()
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package me.weishu.kernelsu.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val YELLOW = Color(0xFFeed502)
|
||||
val YELLOW_LIGHT = Color(0xFFffff52)
|
||||
val SECONDARY_LIGHT = Color(0xffa9817f)
|
||||
|
||||
val YELLOW_DARK = Color(0xFFb7a400)
|
||||
val SECONDARY_DARK = Color(0xFF4c2b2b)
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package me.weishu.kernelsu.ui.theme
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = YELLOW,
|
||||
secondary = YELLOW_DARK,
|
||||
tertiary = SECONDARY_DARK
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = YELLOW,
|
||||
secondary = YELLOW_LIGHT,
|
||||
tertiary = SECONDARY_LIGHT
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun KernelSUTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
val systemUiController = rememberSystemUiController()
|
||||
SideEffect {
|
||||
systemUiController.setStatusBarColor(
|
||||
color = colorScheme.surface,
|
||||
darkIcons = !darkTheme
|
||||
)
|
||||
|
||||
// To match the App Navbar color
|
||||
systemUiController.setNavigationBarColor(
|
||||
color = colorScheme.surfaceColorAtElevation(8.dp),
|
||||
darkIcons = !darkTheme,
|
||||
)
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package me.weishu.kernelsu.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
|
||||
)
|
||||
*/
|
||||
)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package me.weishu.kernelsu.ui.util
|
||||
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
|
||||
val LocalSnackbarHost = compositionLocalOf<SnackbarHostState> {
|
||||
error("CompositionLocal LocalSnackbarController not present")
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
package me.weishu.kernelsu.ui.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.DownloadManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import me.weishu.kernelsu.ui.util.module.LatestVersionInfo
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
* @date 2023/6/22.
|
||||
*/
|
||||
@SuppressLint("Range")
|
||||
fun download(
|
||||
context: Context,
|
||||
url: String,
|
||||
fileName: String,
|
||||
description: String,
|
||||
onDownloaded: (Uri) -> Unit = {},
|
||||
onDownloading: () -> Unit = {}
|
||||
) {
|
||||
val downloadManager =
|
||||
context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
|
||||
val query = DownloadManager.Query()
|
||||
query.setFilterByStatus(DownloadManager.STATUS_RUNNING or DownloadManager.STATUS_PAUSED or DownloadManager.STATUS_PENDING)
|
||||
downloadManager.query(query).use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
val uri = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_URI))
|
||||
val localUri = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))
|
||||
val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
|
||||
val columnTitle = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_TITLE))
|
||||
if (url == uri || fileName == columnTitle) {
|
||||
if (status == DownloadManager.STATUS_RUNNING || status == DownloadManager.STATUS_PENDING) {
|
||||
onDownloading()
|
||||
return
|
||||
} else if (status == DownloadManager.STATUS_SUCCESSFUL) {
|
||||
onDownloaded(Uri.parse(localUri))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val request = DownloadManager.Request(Uri.parse(url))
|
||||
.setDestinationInExternalPublicDir(
|
||||
Environment.DIRECTORY_DOWNLOADS,
|
||||
fileName
|
||||
)
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
.setMimeType("application/zip")
|
||||
.setTitle(fileName)
|
||||
.setDescription(description)
|
||||
|
||||
downloadManager.enqueue(request)
|
||||
}
|
||||
|
||||
fun checkNewVersion(): LatestVersionInfo {
|
||||
val url = "https://api.github.com/repos/tiann/KernelSU/releases/latest"
|
||||
// default null value if failed
|
||||
val defaultValue = LatestVersionInfo()
|
||||
runCatching {
|
||||
okhttp3.OkHttpClient().newCall(okhttp3.Request.Builder().url(url).build()).execute()
|
||||
.use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
return defaultValue
|
||||
}
|
||||
val body = response.body?.string() ?: return defaultValue
|
||||
val json = org.json.JSONObject(body)
|
||||
val changelog = json.optString("body")
|
||||
|
||||
val assets = json.getJSONArray("assets")
|
||||
for (i in 0 until assets.length()) {
|
||||
val asset = assets.getJSONObject(i)
|
||||
val name = asset.getString("name")
|
||||
if (!name.endsWith(".apk")) {
|
||||
continue
|
||||
}
|
||||
|
||||
val regex = Regex("v(.+?)_(\\d+)-")
|
||||
val matchResult = regex.find(name) ?: continue
|
||||
val versionName = matchResult.groupValues[1]
|
||||
val versionCode = matchResult.groupValues[2].toInt()
|
||||
val downloadUrl = asset.getString("browser_download_url")
|
||||
|
||||
return LatestVersionInfo(
|
||||
versionCode,
|
||||
downloadUrl,
|
||||
changelog
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DownloadListener(context: Context, onDownloaded: (Uri) -> Unit) {
|
||||
DisposableEffect(context) {
|
||||
val receiver = object : BroadcastReceiver() {
|
||||
@SuppressLint("Range")
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (intent?.action == DownloadManager.ACTION_DOWNLOAD_COMPLETE) {
|
||||
val id = intent.getLongExtra(
|
||||
DownloadManager.EXTRA_DOWNLOAD_ID, -1
|
||||
)
|
||||
val query = DownloadManager.Query().setFilterById(id)
|
||||
val downloadManager =
|
||||
context?.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
val cursor = downloadManager.query(query)
|
||||
if (cursor.moveToFirst()) {
|
||||
val status = cursor.getInt(
|
||||
cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)
|
||||
)
|
||||
if (status == DownloadManager.STATUS_SUCCESSFUL) {
|
||||
val uri = cursor.getString(
|
||||
cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)
|
||||
)
|
||||
onDownloaded(Uri.parse(uri))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
context.registerReceiver(
|
||||
receiver,
|
||||
IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
|
||||
Context.RECEIVER_EXPORTED
|
||||
)
|
||||
} else {
|
||||
context.registerReceiver(
|
||||
receiver,
|
||||
IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
|
||||
)
|
||||
}
|
||||
onDispose {
|
||||
context.unregisterReceiver(receiver)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,576 @@
|
|||
package me.weishu.kernelsu.ui.util;
|
||||
/*
|
||||
* Copyright (C) 2009 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import java.text.Collator;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* An object to convert Chinese character to its corresponding pinyin string. For characters with
|
||||
* multiple possible pinyin string, only one is selected according to collator. Polyphone is not
|
||||
* supported in this implementation. This class is implemented to achieve the best runtime
|
||||
* performance and minimum runtime resources with tolerable sacrifice of accuracy. This
|
||||
* implementation highly depends on zh_CN ICU collation data and must be always synchronized with
|
||||
* ICU.
|
||||
* <p>
|
||||
* Currently this file is aligned to zh.txt in ICU 4.6
|
||||
*/
|
||||
public class HanziToPinyin {
|
||||
private static final String TAG = "HanziToPinyin";
|
||||
|
||||
// Turn on this flag when we want to check internal data structure.
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
/**
|
||||
* Unihans array.
|
||||
* <p>
|
||||
* Each unihans is the first one within same pinyin when collator is zh_CN.
|
||||
*/
|
||||
public static final char[] UNIHANS = {
|
||||
'\u963f', '\u54ce', '\u5b89', '\u80ae', '\u51f9', '\u516b',
|
||||
'\u6300', '\u6273', '\u90a6', '\u52f9', '\u9642', '\u5954',
|
||||
'\u4f3b', '\u5c44', '\u8fb9', '\u706c', '\u618b', '\u6c43',
|
||||
'\u51ab', '\u7676', '\u5cec', '\u5693', '\u5072', '\u53c2',
|
||||
'\u4ed3', '\u64a1', '\u518a', '\u5d7e', '\u66fd', '\u66fe',
|
||||
'\u5c64', '\u53c9', '\u8286', '\u8fbf', '\u4f25', '\u6284',
|
||||
'\u8f66', '\u62bb', '\u6c88', '\u6c89', '\u9637', '\u5403',
|
||||
'\u5145', '\u62bd', '\u51fa', '\u6b3b', '\u63e3', '\u5ddb',
|
||||
'\u5205', '\u5439', '\u65fe', '\u9034', '\u5472', '\u5306',
|
||||
'\u51d1', '\u7c97', '\u6c46', '\u5d14', '\u90a8', '\u6413',
|
||||
'\u5491', '\u5446', '\u4e39', '\u5f53', '\u5200', '\u561a',
|
||||
'\u6265', '\u706f', '\u6c10', '\u55f2', '\u7538', '\u5201',
|
||||
'\u7239', '\u4e01', '\u4e1f', '\u4e1c', '\u543a', '\u53be',
|
||||
'\u8011', '\u8968', '\u5428', '\u591a', '\u59b8', '\u8bf6',
|
||||
'\u5940', '\u97a5', '\u513f', '\u53d1', '\u5e06', '\u531a',
|
||||
'\u98de', '\u5206', '\u4e30', '\u8985', '\u4ecf', '\u7d11',
|
||||
'\u4f15', '\u65ee', '\u4f85', '\u7518', '\u5188', '\u768b',
|
||||
'\u6208', '\u7ed9', '\u6839', '\u522f', '\u5de5', '\u52fe',
|
||||
'\u4f30', '\u74dc', '\u4e56', '\u5173', '\u5149', '\u5f52',
|
||||
'\u4e28', '\u5459', '\u54c8', '\u548d', '\u4f44', '\u592f',
|
||||
'\u8320', '\u8bc3', '\u9ed2', '\u62eb', '\u4ea8', '\u5677',
|
||||
'\u53ff', '\u9f41', '\u4e6f', '\u82b1', '\u6000', '\u72bf',
|
||||
'\u5ddf', '\u7070', '\u660f', '\u5419', '\u4e0c', '\u52a0',
|
||||
'\u620b', '\u6c5f', '\u827d', '\u9636', '\u5dfe', '\u5755',
|
||||
'\u5182', '\u4e29', '\u51e5', '\u59e2', '\u5658', '\u519b',
|
||||
'\u5494', '\u5f00', '\u520a', '\u5ffc', '\u5c3b', '\u533c',
|
||||
'\u808e', '\u52a5', '\u7a7a', '\u62a0', '\u625d', '\u5938',
|
||||
'\u84af', '\u5bbd', '\u5321', '\u4e8f', '\u5764', '\u6269',
|
||||
'\u5783', '\u6765', '\u5170', '\u5577', '\u635e', '\u808b',
|
||||
'\u52d2', '\u5d1a', '\u5215', '\u4fe9', '\u5941', '\u826f',
|
||||
'\u64a9', '\u5217', '\u62ce', '\u5222', '\u6e9c', '\u56d6',
|
||||
'\u9f99', '\u779c', '\u565c', '\u5a08', '\u7567', '\u62a1',
|
||||
'\u7f57', '\u5463', '\u5988', '\u57cb', '\u5ada', '\u7264',
|
||||
'\u732b', '\u4e48', '\u5445', '\u95e8', '\u753f', '\u54aa',
|
||||
'\u5b80', '\u55b5', '\u4e5c', '\u6c11', '\u540d', '\u8c2c',
|
||||
'\u6478', '\u54de', '\u6bea', '\u55ef', '\u62cf', '\u8149',
|
||||
'\u56e1', '\u56d4', '\u5b6c', '\u7592', '\u5a1e', '\u6041',
|
||||
'\u80fd', '\u59ae', '\u62c8', '\u5b22', '\u9e1f', '\u634f',
|
||||
'\u56dc', '\u5b81', '\u599e', '\u519c', '\u7fba', '\u5974',
|
||||
'\u597b', '\u759f', '\u9ec1', '\u90cd', '\u5594', '\u8bb4',
|
||||
'\u5991', '\u62cd', '\u7705', '\u4e53', '\u629b', '\u5478',
|
||||
'\u55b7', '\u5309', '\u4e15', '\u56e8', '\u527d', '\u6c15',
|
||||
'\u59d8', '\u4e52', '\u948b', '\u5256', '\u4ec6', '\u4e03',
|
||||
'\u6390', '\u5343', '\u545b', '\u6084', '\u767f', '\u4eb2',
|
||||
'\u72c5', '\u828e', '\u4e18', '\u533a', '\u5cd1', '\u7f3a',
|
||||
'\u590b', '\u5465', '\u7a63', '\u5a06', '\u60f9', '\u4eba',
|
||||
'\u6254', '\u65e5', '\u8338', '\u53b9', '\u909a', '\u633c',
|
||||
'\u5827', '\u5a51', '\u77a4', '\u637c', '\u4ee8', '\u6be2',
|
||||
'\u4e09', '\u6852', '\u63bb', '\u95aa', '\u68ee', '\u50e7',
|
||||
'\u6740', '\u7b5b', '\u5c71', '\u4f24', '\u5f30', '\u5962',
|
||||
'\u7533', '\u8398', '\u6552', '\u5347', '\u5c38', '\u53ce',
|
||||
'\u4e66', '\u5237', '\u8870', '\u95e9', '\u53cc', '\u8c01',
|
||||
'\u542e', '\u8bf4', '\u53b6', '\u5fea', '\u635c', '\u82cf',
|
||||
'\u72fb', '\u590a', '\u5b59', '\u5506', '\u4ed6', '\u56fc',
|
||||
'\u574d', '\u6c64', '\u5932', '\u5fd1', '\u71a5', '\u5254',
|
||||
'\u5929', '\u65eb', '\u5e16', '\u5385', '\u56f2', '\u5077',
|
||||
'\u51f8', '\u6e4d', '\u63a8', '\u541e', '\u4e47', '\u7a75',
|
||||
'\u6b6a', '\u5f2f', '\u5c23', '\u5371', '\u6637', '\u7fc1',
|
||||
'\u631d', '\u4e4c', '\u5915', '\u8672', '\u4eda', '\u4e61',
|
||||
'\u7071', '\u4e9b', '\u5fc3', '\u661f', '\u51f6', '\u4f11',
|
||||
'\u5401', '\u5405', '\u524a', '\u5743', '\u4e2b', '\u6079',
|
||||
'\u592e', '\u5e7a', '\u503b', '\u4e00', '\u56d9', '\u5e94',
|
||||
'\u54df', '\u4f63', '\u4f18', '\u625c', '\u56e6', '\u66f0',
|
||||
'\u6655', '\u7b60', '\u7b7c', '\u5e00', '\u707d', '\u5142',
|
||||
'\u5328', '\u50ae', '\u5219', '\u8d3c', '\u600e', '\u5897',
|
||||
'\u624e', '\u635a', '\u6cbe', '\u5f20', '\u957f', '\u9577',
|
||||
'\u4f4b', '\u8707', '\u8d1e', '\u4e89', '\u4e4b', '\u5cd9',
|
||||
'\u5ea2', '\u4e2d', '\u5dde', '\u6731', '\u6293', '\u62fd',
|
||||
'\u4e13', '\u5986', '\u96b9', '\u5b92', '\u5353', '\u4e72',
|
||||
'\u5b97', '\u90b9', '\u79df', '\u94bb', '\u539c', '\u5c0a',
|
||||
'\u6628', '\u5159', '\u9fc3', '\u9fc4',};
|
||||
|
||||
/**
|
||||
* Pinyin array.
|
||||
* <p>
|
||||
* Each pinyin is corresponding to unihans of same
|
||||
* offset in the unihans array.
|
||||
*/
|
||||
public static final byte[][] PINYINS = {
|
||||
{65, 0, 0, 0, 0, 0}, {65, 73, 0, 0, 0, 0},
|
||||
{65, 78, 0, 0, 0, 0}, {65, 78, 71, 0, 0, 0},
|
||||
{65, 79, 0, 0, 0, 0}, {66, 65, 0, 0, 0, 0},
|
||||
{66, 65, 73, 0, 0, 0}, {66, 65, 78, 0, 0, 0},
|
||||
{66, 65, 78, 71, 0, 0}, {66, 65, 79, 0, 0, 0},
|
||||
{66, 69, 73, 0, 0, 0}, {66, 69, 78, 0, 0, 0},
|
||||
{66, 69, 78, 71, 0, 0}, {66, 73, 0, 0, 0, 0},
|
||||
{66, 73, 65, 78, 0, 0}, {66, 73, 65, 79, 0, 0},
|
||||
{66, 73, 69, 0, 0, 0}, {66, 73, 78, 0, 0, 0},
|
||||
{66, 73, 78, 71, 0, 0}, {66, 79, 0, 0, 0, 0},
|
||||
{66, 85, 0, 0, 0, 0}, {67, 65, 0, 0, 0, 0},
|
||||
{67, 65, 73, 0, 0, 0}, {67, 65, 78, 0, 0, 0},
|
||||
{67, 65, 78, 71, 0, 0}, {67, 65, 79, 0, 0, 0},
|
||||
{67, 69, 0, 0, 0, 0}, {67, 69, 78, 0, 0, 0},
|
||||
{67, 69, 78, 71, 0, 0}, {90, 69, 78, 71, 0, 0},
|
||||
{67, 69, 78, 71, 0, 0}, {67, 72, 65, 0, 0, 0},
|
||||
{67, 72, 65, 73, 0, 0}, {67, 72, 65, 78, 0, 0},
|
||||
{67, 72, 65, 78, 71, 0}, {67, 72, 65, 79, 0, 0},
|
||||
{67, 72, 69, 0, 0, 0}, {67, 72, 69, 78, 0, 0},
|
||||
{83, 72, 69, 78, 0, 0}, {67, 72, 69, 78, 0, 0},
|
||||
{67, 72, 69, 78, 71, 0}, {67, 72, 73, 0, 0, 0},
|
||||
{67, 72, 79, 78, 71, 0}, {67, 72, 79, 85, 0, 0},
|
||||
{67, 72, 85, 0, 0, 0}, {67, 72, 85, 65, 0, 0},
|
||||
{67, 72, 85, 65, 73, 0}, {67, 72, 85, 65, 78, 0},
|
||||
{67, 72, 85, 65, 78, 71}, {67, 72, 85, 73, 0, 0},
|
||||
{67, 72, 85, 78, 0, 0}, {67, 72, 85, 79, 0, 0},
|
||||
{67, 73, 0, 0, 0, 0}, {67, 79, 78, 71, 0, 0},
|
||||
{67, 79, 85, 0, 0, 0}, {67, 85, 0, 0, 0, 0},
|
||||
{67, 85, 65, 78, 0, 0}, {67, 85, 73, 0, 0, 0},
|
||||
{67, 85, 78, 0, 0, 0}, {67, 85, 79, 0, 0, 0},
|
||||
{68, 65, 0, 0, 0, 0}, {68, 65, 73, 0, 0, 0},
|
||||
{68, 65, 78, 0, 0, 0}, {68, 65, 78, 71, 0, 0},
|
||||
{68, 65, 79, 0, 0, 0}, {68, 69, 0, 0, 0, 0},
|
||||
{68, 69, 78, 0, 0, 0}, {68, 69, 78, 71, 0, 0},
|
||||
{68, 73, 0, 0, 0, 0}, {68, 73, 65, 0, 0, 0},
|
||||
{68, 73, 65, 78, 0, 0}, {68, 73, 65, 79, 0, 0},
|
||||
{68, 73, 69, 0, 0, 0}, {68, 73, 78, 71, 0, 0},
|
||||
{68, 73, 85, 0, 0, 0}, {68, 79, 78, 71, 0, 0},
|
||||
{68, 79, 85, 0, 0, 0}, {68, 85, 0, 0, 0, 0},
|
||||
{68, 85, 65, 78, 0, 0}, {68, 85, 73, 0, 0, 0},
|
||||
{68, 85, 78, 0, 0, 0}, {68, 85, 79, 0, 0, 0},
|
||||
{69, 0, 0, 0, 0, 0}, {69, 73, 0, 0, 0, 0},
|
||||
{69, 78, 0, 0, 0, 0}, {69, 78, 71, 0, 0, 0},
|
||||
{69, 82, 0, 0, 0, 0}, {70, 65, 0, 0, 0, 0},
|
||||
{70, 65, 78, 0, 0, 0}, {70, 65, 78, 71, 0, 0},
|
||||
{70, 69, 73, 0, 0, 0}, {70, 69, 78, 0, 0, 0},
|
||||
{70, 69, 78, 71, 0, 0}, {70, 73, 65, 79, 0, 0},
|
||||
{70, 79, 0, 0, 0, 0}, {70, 79, 85, 0, 0, 0},
|
||||
{70, 85, 0, 0, 0, 0}, {71, 65, 0, 0, 0, 0},
|
||||
{71, 65, 73, 0, 0, 0}, {71, 65, 78, 0, 0, 0},
|
||||
{71, 65, 78, 71, 0, 0}, {71, 65, 79, 0, 0, 0},
|
||||
{71, 69, 0, 0, 0, 0}, {71, 69, 73, 0, 0, 0},
|
||||
{71, 69, 78, 0, 0, 0}, {71, 69, 78, 71, 0, 0},
|
||||
{71, 79, 78, 71, 0, 0}, {71, 79, 85, 0, 0, 0},
|
||||
{71, 85, 0, 0, 0, 0}, {71, 85, 65, 0, 0, 0},
|
||||
{71, 85, 65, 73, 0, 0}, {71, 85, 65, 78, 0, 0},
|
||||
{71, 85, 65, 78, 71, 0}, {71, 85, 73, 0, 0, 0},
|
||||
{71, 85, 78, 0, 0, 0}, {71, 85, 79, 0, 0, 0},
|
||||
{72, 65, 0, 0, 0, 0}, {72, 65, 73, 0, 0, 0},
|
||||
{72, 65, 78, 0, 0, 0}, {72, 65, 78, 71, 0, 0},
|
||||
{72, 65, 79, 0, 0, 0}, {72, 69, 0, 0, 0, 0},
|
||||
{72, 69, 73, 0, 0, 0}, {72, 69, 78, 0, 0, 0},
|
||||
{72, 69, 78, 71, 0, 0}, {72, 77, 0, 0, 0, 0},
|
||||
{72, 79, 78, 71, 0, 0}, {72, 79, 85, 0, 0, 0},
|
||||
{72, 85, 0, 0, 0, 0}, {72, 85, 65, 0, 0, 0},
|
||||
{72, 85, 65, 73, 0, 0}, {72, 85, 65, 78, 0, 0},
|
||||
{72, 85, 65, 78, 71, 0}, {72, 85, 73, 0, 0, 0},
|
||||
{72, 85, 78, 0, 0, 0}, {72, 85, 79, 0, 0, 0},
|
||||
{74, 73, 0, 0, 0, 0}, {74, 73, 65, 0, 0, 0},
|
||||
{74, 73, 65, 78, 0, 0}, {74, 73, 65, 78, 71, 0},
|
||||
{74, 73, 65, 79, 0, 0}, {74, 73, 69, 0, 0, 0},
|
||||
{74, 73, 78, 0, 0, 0}, {74, 73, 78, 71, 0, 0},
|
||||
{74, 73, 79, 78, 71, 0}, {74, 73, 85, 0, 0, 0},
|
||||
{74, 85, 0, 0, 0, 0}, {74, 85, 65, 78, 0, 0},
|
||||
{74, 85, 69, 0, 0, 0}, {74, 85, 78, 0, 0, 0},
|
||||
{75, 65, 0, 0, 0, 0}, {75, 65, 73, 0, 0, 0},
|
||||
{75, 65, 78, 0, 0, 0}, {75, 65, 78, 71, 0, 0},
|
||||
{75, 65, 79, 0, 0, 0}, {75, 69, 0, 0, 0, 0},
|
||||
{75, 69, 78, 0, 0, 0}, {75, 69, 78, 71, 0, 0},
|
||||
{75, 79, 78, 71, 0, 0}, {75, 79, 85, 0, 0, 0},
|
||||
{75, 85, 0, 0, 0, 0}, {75, 85, 65, 0, 0, 0},
|
||||
{75, 85, 65, 73, 0, 0}, {75, 85, 65, 78, 0, 0},
|
||||
{75, 85, 65, 78, 71, 0}, {75, 85, 73, 0, 0, 0},
|
||||
{75, 85, 78, 0, 0, 0}, {75, 85, 79, 0, 0, 0},
|
||||
{76, 65, 0, 0, 0, 0}, {76, 65, 73, 0, 0, 0},
|
||||
{76, 65, 78, 0, 0, 0}, {76, 65, 78, 71, 0, 0},
|
||||
{76, 65, 79, 0, 0, 0}, {76, 69, 0, 0, 0, 0},
|
||||
{76, 69, 73, 0, 0, 0}, {76, 69, 78, 71, 0, 0},
|
||||
{76, 73, 0, 0, 0, 0}, {76, 73, 65, 0, 0, 0},
|
||||
{76, 73, 65, 78, 0, 0}, {76, 73, 65, 78, 71, 0},
|
||||
{76, 73, 65, 79, 0, 0}, {76, 73, 69, 0, 0, 0},
|
||||
{76, 73, 78, 0, 0, 0}, {76, 73, 78, 71, 0, 0},
|
||||
{76, 73, 85, 0, 0, 0}, {76, 79, 0, 0, 0, 0},
|
||||
{76, 79, 78, 71, 0, 0}, {76, 79, 85, 0, 0, 0},
|
||||
{76, 85, 0, 0, 0, 0}, {76, 85, 65, 78, 0, 0},
|
||||
{76, 85, 69, 0, 0, 0}, {76, 85, 78, 0, 0, 0},
|
||||
{76, 85, 79, 0, 0, 0}, {77, 0, 0, 0, 0, 0},
|
||||
{77, 65, 0, 0, 0, 0}, {77, 65, 73, 0, 0, 0},
|
||||
{77, 65, 78, 0, 0, 0}, {77, 65, 78, 71, 0, 0},
|
||||
{77, 65, 79, 0, 0, 0}, {77, 69, 0, 0, 0, 0},
|
||||
{77, 69, 73, 0, 0, 0}, {77, 69, 78, 0, 0, 0},
|
||||
{77, 69, 78, 71, 0, 0}, {77, 73, 0, 0, 0, 0},
|
||||
{77, 73, 65, 78, 0, 0}, {77, 73, 65, 79, 0, 0},
|
||||
{77, 73, 69, 0, 0, 0}, {77, 73, 78, 0, 0, 0},
|
||||
{77, 73, 78, 71, 0, 0}, {77, 73, 85, 0, 0, 0},
|
||||
{77, 79, 0, 0, 0, 0}, {77, 79, 85, 0, 0, 0},
|
||||
{77, 85, 0, 0, 0, 0}, {78, 0, 0, 0, 0, 0},
|
||||
{78, 65, 0, 0, 0, 0}, {78, 65, 73, 0, 0, 0},
|
||||
{78, 65, 78, 0, 0, 0}, {78, 65, 78, 71, 0, 0},
|
||||
{78, 65, 79, 0, 0, 0}, {78, 69, 0, 0, 0, 0},
|
||||
{78, 69, 73, 0, 0, 0}, {78, 69, 78, 0, 0, 0},
|
||||
{78, 69, 78, 71, 0, 0}, {78, 73, 0, 0, 0, 0},
|
||||
{78, 73, 65, 78, 0, 0}, {78, 73, 65, 78, 71, 0},
|
||||
{78, 73, 65, 79, 0, 0}, {78, 73, 69, 0, 0, 0},
|
||||
{78, 73, 78, 0, 0, 0}, {78, 73, 78, 71, 0, 0},
|
||||
{78, 73, 85, 0, 0, 0}, {78, 79, 78, 71, 0, 0},
|
||||
{78, 79, 85, 0, 0, 0}, {78, 85, 0, 0, 0, 0},
|
||||
{78, 85, 65, 78, 0, 0}, {78, 85, 69, 0, 0, 0},
|
||||
{78, 85, 78, 0, 0, 0}, {78, 85, 79, 0, 0, 0},
|
||||
{79, 0, 0, 0, 0, 0}, {79, 85, 0, 0, 0, 0},
|
||||
{80, 65, 0, 0, 0, 0}, {80, 65, 73, 0, 0, 0},
|
||||
{80, 65, 78, 0, 0, 0}, {80, 65, 78, 71, 0, 0},
|
||||
{80, 65, 79, 0, 0, 0}, {80, 69, 73, 0, 0, 0},
|
||||
{80, 69, 78, 0, 0, 0}, {80, 69, 78, 71, 0, 0},
|
||||
{80, 73, 0, 0, 0, 0}, {80, 73, 65, 78, 0, 0},
|
||||
{80, 73, 65, 79, 0, 0}, {80, 73, 69, 0, 0, 0},
|
||||
{80, 73, 78, 0, 0, 0}, {80, 73, 78, 71, 0, 0},
|
||||
{80, 79, 0, 0, 0, 0}, {80, 79, 85, 0, 0, 0},
|
||||
{80, 85, 0, 0, 0, 0}, {81, 73, 0, 0, 0, 0},
|
||||
{81, 73, 65, 0, 0, 0}, {81, 73, 65, 78, 0, 0},
|
||||
{81, 73, 65, 78, 71, 0}, {81, 73, 65, 79, 0, 0},
|
||||
{81, 73, 69, 0, 0, 0}, {81, 73, 78, 0, 0, 0},
|
||||
{81, 73, 78, 71, 0, 0}, {81, 73, 79, 78, 71, 0},
|
||||
{81, 73, 85, 0, 0, 0}, {81, 85, 0, 0, 0, 0},
|
||||
{81, 85, 65, 78, 0, 0}, {81, 85, 69, 0, 0, 0},
|
||||
{81, 85, 78, 0, 0, 0}, {82, 65, 78, 0, 0, 0},
|
||||
{82, 65, 78, 71, 0, 0}, {82, 65, 79, 0, 0, 0},
|
||||
{82, 69, 0, 0, 0, 0}, {82, 69, 78, 0, 0, 0},
|
||||
{82, 69, 78, 71, 0, 0}, {82, 73, 0, 0, 0, 0},
|
||||
{82, 79, 78, 71, 0, 0}, {82, 79, 85, 0, 0, 0},
|
||||
{82, 85, 0, 0, 0, 0}, {82, 85, 65, 0, 0, 0},
|
||||
{82, 85, 65, 78, 0, 0}, {82, 85, 73, 0, 0, 0},
|
||||
{82, 85, 78, 0, 0, 0}, {82, 85, 79, 0, 0, 0},
|
||||
{83, 65, 0, 0, 0, 0}, {83, 65, 73, 0, 0, 0},
|
||||
{83, 65, 78, 0, 0, 0}, {83, 65, 78, 71, 0, 0},
|
||||
{83, 65, 79, 0, 0, 0}, {83, 69, 0, 0, 0, 0},
|
||||
{83, 69, 78, 0, 0, 0}, {83, 69, 78, 71, 0, 0},
|
||||
{83, 72, 65, 0, 0, 0}, {83, 72, 65, 73, 0, 0},
|
||||
{83, 72, 65, 78, 0, 0}, {83, 72, 65, 78, 71, 0},
|
||||
{83, 72, 65, 79, 0, 0}, {83, 72, 69, 0, 0, 0},
|
||||
{83, 72, 69, 78, 0, 0}, {88, 73, 78, 0, 0, 0},
|
||||
{83, 72, 69, 78, 0, 0}, {83, 72, 69, 78, 71, 0},
|
||||
{83, 72, 73, 0, 0, 0}, {83, 72, 79, 85, 0, 0},
|
||||
{83, 72, 85, 0, 0, 0}, {83, 72, 85, 65, 0, 0},
|
||||
{83, 72, 85, 65, 73, 0}, {83, 72, 85, 65, 78, 0},
|
||||
{83, 72, 85, 65, 78, 71}, {83, 72, 85, 73, 0, 0},
|
||||
{83, 72, 85, 78, 0, 0}, {83, 72, 85, 79, 0, 0},
|
||||
{83, 73, 0, 0, 0, 0}, {83, 79, 78, 71, 0, 0},
|
||||
{83, 79, 85, 0, 0, 0}, {83, 85, 0, 0, 0, 0},
|
||||
{83, 85, 65, 78, 0, 0}, {83, 85, 73, 0, 0, 0},
|
||||
{83, 85, 78, 0, 0, 0}, {83, 85, 79, 0, 0, 0},
|
||||
{84, 65, 0, 0, 0, 0}, {84, 65, 73, 0, 0, 0},
|
||||
{84, 65, 78, 0, 0, 0}, {84, 65, 78, 71, 0, 0},
|
||||
{84, 65, 79, 0, 0, 0}, {84, 69, 0, 0, 0, 0},
|
||||
{84, 69, 78, 71, 0, 0}, {84, 73, 0, 0, 0, 0},
|
||||
{84, 73, 65, 78, 0, 0}, {84, 73, 65, 79, 0, 0},
|
||||
{84, 73, 69, 0, 0, 0}, {84, 73, 78, 71, 0, 0},
|
||||
{84, 79, 78, 71, 0, 0}, {84, 79, 85, 0, 0, 0},
|
||||
{84, 85, 0, 0, 0, 0}, {84, 85, 65, 78, 0, 0},
|
||||
{84, 85, 73, 0, 0, 0}, {84, 85, 78, 0, 0, 0},
|
||||
{84, 85, 79, 0, 0, 0}, {87, 65, 0, 0, 0, 0},
|
||||
{87, 65, 73, 0, 0, 0}, {87, 65, 78, 0, 0, 0},
|
||||
{87, 65, 78, 71, 0, 0}, {87, 69, 73, 0, 0, 0},
|
||||
{87, 69, 78, 0, 0, 0}, {87, 69, 78, 71, 0, 0},
|
||||
{87, 79, 0, 0, 0, 0}, {87, 85, 0, 0, 0, 0},
|
||||
{88, 73, 0, 0, 0, 0}, {88, 73, 65, 0, 0, 0},
|
||||
{88, 73, 65, 78, 0, 0}, {88, 73, 65, 78, 71, 0},
|
||||
{88, 73, 65, 79, 0, 0}, {88, 73, 69, 0, 0, 0},
|
||||
{88, 73, 78, 0, 0, 0}, {88, 73, 78, 71, 0, 0},
|
||||
{88, 73, 79, 78, 71, 0}, {88, 73, 85, 0, 0, 0},
|
||||
{88, 85, 0, 0, 0, 0}, {88, 85, 65, 78, 0, 0},
|
||||
{88, 85, 69, 0, 0, 0}, {88, 85, 78, 0, 0, 0},
|
||||
{89, 65, 0, 0, 0, 0}, {89, 65, 78, 0, 0, 0},
|
||||
{89, 65, 78, 71, 0, 0}, {89, 65, 79, 0, 0, 0},
|
||||
{89, 69, 0, 0, 0, 0}, {89, 73, 0, 0, 0, 0},
|
||||
{89, 73, 78, 0, 0, 0}, {89, 73, 78, 71, 0, 0},
|
||||
{89, 79, 0, 0, 0, 0}, {89, 79, 78, 71, 0, 0},
|
||||
{89, 79, 85, 0, 0, 0}, {89, 85, 0, 0, 0, 0},
|
||||
{89, 85, 65, 78, 0, 0}, {89, 85, 69, 0, 0, 0},
|
||||
{89, 85, 78, 0, 0, 0}, {74, 85, 78, 0, 0, 0},
|
||||
{89, 85, 78, 0, 0, 0}, {90, 65, 0, 0, 0, 0},
|
||||
{90, 65, 73, 0, 0, 0}, {90, 65, 78, 0, 0, 0},
|
||||
{90, 65, 78, 71, 0, 0}, {90, 65, 79, 0, 0, 0},
|
||||
{90, 69, 0, 0, 0, 0}, {90, 69, 73, 0, 0, 0},
|
||||
{90, 69, 78, 0, 0, 0}, {90, 69, 78, 71, 0, 0},
|
||||
{90, 72, 65, 0, 0, 0}, {90, 72, 65, 73, 0, 0},
|
||||
{90, 72, 65, 78, 0, 0}, {90, 72, 65, 78, 71, 0},
|
||||
{67, 72, 65, 78, 71, 0}, {90, 72, 65, 78, 71, 0},
|
||||
{90, 72, 65, 79, 0, 0}, {90, 72, 69, 0, 0, 0},
|
||||
{90, 72, 69, 78, 0, 0}, {90, 72, 69, 78, 71, 0},
|
||||
{90, 72, 73, 0, 0, 0}, {83, 72, 73, 0, 0, 0},
|
||||
{90, 72, 73, 0, 0, 0}, {90, 72, 79, 78, 71, 0},
|
||||
{90, 72, 79, 85, 0, 0}, {90, 72, 85, 0, 0, 0},
|
||||
{90, 72, 85, 65, 0, 0}, {90, 72, 85, 65, 73, 0},
|
||||
{90, 72, 85, 65, 78, 0}, {90, 72, 85, 65, 78, 71},
|
||||
{90, 72, 85, 73, 0, 0}, {90, 72, 85, 78, 0, 0},
|
||||
{90, 72, 85, 79, 0, 0}, {90, 73, 0, 0, 0, 0},
|
||||
{90, 79, 78, 71, 0, 0}, {90, 79, 85, 0, 0, 0},
|
||||
{90, 85, 0, 0, 0, 0}, {90, 85, 65, 78, 0, 0},
|
||||
{90, 85, 73, 0, 0, 0}, {90, 85, 78, 0, 0, 0},
|
||||
{90, 85, 79, 0, 0, 0}, {0, 0, 0, 0, 0, 0},
|
||||
{83, 72, 65, 78, 0, 0}, {0, 0, 0, 0, 0, 0},};
|
||||
|
||||
/**
|
||||
* First and last Chinese character with known Pinyin according to zh collation
|
||||
*/
|
||||
private static final String FIRST_PINYIN_UNIHAN = "\u963F";
|
||||
private static final String LAST_PINYIN_UNIHAN = "\u9FFF";
|
||||
|
||||
private static final Collator COLLATOR = Collator.getInstance(Locale.CHINA);
|
||||
|
||||
private static HanziToPinyin sInstance;
|
||||
private final boolean mHasChinaCollator;
|
||||
|
||||
public static class Token {
|
||||
/**
|
||||
* Separator between target string for each source char
|
||||
*/
|
||||
public static final String SEPARATOR = " ";
|
||||
|
||||
public static final int LATIN = 1;
|
||||
public static final int PINYIN = 2;
|
||||
public static final int UNKNOWN = 3;
|
||||
|
||||
public Token() {
|
||||
}
|
||||
|
||||
public Token(int type, String source, String target) {
|
||||
this.type = type;
|
||||
this.source = source;
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type of this token, ASCII, PINYIN or UNKNOWN.
|
||||
*/
|
||||
public int type;
|
||||
/**
|
||||
* Original string before translation.
|
||||
*/
|
||||
public String source;
|
||||
/**
|
||||
* Translated string of source. For Han, target is corresponding Pinyin. Otherwise target is
|
||||
* original string in source.
|
||||
*/
|
||||
public String target;
|
||||
}
|
||||
|
||||
protected HanziToPinyin(boolean hasChinaCollator) {
|
||||
mHasChinaCollator = hasChinaCollator;
|
||||
}
|
||||
|
||||
public static HanziToPinyin getInstance() {
|
||||
synchronized (HanziToPinyin.class) {
|
||||
if (sInstance != null) {
|
||||
return sInstance;
|
||||
}
|
||||
// Check if zh_CN collation data is available
|
||||
final Locale[] locale = Collator.getAvailableLocales();
|
||||
for (Locale value : locale) {
|
||||
if (value.equals(Locale.CHINA) || value.getLanguage().contains("zh")) {
|
||||
// Do self validation just once.
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Self validation. Result: " + doSelfValidation());
|
||||
}
|
||||
sInstance = new HanziToPinyin(true);
|
||||
return sInstance;
|
||||
}
|
||||
}
|
||||
if (sInstance == null){//这个判断是用于处理国产ROM的兼容性问题
|
||||
if (Locale.CHINA.equals(Locale.getDefault())){
|
||||
sInstance = new HanziToPinyin(true);
|
||||
return sInstance;
|
||||
}
|
||||
}
|
||||
Log.w(TAG, "There is no Chinese collator, HanziToPinyin is disabled");
|
||||
sInstance = new HanziToPinyin(false);
|
||||
return sInstance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if our internal table has some wrong value.
|
||||
*
|
||||
* @return true when the table looks correct.
|
||||
*/
|
||||
private static boolean doSelfValidation() {
|
||||
char lastChar = UNIHANS[0];
|
||||
String lastString = Character.toString(lastChar);
|
||||
for (char c : UNIHANS) {
|
||||
if (lastChar == c) {
|
||||
continue;
|
||||
}
|
||||
final String curString = Character.toString(c);
|
||||
int cmp = COLLATOR.compare(lastString, curString);
|
||||
if (cmp >= 0) {
|
||||
Log.e(TAG, "Internal error in Unihan table. " + "The last string \"" + lastString
|
||||
+ "\" is greater than current string \"" + curString + "\".");
|
||||
return false;
|
||||
}
|
||||
lastString = curString;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private Token getToken(char character) {
|
||||
Token token = new Token();
|
||||
final String letter = Character.toString(character);
|
||||
token.source = letter;
|
||||
int offset = -1;
|
||||
int cmp;
|
||||
if (character < 256) {
|
||||
token.type = Token.LATIN;
|
||||
token.target = letter;
|
||||
return token;
|
||||
} else {
|
||||
cmp = COLLATOR.compare(letter, FIRST_PINYIN_UNIHAN);
|
||||
if (cmp < 0) {
|
||||
token.type = Token.UNKNOWN;
|
||||
token.target = letter;
|
||||
return token;
|
||||
} else if (cmp == 0) {
|
||||
token.type = Token.PINYIN;
|
||||
offset = 0;
|
||||
} else {
|
||||
cmp = COLLATOR.compare(letter, LAST_PINYIN_UNIHAN);
|
||||
if (cmp > 0) {
|
||||
token.type = Token.UNKNOWN;
|
||||
token.target = letter;
|
||||
return token;
|
||||
} else if (cmp == 0) {
|
||||
token.type = Token.PINYIN;
|
||||
offset = UNIHANS.length - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
token.type = Token.PINYIN;
|
||||
if (offset < 0) {
|
||||
int begin = 0;
|
||||
int end = UNIHANS.length - 1;
|
||||
while (begin <= end) {
|
||||
offset = (begin + end) / 2;
|
||||
final String unihan = Character.toString(UNIHANS[offset]);
|
||||
cmp = COLLATOR.compare(letter, unihan);
|
||||
if (cmp == 0) {
|
||||
break;
|
||||
} else if (cmp > 0) {
|
||||
begin = offset + 1;
|
||||
} else {
|
||||
end = offset - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cmp < 0) {
|
||||
offset--;
|
||||
}
|
||||
StringBuilder pinyin = new StringBuilder();
|
||||
for (int j = 0; j < PINYINS[offset].length && PINYINS[offset][j] != 0; j++) {
|
||||
pinyin.append((char) PINYINS[offset][j]);
|
||||
}
|
||||
token.target = pinyin.toString();
|
||||
if (TextUtils.isEmpty(token.target)) {
|
||||
token.type = Token.UNKNOWN;
|
||||
token.target = token.source;
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the input to a array of tokens. The sequence of ASCII or Unknown characters without
|
||||
* space will be put into a Token, One Hanzi character which has pinyin will be treated as a
|
||||
* Token. If these is no China collator, the empty token array is returned.
|
||||
*/
|
||||
public ArrayList<Token> get(final String input) {
|
||||
ArrayList<Token> tokens = new ArrayList<>();
|
||||
if (!mHasChinaCollator || TextUtils.isEmpty(input)) {
|
||||
// return empty tokens.
|
||||
return tokens;
|
||||
}
|
||||
final int inputLength = input.length();
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
int tokenType = Token.LATIN;
|
||||
// Go through the input, create a new token when
|
||||
// a. Token type changed
|
||||
// b. Get the Pinyin of current charater.
|
||||
// c. current character is space.
|
||||
for (int i = 0; i < inputLength; i++) {
|
||||
final char character = input.charAt(i);
|
||||
if (character == ' ') {
|
||||
if (sb.length() > 0) {
|
||||
addToken(sb, tokens, tokenType);
|
||||
}
|
||||
} else if (character < 256) {
|
||||
if (tokenType != Token.LATIN && sb.length() > 0) {
|
||||
addToken(sb, tokens, tokenType);
|
||||
}
|
||||
tokenType = Token.LATIN;
|
||||
sb.append(character);
|
||||
} else {
|
||||
Token t = getToken(character);
|
||||
if (t.type == Token.PINYIN) {
|
||||
if (sb.length() > 0) {
|
||||
addToken(sb, tokens, tokenType);
|
||||
}
|
||||
tokens.add(t);
|
||||
tokenType = Token.PINYIN;
|
||||
} else {
|
||||
if (tokenType != t.type && sb.length() > 0) {
|
||||
addToken(sb, tokens, tokenType);
|
||||
}
|
||||
tokenType = t.type;
|
||||
sb.append(character);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (sb.length() > 0) {
|
||||
addToken(sb, tokens, tokenType);
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
private void addToken(
|
||||
final StringBuilder sb, final ArrayList<Token> tokens, final int tokenType) {
|
||||
String str = sb.toString();
|
||||
tokens.add(new Token(tokenType, str, str));
|
||||
sb.setLength(0);
|
||||
}
|
||||
|
||||
public String toPinyinString(String string) {
|
||||
if (string == null) {
|
||||
return null;
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
ArrayList<Token> tokens = get(string);
|
||||
for (Token token : tokens) {
|
||||
sb.append(token.target);
|
||||
}
|
||||
return sb.toString().toLowerCase();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
package me.weishu.kernelsu.ui.util
|
||||
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLayoutResult
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import java.util.regex.Pattern
|
||||
|
||||
@Composable
|
||||
fun LinkifyText(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val layoutResult = remember {
|
||||
mutableStateOf<TextLayoutResult?>(null)
|
||||
}
|
||||
val linksList = extractUrls(text)
|
||||
val annotatedString = buildAnnotatedString {
|
||||
append(text)
|
||||
linksList.forEach {
|
||||
addStyle(
|
||||
style = SpanStyle(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
textDecoration = TextDecoration.Underline
|
||||
),
|
||||
start = it.start,
|
||||
end = it.end
|
||||
)
|
||||
addStringAnnotation(
|
||||
tag = "URL",
|
||||
annotation = it.url,
|
||||
start = it.start,
|
||||
end = it.end
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = annotatedString,
|
||||
modifier = modifier.pointerInput(Unit) {
|
||||
detectTapGestures { offsetPosition ->
|
||||
layoutResult.value?.let {
|
||||
val position = it.getOffsetForPosition(offsetPosition)
|
||||
annotatedString.getStringAnnotations(position, position).firstOrNull()
|
||||
?.let { result ->
|
||||
if (result.tag == "URL") {
|
||||
uriHandler.openUri(result.item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onTextLayout = { layoutResult.value = it }
|
||||
)
|
||||
}
|
||||
|
||||
private val urlPattern: Pattern = Pattern.compile(
|
||||
"(?:^|[\\W])((ht|f)tp(s?):\\/\\/|www\\.)"
|
||||
+ "(([\\w\\-]+\\.){1,}?([\\w\\-.~]+\\/?)*"
|
||||
+ "[\\p{Alnum}.,%_=?&#\\-+()\\[\\]\\*$~@!:/{};']*)",
|
||||
Pattern.CASE_INSENSITIVE or Pattern.MULTILINE or Pattern.DOTALL
|
||||
)
|
||||
|
||||
private data class LinkInfo(
|
||||
val url: String,
|
||||
val start: Int,
|
||||
val end: Int
|
||||
)
|
||||
|
||||
private fun extractUrls(text: String): List<LinkInfo> = buildList {
|
||||
val matcher = urlPattern.matcher(text)
|
||||
while (matcher.find()) {
|
||||
val matchStart = matcher.start(1)
|
||||
val matchEnd = matcher.end()
|
||||
val url = text.substring(matchStart, matchEnd).replaceFirst("http://", "https://")
|
||||
add(LinkInfo(url, matchStart, matchEnd))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,428 @@
|
|||
package me.weishu.kernelsu.ui.util
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.Parcelable
|
||||
import android.os.SystemClock
|
||||
import android.provider.OpenableColumns
|
||||
import android.util.Log
|
||||
import com.topjohnwu.superuser.CallbackList
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import me.weishu.kernelsu.BuildConfig
|
||||
import me.weishu.kernelsu.Natives
|
||||
import me.weishu.kernelsu.ksuApp
|
||||
import org.json.JSONArray
|
||||
import java.io.File
|
||||
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
* @date 2023/1/1.
|
||||
*/
|
||||
private const val TAG = "KsuCli"
|
||||
|
||||
private fun getKsuDaemonPath(): String {
|
||||
return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libksud.so"
|
||||
}
|
||||
|
||||
object KsuCli {
|
||||
val SHELL: Shell = createRootShell()
|
||||
val GLOBAL_MNT_SHELL: Shell = createRootShell(true)
|
||||
}
|
||||
|
||||
fun getRootShell(globalMnt: Boolean = false): Shell {
|
||||
return if (globalMnt) KsuCli.GLOBAL_MNT_SHELL else {
|
||||
KsuCli.SHELL
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T> withNewRootShell(
|
||||
globalMnt: Boolean = false,
|
||||
block: Shell.() -> T
|
||||
): T {
|
||||
return createRootShell(globalMnt).use(block)
|
||||
}
|
||||
|
||||
fun getFileNameFromUri(context: Context, uri: Uri): String? {
|
||||
var fileName: String? = null
|
||||
val contentResolver: ContentResolver = context.contentResolver
|
||||
val cursor: Cursor? = contentResolver.query(uri, null, null, null, null)
|
||||
cursor?.use {
|
||||
if (it.moveToFirst()) {
|
||||
fileName = it.getString(it.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
|
||||
}
|
||||
}
|
||||
return fileName
|
||||
}
|
||||
|
||||
fun createRootShell(globalMnt: Boolean = false): Shell {
|
||||
Shell.enableVerboseLogging = BuildConfig.DEBUG
|
||||
val builder = Shell.Builder.create()
|
||||
return try {
|
||||
if (globalMnt) {
|
||||
builder.build(getKsuDaemonPath(), "debug", "su", "-g")
|
||||
} else {
|
||||
builder.build(getKsuDaemonPath(), "debug", "su")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.w(TAG, "ksu failed: ", e)
|
||||
try {
|
||||
if (globalMnt) {
|
||||
builder.build("su")
|
||||
} else {
|
||||
builder.build("su", "-mm")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "su failed: ", e)
|
||||
builder.build("sh")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun execKsud(args: String, newShell: Boolean = false): Boolean {
|
||||
return if (newShell) {
|
||||
withNewRootShell {
|
||||
ShellUtils.fastCmdResult(this, "${getKsuDaemonPath()} $args")
|
||||
}
|
||||
} else {
|
||||
ShellUtils.fastCmdResult(getRootShell(), "${getKsuDaemonPath()} $args")
|
||||
}
|
||||
}
|
||||
|
||||
fun install() {
|
||||
val start = SystemClock.elapsedRealtime()
|
||||
val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libmagiskboot.so").absolutePath
|
||||
val result = execKsud("install --magiskboot $magiskboot", true)
|
||||
Log.w(TAG, "install result: $result, cost: ${SystemClock.elapsedRealtime() - start}ms")
|
||||
}
|
||||
|
||||
fun listModules(): String {
|
||||
val shell = getRootShell()
|
||||
|
||||
val out =
|
||||
shell.newJob().add("${getKsuDaemonPath()} module list").to(ArrayList(), null).exec().out
|
||||
return out.joinToString("\n").ifBlank { "[]" }
|
||||
}
|
||||
|
||||
fun getModuleCount(): Int {
|
||||
val result = listModules()
|
||||
runCatching {
|
||||
val array = JSONArray(result)
|
||||
return array.length()
|
||||
}.getOrElse { return 0 }
|
||||
}
|
||||
|
||||
fun getSuperuserCount(): Int {
|
||||
return Natives.allowList.size
|
||||
}
|
||||
|
||||
fun toggleModule(id: String, enable: Boolean): Boolean {
|
||||
val cmd = if (enable) {
|
||||
"module enable $id"
|
||||
} else {
|
||||
"module disable $id"
|
||||
}
|
||||
val result = execKsud(cmd, true)
|
||||
Log.i(TAG, "$cmd result: $result")
|
||||
return result
|
||||
}
|
||||
|
||||
fun uninstallModule(id: String): Boolean {
|
||||
val cmd = "module uninstall $id"
|
||||
val result = execKsud(cmd, true)
|
||||
Log.i(TAG, "uninstall module $id result: $result")
|
||||
return result
|
||||
}
|
||||
|
||||
private fun flashWithIO(
|
||||
cmd: String,
|
||||
onStdout: (String) -> Unit,
|
||||
onStderr: (String) -> Unit
|
||||
): Shell.Result {
|
||||
|
||||
val stdoutCallback: CallbackList<String?> = object : CallbackList<String?>() {
|
||||
override fun onAddElement(s: String?) {
|
||||
onStdout(s ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
val stderrCallback: CallbackList<String?> = object : CallbackList<String?>() {
|
||||
override fun onAddElement(s: String?) {
|
||||
onStderr(s ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
return withNewRootShell {
|
||||
newJob().add(cmd).to(stdoutCallback, stderrCallback).exec()
|
||||
}
|
||||
}
|
||||
|
||||
fun flashModule(
|
||||
uri: Uri,
|
||||
onFinish: (Boolean, Int) -> Unit,
|
||||
onStdout: (String) -> Unit,
|
||||
onStderr: (String) -> Unit
|
||||
): Boolean {
|
||||
val resolver = ksuApp.contentResolver
|
||||
with(resolver.openInputStream(uri)) {
|
||||
val file = File(ksuApp.cacheDir, "module.zip")
|
||||
file.outputStream().use { output ->
|
||||
this?.copyTo(output)
|
||||
}
|
||||
val cmd = "module install ${file.absolutePath}"
|
||||
val result = flashWithIO("${getKsuDaemonPath()} $cmd", onStdout, onStderr)
|
||||
Log.i("KernelSU", "install module $uri result: $result")
|
||||
|
||||
file.delete()
|
||||
|
||||
onFinish(result.isSuccess, result.code)
|
||||
return result.isSuccess
|
||||
}
|
||||
}
|
||||
|
||||
fun restoreBoot(
|
||||
onFinish: (Boolean, Int) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit
|
||||
): Boolean {
|
||||
val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libmagiskboot.so")
|
||||
val result = flashWithIO("${getKsuDaemonPath()} boot-restore -f --magiskboot $magiskboot", onStdout, onStderr)
|
||||
onFinish(result.isSuccess, result.code)
|
||||
return result.isSuccess
|
||||
}
|
||||
|
||||
fun uninstallPermanently(
|
||||
onFinish: (Boolean, Int) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit
|
||||
): Boolean {
|
||||
val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libmagiskboot.so")
|
||||
val result = flashWithIO("${getKsuDaemonPath()} uninstall --magiskboot $magiskboot", onStdout, onStderr)
|
||||
onFinish(result.isSuccess, result.code)
|
||||
return result.isSuccess
|
||||
}
|
||||
|
||||
suspend fun shrinkModules(): Boolean = withContext(Dispatchers.IO) {
|
||||
execKsud("module shrink", true)
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
sealed class LkmSelection : Parcelable {
|
||||
data class LkmUri(val uri: Uri) : LkmSelection()
|
||||
data class KmiString(val value: String) : LkmSelection()
|
||||
data object KmiNone : LkmSelection()
|
||||
}
|
||||
|
||||
fun installBoot(
|
||||
bootUri: Uri?,
|
||||
lkm: LkmSelection,
|
||||
ota: Boolean,
|
||||
onFinish: (Boolean, Int) -> Unit,
|
||||
onStdout: (String) -> Unit,
|
||||
onStderr: (String) -> Unit,
|
||||
): Boolean {
|
||||
val resolver = ksuApp.contentResolver
|
||||
|
||||
val bootFile = bootUri?.let { uri ->
|
||||
with(resolver.openInputStream(uri)) {
|
||||
val bootFile = File(ksuApp.cacheDir, "boot.img")
|
||||
bootFile.outputStream().use { output ->
|
||||
this?.copyTo(output)
|
||||
}
|
||||
|
||||
bootFile
|
||||
}
|
||||
}
|
||||
|
||||
val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libmagiskboot.so")
|
||||
var cmd = "boot-patch --magiskboot ${magiskboot.absolutePath}"
|
||||
|
||||
cmd += if (bootFile == null) {
|
||||
// no boot.img, use -f to force install
|
||||
" -f"
|
||||
} else {
|
||||
" -b ${bootFile.absolutePath}"
|
||||
}
|
||||
|
||||
if (ota) {
|
||||
cmd += " -u"
|
||||
}
|
||||
|
||||
var lkmFile: File? = null
|
||||
when (lkm) {
|
||||
is LkmSelection.LkmUri -> {
|
||||
lkmFile = with(resolver.openInputStream(lkm.uri)) {
|
||||
val file = File(ksuApp.cacheDir, "kernelsu-tmp-lkm.ko")
|
||||
file.outputStream().use { output ->
|
||||
this?.copyTo(output)
|
||||
}
|
||||
|
||||
file
|
||||
}
|
||||
cmd += " -m ${lkmFile.absolutePath}"
|
||||
}
|
||||
|
||||
is LkmSelection.KmiString -> {
|
||||
cmd += " --kmi ${lkm.value}"
|
||||
}
|
||||
|
||||
LkmSelection.KmiNone -> {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
// output dir
|
||||
val downloadsDir =
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||
cmd += " -o $downloadsDir"
|
||||
|
||||
val result = flashWithIO("${getKsuDaemonPath()} $cmd", onStdout, onStderr)
|
||||
Log.i("KernelSU", "install boot result: ${result.isSuccess}")
|
||||
|
||||
bootFile?.delete()
|
||||
lkmFile?.delete()
|
||||
|
||||
// if boot uri is empty, it is direct install, when success, we should show reboot button
|
||||
onFinish(bootUri == null && result.isSuccess, result.code)
|
||||
return result.isSuccess
|
||||
}
|
||||
|
||||
fun reboot(reason: String = "") {
|
||||
val shell = getRootShell()
|
||||
if (reason == "recovery") {
|
||||
// KEYCODE_POWER = 26, hide incorrect "Factory data reset" message
|
||||
ShellUtils.fastCmd(shell, "/system/bin/input keyevent 26")
|
||||
}
|
||||
ShellUtils.fastCmd(shell, "/system/bin/svc power reboot $reason || /system/bin/reboot $reason")
|
||||
}
|
||||
|
||||
fun rootAvailable(): Boolean {
|
||||
val shell = getRootShell()
|
||||
return shell.isRoot
|
||||
}
|
||||
|
||||
fun isAbDevice(): Boolean {
|
||||
val shell = getRootShell()
|
||||
return ShellUtils.fastCmd(shell, "getprop ro.build.ab_update").trim().toBoolean()
|
||||
}
|
||||
|
||||
fun isInitBoot(): Boolean {
|
||||
val shell = getRootShell()
|
||||
if (shell.isRoot) {
|
||||
// if we have root, use /dev/block/by-name/init_boot to check
|
||||
val abDevice = isAbDevice()
|
||||
val initBootBlock = "/dev/block/by-name/init_boot${if (abDevice) "_a" else ""}"
|
||||
val file = SuFile(initBootBlock)
|
||||
file.shell = shell
|
||||
return file.exists()
|
||||
}
|
||||
// https://source.android.com/docs/core/architecture/partitions/generic-boot
|
||||
return ShellUtils.fastCmd(shell, "getprop ro.product.first_api_level").trim()
|
||||
.toInt() >= Build.VERSION_CODES.TIRAMISU
|
||||
}
|
||||
|
||||
suspend fun getCurrentKmi(): String = withContext(Dispatchers.IO) {
|
||||
val shell = getRootShell()
|
||||
val cmd = "boot-info current-kmi"
|
||||
ShellUtils.fastCmd(shell, "${getKsuDaemonPath()} $cmd")
|
||||
}
|
||||
|
||||
suspend fun getSupportedKmis(): List<String> = withContext(Dispatchers.IO) {
|
||||
val shell = getRootShell()
|
||||
val cmd = "boot-info supported-kmi"
|
||||
val out = shell.newJob().add("${getKsuDaemonPath()} $cmd").to(ArrayList(), null).exec().out
|
||||
out.filter { it.isNotBlank() }.map { it.trim() }
|
||||
}
|
||||
|
||||
fun overlayFsAvailable(): Boolean {
|
||||
val shell = getRootShell()
|
||||
// check /proc/filesystems
|
||||
return ShellUtils.fastCmdResult(shell, "cat /proc/filesystems | grep overlay")
|
||||
}
|
||||
|
||||
fun hasMagisk(): Boolean {
|
||||
val shell = getRootShell(true)
|
||||
val result = shell.newJob().add("which magisk").exec()
|
||||
Log.i(TAG, "has magisk: ${result.isSuccess}")
|
||||
return result.isSuccess
|
||||
}
|
||||
|
||||
fun isSepolicyValid(rules: String?): Boolean {
|
||||
if (rules == null) {
|
||||
return true
|
||||
}
|
||||
val shell = getRootShell()
|
||||
val result =
|
||||
shell.newJob().add("${getKsuDaemonPath()} sepolicy check '$rules'").to(ArrayList(), null)
|
||||
.exec()
|
||||
return result.isSuccess
|
||||
}
|
||||
|
||||
fun getSepolicy(pkg: String): String {
|
||||
val shell = getRootShell()
|
||||
val result =
|
||||
shell.newJob().add("${getKsuDaemonPath()} profile get-sepolicy $pkg").to(ArrayList(), null)
|
||||
.exec()
|
||||
Log.i(TAG, "code: ${result.code}, out: ${result.out}, err: ${result.err}")
|
||||
return result.out.joinToString("\n")
|
||||
}
|
||||
|
||||
fun setSepolicy(pkg: String, rules: String): Boolean {
|
||||
val shell = getRootShell()
|
||||
val result = shell.newJob().add("${getKsuDaemonPath()} profile set-sepolicy $pkg '$rules'")
|
||||
.to(ArrayList(), null).exec()
|
||||
Log.i(TAG, "set sepolicy result: ${result.code}")
|
||||
return result.isSuccess
|
||||
}
|
||||
|
||||
fun listAppProfileTemplates(): List<String> {
|
||||
val shell = getRootShell()
|
||||
return shell.newJob().add("${getKsuDaemonPath()} profile list-templates").to(ArrayList(), null)
|
||||
.exec().out
|
||||
}
|
||||
|
||||
fun getAppProfileTemplate(id: String): String {
|
||||
val shell = getRootShell()
|
||||
return shell.newJob().add("${getKsuDaemonPath()} profile get-template '${id}'")
|
||||
.to(ArrayList(), null).exec().out.joinToString("\n")
|
||||
}
|
||||
|
||||
fun setAppProfileTemplate(id: String, template: String): Boolean {
|
||||
val shell = getRootShell()
|
||||
val escapedTemplate = template.replace("\"", "\\\"")
|
||||
val cmd = """${getKsuDaemonPath()} profile set-template "$id" "$escapedTemplate'""""
|
||||
return shell.newJob().add(cmd)
|
||||
.to(ArrayList(), null).exec().isSuccess
|
||||
}
|
||||
|
||||
fun deleteAppProfileTemplate(id: String): Boolean {
|
||||
val shell = getRootShell()
|
||||
return shell.newJob().add("${getKsuDaemonPath()} profile delete-template '${id}'")
|
||||
.to(ArrayList(), null).exec().isSuccess
|
||||
}
|
||||
|
||||
fun forceStopApp(packageName: String) {
|
||||
val shell = getRootShell()
|
||||
val result = shell.newJob().add("am force-stop $packageName").exec()
|
||||
Log.i(TAG, "force stop $packageName result: $result")
|
||||
}
|
||||
|
||||
fun launchApp(packageName: String) {
|
||||
|
||||
val shell = getRootShell()
|
||||
val result =
|
||||
shell.newJob()
|
||||
.add("cmd package resolve-activity --brief $packageName | tail -n 1 | xargs cmd activity start-activity -n")
|
||||
.exec()
|
||||
Log.i(TAG, "launch $packageName result: $result")
|
||||
}
|
||||
|
||||
fun restartApp(packageName: String) {
|
||||
forceStopApp(packageName)
|
||||
launchApp(packageName)
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
package me.weishu.kernelsu.ui.util
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.system.Os
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
import me.weishu.kernelsu.Natives
|
||||
import me.weishu.kernelsu.ui.screen.getManagerVersion
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.FileWriter
|
||||
import java.io.PrintWriter
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
fun getBugreportFile(context: Context): File {
|
||||
|
||||
val bugreportDir = File(context.cacheDir, "bugreport")
|
||||
bugreportDir.mkdirs()
|
||||
|
||||
val dmesgFile = File(bugreportDir, "dmesg.txt")
|
||||
val logcatFile = File(bugreportDir, "logcat.txt")
|
||||
val tombstonesFile = File(bugreportDir, "tombstones.tar.gz")
|
||||
val dropboxFile = File(bugreportDir, "dropbox.tar.gz")
|
||||
val pstoreFile = File(bugreportDir, "pstore.tar.gz")
|
||||
// Xiaomi/Readmi devices have diag in /data/vendor/diag
|
||||
val diagFile = File(bugreportDir, "diag.tar.gz")
|
||||
val opulsFile = File(bugreportDir, "opuls.tar.gz")
|
||||
val bootlogFile = File(bugreportDir, "bootlog.tar.gz")
|
||||
val mountsFile = File(bugreportDir, "mounts.txt")
|
||||
val fileSystemsFile = File(bugreportDir, "filesystems.txt")
|
||||
val adbFileTree = File(bugreportDir, "adb_tree.txt")
|
||||
val adbFileDetails = File(bugreportDir, "adb_details.txt")
|
||||
val ksuFileSize = File(bugreportDir, "ksu_size.txt")
|
||||
val appListFile = File(bugreportDir, "packages.txt")
|
||||
val propFile = File(bugreportDir, "props.txt")
|
||||
val allowListFile = File(bugreportDir, "allowlist.bin")
|
||||
val procModules = File(bugreportDir, "proc_modules.txt")
|
||||
val bootConfig = File(bugreportDir, "boot_config.txt")
|
||||
val kernelConfig = File(bugreportDir, "defconfig.gz")
|
||||
|
||||
val shell = getRootShell(true)
|
||||
|
||||
shell.newJob().add("dmesg > ${dmesgFile.absolutePath}").exec()
|
||||
shell.newJob().add("logcat -d > ${logcatFile.absolutePath}").exec()
|
||||
shell.newJob().add("tar -czf ${tombstonesFile.absolutePath} -C /data/tombstones .").exec()
|
||||
shell.newJob().add("tar -czf ${dropboxFile.absolutePath} -C /data/system/dropbox .").exec()
|
||||
shell.newJob().add("tar -czf ${pstoreFile.absolutePath} -C /sys/fs/pstore .").exec()
|
||||
shell.newJob().add("tar -czf ${diagFile.absolutePath} -C /data/vendor/diag . --exclude=./minidump.gz").exec()
|
||||
shell.newJob().add("tar -czf ${opulsFile.absolutePath} -C /mnt/oplus/op2/media/log/boot_log/ .").exec()
|
||||
shell.newJob().add("tar -czf ${bootlogFile.absolutePath} -C /data/adb/ksu/log .").exec()
|
||||
|
||||
shell.newJob().add("cat /proc/1/mountinfo > ${mountsFile.absolutePath}").exec()
|
||||
shell.newJob().add("cat /proc/filesystems > ${fileSystemsFile.absolutePath}").exec()
|
||||
shell.newJob().add("busybox tree /data/adb > ${adbFileTree.absolutePath}").exec()
|
||||
shell.newJob().add("ls -alRZ /data/adb > ${adbFileDetails.absolutePath}").exec()
|
||||
shell.newJob().add("du -sh /data/adb/ksu/* > ${ksuFileSize.absolutePath}").exec()
|
||||
shell.newJob().add("cp /data/system/packages.list ${appListFile.absolutePath}").exec()
|
||||
shell.newJob().add("getprop > ${propFile.absolutePath}").exec()
|
||||
shell.newJob().add("cp /data/adb/ksu/.allowlist ${allowListFile.absolutePath}").exec()
|
||||
shell.newJob().add("cp /proc/modules ${procModules.absolutePath}").exec()
|
||||
shell.newJob().add("cp /proc/bootconfig ${bootConfig.absolutePath}").exec()
|
||||
shell.newJob().add("cp /proc/config.gz ${kernelConfig.absolutePath}").exec()
|
||||
|
||||
val selinux = ShellUtils.fastCmd(shell, "getenforce")
|
||||
|
||||
// basic information
|
||||
val buildInfo = File(bugreportDir, "basic.txt")
|
||||
PrintWriter(FileWriter(buildInfo)).use { pw ->
|
||||
pw.println("Kernel: ${System.getProperty("os.version")}")
|
||||
pw.println("BRAND: " + Build.BRAND)
|
||||
pw.println("MODEL: " + Build.MODEL)
|
||||
pw.println("PRODUCT: " + Build.PRODUCT)
|
||||
pw.println("MANUFACTURER: " + Build.MANUFACTURER)
|
||||
pw.println("SDK: " + Build.VERSION.SDK_INT)
|
||||
pw.println("PREVIEW_SDK: " + Build.VERSION.PREVIEW_SDK_INT)
|
||||
pw.println("FINGERPRINT: " + Build.FINGERPRINT)
|
||||
pw.println("DEVICE: " + Build.DEVICE)
|
||||
pw.println("Manager: " + getManagerVersion(context))
|
||||
pw.println("SELinux: $selinux")
|
||||
|
||||
val uname = Os.uname()
|
||||
pw.println("KernelRelease: ${uname.release}")
|
||||
pw.println("KernelVersion: ${uname.version}")
|
||||
pw.println("Machine: ${uname.machine}")
|
||||
pw.println("Nodename: ${uname.nodename}")
|
||||
pw.println("Sysname: ${uname.sysname}")
|
||||
|
||||
val ksuKernel = Natives.version
|
||||
pw.println("KernelSU: $ksuKernel")
|
||||
val safeMode = Natives.isSafeMode
|
||||
pw.println("SafeMode: $safeMode")
|
||||
val lkmMode = Natives.isLkmMode
|
||||
pw.println("LKM: $lkmMode")
|
||||
}
|
||||
|
||||
// modules
|
||||
val modulesFile = File(bugreportDir, "modules.json")
|
||||
modulesFile.writeText(listModules())
|
||||
|
||||
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm")
|
||||
val current = LocalDateTime.now().format(formatter)
|
||||
|
||||
val targetFile = File(context.cacheDir, "KernelSU_bugreport_${current}.tar.gz")
|
||||
|
||||
shell.newJob().add("tar czf ${targetFile.absolutePath} -C ${bugreportDir.absolutePath} .").exec()
|
||||
shell.newJob().add("rm -rf ${bugreportDir.absolutePath}").exec()
|
||||
shell.newJob().add("chmod 0644 ${targetFile.absolutePath}").exec()
|
||||
|
||||
return targetFile
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue