]> glassweightruler.freedombox.rocks Git - waydroid.git/commitdiff
Waydroid: Initial commit
authorErfan Abdi <erfangplus@gmail.com>
Sat, 17 Jul 2021 06:53:14 +0000 (11:23 +0430)
committerErfan Abdi <erfangplus@gmail.com>
Fri, 27 Aug 2021 15:40:54 +0000 (20:10 +0430)
40 files changed:
.gitignore [new file with mode: 0644]
LICENSE [new file with mode: 0644]
README.md [new file with mode: 0644]
data/AppIcon.png [new file with mode: 0644]
data/configs/config_1 [new file with mode: 0644]
data/configs/config_2 [new file with mode: 0644]
data/scripts/waydroid-net.sh [new file with mode: 0755]
tools/__init__.py [new file with mode: 0644]
tools/actions/__init__.py [new file with mode: 0644]
tools/actions/app_manager.py [new file with mode: 0644]
tools/actions/container_manager.py [new file with mode: 0644]
tools/actions/initializer.py [new file with mode: 0644]
tools/actions/session_manager.py [new file with mode: 0644]
tools/actions/status.py [new file with mode: 0644]
tools/actions/upgrader.py [new file with mode: 0644]
tools/config/__init__.py [new file with mode: 0644]
tools/config/load.py [new file with mode: 0644]
tools/config/save.py [new file with mode: 0644]
tools/helpers/__init__.py [new file with mode: 0644]
tools/helpers/arch.py [new file with mode: 0644]
tools/helpers/arguments.py [new file with mode: 0644]
tools/helpers/drivers.py [new file with mode: 0644]
tools/helpers/http.py [new file with mode: 0644]
tools/helpers/images.py [new file with mode: 0644]
tools/helpers/logging.py [new file with mode: 0644]
tools/helpers/lxc.py [new file with mode: 0644]
tools/helpers/mount.py [new file with mode: 0644]
tools/helpers/props.py [new file with mode: 0644]
tools/helpers/run.py [new file with mode: 0644]
tools/helpers/run_core.py [new file with mode: 0644]
tools/interfaces/IClipboard.py [new file with mode: 0644]
tools/interfaces/IHardware.py [new file with mode: 0644]
tools/interfaces/IPlatform.py [new file with mode: 0644]
tools/interfaces/IStatusBarService.py [new file with mode: 0644]
tools/interfaces/IUserMonitor.py [new file with mode: 0644]
tools/services/__init__.py [new file with mode: 0644]
tools/services/clipboard_manager.py [new file with mode: 0644]
tools/services/hardware_manager.py [new file with mode: 0644]
tools/services/user_manager.py [new file with mode: 0644]
waydroid.py [new file with mode: 0755]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..7a68a05
--- /dev/null
@@ -0,0 +1,132 @@
+# Pycharm
+/.idea
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+.python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don't work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..f288702
--- /dev/null
+++ b/LICENSE
@@ -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>.
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..9fe872d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,40 @@
+# Waydroid
+
+Waydroid is a container-based approach to boot a full Android system on a
+regular GNU/Linux system like Ubuntu.
+
+## Overview
+
+Waydroid uses Linux namespaces (user, pid, uts, net, mount, ipc) to run a
+full Android system in a container and provide Android applications on
+any GNU/Linux-based platform.
+
+The Android inside the container has direct access to needed hardwares.
+
+The Android runtime environment ships with a minimal customized Android system
+image based on the [LineageOS](https://lineageos.org/).
+The used image is currently based on Android 10
+
+## Install and Run Android Applications
+
+You can install Android applications from the command line.
+
+```sh
+waydroid app install xyz.apk
+```
+
+The apk files you will sometimes find on the internet tend to only have arm
+support, and will therefore not work on x86\_64.
+
+You may want to install [F-Droid](https://f-droid.org/) to get applications
+graphically. Note that the Google Play Store will not work as is, because it
+relies on the proprietary Google Play Services, which are not installed.
+
+## Reporting bugs
+
+If you have found an issue with Waydroid, please [file a bug](https://github.com/Waydroid/waydroid/issues/new).
+
+## Get in Touch
+
+If you want to get in contact with the developers please feel free to join the
+*WayDroid* groups in [Matrix](https://matrix.to/#/#waydroid:connolly.tech) or [Telegram](https://t.me/WayDroid).
diff --git a/data/AppIcon.png b/data/AppIcon.png
new file mode 100644 (file)
index 0000000..0bd597d
Binary files /dev/null and b/data/AppIcon.png differ
diff --git a/data/configs/config_1 b/data/configs/config_1
new file mode 100644 (file)
index 0000000..02d82a7
--- /dev/null
@@ -0,0 +1,25 @@
+# Waydroid LXC Config
+
+lxc.rootfs.path = /home/.waydroid/rootfs
+lxc.utsname = waydroid
+lxc.arch = LXCARCH
+lxc.autodev = 0
+# lxc.autodev.tmpfs.size = 25000000
+lxc.aa_profile = unconfined
+
+lxc.init_cmd = /init
+
+lxc.mount.auto = cgroup:ro sys:ro proc
+
+lxc.network.type = veth
+lxc.network.flags = up
+lxc.network.link = waydroid0
+lxc.network.name = eth0
+lxc.network.hwaddr = 00:16:3e:f9:d3:03
+lxc.network.mtu = 1500
+
+lxc.console.path = none
+
+lxc.include = /home/.waydroid/lxc/waydroid/config_nodes
+
+lxc.hook.post-stop = /dev/null
diff --git a/data/configs/config_2 b/data/configs/config_2
new file mode 100644 (file)
index 0000000..dd2af68
--- /dev/null
@@ -0,0 +1,25 @@
+# Waydroid LXC Config
+
+lxc.rootfs.path = /home/.waydroid/rootfs
+lxc.uts.name = waydroid
+lxc.arch = LXCARCH
+lxc.autodev = 0
+# lxc.autodev.tmpfs.size = 25000000
+lxc.apparmor.profile = unconfined
+
+lxc.init.cmd = /init
+
+lxc.mount.auto = cgroup:ro sys:ro proc
+
+lxc.net.0.type = veth
+lxc.net.0.flags = up
+lxc.net.0.link = waydroid0
+lxc.net.0.name = eth0
+lxc.net.0.hwaddr = 00:16:3e:f9:d3:03
+lxc.net.0.mtu = 1500
+
+lxc.console.path = none
+
+lxc.include = /home/.waydroid/lxc/waydroid/config_nodes
+
+lxc.hook.post-stop = /dev/null
diff --git a/data/scripts/waydroid-net.sh b/data/scripts/waydroid-net.sh
new file mode 100755 (executable)
index 0000000..14da312
--- /dev/null
@@ -0,0 +1,269 @@
+#!/bin/sh -
+
+varrun="/run/waydroid-lxc"
+varlib="/var/lib"
+
+USE_LXC_BRIDGE="true"
+LXC_BRIDGE="waydroid0"
+LXC_BRIDGE_MAC="00:16:3e:00:00:01"
+LXC_ADDR="192.168.250.1"
+LXC_NETMASK="255.255.255.0"
+LXC_NETWORK="192.168.250.0/24"
+LXC_DHCP_RANGE="192.168.250.2,192.168.250.254"
+LXC_DHCP_MAX="253"
+LXC_DHCP_CONFILE=""
+LXC_DHCP_PING="true"
+LXC_DOMAIN=""
+LXC_USE_NFT="true"
+
+LXC_IPV6_ADDR=""
+LXC_IPV6_MASK=""
+LXC_IPV6_NETWORK=""
+LXC_IPV6_NAT="false"
+
+IPTABLES_BIN="$(which iptables-legacy)"
+if [ ! -n "$IPTABLES_BIN" ]; then
+    IPTABLES_BIN="$(which iptables)"
+fi
+IP6TABLES_BIN="$(which ip6tables-legacy)"
+if [ ! -n "$IP6TABLES_BIN" ]; then
+    IP6TABLES_BIN="$(which ip6tables)"
+fi
+
+use_nft() {
+    [ -n "$NFT" ] && nft list ruleset > /dev/null 2>&1 && [ "$LXC_USE_NFT" = "true" ]
+}
+
+NFT="$(which nft)"
+if ! use_nft; then
+    use_iptables_lock="-w"
+    $IPTABLES_BIN -w -L -n > /dev/null 2>&1 || use_iptables_lock=""
+fi
+
+_netmask2cidr ()
+{
+    # Assumes there's no "255." after a non-255 byte in the mask
+    local x=${1##*255.}
+    set -- 0^^^128^192^224^240^248^252^254^ $(( (${#1} - ${#x})*2 )) ${x%%.*}
+    x=${1%%$3*}
+    echo $(( $2 + (${#x}/4) ))
+}
+
+_ifdown() {
+    ip addr flush dev ${LXC_BRIDGE}
+    ip link set dev ${LXC_BRIDGE} down
+}
+
+_ifup() {
+    MASK=`_netmask2cidr ${LXC_NETMASK}`
+    CIDR_ADDR="${LXC_ADDR}/${MASK}"
+    ip addr add ${CIDR_ADDR} broadcast + dev ${LXC_BRIDGE}
+    ip link set dev ${LXC_BRIDGE} address $LXC_BRIDGE_MAC
+    ip link set dev ${LXC_BRIDGE} up
+}
+
+start_ipv6() {
+    LXC_IPV6_ARG=""
+    if [ -n "$LXC_IPV6_ADDR" ] && [ -n "$LXC_IPV6_MASK" ] && [ -n "$LXC_IPV6_NETWORK" ]; then
+        echo 1 > /proc/sys/net/ipv6/conf/all/forwarding
+        echo 0 > /proc/sys/net/ipv6/conf/${LXC_BRIDGE}/autoconf
+        ip -6 addr add dev ${LXC_BRIDGE} ${LXC_IPV6_ADDR}/${LXC_IPV6_MASK}
+        LXC_IPV6_ARG="--dhcp-range=${LXC_IPV6_ADDR},ra-only --listen-address ${LXC_IPV6_ADDR}"
+    fi
+}
+
+start_iptables() {
+    start_ipv6
+    if [ -n "$LXC_IPV6_ARG" ] && [ "$LXC_IPV6_NAT" = "true" ]; then
+        $IP6TABLES_BIN $use_iptables_lock -t nat -A POSTROUTING -s ${LXC_IPV6_NETWORK} ! -d ${LXC_IPV6_NETWORK} -j MASQUERADE
+    fi
+    $IPTABLES_BIN $use_iptables_lock -I INPUT -i ${LXC_BRIDGE} -p udp --dport 67 -j ACCEPT
+    $IPTABLES_BIN $use_iptables_lock -I INPUT -i ${LXC_BRIDGE} -p tcp --dport 67 -j ACCEPT
+    $IPTABLES_BIN $use_iptables_lock -I INPUT -i ${LXC_BRIDGE} -p udp --dport 53 -j ACCEPT
+    $IPTABLES_BIN $use_iptables_lock -I INPUT -i ${LXC_BRIDGE} -p tcp --dport 53 -j ACCEPT
+    $IPTABLES_BIN $use_iptables_lock -I FORWARD -i ${LXC_BRIDGE} -j ACCEPT
+    $IPTABLES_BIN $use_iptables_lock -I FORWARD -o ${LXC_BRIDGE} -j ACCEPT
+    $IPTABLES_BIN $use_iptables_lock -t nat -A POSTROUTING -s ${LXC_NETWORK} ! -d ${LXC_NETWORK} -j MASQUERADE
+    $IPTABLES_BIN $use_iptables_lock -t mangle -A POSTROUTING -o ${LXC_BRIDGE} -p udp -m udp --dport 68 -j CHECKSUM --checksum-fill
+}
+
+start_nftables() {
+    start_ipv6
+    NFT_RULESET=""
+    if [ -n "$LXC_IPV6_ARG" ] && [ "$LXC_IPV6_NAT" = "true" ]; then
+        NFT_RULESET="${NFT_RULESET}
+add table ip6 lxc;
+flush table ip6 lxc;
+add chain ip6 lxc postrouting { type nat hook postrouting priority 100; };
+add rule ip6 lxc postrouting ip saddr ${LXC_IPV6_NETWORK} ip daddr != ${LXC_IPV6_NETWORK} counter masquerade;
+"
+    fi
+    NFT_RULESET="${NFT_RULESET};
+add table inet lxc;
+flush table inet lxc;
+add chain inet lxc input { type filter hook input priority 0; };
+add rule inet lxc input iifname ${LXC_BRIDGE} udp dport { 53, 67 } accept;
+add rule inet lxc input iifname ${LXC_BRIDGE} tcp dport { 53, 67 } accept;
+add chain inet lxc forward { type filter hook forward priority 0; };
+add rule inet lxc forward iifname ${LXC_BRIDGE} accept;
+add rule inet lxc forward oifname ${LXC_BRIDGE} accept;
+add table ip lxc;
+flush table ip lxc;
+add chain ip lxc postrouting { type nat hook postrouting priority 100; };
+add rule ip lxc postrouting ip saddr ${LXC_NETWORK} ip daddr != ${LXC_NETWORK} counter masquerade"
+    nft "${NFT_RULESET}"
+}
+
+start() {
+    [ "x$USE_LXC_BRIDGE" = "xtrue" ] || { exit 0; }
+
+    [ ! -f "${varrun}/network_up" ] || { echo "waydroid-net is already running"; exit 1; }
+
+    if [ -d /sys/class/net/${LXC_BRIDGE} ]; then
+        stop force || true
+    fi
+
+    FAILED=1
+
+    cleanup() {
+        set +e
+        if [ "$FAILED" = "1" ]; then
+            echo "Failed to setup waydroid-net." >&2
+            stop force
+            exit 1
+        fi
+    }
+
+    trap cleanup EXIT HUP INT TERM
+    set -e
+
+    # set up the lxc network
+    [ ! -d /sys/class/net/${LXC_BRIDGE} ] && ip link add dev ${LXC_BRIDGE} type bridge
+    echo 1 > /proc/sys/net/ipv4/ip_forward
+    echo 0 > /proc/sys/net/ipv6/conf/${LXC_BRIDGE}/accept_dad || true
+
+    # if we are run from systemd on a system with selinux enabled,
+    # the mkdir will create /run/lxc as init_var_run_t which dnsmasq
+    # can't write its pid into, so we restorecon it (to var_run_t)
+    if [ ! -d "${varrun}" ]; then
+        mkdir -p "${varrun}"
+        if which restorecon >/dev/null 2>&1; then
+            restorecon "${varrun}"
+        fi
+    fi
+
+    _ifup
+
+    if use_nft; then
+        start_nftables
+    else
+        start_iptables
+    fi
+
+    LXC_DOMAIN_ARG=""
+    if [ -n "$LXC_DOMAIN" ]; then
+        LXC_DOMAIN_ARG="-s $LXC_DOMAIN -S /$LXC_DOMAIN/"
+    fi
+
+    # lxc's dnsmasq should be hermetic and not read `/etc/dnsmasq.conf` (which
+    # it does by default if `--conf-file` is not present
+    LXC_DHCP_CONFILE_ARG="--conf-file=${LXC_DHCP_CONFILE:-/dev/null}"
+
+    # https://lists.linuxcontainers.org/pipermail/lxc-devel/2014-October/010561.html
+    for DNSMASQ_USER in lxc-dnsmasq dnsmasq nobody
+    do
+        if getent passwd ${DNSMASQ_USER} >/dev/null; then
+            break
+        fi
+    done
+
+    LXC_DHCP_PING_ARG=""
+    if [ "x$LXC_DHCP_PING" = "xfalse" ]; then
+        LXC_DHCP_PING_ARG="--no-ping"
+    fi
+
+    dnsmasq $LXC_DHCP_CONFILE_ARG $LXC_DOMAIN_ARG $LXC_DHCP_PING_ARG -u ${DNSMASQ_USER} \
+            --strict-order --bind-interfaces --pid-file="${varrun}"/dnsmasq.pid \
+            --listen-address ${LXC_ADDR} --dhcp-range ${LXC_DHCP_RANGE} \
+            --dhcp-lease-max=${LXC_DHCP_MAX} --dhcp-no-override \
+            --except-interface=lo --interface=${LXC_BRIDGE} \
+            --dhcp-leasefile="${varlib}"/misc/dnsmasq.${LXC_BRIDGE}.leases \
+            --dhcp-authoritative $LXC_IPV6_ARG || cleanup
+
+    touch "${varrun}"/network_up
+    FAILED=0
+}
+
+stop_iptables() {
+    $IPTABLES_BIN $use_iptables_lock -D INPUT -i ${LXC_BRIDGE} -p udp --dport 67 -j ACCEPT
+    $IPTABLES_BIN $use_iptables_lock -D INPUT -i ${LXC_BRIDGE} -p tcp --dport 67 -j ACCEPT
+    $IPTABLES_BIN $use_iptables_lock -D INPUT -i ${LXC_BRIDGE} -p udp --dport 53 -j ACCEPT
+    $IPTABLES_BIN $use_iptables_lock -D INPUT -i ${LXC_BRIDGE} -p tcp --dport 53 -j ACCEPT
+    $IPTABLES_BIN $use_iptables_lock -D FORWARD -i ${LXC_BRIDGE} -j ACCEPT
+    $IPTABLES_BIN $use_iptables_lock -D FORWARD -o ${LXC_BRIDGE} -j ACCEPT
+    $IPTABLES_BIN $use_iptables_lock -t nat -D POSTROUTING -s ${LXC_NETWORK} ! -d ${LXC_NETWORK} -j MASQUERADE
+    $IPTABLES_BIN $use_iptables_lock -t mangle -D POSTROUTING -o ${LXC_BRIDGE} -p udp -m udp --dport 68 -j CHECKSUM --checksum-fill
+    if [ "$LXC_IPV6_NAT" = "true" ]; then
+        $IP6TABLES_BIN $use_iptables_lock -t nat -D POSTROUTING -s ${LXC_IPV6_NETWORK} ! -d ${LXC_IPV6_NETWORK} -j MASQUERADE
+    fi
+}
+
+stop_nftables() {
+    # Adding table before removing them is just to avoid
+    # delete error for non-existent table
+    NFT_RULESET="add table inet lxc;
+delete table inet lxc;
+add table ip lxc;
+delete table ip lxc;
+"
+    if [ "$LXC_IPV6_NAT" = "true" ]; then
+        NFT_RULESET="${NFT_RULESET};
+add table ip6 lxc;
+delete table ip6 lxc;"
+    fi
+    nft "${NFT_RULESET}"
+}
+
+stop() {
+    [ "x$USE_LXC_BRIDGE" = "xtrue" ] || { exit 0; }
+
+    [ -f "${varrun}/network_up" ] || [ "$1" = "force" ] || { echo "waydroid-net isn't running"; exit 1; }
+
+    if [ -d /sys/class/net/${LXC_BRIDGE} ]; then
+        _ifdown
+        if use_nft; then
+            stop_nftables
+        else
+            stop_iptables
+        fi
+
+        pid=`cat "${varrun}"/dnsmasq.pid 2>/dev/null` && kill -9 $pid
+        rm -f "${varrun}"/dnsmasq.pid
+        # if $LXC_BRIDGE has attached interfaces, don't destroy the bridge
+        ls /sys/class/net/${LXC_BRIDGE}/brif/* > /dev/null 2>&1 || ip link delete ${LXC_BRIDGE}
+    fi
+
+    rm -f "${varrun}"/network_up
+}
+
+# See how we were called.
+case "$1" in
+    start)
+        start
+    ;;
+
+    stop)
+        stop
+    ;;
+
+    restart|reload|force-reload)
+        $0 stop
+        $0 start
+    ;;
+
+    *)
+        echo "Usage: $0 {start|stop|restart|reload|force-reload}"
+        exit 2
+esac
+
+exit $?
diff --git a/tools/__init__.py b/tools/__init__.py
new file mode 100644 (file)
index 0000000..555ea00
--- /dev/null
@@ -0,0 +1,135 @@
+# Copyright 2021 Oliver Smith
+# SPDX-License-Identifier: GPL-3.0-or-later
+# PYTHON_ARGCOMPLETE_OK
+import sys
+import logging
+import os
+import traceback
+
+from . import actions
+from . import config
+from . import helpers
+from .helpers import logging as tools_logging
+
+
+def main():
+    def actionNeedRoot(action):
+        if os.geteuid() != 0:
+            raise RuntimeError(
+                "Action \"{}\" needs root access".format(action))
+
+    # Wrap everything to display nice error messages
+    args = None
+    try:
+        os.umask(0o000)
+        # Parse arguments, set up logging
+        args = helpers.arguments()
+        args.cache = {}
+        args.work = config.defaults["work"]
+        args.config = args.work + "/waydroid.cfg"
+        args.log = args.work + "/tools.log"
+        args.sudo_timer = True
+        args.timeout = 1800
+
+        if not os.path.isfile(args.config):
+            if args.action and args.action != "init":
+                print('ERROR: WayDroid is not initialized, run "waydroid init"')
+                return 0
+            elif os.geteuid() == 0 and args.action == "init":
+                os.mkdir(args.work)
+            else:
+                args.log = "/tmp/tools.log"
+
+        tools_logging.init(args)
+
+        # Initialize or require config
+        if args.action == "init":
+            actionNeedRoot(args.action)
+            actions.init(args)
+        elif args.action == "upgrade":
+            actionNeedRoot(args.action)
+            actions.upgrade(args)
+        elif args.action == "session":
+            if args.subaction == "start":
+                actions.session_manager.start(args)
+            elif args.subaction == "stop":
+                actions.session_manager.stop(args)
+            else:
+                logging.info(
+                    "Run waydroid {} -h for usage information.".format(args.action))
+        elif args.action == "container":
+            actionNeedRoot(args.action)
+            if args.subaction == "start":
+                actions.container_manager.start(args)
+            elif args.subaction == "stop":
+                actions.container_manager.stop(args)
+            elif args.subaction == "freeze":
+                actions.container_manager.freeze(args)
+            elif args.subaction == "unfreeze":
+                actions.container_manager.unfreeze(args)
+            else:
+                logging.info(
+                    "Run waydroid {} -h for usage information.".format(args.action))
+        elif args.action == "app":
+            if args.subaction == "install":
+                actions.app_manager.install(args)
+            elif args.subaction == "remove":
+                actions.app_manager.remove(args)
+            elif args.subaction == "launch":
+                actions.app_manager.launch(args)
+            elif args.subaction == "list":
+                actions.app_manager.list(args)
+            else:
+                logging.info(
+                    "Run waydroid {} -h for usage information.".format(args.action))
+        elif args.action == "prop":
+            if args.subaction == "get":
+                ret = helpers.props.get(args, args.key)
+                if ret:
+                    print(ret)
+            elif args.subaction == "set":
+                helpers.props.set(args, args.key, args.value)
+            else:
+                logging.info(
+                    "Run waydroid {} -h for usage information.".format(args.action))
+        elif args.action == "shell":
+            actionNeedRoot(args.action)
+            helpers.lxc.shell(args)
+        elif args.action == "logcat":
+            actionNeedRoot(args.action)
+            helpers.lxc.logcat(args)
+        elif args.action == "show-full-ui":
+            actions.app_manager.showFullUI(args)
+        elif args.action == "status":
+            actions.status.print_status(args)
+        elif args.action == "log":
+            if args.clear_log:
+                helpers.run.user(args, ["truncate", "-s", "0", args.log])
+            helpers.run.user(
+                args, ["tail", "-n", args.lines, "-F", args.log], output="tui")
+        else:
+            logging.info("Run waydroid -h for usage information.")
+
+        #logging.info("Done")
+
+    except Exception as e:
+        # Dump log to stdout when args (and therefore logging) init failed
+        if not args:
+            logging.getLogger().setLevel(logging.DEBUG)
+
+        logging.info("ERROR: " + str(e))
+        logging.info("See also: <https://github.com/waydroid>")
+        logging.debug(traceback.format_exc())
+
+        # Hints about the log file (print to stdout only)
+        log_hint = "Run 'waydroid log' for details."
+        if not args or not os.path.exists(args.log):
+            log_hint += (" Alternatively you can use '--details-to-stdout' to"
+                         " get more output, e.g. 'waydroid"
+                         " --details-to-stdout init'.")
+        print(log_hint)
+        return 1
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/tools/actions/__init__.py b/tools/actions/__init__.py
new file mode 100644 (file)
index 0000000..558ed83
--- /dev/null
@@ -0,0 +1,8 @@
+# Copyright 2021 Erfan Abdi
+# SPDX-License-Identifier: GPL-3.0-or-later
+from tools.actions.initializer import init
+from tools.actions.upgrader import upgrade
+from tools.actions.session_manager import start, stop
+from tools.actions.container_manager import start, stop, freeze, unfreeze
+from tools.actions.app_manager import install, remove, launch, list
+from tools.actions.status import print_status
diff --git a/tools/actions/app_manager.py b/tools/actions/app_manager.py
new file mode 100644 (file)
index 0000000..7bb19a2
--- /dev/null
@@ -0,0 +1,113 @@
+# Copyright 2021 Erfan Abdi
+# SPDX-License-Identifier: GPL-3.0-or-later
+import logging
+import os
+import shutil
+import time
+import tools.config
+import tools.helpers.props
+from tools.interfaces import IPlatform
+from tools.interfaces import IStatusBarService
+
+def install(args):
+    if os.path.exists(tools.config.session_defaults["config_path"]):
+        session_cfg = tools.config.load_session()
+        if session_cfg["session"]["state"] == "RUNNING":
+            tmp_dir = session_cfg["session"]["waydroid_data"] + "/waydroid_tmp"
+            if not os.path.exists(tmp_dir):
+                os.makedirs(tmp_dir)
+
+            shutil.copyfile(args.PACKAGE, tmp_dir + "/base.apk")
+            platformService = IPlatform.get_service(args)
+            if platformService:
+                platformService.installApp("/data/waydroid_tmp/base.apk")
+            shutil.rmtree(tmp_dir)
+        else:
+            logging.error("WayDroid container is {}".format(
+                session_cfg["session"]["state"]))
+    else:
+        logging.error("WayDroid session is stopped")
+
+def remove(args):
+    if os.path.exists(tools.config.session_defaults["config_path"]):
+        session_cfg = tools.config.load_session()
+        if session_cfg["session"]["state"] == "RUNNING":
+            platformService = IPlatform.get_service(args)
+            if platformService:
+                ret = platformService.removeApp(args.PACKAGE)
+                if ret != 0:
+                    logging.error("Failed to uninstall package: {}".format(args.PACKAGE))
+            else:
+                logging.error("Failed to access IPlatform service")
+        else:
+            logging.error("WayDroid container is {}".format(
+                session_cfg["session"]["state"]))
+    else:
+        logging.error("WayDroid session is stopped")
+
+def launch(args):
+    def justLaunch():
+        platformService = IPlatform.get_service(args)
+        if platformService:
+            platformService.setprop("waydroid.active_apps", args.PACKAGE)
+            ret = platformService.launchApp(args.PACKAGE)
+            multiwin = platformService.getprop(
+                "persist.waydroid.multi_windows", "false")
+            if multiwin == "false":
+                platformService.settingsPutString(
+                    2, "policy_control", "immersive.status=*")
+            else:
+                platformService.settingsPutString(
+                    2, "policy_control", "immersive.full=*")
+        else:
+            logging.error("Failed to access IPlatform service")
+
+    if os.path.exists(tools.config.session_defaults["config_path"]):
+        session_cfg = tools.config.load_session()
+
+        if session_cfg["session"]["state"] == "RUNNING":
+            justLaunch()
+        elif session_cfg["session"]["state"] == "FROZEN" or session_cfg["session"]["state"] == "UNFREEZE":
+            session_cfg["session"]["state"] = "UNFREEZE"
+            tools.config.save_session(session_cfg)
+            while session_cfg["session"]["state"] != "RUNNING":
+                session_cfg = tools.config.load_session()
+            justLaunch()
+        else:
+            logging.error("WayDroid container is {}".format(
+                session_cfg["session"]["state"]))
+    else:
+        logging.error("WayDroid session is stopped")
+
+def list(args):
+    if os.path.exists(tools.config.session_defaults["config_path"]):
+        session_cfg = tools.config.load_session()
+        if session_cfg["session"]["state"] == "RUNNING":
+            platformService = IPlatform.get_service(args)
+            if platformService:
+                appsList = platformService.getAppsInfo()
+                for app in appsList:
+                    print("Name: " + app["name"])
+                    print("packageName: " + app["packageName"])
+                    print("categories:")
+                    for cat in app["categories"]:
+                        print("\t" + cat)
+            else:
+                logging.error("Failed to access IPlatform service")
+        else:
+            logging.error("WayDroid container is {}".format(
+                session_cfg["session"]["state"]))
+    else:
+        logging.error("WayDroid session is stopped")
+
+def showFullUI(args):
+    platformService = IPlatform.get_service(args)
+    if platformService:
+        platformService.setprop("waydroid.active_apps", "Waydroid")
+        platformService.settingsPutString(2, "policy_control", "null*")
+        #HACK: Refresh display contents
+        statusBarService = IStatusBarService.get_service(args)
+        if statusBarService:
+            statusBarService.expand()
+            time.sleep(0.5)
+            statusBarService.collapse()
diff --git a/tools/actions/container_manager.py b/tools/actions/container_manager.py
new file mode 100644 (file)
index 0000000..994dcea
--- /dev/null
@@ -0,0 +1,227 @@
+# Copyright 2021 Erfan Abdi
+# SPDX-License-Identifier: GPL-3.0-or-later
+from shutil import which
+import logging
+import os
+import time
+import glob
+import signal
+import sys
+import tools.config
+from tools import helpers
+from tools import services
+
+
+def start(args):
+    def make_prop(full_props_path):
+        def add_prop(key, cfg_key):
+            value = session_cfg["session"][cfg_key]
+            if value != "None":
+                props.append(key + "=" + session_cfg["session"][cfg_key])
+
+        if not os.path.isfile(args.work + "/waydroid_base.prop"):
+            raise RuntimeError("waydroid_base.prop Not found")
+        with open(args.work + "/waydroid_base.prop") as f:
+            props = f.read().splitlines()
+        if not props:
+            raise RuntimeError("waydroid_base.prop is broken!!?")
+
+        add_prop("waydroid.host.user", "user_name")
+        add_prop("waydroid.host.uid", "user_id")
+        add_prop("waydroid.host.gid", "group_id")
+        add_prop("waydroid.xdg_runtime_dir", "xdg_runtime_dir")
+        add_prop("waydroid.pulse_runtime_path", "pulse_runtime_path")
+        add_prop("waydroid.wayland_display", "wayland_display")
+        if which("waydroid-sensord") is None:
+            props.append("waydroid.stub_sensors_hal=1")
+        dpi = session_cfg["session"]["lcd_density"]
+        if dpi != "0":
+            props.append("ro.sf.lcd_density=" + dpi)
+
+        final_props = open(full_props_path, "w")
+        for prop in props:
+            final_props.write(prop + "\n")
+        final_props.close()
+        os.chmod(full_props_path, 0o644)
+
+    def set_permissions(perm_list=None, mode="777"):
+        def chmod(path, mode):
+            if os.path.exists(path):
+                command = ["chmod", mode, "-R", path]
+                tools.helpers.run.root(args, command, check=False)
+
+        # Nodes list
+        if not perm_list:
+            perm_list = [
+                "/dev/ashmem",
+
+                # sw_sync for HWC
+                "/dev/sw_sync",
+                "/sys/kernel/debug/sync/sw_sync",
+
+                # Media
+                "/dev/Vcodec",
+                "/dev/MTK_SMI",
+                "/dev/mdp_sync",
+                "/dev/mtk_cmdq",
+                "/dev/video32",
+                "/dev/video33",
+
+                # Graphics
+                "/dev/dri",
+                "/dev/graphics",
+
+                # Wayland and pulse socket permissions
+                session_cfg["session"]["pulse_runtime_path"],
+                session_cfg["session"]["xdg_runtime_dir"]
+            ]
+
+            # Framebuffers
+            perm_list.extend(glob.glob("/dev/fb*"))
+
+        for path in perm_list:
+            chmod(path, mode)
+
+    def signal_handler(sig, frame):
+        services.hardware_manager.stop(args)
+        stop(args)
+        sys.exit(0)
+
+    status = helpers.lxc.status(args)
+    if status == "STOPPED":
+        # Load binder and ashmem drivers
+        cfg = tools.config.load(args)
+        if cfg["waydroid"]["vendor_type"] == "MAINLINE":
+            if helpers.drivers.probeBinderDriver(args) != 0:
+                logging.error("Failed to load Binder driver")
+            if helpers.drivers.probeAshmemDriver(args) != 0:
+                logging.error("Failed to load Ashmem driver")
+        helpers.drivers.loadBinderNodes(args)
+        set_permissions([
+            "/dev/" + args.BINDER_DRIVER,
+            "/dev/" + args.VNDBINDER_DRIVER,
+            "/dev/" + args.HWBINDER_DRIVER
+        ], "666")
+
+        if os.path.exists(tools.config.session_defaults["config_path"]):
+            session_cfg = tools.config.load_session()
+            if session_cfg["session"]["state"] != "STOPPED":
+                logging.warning("Found session config on state: {}, restart session".format(
+                    session_cfg["session"]["state"]))
+                os.remove(tools.config.session_defaults["config_path"])
+        logging.debug("Container manager is waiting for session to load")
+        while not os.path.exists(tools.config.session_defaults["config_path"]):
+            time.sleep(1)
+        
+        # Load session configs
+        session_cfg = tools.config.load_session()
+        
+        # Generate props
+        make_prop(args.work + "/waydroid.prop")
+
+        # Networking
+        command = [tools.config.tools_src +
+                   "/data/scripts/waydroid-net.sh", "start"]
+        tools.helpers.run.root(args, command, check=False)
+
+        # Sensors
+        tools.helpers.run.root(
+            args, ["waydroid-sensord", args.HWBINDER_DRIVER], output="background")
+
+        # Mount rootfs
+        helpers.images.mount_rootfs(args, cfg["waydroid"]["images_path"])
+
+        # Mount data
+        helpers.mount.bind(args, session_cfg["session"]["waydroid_data"],
+                           tools.config.defaults["data"])
+
+        # Cgroup hacks
+        if which("start"):
+            command = ["start", "cgroup-lite"]
+            tools.helpers.run.root(args, command, check=False)
+        helpers.mount.umount_all(args, "/sys/fs/cgroup/schedtune")
+
+        #TODO: remove NFC hacks
+        if which("stop"):
+            command = ["stop", "nfcd"]
+            tools.helpers.run.root(args, command, check=False)
+
+        # Set permissions
+        set_permissions()
+        
+        helpers.lxc.start(args)
+        session_cfg["session"]["state"] = helpers.lxc.status(args)
+        tools.config.save_session(session_cfg)
+
+        if not hasattr(args, 'hardwareLoop'):
+            services.hardware_manager.start(args)
+
+        signal.signal(signal.SIGINT, signal_handler)
+        while os.path.exists(tools.config.session_defaults["config_path"]):
+            session_cfg = tools.config.load_session()
+            if session_cfg["session"]["state"] == "STOPPED":
+                services.hardware_manager.stop(args)
+                sys.exit(0)
+            elif session_cfg["session"]["state"] == "UNFREEZE":
+                session_cfg["session"]["state"] = helpers.lxc.status(args)
+                tools.config.save_session(session_cfg)
+                unfreeze(args)
+            time.sleep(1)
+
+        logging.warning("session manager stopped, stopping container and waiting...")
+        stop(args)
+        start(args)
+    else:
+        logging.error("WayDroid container is {}".format(status))
+
+def stop(args):
+    status = helpers.lxc.status(args)
+    if status != "STOPPED":
+        helpers.lxc.stop(args)
+        if os.path.exists(tools.config.session_defaults["config_path"]):
+            session_cfg = tools.config.load_session()
+            session_cfg["session"]["state"] = helpers.lxc.status(args)
+            tools.config.save_session(session_cfg)
+
+        # Networking
+        command = [tools.config.tools_src +
+                   "/data/scripts/waydroid-net.sh", "stop"]
+        tools.helpers.run.root(args, command, check=False)
+
+        #TODO: remove NFC hacks
+        if which("start"):
+            command = ["start", "nfcd"]
+            tools.helpers.run.root(args, command, check=False)
+
+        # Sensors
+        if which("waydroid-sensord"):
+            command = ["pidof", "waydroid-sensord"]
+            pid = tools.helpers.run.root(args, command, check=False, output_return=True)
+            if pid:
+                command = ["killall", pid]
+                tools.helpers.run.root(args, command, check=False)
+
+    else:
+        logging.error("WayDroid container is {}".format(status))
+
+def freeze(args):
+    status = helpers.lxc.status(args)
+    if status == "RUNNING":
+        helpers.lxc.freeze(args)
+        if os.path.exists(tools.config.session_defaults["config_path"]):
+            session_cfg = tools.config.load_session()
+            session_cfg["session"]["state"] = helpers.lxc.status(args)
+            tools.config.save_session(session_cfg)
+    else:
+        logging.error("WayDroid container is {}".format(status))
+
+def unfreeze(args):
+    status = helpers.lxc.status(args)
+    if status == "FROZEN":
+        helpers.lxc.unfreeze(args)
+        if os.path.exists(tools.config.session_defaults["config_path"]):
+            session_cfg = tools.config.load_session()
+            session_cfg["session"]["state"] = helpers.lxc.status(args)
+            tools.config.save_session(session_cfg)
+    else:
+        logging.error("WayDroid container is {}".format(status))
diff --git a/tools/actions/initializer.py b/tools/actions/initializer.py
new file mode 100644 (file)
index 0000000..c5783a4
--- /dev/null
@@ -0,0 +1,88 @@
+# Copyright 2021 Erfan Abdi
+# SPDX-License-Identifier: GPL-3.0-or-later
+import logging
+import os
+import requests
+from tools import helpers
+import tools.config
+
+
+def get_vendor_type(args):
+    vndk_str = helpers.props.host_get(args, "ro.vndk.version")
+    ret = "MAINLINE"
+    if vndk_str != "":
+        vndk = int(vndk_str)
+        if vndk > 19:
+            ret = "HALIUM_" + str(vndk - 19)
+
+    return ret
+
+def setup_config(args):
+    cfg = tools.config.load(args)
+    args.arch = helpers.arch.host()
+    cfg["waydroid"]["arch"] = args.arch
+    cfg["waydroid"]["images_path"] = args.images_path
+
+    channels_cfg = tools.config.load_channels()
+    if not args.system_channel:
+        args.system_channel = channels_cfg["channels"]["system_channel"]
+    if not args.vendor_channel:
+        args.vendor_channel = channels_cfg["channels"]["vendor_channel"]
+    if not args.rom_type:
+        args.rom_type = channels_cfg["channels"]["rom_type"]
+    if not args.system_type:
+        args.system_type = channels_cfg["channels"]["system_type"]
+
+    args.system_ota = args.system_channel + "/" + args.rom_type + \
+        "/waydroid_" + args.arch + "/" + args.system_type + ".json"
+    system_request = requests.get(args.system_ota)
+    if system_request.status_code != 200:
+        raise ValueError(
+            "Failed to get system OTA channel: {}".format(args.system_ota))
+
+    device_codename = helpers.props.host_get(args, "ro.product.device")
+    args.vendor_type = None
+    for vendor in [device_codename, get_vendor_type(args)]:
+        vendor_ota = args.vendor_channel + "/waydroid_" + \
+            args.arch + "/" + vendor + ".json"
+        vendor_request = requests.get(vendor_ota)
+        if vendor_request.status_code == 200:
+            args.vendor_type = vendor
+            args.vendor_ota = vendor_ota
+            break
+
+    if not args.vendor_type:
+        raise ValueError(
+            "Failed to get vendor OTA channel: {}".format(vendor_ota))
+
+    cfg["waydroid"]["vendor_type"] = args.vendor_type
+    cfg["waydroid"]["system_ota"] = args.system_ota
+    cfg["waydroid"]["vendor_ota"] = args.vendor_ota
+    helpers.drivers.setupBinderNodes(args)
+    cfg["waydroid"]["binder"] = args.BINDER_DRIVER
+    cfg["waydroid"]["vndbinder"] = args.VNDBINDER_DRIVER
+    cfg["waydroid"]["hwbinder"] = args.HWBINDER_DRIVER
+    tools.config.save(args, cfg)
+
+def init(args):
+    if not os.path.isfile(args.config) or args.force:
+        setup_config(args)
+        status = "STOPPED"
+        if os.path.exists(tools.config.defaults["lxc"] + "/waydroid"):
+            status = helpers.lxc.status(args)
+        if status != "STOPPED":
+            logging.info("Stopping container")
+            helpers.lxc.stop(args)
+        helpers.images.umount_rootfs(args)
+        helpers.images.get(args)
+        if not os.path.isdir(tools.config.defaults["rootfs"]):
+            os.mkdir(tools.config.defaults["rootfs"])
+        helpers.lxc.setup_host_perms(args)
+        helpers.lxc.set_lxc_config(args)
+        helpers.lxc.make_base_props(args)
+        if status != "STOPPED":
+            logging.info("Starting container")
+            helpers.images.mount_rootfs(args, args.images_path)
+            helpers.lxc.start(args)
+    else:
+        logging.info("Already initialized")
diff --git a/tools/actions/session_manager.py b/tools/actions/session_manager.py
new file mode 100644 (file)
index 0000000..7d89757
--- /dev/null
@@ -0,0 +1,51 @@
+# Copyright 2021 Erfan Abdi
+# SPDX-License-Identifier: GPL-3.0-or-later
+import logging
+import os
+import time
+import signal
+import sys
+import tools.config
+from tools import services
+
+
+def start(args):
+    def signal_handler(sig, frame):
+        stop(args)
+        sys.exit(0)
+
+    xdg_session = os.getenv("XDG_SESSION_TYPE")
+    if xdg_session != "wayland":
+        logging.warning('XDG Session is not "wayland"')
+
+    cfg = tools.config.load_session()
+    waydroid_data = cfg["session"]["waydroid_data"]
+    if not os.path.isdir(waydroid_data):
+        os.makedirs(waydroid_data)
+    dpi = tools.helpers.props.host_get(args, "ro.sf.lcd_density")
+    if dpi == "":
+        dpi = os.getenv("GRID_UNIT_PX")
+        if dpi is not None:
+            dpi = str(int(dpi) * 20)
+        else:
+            dpi = "0"
+    cfg["session"]["lcd_density"] = dpi
+    tools.config.save_session(cfg)
+
+    services.user_manager.start(args)
+    services.clipboard_manager.start(args)
+
+    signal.signal(signal.SIGINT, signal_handler)
+    while os.path.exists(tools.config.session_defaults["config_path"]):
+        time.sleep(1)
+    services.user_manager.stop(args)
+    services.clipboard_manager.stop(args)
+
+def stop(args):
+    config_path = tools.config.session_defaults["config_path"]
+    if os.path.isfile(config_path):
+        services.user_manager.stop(args)
+        services.clipboard_manager.stop(args)
+        os.remove(config_path)
+    else:
+        logging.error("WayDroid session is not started")
diff --git a/tools/actions/status.py b/tools/actions/status.py
new file mode 100644 (file)
index 0000000..00261ce
--- /dev/null
@@ -0,0 +1,19 @@
+# Copyright 2021 Erfan Abdi
+# SPDX-License-Identifier: GPL-3.0-or-later
+import os
+import tools.config
+
+def print_status(args):
+    cfg = tools.config.load(args)
+    if os.path.exists(tools.config.session_defaults["config_path"]):
+        session_cfg = tools.config.load_session()
+        print("Session:\tRUNNING")
+        print("Container:\t" + session_cfg["session"]["state"])
+        print("Vendor type:\t" + cfg["waydroid"]["vendor_type"])
+        print("Session user:\t{}({})".format(
+            session_cfg["session"]["user_name"], session_cfg["session"]["user_id"]))
+        print("Wayland display:\t" +
+                     session_cfg["session"]["wayland_display"])
+    else:
+        print("Session:\tSTOPPED")
+        print("Vendor type:\t" + cfg["waydroid"]["vendor_type"])
diff --git a/tools/actions/upgrader.py b/tools/actions/upgrader.py
new file mode 100644 (file)
index 0000000..cb1aaa5
--- /dev/null
@@ -0,0 +1,35 @@
+# Copyright 2021 Erfan Abdi
+# SPDX-License-Identifier: GPL-3.0-or-later
+import logging
+import os
+from tools import helpers
+import tools.config
+
+
+def get_config(args):
+    cfg = tools.config.load(args)
+    args.arch = cfg["waydroid"]["arch"]
+    args.images_path = cfg["waydroid"]["images_path"]
+    args.vendor_type = cfg["waydroid"]["vendor_type"]
+    args.system_ota = cfg["waydroid"]["system_ota"]
+    args.vendor_ota = cfg["waydroid"]["vendor_ota"]
+
+def upgrade(args):
+    get_config(args)
+    status = "STOPPED"
+    if os.path.exists(tools.config.defaults["lxc"] + "/waydroid"):
+        status = helpers.lxc.status(args)
+    if status != "STOPPED":
+        logging.info("Stopping container")
+        helpers.lxc.stop(args)
+    helpers.images.umount_rootfs(args)
+    helpers.drivers.loadBinderNodes(args)
+    if not args.offline:
+        helpers.images.get(args)
+    helpers.lxc.setup_host_perms(args)
+    helpers.lxc.set_lxc_config(args)
+    helpers.lxc.make_base_props(args)
+    if status != "STOPPED":
+        logging.info("Starting container")
+        helpers.images.mount_rootfs(args, args.images_path)
+        helpers.lxc.start(args)
diff --git a/tools/config/__init__.py b/tools/config/__init__.py
new file mode 100644 (file)
index 0000000..4c16de2
--- /dev/null
@@ -0,0 +1,79 @@
+# Copyright 2021 Oliver Smith
+# SPDX-License-Identifier: GPL-3.0-or-later
+import os
+import pwd
+
+#
+# Exported functions
+#
+from tools.config.load import load, load_session, load_channels
+from tools.config.save import save, save_session
+
+#
+# Exported variables (internal configuration)
+#
+version = "1.0.0"
+tools_src = os.path.normpath(os.path.realpath(__file__) + "/../../..")
+
+# Keys saved in the config file (mostly what we ask in 'waydroid init')
+config_keys = ["arch",
+               "images_path",
+               "vendor_type",
+               "system_datetime",
+               "vendor_datetime"]
+
+session_config_keys = ["user_name",
+                       "user_id",
+                       "group_id",
+                       "host_user",
+                       "waydroid_data",
+                       "xdg_runtime_dir",
+                       "wayland_display",
+                       "pulse_runtime_path",
+                       "state",
+                       "lcd_density"]
+
+# Config file/commandline default values
+# $WORK gets replaced with the actual value for args.work (which may be
+# overridden on the commandline)
+defaults = {
+    "arch": "arm64",
+    "work": "/home/.waydroid",
+    "vendor_type": "MAINLINE",
+    "system_datetime": "0",
+    "vendor_datetime": "0"
+}
+defaults["images_path"] = defaults["work"] + "/images"
+defaults["rootfs"] = defaults["work"] + "/rootfs"
+defaults["data"] = defaults["work"] + "/data"
+defaults["lxc"] = defaults["work"] + "/lxc"
+defaults["host_perms"] = defaults["work"] + "/host-permissions"
+
+session_defaults = {
+    "user_name": pwd.getpwuid(os.getuid()).pw_name,
+    "user_id": str(os.getuid()),
+    "group_id": str(os.getgid()),
+    "host_user": os.path.expanduser("~"),
+    "xdg_runtime_dir": str(os.environ.get('XDG_RUNTIME_DIR')),
+    "wayland_display": str(os.environ.get('WAYLAND_DISPLAY')),
+    "pulse_runtime_path": str(os.environ.get('PULSE_RUNTIME_PATH')),
+    "state": "STOPPED",
+    "lcd_density": "0"
+}
+session_defaults["config_path"] = defaults["work"] + "/session.cfg"
+session_defaults["waydroid_data"] = session_defaults["host_user"] + \
+    "/waydroid/data"
+if session_defaults["pulse_runtime_path"] == "None":
+    session_defaults["pulse_runtime_path"] = session_defaults["xdg_runtime_dir"] + "/pulse"
+
+channels_defaults = {
+    "config_path": "/usr/share/waydroid-extra/channels.cfg",
+    "system_channel": "https://raw.githubusercontent.com/waydroid/OTA/master/systems",
+    "vendor_channel": "https://raw.githubusercontent.com/waydroid/OTA/master/vendor",
+    "rom_type": "lineage",
+    "system_type": "VANILLA"
+}
+channels_config_keys = ["system_channel",
+                        "vendor_channel",
+                        "rom_type",
+                        "system_type"]
diff --git a/tools/config/load.py b/tools/config/load.py
new file mode 100644 (file)
index 0000000..6826c21
--- /dev/null
@@ -0,0 +1,69 @@
+# Copyright 2021 Oliver Smith
+# SPDX-License-Identifier: GPL-3.0-or-later
+import logging
+import configparser
+import os
+import tools.config
+
+
+def load(args):
+    cfg = configparser.ConfigParser()
+    if os.path.isfile(args.config):
+        cfg.read(args.config)
+
+    if "waydroid" not in cfg:
+        cfg["waydroid"] = {}
+
+    for key in tools.config.defaults:
+        if key in tools.config.config_keys and key not in cfg["waydroid"]:
+            cfg["waydroid"][key] = str(tools.config.defaults[key])
+
+        # We used to save default values in the config, which can *not* be
+        # configured in "waydroid init". That doesn't make sense, we always
+        # want to use the defaults from tools/config/__init__.py in that case,
+        if key not in tools.config.config_keys and key in cfg["waydroid"]:
+            logging.debug("Ignored unconfigurable and possibly outdated"
+                          " default value from config: {}".format(cfg['waydroid'][key]))
+            del cfg["waydroid"][key]
+
+    return cfg
+
+def load_session():
+    config_path = tools.config.session_defaults["config_path"]
+    cfg = configparser.ConfigParser()
+    if os.path.isfile(config_path):
+        cfg.read(config_path)
+
+    if "session" not in cfg:
+        cfg["session"] = {}
+
+    for key in tools.config.session_defaults:
+        if key in tools.config.session_config_keys and key not in cfg["session"]:
+            cfg["session"][key] = str(tools.config.session_defaults[key])
+
+        if key not in tools.config.session_config_keys and key in cfg["session"]:
+            logging.debug("Ignored unconfigurable and possibly outdated"
+                          " default value from config: {}".format(cfg['session'][key]))
+            del cfg["session"][key]
+
+    return cfg
+
+def load_channels():
+    config_path = tools.config.channels_defaults["config_path"]
+    cfg = configparser.ConfigParser()
+    if os.path.isfile(config_path):
+        cfg.read(config_path)
+
+    if "channels" not in cfg:
+        cfg["channels"] = {}
+
+    for key in tools.config.channels_defaults:
+        if key in tools.config.channels_config_keys and key not in cfg["channels"]:
+            cfg["channels"][key] = str(tools.config.channels_defaults[key])
+
+        if key not in tools.config.channels_config_keys and key in cfg["channels"]:
+            logging.debug("Ignored unconfigurable and possibly outdated"
+                          " default value from config: {}".format(cfg['channels'][key]))
+            del cfg["channels"][key]
+
+    return cfg
diff --git a/tools/config/save.py b/tools/config/save.py
new file mode 100644 (file)
index 0000000..67e25ea
--- /dev/null
@@ -0,0 +1,19 @@
+# Copyright 2021 Oliver Smith
+# SPDX-License-Identifier: GPL-3.0-or-later
+import os
+import logging
+import tools.config
+
+
+def save(args, cfg):
+    logging.debug("Save config: " + args.config)
+    os.makedirs(os.path.dirname(args.config), 0o700, True)
+    with open(args.config, "w") as handle:
+        cfg.write(handle)
+
+def save_session(cfg):
+    config_path = tools.config.session_defaults["config_path"]
+    logging.debug("Save session config: " + config_path)
+    os.makedirs(os.path.dirname(config_path), 0o700, True)
+    with open(config_path, "w") as handle:
+        cfg.write(handle)
diff --git a/tools/helpers/__init__.py b/tools/helpers/__init__.py
new file mode 100644 (file)
index 0000000..21357cf
--- /dev/null
@@ -0,0 +1,10 @@
+# Copyright 2021 Oliver Smith
+# SPDX-License-Identifier: GPL-3.0-or-later
+from tools.helpers.arguments import arguments
+import tools.helpers.arch
+import tools.helpers.props
+import tools.helpers.lxc
+import tools.helpers.images
+import tools.helpers.drivers
+import tools.helpers.mount
+import tools.helpers.http
diff --git a/tools/helpers/arch.py b/tools/helpers/arch.py
new file mode 100644 (file)
index 0000000..3c0bb76
--- /dev/null
@@ -0,0 +1,17 @@
+# Copyright 2021 Oliver Smith
+# SPDX-License-Identifier: GPL-3.0-or-later
+import platform
+
+def host():
+    machine = platform.machine()
+
+    mapping = {
+        "i686": "x86",
+        "x86_64": "x86_64",
+        "aarch64": "arm64",
+        "armv7l": "arm"
+    }
+    if machine in mapping:
+        return mapping[machine]
+    raise ValueError("platform.machine '" + machine + "'"
+                     " architecture is not supported")
diff --git a/tools/helpers/arguments.py b/tools/helpers/arguments.py
new file mode 100644 (file)
index 0000000..4c95287
--- /dev/null
@@ -0,0 +1,153 @@
+# Copyright 2021 Oliver Smith
+# SPDX-License-Identifier: GPL-3.0-or-later
+import argparse
+
+try:
+    import argcomplete
+except ImportError:
+    argcomplete = False
+
+import tools.config
+
+""" This file is about parsing command line arguments passed to waydroid, as
+    well as generating the help pages (waydroid -h). All this is done with
+    Python's argparse. The parsed arguments get extended and finally stored in
+    the "args" variable, which is prominently passed to most functions all
+    over the waydroid code base.
+
+    See tools/helpers/args.py for more information about the args variable. """
+
+def arguments_init(subparser):
+    ret = subparser.add_parser("init", help="set up waydroid specific"
+                               " configs and install images")
+    ret.add_argument("-i", "--images_path",
+                        default=tools.config.defaults["images_path"],
+                        help="custom path to waeiod images (default in"
+                             " /home/.waydroid/images)")
+    ret.add_argument("-f", "--force", action="store_true",
+                     help="re-initialize configs and images")
+    ret.add_argument("-c", "--system_channel",
+                     help="custom system channel (options: OTA channel URL; default is Official OTA server)")
+    ret.add_argument("-v", "--vendor_channel",
+                     help="custom vendor channel (options: OTA channel URL; default is Official OTA server)")
+    ret.add_argument("-r", "--rom_type",
+                     help="rom type (options: \"lineage\", \"bliss\" or OTA channel URL; default is LineageOS)")
+    ret.add_argument("-s", "--system_type",
+                     help="system type (options: VANILLA, FOSS or GAPPS; default is VANILLA)")
+    return ret
+
+def arguments_status(subparser):
+    ret = subparser.add_parser("status",
+                               help="quick check for the waydroid")
+    return ret
+
+def arguments_upgrade(subparser):
+    ret = subparser.add_parser("upgrade", help="upgrade images")
+    ret.add_argument("-o", "--offline", action="store_true",
+                     help="just for updating configs")
+    return ret
+
+def arguments_log(subparser):
+    ret = subparser.add_parser("log", help="follow the waydroid logfile")
+    ret.add_argument("-n", "--lines", default="60",
+                     help="count of initial output lines")
+    ret.add_argument("-c", "--clear", help="clear the log",
+                     action="store_true", dest="clear_log")
+    return ret
+
+def arguments_session(subparser):
+    ret = subparser.add_parser("session", help="session controller")
+    sub = ret.add_subparsers(title="subaction", dest="subaction")
+    sub.add_parser("start", help="start session")
+    sub.add_parser("stop", help="start session")
+    return ret
+
+def arguments_container(subparser):
+    ret = subparser.add_parser("container", help="container controller")
+    sub = ret.add_subparsers(title="subaction", dest="subaction")
+    sub.add_parser("start", help="start container")
+    sub.add_parser("stop", help="start container")
+    sub.add_parser("freeze", help="freeze container")
+    sub.add_parser("unfreeze", help="unfreeze container")
+    return ret
+
+def arguments_app(subparser):
+    ret = subparser.add_parser("app", help="applications controller")
+    sub = ret.add_subparsers(title="subaction", dest="subaction")
+    install = sub.add_parser(
+        "install", help="push a single package to the container and install it")
+    install.add_argument('PACKAGE', help="path to apk file")
+    remove = sub.add_parser(
+        "remove", help="remove single app package from the container")
+    remove.add_argument('PACKAGE', help="package name of app to remove")
+    launch = sub.add_parser("launch", help="start single application")
+    launch.add_argument('PACKAGE', help="package name of app to launch")
+    sub.add_parser("list", help="list installed applications")
+    return ret
+
+def arguments_prop(subparser):
+    ret = subparser.add_parser("prop", help="android properties controller")
+    sub = ret.add_subparsers(title="subaction", dest="subaction")
+    get = sub.add_parser(
+        "get", help="get value of property from container")
+    get.add_argument('key', help="key of the property to get")
+    set = sub.add_parser(
+        "set", help="set value to property on container")
+    set.add_argument('key', help="key of the property to set")
+    set.add_argument('value', help="value of the property to set")
+    return ret
+
+def arguments_fullUI(subparser):
+    ret = subparser.add_parser("show-full-ui", help="show android full screen in window")
+    return ret
+
+def arguments_shell(subparser):
+    ret = subparser.add_parser("shell", help="run remote shell command")
+    ret.add_argument('COMMAND', nargs='?', help="command to run")
+    return ret
+
+def arguments_logcat(subparser):
+    ret = subparser.add_parser("logcat", help="show android logcat")
+    return ret
+
+def arguments():
+    parser = argparse.ArgumentParser(prog="waydroid")
+
+    # Other
+    parser.add_argument("-V", "--version", action="version",
+                        version=tools.config.version)
+
+    # Logging
+    parser.add_argument("-l", "--log", dest="log", default=None,
+                        help="path to log file")
+    parser.add_argument("--details-to-stdout", dest="details_to_stdout",
+                        help="print details (e.g. build output) to stdout,"
+                             " instead of writing to the log",
+                        action="store_true")
+    parser.add_argument("-v", "--verbose", dest="verbose",
+                        action="store_true", help="write even more to the"
+                        " logfiles (this may reduce performance)")
+    parser.add_argument("-q", "--quiet", dest="quiet", action="store_true",
+                        help="do not output any log messages")
+
+    # Actions
+    sub = parser.add_subparsers(title="action", dest="action")
+
+    arguments_status(sub)
+    arguments_log(sub)
+    arguments_init(sub)
+    arguments_upgrade(sub)
+    arguments_session(sub)
+    arguments_container(sub)
+    arguments_app(sub)
+    arguments_prop(sub)
+    arguments_fullUI(sub)
+    arguments_shell(sub)
+    arguments_logcat(sub)
+
+    if argcomplete:
+        argcomplete.autocomplete(parser, always_complete_options="long")
+
+    # Parse and extend arguments (also backup unmodified result from argparse)
+    args = parser.parse_args()
+    return args
diff --git a/tools/helpers/drivers.py b/tools/helpers/drivers.py
new file mode 100644 (file)
index 0000000..e5a0335
--- /dev/null
@@ -0,0 +1,135 @@
+# Copyright 2021 Erfan Abdi
+# SPDX-License-Identifier: GPL-3.0-or-later
+import logging
+import os
+import glob
+import tools.config
+import tools.helpers.run
+
+
+BINDER_DRIVERS = [
+    "anbox-binder",
+    "puddlejumper",
+    "binder"
+]
+VNDBINDER_DRIVERS = [
+    "anbox-vndbinder",
+    "vndpuddlejumper",
+    "vndbinder"
+]
+HWBINDER_DRIVERS = [
+    "anbox-hwbinder",
+    "hwpuddlejumper",
+    "hwbinder"
+]
+
+def probeBinderDriver(args):
+    binder_dev_nodes = []
+    has_binder = False
+    has_vndbinder = False
+    has_hwbinder = False
+    for node in BINDER_DRIVERS:
+        if os.path.exists("/dev/" + node):
+            has_binder = True
+    if not has_binder:
+        binder_dev_nodes.append(BINDER_DRIVERS[0])
+    for node in VNDBINDER_DRIVERS:
+        if os.path.exists("/dev/" + node):
+            has_vndbinder = True
+    if not has_vndbinder:
+        binder_dev_nodes.append(VNDBINDER_DRIVERS[0])
+    for node in HWBINDER_DRIVERS:
+        if os.path.exists("/dev/" + node):
+            has_hwbinder = True
+    if not has_hwbinder:
+        binder_dev_nodes.append(HWBINDER_DRIVERS[0])
+
+    if len(binder_dev_nodes) > 0:
+        devices = ','.join(binder_dev_nodes)
+        command = ["modprobe", "binder_linux", "devices=\"{}\"".format(devices)]
+        output = tools.helpers.run.root(args, command, check=False, output_return=True)
+        if output:
+            logging.error("Failed to load binder driver for devices: {}".format(devices))
+            logging.error(output.strip())
+        else:
+            command = ["mkdir", "-p", "/dev/binderfs"]
+            tools.helpers.run.root(args, command, check=False)
+            command = ["mount", "-t", "binder", "binder", "/dev/binderfs"]
+            tools.helpers.run.root(args, command, check=False)
+            command = ["ln", "-s"]
+            command.extend(glob.glob("/dev/binderfs/*"))
+            command.append("/dev/")
+            tools.helpers.run.root(args, command, check=False)
+
+    for node in binder_dev_nodes:
+        if not os.path.exists("/dev/" + node):
+            return -1
+
+    return 0
+
+def probeAshmemDriver(args):
+    if not os.path.exists("/dev/ashmem"):
+        command = ["modprobe", "ashmem_linux"]
+        output = tools.helpers.run.root(args, command, check=False, output_return=True)
+        if output:
+            logging.error("Failed to load ashmem driver")
+            logging.error(output.strip())
+
+    if not os.path.exists("/dev/ashmem"):
+        return -1
+    
+    return 0
+
+def setupBinderNodes(args):
+    has_binder = False
+    has_vndbinder = False
+    has_hwbinder = False
+    if args.vendor_type == "MAINLINE":
+        probeBinderDriver(args)
+        for node in BINDER_DRIVERS:
+            if os.path.exists("/dev/" + node):
+                has_binder = True
+                args.BINDER_DRIVER = node
+        if not has_binder:
+            raise OSError('Binder node "binder" for waydroid not found')
+
+        for node in VNDBINDER_DRIVERS:
+            if os.path.exists("/dev/" + node):
+                has_vndbinder = True
+                args.VNDBINDER_DRIVER = node
+        if not has_vndbinder:
+            raise OSError('Binder node "vndbinder" for waydroid not found')
+
+        for node in HWBINDER_DRIVERS:
+            if os.path.exists("/dev/" + node):
+                has_hwbinder = True
+                args.HWBINDER_DRIVER = node
+        if not has_hwbinder:
+            raise OSError('Binder node "hwbinder" for waydroid not found')
+    else:
+        for node in BINDER_DRIVERS[:-1]:
+            if os.path.exists("/dev/" + node):
+                has_binder = True
+                args.BINDER_DRIVER = node
+        if not has_binder:
+            raise OSError('Binder node "binder" for waydroid not found')
+
+        for node in VNDBINDER_DRIVERS[:-1]:
+            if os.path.exists("/dev/" + node):
+                has_vndbinder = True
+                args.VNDBINDER_DRIVER = node
+        if not has_vndbinder:
+            raise OSError('Binder node "vndbinder" for waydroid not found')
+
+        for node in HWBINDER_DRIVERS[:-1]:
+            if os.path.exists("/dev/" + node):
+                has_hwbinder = True
+                args.HWBINDER_DRIVER = node
+        if not has_hwbinder:
+            raise OSError('Binder node "hwbinder" for waydroid not found')
+
+def loadBinderNodes(args):
+    cfg = tools.config.load(args)
+    args.BINDER_DRIVER = cfg["waydroid"]["binder"]
+    args.VNDBINDER_DRIVER = cfg["waydroid"]["vndbinder"]
+    args.HWBINDER_DRIVER = cfg["waydroid"]["hwbinder"]
diff --git a/tools/helpers/http.py b/tools/helpers/http.py
new file mode 100644 (file)
index 0000000..d05522b
--- /dev/null
@@ -0,0 +1,89 @@
+# Copyright 2021 Oliver Smith
+# SPDX-License-Identifier: GPL-3.0-or-later
+import hashlib
+import json
+import logging
+import os
+import shutil
+import urllib.request
+
+import tools.helpers.run
+
+
+def download(args, url, prefix, cache=True, loglevel=logging.INFO,
+             allow_404=False):
+    """ Download a file to disk.
+
+        :param url: the http(s) address of to the file to download
+        :param prefix: for the cache, to make it easier to find (cache files
+                       get a hash of the URL after the prefix)
+        :param cache: if True, and url is cached, do not download it again
+        :param loglevel: change to logging.DEBUG to only display the download
+                         message in 'waydroid log', not in stdout. We use
+                         this when downloading many APKINDEX files at once, no
+                         point in showing a dozen messages.
+        :param allow_404: do not raise an exception when the server responds
+                          with a 404 Not Found error. Only display a warning on
+                          stdout (no matter if loglevel is changed).
+        :returns: path to the downloaded file in the cache or None on 404 """
+    # Create cache folder
+    if not os.path.exists(args.work + "/cache_http"):
+        tools.helpers.run.user(args, ["mkdir", "-p", args.work + "/cache_http"])
+
+    # Check if file exists in cache
+    prefix = prefix.replace("/", "_")
+    path = (args.work + "/cache_http/" + prefix + "_" +
+            hashlib.sha256(url.encode("utf-8")).hexdigest())
+    if os.path.exists(path):
+        if cache:
+            return path
+        tools.helpers.run.user(args, ["rm", path])
+
+    # Download the file
+    logging.log(loglevel, "Download " + url)
+    try:
+        with urllib.request.urlopen(url) as response:
+            with open(path, "wb") as handle:
+                shutil.copyfileobj(response, handle)
+    # Handle 404
+    except urllib.error.HTTPError as e:
+        if e.code == 404 and allow_404:
+            logging.warning("WARNING: file not found: " + url)
+            return None
+        raise
+
+    # Return path in cache
+    return path
+
+
+def retrieve(url, headers=None, allow_404=False):
+    """ Fetch the content of a URL and returns it as string.
+
+        :param url: the http(s) address of to the resource to fetch
+        :param headers: dict of HTTP headers to use
+        :param allow_404: do not raise an exception when the server responds
+                          with a 404 Not Found error. Only display a warning
+        :returns: str with the content of the response
+    """
+    # Download the file
+    logging.verbose("Retrieving " + url)
+
+    if headers is None:
+        headers = {}
+
+    req = urllib.request.Request(url, headers=headers)
+    try:
+        with urllib.request.urlopen(req) as response:
+            return response.read()
+    # Handle 404
+    except urllib.error.HTTPError as e:
+        if e.code == 404 and allow_404:
+            logging.warning("WARNING: failed to retrieve content from: " + url)
+            return None
+        raise
+
+
+def retrieve_json(*args, **kwargs):
+    """ Fetch the contents of a URL, parse it as JSON and return it. See
+        retrieve() for the list of all parameters. """
+    return json.loads(retrieve(*args, **kwargs))
diff --git a/tools/helpers/images.py b/tools/helpers/images.py
new file mode 100644 (file)
index 0000000..aaeadff
--- /dev/null
@@ -0,0 +1,109 @@
+# Copyright 2021 Erfan Abdi
+# SPDX-License-Identifier: GPL-3.0-or-later
+import logging
+import zipfile
+import requests
+import hashlib
+import os
+import tools.config
+from tools import helpers
+
+
+def sha256sum(filename):
+    h = hashlib.sha256()
+    b = bytearray(128*1024)
+    mv = memoryview(b)
+    with open(filename, 'rb', buffering=0) as f:
+        for n in iter(lambda: f.readinto(mv), 0):
+            h.update(mv[:n])
+    return h.hexdigest()
+
+
+def get(args):
+    cfg = tools.config.load(args)
+    system_ota = cfg["waydroid"]["system_ota"]
+    system_request = requests.get(system_ota)
+    if system_request.status_code != 200:
+        raise ValueError(
+            "Failed to get system OTA channel: {}".format(system_ota))
+    system_responses = system_request.json()["response"]
+    if len(system_responses) < 1:
+        raise ValueError("No images found on system channel")
+
+    for system_response in system_responses:
+        if system_response['datetime'] > int(cfg["waydroid"]["system_datetime"]):
+            images_zip = helpers.http.download(
+                args, system_response['url'], system_response['filename'], cache=False)
+            logging.info("Validating system image")
+            if sha256sum(images_zip) != system_response['id']:
+                raise ValueError("Downloaded system image hash doesn't match, expected: {}".format(
+                    system_response['id']))
+            logging.info("Extracting to " + args.images_path)
+            with zipfile.ZipFile(images_zip, 'r') as zip_ref:
+                zip_ref.extractall(args.images_path)
+            cfg["waydroid"]["system_datetime"] = str(system_response['datetime'])
+            tools.config.save(args, cfg)
+            os.remove(images_zip)
+            break
+
+    vendor_ota = cfg["waydroid"]["vendor_ota"]
+    vendor_request = requests.get(vendor_ota)
+    if vendor_request.status_code != 200:
+        raise ValueError(
+            "Failed to get vendor OTA channel: {}".format(vendor_ota))
+    vendor_responses = vendor_request.json()["response"]
+    if len(vendor_responses) < 1:
+        raise ValueError("No images found on vendor channel")
+
+    for vendor_response in vendor_responses:
+        if vendor_response['datetime'] > int(cfg["waydroid"]["vendor_datetime"]):
+            images_zip = helpers.http.download(
+                args, vendor_response['url'], vendor_response['filename'], cache=False)
+            logging.info("Validating vendor image")
+            if sha256sum(images_zip) != vendor_response['id']:
+                raise ValueError("Downloaded vendor image hash doesn't match, expected: {}".format(
+                    vendor_response['id']))
+            logging.info("Extracting to " + args.images_path)
+            with zipfile.ZipFile(images_zip, 'r') as zip_ref:
+                zip_ref.extractall(args.images_path)
+            cfg["waydroid"]["vendor_datetime"] = str(vendor_response['datetime'])
+            tools.config.save(args, cfg)
+            os.remove(images_zip)
+            break
+
+def replace(args, system_zip, system_time, vendor_zip, vendor_time):
+    cfg = tools.config.load(args)
+    args.images_path = cfg["waydroid"]["images_path"]
+    if os.path.exists(system_zip):
+        with zipfile.ZipFile(system_zip, 'r') as zip_ref:
+            zip_ref.extractall(args.images_path)
+        cfg["waydroid"]["system_datetime"] = str(system_time)
+        tools.config.save(args, cfg)
+    if os.path.exists(vendor_zip):
+        with zipfile.ZipFile(vendor_zip, 'r') as zip_ref:
+            zip_ref.extractall(args.images_path)
+        cfg["waydroid"]["vendor_datetime"] = str(vendor_time)
+        tools.config.save(args, cfg)
+
+
+def mount_rootfs(args, images_dir):
+    helpers.mount.mount(args, images_dir + "/system.img",
+                        tools.config.defaults["rootfs"], umount=True)
+    helpers.mount.mount(args, images_dir + "/vendor.img",
+                           tools.config.defaults["rootfs"] + "/vendor")
+    for egl_path in ["/vendor/lib/egl", "/vendor/lib64/egl"]:
+        if os.path.isdir(egl_path):
+            helpers.mount.bind(
+                args, egl_path, tools.config.defaults["rootfs"] + egl_path)
+    if helpers.mount.ismount("/odm"):
+        helpers.mount.bind(
+            args, "/odm", tools.config.defaults["rootfs"] + "/odm_extra")
+    else:
+        if os.path.isdir("/vendor/odm"):
+            helpers.mount.bind(
+                args, "/vendor/odm", tools.config.defaults["rootfs"] + "/odm_extra")
+    helpers.mount.bind_file(args, args.work + "/waydroid.prop",
+                            tools.config.defaults["rootfs"] + "/vendor/waydroid.prop")
+
+def umount_rootfs(args):
+    helpers.mount.umount_all(args, tools.config.defaults["rootfs"])
diff --git a/tools/helpers/logging.py b/tools/helpers/logging.py
new file mode 100644 (file)
index 0000000..094bf82
--- /dev/null
@@ -0,0 +1,99 @@
+# Copyright 2021 Oliver Smith
+# SPDX-License-Identifier: GPL-3.0-or-later
+import logging
+import os
+import sys
+
+
+class log_handler(logging.StreamHandler):
+    """
+    Write to stdout and to the already opened log file.
+    """
+    _args = None
+
+    def emit(self, record):
+        try:
+            msg = self.format(record)
+
+            # INFO or higher: Write to stdout
+            if (not self._args.details_to_stdout and
+                not self._args.quiet and
+                    record.levelno >= logging.INFO):
+                stream = self.stream
+                stream.write(msg)
+                stream.write(self.terminator)
+                self.flush()
+
+            # Everything: Write to logfd
+            msg = "(" + str(os.getpid()).zfill(6) + ") " + msg
+            self._args.logfd.write(msg + "\n")
+            self._args.logfd.flush()
+
+        except (KeyboardInterrupt, SystemExit):
+            raise
+        except BaseException:
+            self.handleError(record)
+
+
+def add_verbose_log_level():
+    """
+    Add a new log level "verbose", which is below "debug". Also monkeypatch
+    logging, so it can be used with logging.verbose().
+
+    This function is based on work by Voitek Zylinski and sleepycal:
+    https://stackoverflow.com/a/20602183
+    All stackoverflow user contributions are licensed as CC-BY-SA:
+    https://creativecommons.org/licenses/by-sa/3.0/
+    """
+    logging.VERBOSE = 5
+    logging.addLevelName(logging.VERBOSE, "VERBOSE")
+    logging.Logger.verbose = lambda inst, msg, * \
+        args, **kwargs: inst.log(logging.VERBOSE, msg, *args, **kwargs)
+    logging.verbose = lambda msg, *args, **kwargs: logging.log(logging.VERBOSE,
+                                                               msg, *args,
+                                                               **kwargs)
+
+
+def init(args):
+    """
+    Set log format and add the log file descriptor to args.logfd, add the
+    verbose log level.
+    """
+    # Set log file descriptor (logfd)
+    if args.details_to_stdout:
+        setattr(args, "logfd", sys.stdout)
+    else:
+        # Require containing directory to exist (so we don't create the work
+        # folder and break the folder migration logic, which needs to set the
+        # version upon creation)
+        dir = os.path.dirname(args.log)
+        if os.path.exists(dir):
+            setattr(args, "logfd", open(args.log, "a+"))
+        else:
+            setattr(args, "logfd", open(os.devnull, "a+"))
+            if args.action != "init":
+                print("WARNING: Can't create log file in '" + dir + "', path"
+                      " does not exist!")
+
+    # Set log format
+    root_logger = logging.getLogger()
+    root_logger.handlers = []
+    formatter = logging.Formatter("[%(asctime)s] %(message)s",
+                                  datefmt="%H:%M:%S")
+
+    # Set log level
+    add_verbose_log_level()
+    root_logger.setLevel(logging.DEBUG)
+    if args.verbose:
+        root_logger.setLevel(logging.VERBOSE)
+
+    # Add a custom log handler
+    handler = log_handler()
+    log_handler._args = args
+    handler.setFormatter(formatter)
+    root_logger.addHandler(handler)
+
+
+def disable():
+    logger = logging.getLogger()
+    logger.disabled = True
diff --git a/tools/helpers/lxc.py b/tools/helpers/lxc.py
new file mode 100644 (file)
index 0000000..c46f2a7
--- /dev/null
@@ -0,0 +1,269 @@
+# Copyright 2021 Erfan Abdi
+# SPDX-License-Identifier: GPL-3.0-or-later
+import subprocess
+import os
+import re
+import logging
+import glob
+import shutil
+import platform
+import tools.config
+import tools.helpers.run
+
+
+def get_lxc_version(args):
+    if shutil.which("lxc-info") is not None:
+        command = ["lxc-info", "--version"]
+        version_str = tools.helpers.run.user(args, command, output_return=True)
+        return int(version_str[0])
+    else:
+        return 0
+
+
+def generate_nodes_lxc_config(args):
+    def make_entry(src, dist=None, mnt_type="none", options="bind,create=file,optional 0 0", check=True):
+        if check and not os.path.exists(src):
+            return False
+        entry = "lxc.mount.entry = "
+        entry += src + " "
+        if dist is None:
+            dist = src[1:]
+        entry += dist + " "
+        entry += mnt_type + " "
+        entry += options
+        nodes.append(entry)
+        return True
+
+    nodes = []
+    # Necessary dev nodes
+    make_entry("tmpfs", "dev", "tmpfs", "nosuid 0 0", False)
+    make_entry("/dev/zero")
+    make_entry("/dev/full")
+    make_entry("/dev/ashmem", check=False)
+    make_entry("/dev/fuse")
+    make_entry("/dev/ion")
+    make_entry("/dev/char", options="bind,create=dir,optional 0 0")
+
+    # Graphic dev nodes
+    make_entry("/dev/kgsl-3d0")
+    make_entry("/dev/mali0")
+    make_entry("/dev/pvr_sync")
+    make_entry("/dev/pmsg0")
+    make_entry("/dev/fb0")
+    make_entry("/dev/graphics/fb0")
+    make_entry("/dev/fb1")
+    make_entry("/dev/graphics/fb1")
+    make_entry("/dev/fb2")
+    make_entry("/dev/graphics/fb2")
+    make_entry("/dev/dri", options="bind,create=dir,optional 0 0")
+
+    # Binder dev nodes
+    make_entry("/dev/" + args.BINDER_DRIVER, "dev/binder")
+    make_entry("/dev/" + args.VNDBINDER_DRIVER, "dev/vndbinder")
+    make_entry("/dev/" + args.HWBINDER_DRIVER, "dev/hwbinder")
+
+    if args.vendor_type != "MAINLINE":
+        if not make_entry("/dev/hwbinder", "dev/host_hwbinder"):
+            raise OSError('Binder node "hwbinder" of host not found')
+        make_entry("/vendor", "vendor_extra", options="bind,optional 0 0")
+
+    # Necessary device nodes for adb
+    make_entry("none", "dev/pts", "devpts", "defaults,mode=644,ptmxmode=666,create=dir 0 0", False)
+    make_entry("/dev/uhid")
+
+    # Low memory killer sys node
+    make_entry("/sys/module/lowmemorykiller", options="bind,create=dir,optional 0 0")
+
+    # Mount /data
+    make_entry("tmpfs", "mnt", "tmpfs", "mode=0755,uid=0,gid=1000", False)
+    make_entry(tools.config.defaults["data"], "data", options="bind 0 0", check=False)
+
+    # Mount host permissions
+    make_entry(tools.config.defaults["host_perms"],
+               "vendor/etc/host-permissions", options="bind,optional 0 0")
+
+    # Recursive mount /run to provide necessary host sockets
+    make_entry("/run", options="rbind,create=dir 0 0")
+
+    # Necessary sw_sync node for HWC
+    make_entry("/dev/sw_sync")
+    make_entry("/sys/kernel/debug", options="rbind,create=dir,optional 0 0")
+
+    # Media dev nodes (for Mediatek)
+    make_entry("/dev/Vcodec")
+    make_entry("/dev/MTK_SMI")
+    make_entry("/dev/mdp_sync")
+    make_entry("/dev/mtk_cmdq")
+
+    # Media dev nodes (for Qcom)
+    make_entry("/dev/video32")
+    make_entry("/dev/video33")
+
+    return nodes
+
+
+def set_lxc_config(args):
+    lxc_path = tools.config.defaults["lxc"] + "/waydroid"
+    config_file = "config_2"
+    lxc_ver = get_lxc_version(args)
+    if lxc_ver == 0:
+        raise OSError("LXC is not installed")
+    elif lxc_ver <= 2:
+        config_file = "config_1"
+    config_path = tools.config.tools_src + "/data/configs/" + config_file
+
+    command = ["mkdir", "-p", lxc_path]
+    tools.helpers.run.root(args, command)
+    command = ["cp", "-fpr", config_path, lxc_path + "/config"]
+    tools.helpers.run.root(args, command)
+    command = ["sed", "-i", "s/LXCARCH/{}/".format(platform.machine()), lxc_path + "/config"]
+    tools.helpers.run.root(args, command)
+
+    nodes = generate_nodes_lxc_config(args)
+    config_nodes_tmp_path = args.work + "/config_nodes"
+    config_nodes = open(config_nodes_tmp_path, "w")
+    for node in nodes:
+        config_nodes.write(node + "\n")
+    config_nodes.close()
+    command = ["mv", config_nodes_tmp_path, lxc_path]
+    tools.helpers.run.root(args, command)
+
+
+def make_base_props(args):
+    def find_hal(hardware):
+        hardware_props = [
+            "ro.hardware." + hardware,
+            "ro.hardware",
+            "ro.product.board",
+            "ro.arch",
+            "ro.board.platform"]
+        for p in hardware_props:
+            prop = tools.helpers.props.host_get(args, p)
+            hal_prop = ""
+            if prop != "":
+                for lib in ["lib", "lib64"]:
+                    hal_file = "/vendor/" + lib + "/hw/" + hardware + "." + prop + ".so"
+                    command = ["readlink", "-f", hal_file]
+                    hal_file_path = tools.helpers.run.root(args, command, output_return=True).strip()
+                    if os.path.isfile(hal_file_path):
+                        hal_prop = re.sub(".*" + hardware + ".", "", hal_file_path)
+                        hal_prop = re.sub(".so", "", hal_prop)
+                        if hal_prop != "":
+                            return hal_prop
+            if hal_prop != "":
+                return hal_prop
+        return ""
+
+    props = []
+    gralloc = find_hal("gralloc")
+    if gralloc == "":
+        gralloc = "gbm"
+        props.append("ro.hardware.egl=mesa")
+        props.append("debug.stagefright.ccodec=0")
+    props.append("ro.hardware.gralloc=" + gralloc)
+
+    egl = tools.helpers.props.host_get(args, "ro.hardware.egl")
+    if egl != "":
+        props.append("ro.hardware.egl=" + egl)
+
+    media_profiles = tools.helpers.props.host_get(args, "media.settings.xml")
+    if media_profiles != "":
+        media_profiles = media_profiles.replace("vendor/", "vendor_extra/")
+        media_profiles = media_profiles.replace("odm/", "odm_extra/")
+        props.append("media.settings.xml=" + media_profiles)
+
+    ccodec = tools.helpers.props.host_get(args, "debug.stagefright.ccodec")
+    if ccodec != "":
+        props.append("debug.stagefright.ccodec=" + ccodec)
+
+    ext_library = tools.helpers.props.host_get(args, "ro.vendor.extension_library")
+    if ext_library != "":
+        ext_library = ext_library.replace("vendor/", "vendor_extra/")
+        ext_library = ext_library.replace("odm/", "odm_extra/")
+        props.append("ro.vendor.extension_library=" + ext_library)
+
+    vulkan = find_hal("vulkan")
+    if vulkan != "":
+        props.append("ro.hardware.vulkan=" + vulkan)
+
+    opengles = tools.helpers.props.host_get(args, "ro.opengles.version")
+    if opengles == "":
+        opengles = "196608"
+    props.append("ro.opengles.version=" + opengles)
+
+    props.append("waydroid.system_ota=" + args.system_ota)
+    props.append("waydroid.vendor_ota=" + args.vendor_ota)
+    props.append("waydroid.tools_version=" + tools.config.version)
+
+    base_props = open(args.work + "/waydroid_base.prop", "w")
+    for prop in props:
+        base_props.write(prop + "\n")
+    base_props.close()
+
+
+def setup_host_perms(args):
+    sku = tools.helpers.props.host_get(args, "ro.boot.product.hardware.sku")
+    copy_list = []
+    copy_list.extend(
+        glob.glob("/vendor/etc/permissions/android.hardware.nfc.*"))
+    if os.path.exists("/vendor/etc/permissions/android.hardware.consumerir.xml"):
+        copy_list.append("/vendor/etc/permissions/android.hardware.consumerir.xml")
+    copy_list.extend(
+        glob.glob("/odm/etc/permissions/android.hardware.nfc.*"))
+    if os.path.exists("/odm/etc/permissions/android.hardware.consumerir.xml"):
+        copy_list.append("/odm/etc/permissions/android.hardware.consumerir.xml")
+    if sku != "":
+        copy_list.extend(
+            glob.glob("/odm/etc/permissions/sku_{}/android.hardware.nfc.*".format(sku)))
+        if os.path.exists("/odm/etc/permissions/sku_{}/android.hardware.consumerir.xml".format(sku)):
+            copy_list.append(
+                "/odm/etc/permissions/sku_{}/android.hardware.consumerir.xml".format(sku))
+
+    if not os.path.exists(tools.config.defaults["host_perms"]):
+        os.mkdir(tools.config.defaults["host_perms"])
+
+    for filename in copy_list:
+        shutil.copy(filename, tools.config.defaults["host_perms"])
+
+def status(args):
+    command = ["sudo", "lxc-info", "-P", tools.config.defaults["lxc"], "-n", "waydroid", "-sH"]
+    return subprocess.run(command, stdout=subprocess.PIPE).stdout.decode('utf-8').strip()
+
+def start(args):
+    command = ["lxc-start", "-P", tools.config.defaults["lxc"],
+               "-F", "-n", "waydroid", "--", "/init"]
+    tools.helpers.run.root(args, command, output="background")
+
+def stop(args):
+    command = ["lxc-stop", "-P",
+               tools.config.defaults["lxc"], "-n", "waydroid", "-k"]
+    tools.helpers.run.root(args, command)
+
+def freeze(args):
+    command = ["lxc-freeze", "-P", tools.config.defaults["lxc"], "-n", "waydroid"]
+    tools.helpers.run.root(args, command)
+
+def unfreeze(args):
+    command = ["lxc-unfreeze", "-P",
+               tools.config.defaults["lxc"], "-n", "waydroid"]
+    tools.helpers.run.root(args, command)
+
+def shell(args):
+    if status(args) != "RUNNING":
+        logging.error("WayDroid container is {}".format(status(args)))
+        return
+    command = ["lxc-attach", "-P", tools.config.defaults["lxc"],
+               "-n", "waydroid", "--"]
+    if args.COMMAND:
+        command.append(args.COMMAND)
+    else:
+        command.append("/system/bin/sh")
+    subprocess.run(command)
+
+def logcat(args):
+    if status(args) != "RUNNING":
+        logging.error("WayDroid container is {}".format(status(args)))
+        return
+    command = ["lxc-attach", "-P", tools.config.defaults["lxc"],
+               "-n", "waydroid", "--", "/system/bin/logcat"]
+    subprocess.run(command)
diff --git a/tools/helpers/mount.py b/tools/helpers/mount.py
new file mode 100644 (file)
index 0000000..4d77206
--- /dev/null
@@ -0,0 +1,137 @@
+# Copyright 2021 Oliver Smith
+# SPDX-License-Identifier: GPL-3.0-or-later
+import os
+import tools.helpers.run
+
+
+def ismount(folder):
+    """
+    Ismount() implementation, that works for mount --bind.
+    Workaround for: https://bugs.python.org/issue29707
+    """
+    folder = os.path.realpath(os.path.realpath(folder))
+    with open("/proc/mounts", "r") as handle:
+        for line in handle:
+            words = line.split()
+            if len(words) >= 2 and words[1] == folder:
+                return True
+            if words[0] == folder:
+                return True
+    return False
+
+
+def bind(args, source, destination, create_folders=True, umount=False):
+    """
+    Mount --bind a folder and create necessary directory structure.
+    :param umount: when destination is already a mount point, umount it first.
+    """
+    # Check/umount destination
+    if ismount(destination):
+        if umount:
+            umount_all(args, destination)
+        else:
+            return
+
+    # Check/create folders
+    for path in [source, destination]:
+        if os.path.exists(path):
+            continue
+        if create_folders:
+            tools.helpers.run.root(args, ["mkdir", "-p", path])
+        else:
+            raise RuntimeError("Mount failed, folder does not exist: " +
+                               path)
+
+    # Actually mount the folder
+    tools.helpers.run.root(args, ["mount", "-o", "bind", source, destination])
+
+    # Verify, that it has worked
+    if not ismount(destination):
+        raise RuntimeError("Mount failed: " + source + " -> " + destination)
+
+
+def bind_file(args, source, destination, create_folders=False):
+    """
+    Mount a file with the --bind option, and create the destination file,
+    if necessary.
+    """
+    # Skip existing mountpoint
+    if ismount(destination):
+        return
+
+    # Create empty file
+    if not os.path.exists(destination):
+        if create_folders:
+            dir = os.path.dirname(destination)
+            if not os.path.isdir(dir):
+                tools.helpers.run.root(args, ["mkdir", "-p", dir])
+
+        tools.helpers.run.root(args, ["touch", destination])
+
+    # Mount
+    tools.helpers.run.root(args, ["mount", "-o", "bind", source,
+                                destination])
+
+
+def umount_all_list(prefix, source="/proc/mounts"):
+    """
+    Parses `/proc/mounts` for all folders beginning with a prefix.
+    :source: can be changed for testcases
+    :returns: a list of folders, that need to be umounted
+    """
+    ret = []
+    prefix = os.path.realpath(prefix)
+    with open(source, "r") as handle:
+        for line in handle:
+            words = line.split()
+            if len(words) < 2:
+                raise RuntimeError("Failed to parse line in " + source + ": " +
+                                   line)
+            mountpoint = words[1]
+            if mountpoint.startswith(prefix):
+                # Remove "\040(deleted)" suffix (#545)
+                deleted_str = r"\040(deleted)"
+                if mountpoint.endswith(deleted_str):
+                    mountpoint = mountpoint[:-len(deleted_str)]
+                ret.append(mountpoint)
+    ret.sort(reverse=True)
+    return ret
+
+
+def umount_all(args, folder):
+    """
+    Umount all folders, that are mounted inside a given folder.
+    """
+    for mountpoint in umount_all_list(folder):
+        tools.helpers.run.root(args, ["umount", mountpoint])
+        if ismount(mountpoint):
+            raise RuntimeError("Failed to umount: " + mountpoint)
+
+def mount(args, source, destination, create_folders=True, umount=False, readonly=True):
+    """
+    Mount and create necessary directory structure.
+    :param umount: when destination is already a mount point, umount it first.
+    """
+    # Check/umount destination
+    if ismount(destination):
+        if umount:
+            umount_all(args, destination)
+        else:
+            return
+
+    # Check/create folders
+    if not os.path.exists(destination):
+        if create_folders:
+            tools.helpers.run.root(args, ["mkdir", "-p", destination])
+        else:
+            raise RuntimeError("Mount failed, folder does not exist: " +
+                            destination)
+
+    # Actually mount the folder
+    tools.helpers.run.root(args, ["mount", source, destination])
+    if readonly:
+        tools.helpers.run.root(args, ["mount", "-o", "remount,ro", source, destination])
+
+    # Verify, that it has worked
+    if not ismount(destination):
+        raise RuntimeError("Mount failed: " + source + " -> " + destination)
diff --git a/tools/helpers/props.py b/tools/helpers/props.py
new file mode 100644 (file)
index 0000000..bcc7c34
--- /dev/null
@@ -0,0 +1,50 @@
+# Copyright 2021 Oliver Smith
+# SPDX-License-Identifier: GPL-3.0-or-later
+from shutil import which
+import logging
+import os
+import tools.helpers.run
+from tools.interfaces import IPlatform
+
+
+def host_get(args, prop):
+    if which("getprop") is not None:
+        command = ["getprop", prop]
+        return tools.helpers.run.user(args, command, output_return=True).strip()
+    else:
+        return ""
+
+def host_set(args, prop, value):
+    if which("setprop") is not None:
+        command = ["setprop", prop, value]
+        tools.helpers.run.user(args, command)
+
+def get(args, prop):
+    if os.path.exists(tools.config.session_defaults["config_path"]):
+        session_cfg = tools.config.load_session()
+        if session_cfg["session"]["state"] == "RUNNING":
+            platformService = IPlatform.get_service(args)
+            if platformService:
+                return platformService.getprop(prop, "")
+            else:
+                logging.error("Failed to access IPlatform service")
+        else:
+            logging.error("WayDroid container is {}".format(
+                session_cfg["session"]["state"]))
+    else:
+        logging.error("WayDroid session is stopped")
+
+def set(args, prop, value):
+    if os.path.exists(tools.config.session_defaults["config_path"]):
+        session_cfg = tools.config.load_session()
+        if session_cfg["session"]["state"] == "RUNNING":
+            platformService = IPlatform.get_service(args)
+            if platformService:
+                platformService.setprop(prop, value)
+            else:
+                logging.error("Failed to access IPlatform service")
+        else:
+            logging.error("WayDroid container is {}".format(
+                session_cfg["session"]["state"]))
+    else:
+        logging.error("WayDroid session is stopped")
diff --git a/tools/helpers/run.py b/tools/helpers/run.py
new file mode 100644 (file)
index 0000000..d3c7738
--- /dev/null
@@ -0,0 +1,78 @@
+# Copyright 2021 Oliver Smith
+# SPDX-License-Identifier: GPL-3.0-or-later
+import shlex
+import tools.helpers.run_core
+
+
+def flat_cmd(cmd, working_dir=None, env={}):
+    """
+    Convert a shell command passed as list into a flat shell string with
+    proper escaping.
+
+    :param cmd: command as list, e.g. ["echo", "string with spaces"]
+    :param working_dir: when set, prepend "cd ...;" to execute the command
+                        in the given working directory
+    :param env: dict of environment variables to be passed to the command, e.g.
+                {"JOBS": "5"}
+    :returns: the flat string, e.g.
+              echo 'string with spaces'
+              cd /home/pmos;echo 'string with spaces'
+    """
+    # Merge env and cmd into escaped list
+    escaped = []
+    for key, value in env.items():
+        escaped.append(key + "=" + shlex.quote(value))
+    for i in range(len(cmd)):
+        escaped.append(shlex.quote(cmd[i]))
+
+    # Prepend working dir
+    ret = " ".join(escaped)
+    if working_dir:
+        ret = "cd " + shlex.quote(working_dir) + ";" + ret
+
+    return ret
+
+
+def user(args, cmd, working_dir=None, output="log", output_return=False,
+         check=None, env={}, sudo=False):
+    """
+    Run a command on the host system as user.
+
+    :param env: dict of environment variables to be passed to the command, e.g.
+                {"JOBS": "5"}
+
+    See tools.helpers.run_core.core() for a detailed description of all other
+    arguments and the return value.
+    """
+    # Readable log message (without all the escaping)
+    msg = "% "
+    for key, value in env.items():
+        msg += key + "=" + value + " "
+    if working_dir:
+        msg += "cd " + working_dir + "; "
+    msg += " ".join(cmd)
+
+    # Add environment variables and run
+    if env:
+        cmd = ["sh", "-c", flat_cmd(cmd, env=env)]
+    return tools.helpers.run_core.core(args, msg, cmd, working_dir, output,
+                                     output_return, check, sudo)
+
+
+def root(args, cmd, working_dir=None, output="log", output_return=False,
+         check=None, env={}):
+    """
+    Run a command on the host system as root, with sudo.
+
+    :param env: dict of environment variables to be passed to the command, e.g.
+                {"JOBS": "5"}
+
+    See tools.helpers.run_core.core() for a detailed description of all other
+    arguments and the return value.
+    """
+    if env:
+        cmd = ["sh", "-c", flat_cmd(cmd, env=env)]
+    cmd = ["sudo"] + cmd
+
+    return user(args, cmd, working_dir, output, output_return, check, env,
+                True)
diff --git a/tools/helpers/run_core.py b/tools/helpers/run_core.py
new file mode 100644 (file)
index 0000000..dcc4c53
--- /dev/null
@@ -0,0 +1,346 @@
+# Copyright 2021 Oliver Smith
+# SPDX-License-Identifier: GPL-3.0-or-later
+import fcntl
+import logging
+import selectors
+import subprocess
+import sys
+import threading
+import time
+import os
+import tools.helpers.run
+
+""" For a detailed description of all output modes, read the description of
+    core() at the bottom. All other functions in this file get (indirectly)
+    called by core(). """
+
+
+def sanity_checks(output="log", output_return=False, check=None):
+    """
+    Raise an exception if the parameters passed to core() don't make sense
+    (all parameters are described in core() below).
+    """
+    vals = ["log", "stdout", "interactive", "tui", "background", "pipe"]
+    if output not in vals:
+        raise RuntimeError("Invalid output value: " + str(output))
+
+    # Prevent setting the check parameter with output="background".
+    # The exit code won't be checked when running in background, so it would
+    # always by check=False. But we prevent it from getting set to check=False
+    # as well, so it does not look like you could change it to check=True.
+    if check is not None and output == "background":
+        raise RuntimeError("Can't use check with output: background")
+
+    if output_return and output in ["tui", "background"]:
+        raise RuntimeError("Can't use output_return with output: " + output)
+
+
+def background(args, cmd, working_dir=None):
+    """ Run a subprocess in background and redirect its output to the log. """
+    ret = subprocess.Popen(cmd, stdout=args.logfd, stderr=args.logfd,
+                           cwd=working_dir)
+    logging.debug("New background process: pid={}, output=background".format(ret.pid))
+    return ret
+
+
+def pipe(args, cmd, working_dir=None):
+    """ Run a subprocess in background and redirect its output to a pipe. """
+    ret = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=args.logfd,
+                           cwd=working_dir)
+    logging.verbose("New background process: pid={}, output=pipe".format(ret.pid))
+    return ret
+
+
+def pipe_read(args, process, output_to_stdout=False, output_return=False,
+              output_return_buffer=False):
+    """
+    Read all available output from a subprocess and copy it to the log and
+    optionally stdout and a buffer variable. This is only meant to be called by
+    foreground_pipe() below.
+
+    :param process: subprocess.Popen instance
+    :param output_to_stdout: copy all output to waydroid's stdout
+    :param output_return: when set to True, output_return_buffer will be
+                          extended
+    :param output_return_buffer: list of bytes that gets extended with the
+                                 current output in case output_return is True.
+    """
+    while True:
+        # Copy available output
+        out = process.stdout.readline()
+        if len(out):
+            args.logfd.buffer.write(out)
+            if output_to_stdout:
+                sys.stdout.buffer.write(out)
+            if output_return:
+                output_return_buffer.append(out)
+            continue
+
+        # No more output (flush buffers)
+        args.logfd.flush()
+        if output_to_stdout:
+            sys.stdout.flush()
+        return
+
+
+def kill_process_tree(args, pid, ppids, sudo):
+    """
+    Recursively kill a pid and its child processes
+
+    :param pid: process id that will be killed
+    :param ppids: list of process id and parent process id tuples (pid, ppid)
+    :param sudo: use sudo to kill the process
+    """
+    if sudo:
+        tools.helpers.run.root(args, ["kill", "-9", str(pid)],
+                             check=False)
+    else:
+        tools.helpers.run.user(args, ["kill", "-9", str(pid)],
+                             check=False)
+
+    for (child_pid, child_ppid) in ppids:
+        if child_ppid == str(pid):
+            kill_process_tree(args, child_pid, ppids, sudo)
+
+
+def kill_command(args, pid, sudo):
+    """
+    Kill a command process and recursively kill its child processes
+
+    :param pid: process id that will be killed
+    :param sudo: use sudo to kill the process
+    """
+    cmd = ["ps", "-e", "-o", "pid,ppid"]
+    ret = subprocess.run(cmd, check=True, stdout=subprocess.PIPE)
+    ppids = []
+    proc_entries = ret.stdout.decode("utf-8").rstrip().split('\n')[1:]
+    for row in proc_entries:
+        items = row.split()
+        if len(items) != 2:
+            raise RuntimeError("Unexpected ps output: " + row)
+        ppids.append(items)
+
+    kill_process_tree(args, pid, ppids, sudo)
+
+
+def foreground_pipe(args, cmd, working_dir=None, output_to_stdout=False,
+                    output_return=False, output_timeout=True,
+                    sudo=False):
+    """
+    Run a subprocess in foreground with redirected output and optionally kill
+    it after being silent for too long.
+
+    :param cmd: command as list, e.g. ["echo", "string with spaces"]
+    :param working_dir: path in host system where the command should run
+    :param output_to_stdout: copy all output to waydroid's stdout
+    :param output_return: return the output of the whole program
+    :param output_timeout: kill the process when it doesn't print any output
+                           after a certain time (configured with --timeout)
+                           and raise a RuntimeError exception
+    :param sudo: use sudo to kill the process when it hits the timeout
+    :returns: (code, output)
+              * code: return code of the program
+              * output: ""
+              * output: full program output string (output_return is True)
+    """
+    # Start process in background (stdout and stderr combined)
+    process = subprocess.Popen(cmd, stdout=subprocess.PIPE,
+                               stderr=subprocess.STDOUT, cwd=working_dir)
+
+    # Make process.stdout non-blocking
+    handle = process.stdout.fileno()
+    flags = fcntl.fcntl(handle, fcntl.F_GETFL)
+    fcntl.fcntl(handle, fcntl.F_SETFL, flags | os.O_NONBLOCK)
+
+    # While process exists wait for output (with timeout)
+    output_buffer = []
+    sel = selectors.DefaultSelector()
+    sel.register(process.stdout, selectors.EVENT_READ)
+    timeout = args.timeout if output_timeout else None
+    while process.poll() is None:
+        wait_start = time.perf_counter() if output_timeout else None
+        sel.select(timeout)
+
+        # On timeout raise error (we need to measure time on our own, because
+        # select() may exit early even if there is no data to read and the
+        # timeout was not reached.)
+        if output_timeout:
+            wait_end = time.perf_counter()
+            if wait_end - wait_start >= args.timeout:
+                logging.info("Process did not write any output for " +
+                             str(args.timeout) + " seconds. Killing it.")
+                logging.info("NOTE: The timeout can be increased with"
+                             " 'waydroid -t'.")
+                kill_command(args, process.pid, sudo)
+                continue
+
+        # Read all currently available output
+        pipe_read(args, process, output_to_stdout, output_return,
+                  output_buffer)
+
+    # There may still be output after the process quit
+    pipe_read(args, process, output_to_stdout, output_return, output_buffer)
+
+    # Return the return code and output (the output gets built as list of
+    # output chunks and combined at the end, this is faster than extending the
+    # combined string with each new chunk)
+    return (process.returncode, b"".join(output_buffer).decode("utf-8"))
+
+
+def foreground_tui(cmd, working_dir=None):
+    """
+    Run a subprocess in foreground without redirecting any of its output.
+
+    This is the only way text-based user interfaces (ncurses programs like
+    vim, nano or the kernel's menuconfig) work properly.
+    """
+
+    logging.debug("*** output passed to waydroid stdout, not to this log"
+                  " ***")
+    process = subprocess.Popen(cmd, cwd=working_dir)
+    return process.wait()
+
+
+def check_return_code(args, code, log_message):
+    """
+    Check the return code of a command.
+
+    :param code: exit code to check
+    :param log_message: simplified and more readable form of the command, e.g.
+                        "(native) % echo test" instead of the full command with
+                        entering the chroot and more escaping
+    :raises RuntimeError: when the code indicates that the command failed
+    """
+
+    if code:
+        logging.debug("^" * 70)
+        logging.info("NOTE: The failed command's output is above the ^^^ line"
+                     " in the log file: " + args.log)
+        raise RuntimeError("Command failed: " + log_message)
+
+
+def sudo_timer_iterate():
+    """
+    Run sudo -v and schedule a new timer to repeat the same.
+    """
+
+    subprocess.Popen(["sudo", "-v"]).wait()
+
+    timer = threading.Timer(interval=60, function=sudo_timer_iterate)
+    timer.daemon = True
+    timer.start()
+
+
+def sudo_timer_start(args):
+    """
+    Start a timer to call sudo -v periodically, so that the password is only
+    needed once.
+    """
+
+    if "sudo_timer_active" in args.cache:
+        return
+    args.cache["sudo_timer_active"] = True
+
+    sudo_timer_iterate()
+
+
+def core(args, log_message, cmd, working_dir=None, output="log",
+         output_return=False, check=None, sudo=False, disable_timeout=False):
+    """
+    Run a command and create a log entry.
+
+    This is a low level function not meant to be used directly. Use one of the
+    following instead: tools.helpers.run.user(), tools.helpers.run.root(),
+                       tools.chroot.user(), tools.chroot.root()
+
+    :param log_message: simplified and more readable form of the command, e.g.
+                        "(native) % echo test" instead of the full command with
+                        entering the chroot and more escaping
+    :param cmd: command as list, e.g. ["echo", "string with spaces"]
+    :param working_dir: path in host system where the command should run
+    :param output: where to write the output (stdout and stderr) of the
+                   process. We almost always write to the log file, which can
+                   be read with "waydroid log" (output values: "log",
+                   "stdout", "interactive", "background"), so it's easy to
+                   trace what waydroid does.
+
+                   The exceptions are "tui" (text-based user interface), where
+                   it does not make sense to write to the log file (think of
+                   ncurses UIs, such as "menuconfig") and "pipe" where the
+                   output is written to a pipe for manual asynchronous
+                   consumption by the caller.
+
+                   When the output is not set to "interactive", "tui",
+                   "background" or "pipe", we kill the process if it does not
+                   output anything for 5 minutes (time can be set with
+                   "waydroid --timeout").
+
+                   The table below shows all possible values along with
+                   their properties. "wait" indicates that we wait for the
+                   process to complete.
+
+                   output value  | timeout | out to log | out to stdout | wait
+                   -----------------------------------------------------------
+                   "log"         | x       | x          |               | x
+                   "stdout"      | x       | x          | x             | x
+                   "interactive" |         | x          | x             | x
+                   "tui"         |         |            | x             | x
+                   "background"  |         | x          |               |
+                   "pipe"        |         |            |               |
+
+    :param output_return: in addition to writing the program's output to the
+                          destinations above in real time, write to a buffer
+                          and return it as string when the command has
+                          completed. This is not possible when output is
+                          "background", "pipe" or "tui".
+    :param check: an exception will be raised when the command's return code
+                  is not 0. Set this to False to disable the check. This
+                  parameter can not be used when the output is "background" or
+                  "pipe".
+    :param sudo: use sudo to kill the process when it hits the timeout.
+    :returns: * program's return code (default)
+              * subprocess.Popen instance (output is "background" or "pipe")
+              * the program's entire output (output_return is True)
+    """
+    sanity_checks(output, output_return, check)
+
+    if args.sudo_timer and sudo:
+        sudo_timer_start(args)
+
+    # Log simplified and full command (waydroid -v)
+    logging.debug(log_message)
+    logging.verbose("run: " + str(cmd))
+
+    # Background
+    if output == "background":
+        return background(args, cmd, working_dir)
+
+    # Pipe
+    if output == "pipe":
+        return pipe(args, cmd, working_dir)
+
+    # Foreground
+    output_after_run = ""
+    if output == "tui":
+        # Foreground TUI
+        code = foreground_tui(cmd, working_dir)
+    else:
+        # Foreground pipe (always redirects to the error log file)
+        output_to_stdout = False
+        if not args.details_to_stdout and output in ["stdout", "interactive"]:
+            output_to_stdout = True
+
+        output_timeout = output in ["log", "stdout"] and not disable_timeout
+
+        (code, output_after_run) = foreground_pipe(args, cmd, working_dir,
+                                                   output_to_stdout,
+                                                   output_return,
+                                                   output_timeout,
+                                                   sudo)
+
+    # Check the return code
+    if check is not False:
+        check_return_code(args, code, log_message)
+
+    # Return (code or output string)
+    return output_after_run if output_return else code
diff --git a/tools/interfaces/IClipboard.py b/tools/interfaces/IClipboard.py
new file mode 100644 (file)
index 0000000..7d09d92
--- /dev/null
@@ -0,0 +1,48 @@
+import gbinder
+import logging
+from tools import helpers
+from gi.repository import GLib
+
+
+INTERFACE = "lineageos.waydroid.IClipboard"
+SERVICE_NAME = "waydroidclipboard"
+
+TRANSACTION_sendClipboardData = 1
+TRANSACTION_getClipboardData = 2
+
+def add_service(args, sendClipboardData, getClipboardData):
+    helpers.drivers.loadBinderNodes(args)
+    serviceManager = gbinder.ServiceManager("/dev/" + args.BINDER_DRIVER)
+
+    def response_handler(req, code, flags):
+        logging.debug(
+            "{}: Received transaction: {}".format(SERVICE_NAME, code))
+        reader = req.init_reader()
+        local_response = response.new_reply()
+        if code == TRANSACTION_sendClipboardData:
+            arg1 = reader.read_string16()
+            sendClipboardData(arg1)
+            local_response.append_int32(0)
+        if code == TRANSACTION_getClipboardData:
+            ret = getClipboardData()
+            local_response.append_int32(0)
+            local_response.append_string16(ret)
+
+        return local_response, 0
+
+    def binder_presence():
+        if serviceManager.is_present():
+            status = serviceManager.add_service_sync(SERVICE_NAME, response)
+
+            if status:
+                logging.error("Failed to add service " + SERVICE_NAME)
+                args.clipboardLoop.quit()
+
+    response = serviceManager.new_local_object(INTERFACE, response_handler)
+    args.clipboardLoop = GLib.MainLoop()
+    binder_presence()
+    status = serviceManager.add_presence_handler(binder_presence)
+    if status:
+        args.clipboardLoop.run()
+    else:
+        logging.error("Failed to add presence handler: {}".format(status))
diff --git a/tools/interfaces/IHardware.py b/tools/interfaces/IHardware.py
new file mode 100644 (file)
index 0000000..347a89c
--- /dev/null
@@ -0,0 +1,66 @@
+import gbinder
+import logging
+from tools import helpers
+from gi.repository import GLib
+
+
+INTERFACE = "lineageos.waydroid.IHardware"
+SERVICE_NAME = "waydroidhardware"
+
+TRANSACTION_enableNFC = 1
+TRANSACTION_enableBluetooth = 2
+TRANSACTION_suspend = 3
+TRANSACTION_reboot = 4
+TRANSACTION_upgrade = 5
+
+def add_service(args, enableNFC, enableBluetooth, suspend, reboot, upgrade):
+    helpers.drivers.loadBinderNodes(args)
+    serviceManager = gbinder.ServiceManager("/dev/" + args.BINDER_DRIVER)
+
+    def response_handler(req, code, flags):
+        logging.debug(
+            "{}: Received transaction: {}".format(SERVICE_NAME, code))
+        reader = req.init_reader()
+        local_response = response.new_reply()
+        if code == TRANSACTION_enableNFC:
+            status, arg1 = reader.read_int32()
+            ret = enableNFC(arg1 != 0)
+            local_response.append_int32(0)
+            local_response.append_int32(ret)
+        if code == TRANSACTION_enableBluetooth:
+            status, arg1 = reader.read_int32()
+            ret = enableBluetooth(arg1 != 0)
+            local_response.append_int32(0)
+            local_response.append_int32(ret)
+        if code == TRANSACTION_suspend:
+            suspend()
+            local_response.append_int32(0)
+        if code == TRANSACTION_reboot:
+            reboot()
+            local_response.append_int32(0)
+        if code == TRANSACTION_upgrade:
+            arg1 = reader.read_string16()
+            status, arg2 = reader.read_int32()
+            arg3 = reader.read_string16()
+            status, arg4 = reader.read_int32()
+            upgrade(arg1, arg2, arg3, arg4)
+            local_response.append_int32(0)
+
+        return local_response, 0
+
+    def binder_presence():
+        if serviceManager.is_present():
+            status = serviceManager.add_service_sync(SERVICE_NAME, response)
+
+            if status:
+                logging.error("Failed to add service " + SERVICE_NAME)
+                args.hardwareLoop.quit()
+
+    response = serviceManager.new_local_object(INTERFACE, response_handler)
+    args.hardwareLoop = GLib.MainLoop()
+    binder_presence()
+    status = serviceManager.add_presence_handler(binder_presence)
+    if status:
+        args.hardwareLoop.run()
+    else:
+        logging.error("Failed to add presence handler: {}".format(status))
diff --git a/tools/interfaces/IPlatform.py b/tools/interfaces/IPlatform.py
new file mode 100644 (file)
index 0000000..9887d13
--- /dev/null
@@ -0,0 +1,292 @@
+import gbinder
+import logging
+import time
+from tools import helpers
+
+
+INTERFACE = "lineageos.waydroid.IPlatform"
+SERVICE_NAME = "waydroidplatform"
+
+TRANSACTION_getprop = 1
+TRANSACTION_setprop = 2
+TRANSACTION_getAppsInfo = 3
+TRANSACTION_getAppInfo = 4
+TRANSACTION_installApp = 5
+TRANSACTION_removeApp = 6
+TRANSACTION_launchApp = 7
+TRANSACTION_getAppName = 8
+TRANSACTION_settingsPutString = 9
+TRANSACTION_settingsGetString = 10
+TRANSACTION_settingsPutInt = 11
+TRANSACTION_getAppName = 12
+
+class IPlatform:
+    def __init__(self, remote):
+        self.client = gbinder.Client(remote, INTERFACE)
+
+    def getprop(self, arg1, arg2):
+        request = self.client.new_request()
+        request.append_string16(arg1)
+        request.append_string16(arg2)
+        reply, status = self.client.transact_sync_reply(
+            TRANSACTION_getprop, request)
+
+        if status:
+            logging.error("Sending reply failed")
+        else:
+            reader = reply.init_reader()
+            status, exception = reader.read_int32()
+            if exception == 0:
+                rep1 = reader.read_string16()
+                return rep1
+            else:
+                logging.error("Failed with code: {}".format(exception))
+
+        return None
+
+    def setprop(self, arg1, arg2):
+        request = self.client.new_request()
+        request.append_string16(arg1)
+        request.append_string16(arg2)
+        reply, status = self.client.transact_sync_reply(
+            TRANSACTION_setprop, request)
+
+        if status:
+            logging.error("Sending reply failed")
+        else:
+            reader = reply.init_reader()
+            status, exception = reader.read_int32()
+            if exception == 0:
+                return
+            else:
+                logging.error("Failed with code: {}".format(exception))
+
+        return
+
+    def getAppsInfo(self):
+        request = self.client.new_request()
+        reply, status = self.client.transact_sync_reply(
+            TRANSACTION_getAppsInfo, request)
+
+        if status:
+            logging.error("Sending reply failed")
+        else:
+            reader = reply.init_reader()
+            apps_list = []
+            status, exception = reader.read_int32()
+            if exception == 0:
+                status, apps = reader.read_int32()
+                for j in range(apps):
+                    status, has_value = reader.read_int32()
+                    if has_value == 1:
+                        appinfo = {
+                            "name": reader.read_string16(),
+                            "packageName": reader.read_string16(),
+                            "action": reader.read_string16(),
+                            "launchIntent": reader.read_string16(),
+                            "componentPackageName": reader.read_string16(),
+                            "componentClassName": reader.read_string16(),
+                            "categories": []
+                        }
+                        status, categories = reader.read_int32()
+                        for i in range(categories):
+                            appinfo["categories"].append(reader.read_string16())
+                        apps_list.append(appinfo)
+            else:
+                logging.error("Failed with code: {}".format(exception))
+
+        return apps_list
+
+    def getAppInfo(self, arg1):
+        request = self.client.new_request()
+        request.append_string16(arg1)
+        reply, status = self.client.transact_sync_reply(
+            TRANSACTION_getAppInfo, request)
+
+        if status:
+            logging.error("Sending reply failed")
+        else:
+            reader = reply.init_reader()
+            status, exception = reader.read_int32()
+            if exception == 0:
+                status, has_value = reader.read_int32()
+                if has_value == 1:
+                    appinfo = {
+                        "name": reader.read_string16(),
+                        "packageName": reader.read_string16(),
+                        "action": reader.read_string16(),
+                        "launchIntent": reader.read_string16(),
+                        "componentPackageName": reader.read_string16(),
+                        "componentClassName": reader.read_string16(),
+                        "categories": []
+                    }
+                    status, categories = reader.read_int32()
+                    for i in range(categories):
+                        appinfo["categories"].append(reader.read_string16())
+
+                    return appinfo
+            else:
+                logging.error("Failed with code: {}".format(exception))
+
+        return None
+
+    def installApp(self, arg1):
+        request = self.client.new_request()
+        request.append_string16(arg1)
+        reply, status = self.client.transact_sync_reply(
+            TRANSACTION_installApp, request)
+
+        if status:
+            logging.error("Sending reply failed")
+        else:
+            reader = reply.init_reader()
+            status, exception = reader.read_int32()
+            if exception == 0:
+                status, ret = reader.read_int32()
+                return ret
+            else:
+                logging.error("Failed with code: {}".format(exception))
+
+        return None
+
+    def removeApp(self, arg1):
+        request = self.client.new_request()
+        request.append_string16(arg1)
+        reply, status = self.client.transact_sync_reply(
+            TRANSACTION_removeApp, request)
+
+        if status:
+            logging.error("Sending reply failed")
+        else:
+            reader = reply.init_reader()
+            status, exception = reader.read_int32()
+            if exception == 0:
+                status, ret = reader.read_int32()
+                return ret
+            else:
+                logging.error("Failed with code: {}".format(exception))
+
+        return None
+
+    def launchApp(self, arg1):
+        request = self.client.new_request()
+        request.append_string16(arg1)
+        reply, status = self.client.transact_sync_reply(
+            TRANSACTION_launchApp, request)
+
+        if status:
+            logging.error("Sending reply failed")
+        else:
+            reader = reply.init_reader()
+            status, exception = reader.read_int32()
+            if exception != 0:
+                logging.error("Failed with code: {}".format(exception))
+
+    def getAppName(self, arg1):
+        request = self.client.new_request()
+        request.append_string16(arg1)
+        reply, status = self.client.transact_sync_reply(
+            TRANSACTION_getAppName, request)
+
+        if status:
+            logging.error("Sending reply failed")
+        else:
+            reader = reply.init_reader()
+            status, exception = reader.read_int32()
+            if exception == 0:
+                rep1 = reader.read_string16()
+                return rep1
+            else:
+                logging.error("Failed with code: {}".format(exception))
+
+        return None
+
+    def settingsPutString(self, arg1, arg2, arg3):
+        request = self.client.new_request()
+        request.append_int32(arg1)
+        request.append_string16(arg2)
+        request.append_string16(arg3)
+        reply, status = self.client.transact_sync_reply(
+            TRANSACTION_settingsPutString, request)
+
+        if status:
+            logging.error("Sending reply failed")
+        else:
+            reader = reply.init_reader()
+            status, exception = reader.read_int32()
+            if exception != 0:
+                logging.error("Failed with code: {}".format(exception))
+
+    def settingsGetString(self, arg1, arg2):
+        request = self.client.new_request()
+        request.append_int32(arg1)
+        request.append_string16(arg2)
+        reply, status = self.client.transact_sync_reply(
+            TRANSACTION_settingsGetString, request)
+
+        if status:
+            logging.error("Sending reply failed")
+        else:
+            reader = reply.init_reader()
+            status, exception = reader.read_int32()
+            if exception == 0:
+                rep1 = reader.read_string16()
+                return rep1
+            else:
+                logging.error("Failed with code: {}".format(exception))
+
+        return None
+
+    def settingsPutInt(self, arg1, arg2, arg3):
+        request = self.client.new_request()
+        request.append_int32(arg1)
+        request.append_string16(arg2)
+        request.append_int32(arg3)
+        reply, status = self.client.transact_sync_reply(
+            TRANSACTION_settingsPutInt, request)
+
+        if status:
+            logging.error("Sending reply failed")
+        else:
+            reader = reply.init_reader()
+            status, exception = reader.read_int32()
+            if exception != 0:
+                logging.error("Failed with code: {}".format(exception))
+
+    def settingsGetInt(self, arg1, arg2):
+        request = self.client.new_request()
+        request.append_int32(arg1)
+        request.append_string16(arg2)
+        reply, status = self.client.transact_sync_reply(
+            TRANSACTION_settingsGetString, request)
+
+        if status:
+            logging.error("Sending reply failed")
+        else:
+            reader = reply.init_reader()
+            status, exception = reader.read_int32()
+            if exception == 0:
+                status, rep1 = reader.read_int32()
+                return rep1
+            else:
+                logging.error("Failed with code: {}".format(exception))
+
+        return None
+
+def get_service(args):
+    helpers.drivers.loadBinderNodes(args)
+    serviceManager = gbinder.ServiceManager("/dev/" + args.BINDER_DRIVER)
+    tries = 1000
+
+    remote, status = serviceManager.get_service_sync(SERVICE_NAME)
+    while(not remote):
+        if tries > 0:
+            logging.warning(
+                "Failed to get service {}, trying again...".format(SERVICE_NAME))
+            time.sleep(1)
+            remote, status = serviceManager.get_service_sync(SERVICE_NAME)
+            tries = tries - 1
+        else:
+            return None
+
+    return IPlatform(remote)
diff --git a/tools/interfaces/IStatusBarService.py b/tools/interfaces/IStatusBarService.py
new file mode 100644 (file)
index 0000000..0fe30a7
--- /dev/null
@@ -0,0 +1,59 @@
+import gbinder
+import logging
+import time
+from tools import helpers
+
+
+INTERFACE = "com.android.internal.statusbar.IStatusBarService"
+SERVICE_NAME = "statusbar"
+
+TRANSACTION_expand = 1
+TRANSACTION_collapse = 2
+
+class IStatusBarService:
+    def __init__(self, remote):
+        self.client = gbinder.Client(remote, INTERFACE)
+
+    def expand(self):
+        request = self.client.new_request()
+        reply, status = self.client.transact_sync_reply(
+            TRANSACTION_expand, request)
+
+        if status:
+            logging.error("Sending reply failed")
+        else:
+            reader = reply.init_reader()
+            status, exception = reader.read_int32()
+            if exception != 0:
+                logging.error("Failed with code: {}".format(exception))
+
+    def collapse(self):
+        request = self.client.new_request()
+        reply, status = self.client.transact_sync_reply(
+            TRANSACTION_collapse, request)
+
+        if status:
+            logging.error("Sending reply failed")
+        else:
+            reader = reply.init_reader()
+            status, exception = reader.read_int32()
+            if exception != 0:
+                logging.error("Failed with code: {}".format(exception))
+
+def get_service(args):
+    helpers.drivers.loadBinderNodes(args)
+    serviceManager = gbinder.ServiceManager("/dev/" + args.BINDER_DRIVER)
+    tries = 1000
+
+    remote, status = serviceManager.get_service_sync(SERVICE_NAME)
+    while(not remote):
+        if tries > 0:
+            logging.warning(
+                "Failed to get service {}, trying again...".format(SERVICE_NAME))
+            time.sleep(1)
+            remote, status = serviceManager.get_service_sync(SERVICE_NAME)
+            tries = tries - 1
+        else:
+            return None
+
+    return IStatusBarService(remote)
diff --git a/tools/interfaces/IUserMonitor.py b/tools/interfaces/IUserMonitor.py
new file mode 100644 (file)
index 0000000..6652778
--- /dev/null
@@ -0,0 +1,51 @@
+import gbinder
+import logging
+from tools import helpers
+from gi.repository import GLib
+
+
+INTERFACE = "lineageos.waydroid.IUserMonitor"
+SERVICE_NAME = "waydroidusermonitor"
+
+TRANSACTION_userUnlocked = 1
+TRANSACTION_packageStateChanged = 2
+
+def add_service(args, userUnlocked, packageStateChanged):
+    helpers.drivers.loadBinderNodes(args)
+    serviceManager = gbinder.ServiceManager("/dev/" + args.BINDER_DRIVER)
+
+    def response_handler(req, code, flags):
+        logging.debug(
+            "{}: Received transaction: {}".format(SERVICE_NAME, code))
+        reader = req.init_reader()
+        local_response = response.new_reply()
+        if code == TRANSACTION_userUnlocked:
+            status, arg1 = reader.read_int32()
+            userUnlocked(arg1)
+            local_response.append_int32(0)
+        if code == TRANSACTION_packageStateChanged:
+            status, arg1 = reader.read_int32()
+            arg2 = reader.read_string16()
+            status, arg3 = reader.read_int32()
+            packageStateChanged(arg1, arg2, arg3)
+            local_response.append_int32(0)
+
+        return local_response, 0
+
+    def binder_presence():
+        if serviceManager.is_present():
+            status = serviceManager.add_service_sync(SERVICE_NAME, response)
+
+            if status:
+                logging.error("Failed to add service " + SERVICE_NAME)
+                args.userMonitorLoop.quit()
+
+    response = serviceManager.new_local_object(INTERFACE, response_handler)
+    args.userMonitorLoop = GLib.MainLoop()
+    binder_presence()
+    status = serviceManager.add_presence_handler(binder_presence)
+    if status:
+        args.userMonitorLoop.run()
+    else:
+        logging.error("Failed to add presence handler: {}".format(status))
+
diff --git a/tools/services/__init__.py b/tools/services/__init__.py
new file mode 100644 (file)
index 0000000..8510c33
--- /dev/null
@@ -0,0 +1,5 @@
+# Copyright 2021 Erfan Abdi
+# SPDX-License-Identifier: GPL-3.0-or-later
+from tools.services.user_manager import start, stop
+from tools.services.clipboard_manager import start, stop
+from tools.services.hardware_manager import start, stop
diff --git a/tools/services/clipboard_manager.py b/tools/services/clipboard_manager.py
new file mode 100644 (file)
index 0000000..72fe62a
--- /dev/null
@@ -0,0 +1,41 @@
+# Copyright 2021 Erfan Abdi
+# SPDX-License-Identifier: GPL-3.0-or-later
+import logging
+import threading
+from tools.interfaces import IClipboard
+
+try:
+    import pyclip
+    canClip = True
+except Exception as e:
+    logging.debug(str(e))
+    canClip = False
+
+def start(args):
+    def sendClipboardData(value):
+        try:
+            pyclip.copy(value)
+        except Exception as e:
+            logging.debug(str(e))
+
+    def getClipboardData():
+        try:
+            return pyclip.paste()
+        except Exception as e:
+            logging.debug(str(e))
+
+    def service_thread():
+        IClipboard.add_service(args, sendClipboardData, getClipboardData)
+
+    if canClip:
+        args.clipboard_manager = threading.Thread(target=service_thread)
+        args.clipboard_manager.start()
+    else:
+        logging.warning("Failed to start Clipboard manager service, check logs")
+
+def stop(args):
+    try:
+        if args.clipboardLoop:
+            args.clipboardLoop.quit()
+    except AttributeError:
+        logging.debug("Clipboard service is not even started")
diff --git a/tools/services/hardware_manager.py b/tools/services/hardware_manager.py
new file mode 100644 (file)
index 0000000..529348d
--- /dev/null
@@ -0,0 +1,44 @@
+# Copyright 2021 Erfan Abdi
+# SPDX-License-Identifier: GPL-3.0-or-later
+import logging
+import threading
+import tools.actions.container_manager
+from tools import helpers
+from tools.interfaces import IHardware
+
+
+def start(args):
+    def enableNFC(enable):
+        logging.debug("Function enableNFC not implemented")
+
+    def enableBluetooth(enable):
+        logging.debug("Function enableBluetooth not implemented")
+
+    def suspend():
+        tools.actions.container_manager.freeze(args)
+
+    def reboot():
+        helpers.lxc.stop(args)
+        helpers.lxc.start(args)
+
+    def upgrade(system_zip, system_time, vendor_zip, vendor_time):
+        helpers.lxc.stop(args)
+        helpers.images.umount_rootfs(args)
+        helpers.images.replace(args, system_zip, system_time,
+                               vendor_zip, vendor_time)
+        helpers.images.mount_rootfs(args, args.images_path)
+        helpers.lxc.start(args)
+
+    def service_thread():
+        IHardware.add_service(
+            args, enableNFC, enableBluetooth, suspend, reboot, upgrade)
+
+    args.hardware_manager = threading.Thread(target=service_thread)
+    args.hardware_manager.start()
+
+def stop(args):
+    try:
+        if args.hardwareLoop:
+            args.hardwareLoop.quit()
+    except AttributeError:
+        logging.debug("Hardware service is not even started")
diff --git a/tools/services/user_manager.py b/tools/services/user_manager.py
new file mode 100644 (file)
index 0000000..41fea83
--- /dev/null
@@ -0,0 +1,98 @@
+# Copyright 2021 Erfan Abdi
+# SPDX-License-Identifier: GPL-3.0-or-later
+import logging
+import os
+import threading
+import tools.config
+from tools.interfaces import IUserMonitor
+from tools.interfaces import IPlatform
+
+
+def start(args):
+    def makeDesktopFile(appInfo):
+        showApp = False
+        for cat in appInfo["categories"]:
+            if cat.strip() == "android.intent.category.LAUNCHER":
+                showApp = True
+        if not showApp:
+            return -1
+        
+        packageName = appInfo["packageName"]
+
+        desktop_file_path = args.host_user + \
+            "/.local/share/applications/" + packageName + ".desktop"
+        if not os.path.exists(desktop_file_path):
+            lines = ["[Desktop Entry]", "Type=Application"]
+            lines.append("Name=" + appInfo["name"])
+            lines.append("Exec=waydroid app launch " + packageName)
+            lines.append("Icon=" + args.waydroid_data + "/icons/" + packageName + ".png")
+            desktop_file = open(desktop_file_path, "w")
+            for line in lines:
+                desktop_file.write(line + "\n")
+            desktop_file.close()
+            os.chmod(desktop_file_path, 0o755)
+            return 0
+
+    def makeWaydroidDesktopFile():
+        desktop_file_path = args.host_user + \
+            "/.local/share/applications/Waydroid.desktop"
+        if not os.path.exists(desktop_file_path):
+            lines = ["[Desktop Entry]", "Type=Application"]
+            lines.append("Name=Waydroid")
+            lines.append("Exec=waydroid show-full-ui")
+            lines.append("Icon=" + tools.config.tools_src + "/data/AppIcon.png")
+            desktop_file = open(desktop_file_path, "w")
+            for line in lines:
+                desktop_file.write(line + "\n")
+            desktop_file.close()
+            os.chmod(desktop_file_path, 0o755)
+
+    def userUnlocked(uid):
+        logging.info("Android with user {} is ready".format(uid))
+        session_cfg = tools.config.load_session()
+        args.waydroid_data = session_cfg["session"]["waydroid_data"]
+        args.host_user = session_cfg["session"]["host_user"]
+
+        platformService = IPlatform.get_service(args)
+        if platformService:
+            appsList = platformService.getAppsInfo()
+            for app in appsList:
+                makeDesktopFile(app)
+            multiwin = platformService.getprop("persist.waydroid.multi_windows", "false")
+            if multiwin == "false":
+                makeWaydroidDesktopFile()
+            else:
+                desktop_file_path = args.host_user + \
+                    "/.local/share/applications/Waydroid.desktop"
+                if os.path.isfile(desktop_file_path):
+                    os.remove(desktop_file_path)
+
+    def packageStateChanged(mode, packageName, uid):
+        platformService = IPlatform.get_service(args)
+        if platformService:
+            appInfo = platformService.getAppInfo(packageName)
+            desktop_file_path = args.host_user + \
+                "/.local/share/applications/" + packageName + ".desktop"
+            if mode == 0:
+                # Package added
+                makeDesktopFile(appInfo)
+            elif mode == 1:
+                if os.path.isfile(desktop_file_path):
+                    os.remove(desktop_file_path)
+            else:
+                if os.path.isfile(desktop_file_path):
+                    if makeDesktopFile(appInfo) == -1:
+                        os.remove(desktop_file_path)
+
+    def service_thread():
+        IUserMonitor.add_service(args, userUnlocked, packageStateChanged)
+
+    args.user_manager = threading.Thread(target=service_thread)
+    args.user_manager.start()
+
+def stop(args):
+    try:
+        if args.userMonitorLoop:
+            args.userMonitorLoop.quit()
+    except AttributeError:
+        logging.debug("UserMonitor service is not even started")
diff --git a/waydroid.py b/waydroid.py
new file mode 100755 (executable)
index 0000000..a79b015
--- /dev/null
@@ -0,0 +1,9 @@
+#!/usr/bin/env python3
+# Copyright 2021 Oliver Smith
+# SPDX-License-Identifier: GPL-3.0-or-later
+# PYTHON_ARGCOMPLETE_OK
+import sys
+import tools
+
+if __name__ == "__main__":
+    sys.exit(tools.main())