diff --git a/.idea/.name b/.idea/.name
new file mode 100644
index 0000000..d36af14
--- /dev/null
+++ b/.idea/.name
@@ -0,0 +1 @@
+control
\ No newline at end of file
diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml
deleted file mode 100644
index 4a53bee..0000000
--- a/.idea/AndroidProjectSystem.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index 1825f66..6ae7c0e 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -1,40 +1,6 @@
-
-
-
+
@@ -153,8 +119,6 @@
-
-
\ No newline at end of file
diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml
new file mode 100644
index 0000000..040b11f
--- /dev/null
+++ b/.idea/deploymentTargetDropDown.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
index 3b52439..69c633e 100644
--- a/.idea/deploymentTargetSelector.xml
+++ b/.idea/deploymentTargetSelector.xml
@@ -2,8 +2,15 @@
-
+
+
+
+
+
+
+
+
@@ -13,7 +20,7 @@
-
+
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 639c779..7b3006b 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -13,6 +13,7 @@
+
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
deleted file mode 100644
index 103e00c..0000000
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
new file mode 100644
index 0000000..148fdd2
--- /dev/null
+++ b/.idea/kotlinc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index b2c751a..0d16216 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,9 +1,25 @@
+
-
+
+
+
+
+
+
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index f288702..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,674 +0,0 @@
- GNU GENERAL PUBLIC LICENSE
- Version 3, 29 June 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc.
- 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.
-
-
- Copyright (C)
-
- 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 .
-
-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:
-
- Copyright (C)
- 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
-.
-
- 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
-.
diff --git a/README.md b/README.md
index 914eb7b..e2b14c7 100644
--- a/README.md
+++ b/README.md
@@ -1,53 +1,39 @@
-# Android 局域网无信令 WebRTC 录屏与远程控制示例
-
-本示例演示了如何在没有中心信令服务器的情况下,通过局域网在两个 Android 设备之间直接利用 WebRTC 技术进行实时的**屏幕录制**传输和远程控制。
-
-## 关键特性
-
-* **纯局域网直连:** 无需外部信令服务器,设备间直接通信。
-* **屏幕录制传输:** 实时传输本设备的**屏幕画面**至对端设备。
-* **远程控制:** 提供触摸输入转发功能。
-
-## 技术栈
-
-* **WebRTC:** 实现点对点实时通信的核心技术。
-* **Kotlin:** 主要的应用程序开发语言。
-* **Android SDK:** 用于构建 Android 应用程序。
-
-## 快速开始
-
-### 环境要求
-
-* Android Studio 已安装并配置完毕。
-* 至少两部 Android 设备。
-* 两台设备必须连接至同一局域网。
-
-### 运行步骤
-
-1. **克隆代码:**
-2. **编译并安装应用:**
- * 在 Android Studio 中打开 `app` 模块。
- * 将应用分别安装到至少两台 Android 设备或模拟器上。
-3. **设备发现与连接:**
- * 在一个设备上发起连接,另一个设备接受连接请求。
-4. **录屏与控制:**
- * 成功建立连接后,一方的**屏幕内容**将实时显示在另一方设备上。
- * 可以进行触摸操作,并在远端设备上生效。
-
-## 详细说明
-
-* **无信令连接:** 本示例**不依赖**传统的信令服务器。设备之间将采用特定的**直接交换**机制来传递 SDP (Session Description Protocol) 和 ICE (Interactive Connectivity Establishment) 候选者,以建立 WebRTC 连接。
-* **WebRTC 实现:** 基于 Android WebRTC SDK 构建音视频引擎和数据通道,并使用 MediaProjection API 实现屏幕录制。
-* **所需权限:** 应用需要**屏幕录制**和网络访问权限。
-
-
-
-## 重要提示
-
-* 本应用旨在演示**无信令**局域网 WebRTC **屏幕录制**直连的概念,其设备发现和连接机制可能较为基础,不适用于复杂的生产环境。
-* 远程控制功能仅为演示,可以根据实际应用场景进行扩展。
-* 请确保两台设备处于同一局域网,且网络通信顺畅。
-
-## 许可协议
-
-本项目采用 [GPL-3.0 License](LICENSE) 许可证。
\ No newline at end of file
+# Android 局域网 WebRTC 视频流传输与远程控制示例应用
+
+## 简介
+该应用通过以下步骤实现:
+1. 使用 Android 的录屏权限捕获屏幕内容。
+2. 将屏幕内容编码为 AVC(H.264)视频流。
+3. 通过 WebRTC 连接将编码的视频流传输到局域网中的另一台设备。
+4. 使用反射调用 `InputManager` 的 `injectInputEvent` 方法,实现远程设备的控制。
+
+## 功能
+- 屏幕捕获与实时编码
+- WebRTC 视频流传输
+- 远程控制事件注入
+
+## 环境要求
+- Android 5.0(API Level 21)及以上
+- 局域网内的另一台设备作为客户端(接收端)
+- WebRTC 支持
+
+## 安装
+1. 将项目克隆到本地:
+2. 打开 Android Studio 并加载该项目。
+3. 连接测试设备,点击"运行"以安装应用。
+
+## 使用
+1. 启动应用,授予屏幕录制权限。
+2. 设置接收端设备的 IP 地址。
+3. 点击"开始传输"以启动 WebRTC 视频流。
+4. 使用接收端应用连接并观看视频流,同时可通过触控事件进行远程控制。
+
+## 代码结构
+- `ScreenCaptureService`:用于捕获屏幕内容并编码。
+- `WebRTCConnection`:用于设置 WebRTC 连接并传输数据。
+- `InputManagerInjector`:通过反射注入触摸与按键事件。
+
+## 注意事项
+- 反射调用 `InputManager` 属于非公开 API,可能会影响应用兼容性。
+- 确保接收端设备在同一局域网中,并正确配置了接收端应用。
+- 请仅在合法授权的设备上使用本应用。
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..9821f33
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,75 @@
+plugins {
+ id 'com.android.application'
+ id 'org.jetbrains.kotlin.android'
+}
+
+android {
+ compileSdk 31
+ namespace "com.github.webrtc"
+
+ defaultConfig {
+ applicationId "com.github.webrtc"
+ minSdk 21
+ targetSdk 32
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ signingConfigs {
+ release {
+ storeFile file("../platform.jks")
+ storePassword "123456"
+ keyAlias "quectel"
+ keyPassword "123456"
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ signingConfig signingConfigs.release
+ }
+ debug {
+ signingConfig signingConfigs.release
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+ buildFeatures {
+ buildConfig true
+ viewBinding true
+ }
+}
+
+dependencies {
+// implementation(name: 'libwebrtc', ext: 'aar')
+// implementation fileTree(dir: 'libs', include: ['*.aar'])
+// implementation "org.webrtc:google-webrtc:1.0.32006"
+
+ implementation "androidx.core:core-ktx:1.8.0"
+ implementation "androidx.appcompat:appcompat:1.4.2"
+ implementation "com.google.android.material:material:1.6.1"
+ implementation "androidx.constraintlayout:constraintlayout:2.1.4"
+ testImplementation "junit:junit:4.13.2"
+ androidTestImplementation "androidx.test.ext:junit:1.1.3"
+ androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0"
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.3"
+ implementation "io.netty:netty-all:4.1.17.Final"
+// implementation "com.google.code.gson:gson:2.9.0"
+
+// implementation "com.squareup.retrofit2:retrofit:2.9.0"
+// implementation "com.squareup.retrofit2:converter-gson:2.9.0"
+// implementation "com.blankj:utilcodex:1.31.0"
+
+// implementation("io.ktor:ktor-network:3.0.3")
+ implementation("io.getstream:stream-webrtc-android:1.3.7")
+ implementation ("com.blankj:utilcodex:1.31.0")
+}
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
deleted file mode 100644
index d8504d8..0000000
--- a/app/build.gradle.kts
+++ /dev/null
@@ -1,88 +0,0 @@
-plugins {
- id("com.android.application")
- id("org.jetbrains.kotlin.android")
- id("org.jetbrains.kotlin.plugin.compose")
-}
-
-android {
- namespace = "com.github.control"
- compileSdk = 35
-
- defaultConfig {
- applicationId = "com.github.control"
- minSdk = 28
- targetSdk = 35
- versionCode = 1
- versionName = "1.0"
-
- testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- vectorDrawables {
- useSupportLibrary = true
- }
- }
-
- buildTypes {
- release {
- isMinifyEnabled = false
- proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
- }
- }
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_11
- targetCompatibility = JavaVersion.VERSION_11
- }
- kotlinOptions {
- jvmTarget = "11"
- }
- buildFeatures {
-// aidl = true
-// buildConfig = true
- viewBinding = true
- compose = true
- }
-// composeOptions {
-// kotlinCompilerExtensionVersion = "1.5.1"
-// }
- packaging {
- resources {
- excludes += "/META-INF/{AL2.0,LGPL2.1}"
- }
- }
-}
-
-dependencies {
-
- implementation("androidx.core:core-ktx:1.15.0")
- implementation("androidx.appcompat:appcompat:1.7.0")
- implementation("com.google.android.material:material:1.12.0")
- implementation("androidx.activity:activity-ktx:1.10.1")
- implementation("androidx.constraintlayout:constraintlayout:2.2.1")
- implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
- implementation("androidx.activity:activity-compose:1.10.1")
- implementation(platform("androidx.compose:compose-bom:20250200"))
- implementation("androidx.compose.ui:ui")
- implementation("androidx.compose.ui:ui-graphics")
- implementation("androidx.compose.ui:ui-tooling-preview")
- implementation("androidx.compose.material3:material3")
- testImplementation("junit:junit:4.13.2")
- androidTestImplementation("androidx.test.ext:junit:1.2.1")
- androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
- androidTestImplementation(platform("androidx.compose:compose-bom:20250200"))
- androidTestImplementation("androidx.compose.ui:ui-test-junit4")
- debugImplementation("androidx.compose.ui:ui-tooling")
- debugImplementation("androidx.compose.ui:ui-test-manifest")
-
- implementation("androidx.lifecycle:lifecycle-service:2.8.7")
-
- implementation("androidx.navigation:navigation-compose:2.8.8")
-
-// implementation("io.netty:netty-all:4.1.17.Final")
- implementation("io.getstream:stream-webrtc-android:1.3.7")
- implementation("com.blankj:utilcodex:1.31.1")
-
- implementation("io.insert-koin:koin-android:4.1.0-Beta5")
- implementation("io.insert-koin:koin-androidx-compose:4.1.0-Beta5")
- implementation("io.insert-koin:koin-androidx-compose-navigation:4.1.0-Beta5")
-
-
-}
\ No newline at end of file
diff --git a/app/libs/libwebrtc.aar b/app/libs/libwebrtc.aar
new file mode 100644
index 0000000..4db3114
Binary files /dev/null and b/app/libs/libwebrtc.aar differ
diff --git a/app/src/androidTest/java/com/github/control/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/github/webrtc/ExampleInstrumentedTest.kt
similarity index 85%
rename from app/src/androidTest/java/com/github/control/ExampleInstrumentedTest.kt
rename to app/src/androidTest/java/com/github/webrtc/ExampleInstrumentedTest.kt
index eb50665..2b9ae73 100644
--- a/app/src/androidTest/java/com/github/control/ExampleInstrumentedTest.kt
+++ b/app/src/androidTest/java/com/github/webrtc/ExampleInstrumentedTest.kt
@@ -1,4 +1,4 @@
-package com.github.control
+package com.github.webrtc
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -19,6 +19,6 @@ class ExampleInstrumentedTest {
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
- assertEquals("com.github.control", appContext.packageName)
+ assertEquals("com.example.test_webrtc", appContext.packageName)
}
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 285b972..a25128f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,19 +1,35 @@
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.github.webrtc"
+>
+
+
+
+
+
+
+
+
-
+
@@ -21,28 +37,12 @@
-
-
-
-
+
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/java/com/github/control/MainActivity.kt b/app/src/main/java/com/github/control/MainActivity.kt
deleted file mode 100644
index b69645f..0000000
--- a/app/src/main/java/com/github/control/MainActivity.kt
+++ /dev/null
@@ -1,64 +0,0 @@
-package com.github.control
-
-import android.content.Intent
-import android.os.Bundle
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.Button
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBar
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
-
-class MainActivity : ComponentActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContent {
- MainScreen()
- }
- }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-private fun MainScreen() {
- val context = LocalContext.current
- Scaffold(
- modifier = Modifier.fillMaxSize(),
- topBar = {
- TopAppBar(title = {
- Text(text = "Remote Desktop Connection Software")
- })
- },
- content = { innerPadding ->
- Column(
- modifier = Modifier
- .fillMaxSize()
- .padding(innerPadding),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- Button(
- onClick = {
- context.startActivity(Intent(context, MasterActivity::class.java))
- },
- ) {
- Text(text = "这是主控端")
- }
- Button(
- onClick = {
- context.startActivity(Intent(context, SlaveActivity::class.java))
- },
- ) {
- Text(text = "这是受控端")
- }
- }
- }
- )
-}
diff --git a/app/src/main/java/com/github/control/MainApplication.kt b/app/src/main/java/com/github/control/MainApplication.kt
deleted file mode 100644
index 7cc9a59..0000000
--- a/app/src/main/java/com/github/control/MainApplication.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-package com.github.control
-
-import android.app.Application
-import android.hardware.usb.UsbManager
-import android.media.projection.MediaProjectionManager
-import android.net.nsd.NsdManager
-import androidx.core.content.ContextCompat
-import com.github.control.gesture.Controller
-import org.koin.android.ext.koin.androidContext
-import org.koin.android.ext.koin.androidLogger
-import org.koin.core.context.startKoin
-import org.koin.core.module.dsl.singleOf
-import org.koin.dsl.module
-import org.webrtc.PeerConnectionFactory
-
-
-class MainApplication : Application() {
- override fun onCreate() {
- super.onCreate()
- PeerConnectionFactory.initialize(
- PeerConnectionFactory.InitializationOptions.builder(applicationContext)
- .createInitializationOptions()
- )
-
- startKoin {
- androidLogger()
- androidContext(applicationContext)
- modules(module {
- single { ContextCompat.getSystemService(get(), NsdManager::class.java) }
- single { ContextCompat.getSystemService(get(), UsbManager::class.java) }
- single { ContextCompat.getSystemService(get(), MediaProjectionManager::class.java) }
- singleOf(::Controller)
- })
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/control/MasterActivity.kt b/app/src/main/java/com/github/control/MasterActivity.kt
deleted file mode 100644
index 8fb6b5c..0000000
--- a/app/src/main/java/com/github/control/MasterActivity.kt
+++ /dev/null
@@ -1,131 +0,0 @@
-package com.github.control
-
-import android.content.Context
-import android.net.nsd.NsdManager
-import android.net.nsd.NsdServiceInfo
-import android.os.Bundle
-import android.util.Log
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.material3.Button
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBar
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.mutableStateListOf
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
-import org.koin.compose.koinInject
-
-class MasterActivity : ComponentActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContent {
- MasterScreen()
- }
- }
-}
-
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-private fun MasterScreen() {
- val nsdManager = koinInject()
- val context = LocalContext.current
- val serviceList = remember { mutableStateListOf() }
- DisposableEffect(Unit) {
- val discoveryListener = object : NsdManager.DiscoveryListener {
- private val TAG = "NsdManager"
- override fun onDiscoveryStarted(serviceType: String) {
- Log.d(TAG, "onDiscoveryStarted: ")
- }
-
- override fun onServiceFound(serviceInfo: NsdServiceInfo) {
- Log.d(TAG, "onServiceFound: ${serviceInfo}")
- serviceList.add(serviceInfo)
- }
-
- override fun onServiceLost(serviceInfo: NsdServiceInfo) {
- Log.d(TAG, "onServiceLost: ${serviceInfo}")
- serviceList.removeIf {
- it.serviceName == serviceInfo.serviceName
- }
- }
-
- override fun onDiscoveryStopped(serviceType: String) {
- Log.d(TAG, "onDiscoveryStopped: ")
- }
-
- override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
- Log.d(TAG, "onStartDiscoveryFailed: ${errorCode}")
- }
-
- override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {
- Log.d(TAG, "onStopDiscoveryFailed: ${errorCode}")
- }
- }
-
- nsdManager.discoverServices("_control._tcp.", NsdManager.PROTOCOL_DNS_SD, discoveryListener)
- onDispose {
- nsdManager.stopServiceDiscovery(discoveryListener)
- }
- }
- Scaffold(
- topBar = {
- TopAppBar(title = {
- Text(text = "设备列表")
- })
- }
- ) { innerPadding ->
- LazyColumn(
- modifier = Modifier
- .fillMaxSize()
- .padding(innerPadding)
- ) {
- items(serviceList.size) {
- val serviceInfo = serviceList[it]
- Row(
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Text(text = serviceInfo.serviceName, modifier = Modifier.weight(1f))
- Button(onClick = {
- resolveAndStartService(serviceInfo, context, nsdManager)
- }) {
- Text(text = "连接")
- }
- }
- }
- }
- }
-}
-
-private val resolveServiceCache = mutableMapOf()
-private fun resolveAndStartService(
- serviceInfo: NsdServiceInfo,
- context: Context,
- nsdManager: NsdManager,
-) {
- if (resolveServiceCache.contains(serviceInfo.serviceName)) {
- val resolveServiceInfo = resolveServiceCache[serviceInfo.serviceName]!!
- ScreenCaptureActivity.start(context, resolveServiceInfo.host.hostName, resolveServiceInfo.port)
- } else {
- nsdManager.resolveService(serviceInfo, object : NsdManager.ResolveListener {
- override fun onResolveFailed(serviceInfo: NsdServiceInfo?, errorCode: Int) {
-
- }
-
- override fun onServiceResolved(resolveServiceInfo: NsdServiceInfo) {
- resolveServiceCache[serviceInfo.serviceName] = resolveServiceInfo
- ScreenCaptureActivity.start(context, resolveServiceInfo.host.hostName, resolveServiceInfo.port)
- }
- })
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/control/ScreenCaptureActivity.kt b/app/src/main/java/com/github/control/ScreenCaptureActivity.kt
deleted file mode 100644
index 0df0d92..0000000
--- a/app/src/main/java/com/github/control/ScreenCaptureActivity.kt
+++ /dev/null
@@ -1,228 +0,0 @@
-package com.github.control
-
-import android.accessibilityservice.AccessibilityService
-import android.annotation.SuppressLint
-import android.content.Context
-import android.content.Intent
-import android.os.Bundle
-import android.util.Log
-import androidx.appcompat.app.AppCompatActivity
-import androidx.lifecycle.lifecycleScope
-import com.blankj.utilcode.util.ThreadUtils
-import com.blankj.utilcode.util.ToastUtils
-import com.github.control.databinding.ActivityScreenCaptureBinding
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.isActive
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import org.webrtc.DataChannel
-import org.webrtc.EglBase
-import org.webrtc.HardwareVideoDecoderFactory
-import org.webrtc.HardwareVideoEncoderFactory
-import org.webrtc.IceCandidate
-import org.webrtc.MediaConstraints
-import org.webrtc.MediaStream
-import org.webrtc.PeerConnection
-import org.webrtc.PeerConnectionFactory
-import org.webrtc.RtpReceiver
-import org.webrtc.SdpObserver
-import org.webrtc.SessionDescription
-import org.webrtc.VideoTrack
-import java.io.DataInputStream
-import java.io.DataOutputStream
-import java.net.Socket
-
-@SuppressLint("ClickableViewAccessibility")
-class ScreenCaptureActivity : AppCompatActivity() {
- private val context = this
- private lateinit var binding: ActivityScreenCaptureBinding
- private val host by lazy { intent.getStringExtra("host") }
- private val port by lazy { intent.getIntExtra("port", 40000) }
- private lateinit var peerConnection: PeerConnection
- private var socket: Socket? = null
- private lateinit var inputStream: DataInputStream
- private lateinit var outputStream: DataOutputStream
- private val eglBase = EglBase.create()
- private val eglBaseContext = eglBase.getEglBaseContext()
- private val peerConnectionFactory = PeerConnectionFactory.builder()
- .setVideoEncoderFactory(HardwareVideoEncoderFactory(eglBaseContext, false, false))
- .setVideoDecoderFactory(HardwareVideoDecoderFactory(eglBaseContext))
- .createPeerConnectionFactory()
- private val rtcConfig = PeerConnection.RTCConfiguration(emptyList())
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- binding = ActivityScreenCaptureBinding.inflate(layoutInflater)
- setContentView(binding.root)
- initView()
- lifecycleScope.launch(Dispatchers.IO) {
- socket = Socket(host, port)
- inputStream = DataInputStream(socket!!.inputStream)
- outputStream = DataOutputStream(socket!!.outputStream)
- startReadThread()
- createPeerConnection()
- }
- }
-
- override fun onDestroy() {
- super.onDestroy()
- socket?.close()
- eglBase.release()
- binding.renderer.clearImage()
- binding.renderer.release()
- }
-
- private fun createPeerConnection() {
-
- peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, object : PeerConnection.Observer {
- override fun onAddTrack(rtpReceiver: RtpReceiver, mediaStreams: Array) {
- val track = rtpReceiver.track()
- if (track is VideoTrack) {
- track.addSink(binding.renderer)
- }
- }
-
- override fun onSignalingChange(p0: PeerConnection.SignalingState?) {
-
- }
-
- override fun onIceConnectionChange(p0: PeerConnection.IceConnectionState?) {
-
- }
-
- override fun onIceConnectionReceivingChange(p0: Boolean) {
-
- }
-
- override fun onIceGatheringChange(p0: PeerConnection.IceGatheringState?) {
-
- }
-
- override fun onIceCandidate(iceCandidate: IceCandidate) {
- sendIceCandidate(outputStream, iceCandidate)
- }
-
- override fun onIceCandidatesRemoved(p0: Array?) {
-
- }
-
- override fun onAddStream(p0: MediaStream?) {
-
- }
-
- override fun onRemoveStream(p0: MediaStream?) {
-
- }
-
- override fun onDataChannel(p0: DataChannel?) {
-
- }
-
- override fun onRenegotiationNeeded() {
-
- }
- })!!
- }
-
- private fun startReadThread() {
- lifecycleScope.launch(Dispatchers.IO) {
- try {
- while (isActive) {
- val type = inputStream.readInt()
- when (type) {
- ICE_CANDIDATE -> receiveIceCandidate(inputStream, peerConnection)
- SESSION_DESCRIPTION -> {
- receiveSessionDescription(inputStream, peerConnection)
- createAnswer()
- }
-
- CONFIGURATION_CHANGED -> {
- receiveConfigurationChanged(inputStream, binding.renderer)
- }
- }
- }
- } catch (e: Exception) {
- ToastUtils.showLong("对端关闭")
- withContext(Dispatchers.Main) {
- finish()
- }
- }
- }
- }
-
- private fun createAnswer() {
- peerConnection.createAnswer(object : SdpObserver {
- override fun onCreateSuccess(description: SessionDescription) {
- peerConnection.setLocalDescription(object : SdpObserver {
- override fun onCreateSuccess(sdp: SessionDescription) {
-
- }
-
- override fun onSetSuccess() {
-
- }
-
- override fun onCreateFailure(error: String) {
-
- }
-
- override fun onSetFailure(error: String) {
-
- }
- }, description)
- sendSessionDescription(outputStream, description)
- }
-
- override fun onSetSuccess() {
-
- }
-
- override fun onCreateFailure(error: String) {
-
- }
-
- override fun onSetFailure(error: String) {
-
- }
- }, MediaConstraints())
- }
-
- private val executorService = ThreadUtils.getSinglePool()
- private fun initView() {
- binding.renderer.init(eglBaseContext, null)
- binding.renderer.setOnTouchListener { _, event ->
- executorService.submit {
- sendTouchEvent(outputStream, event, binding.renderer)
- }
- true
- }
- binding.back.setOnClickListener {
- executorService.submit {
- sendGlobalActionEvent(outputStream, AccessibilityService.GLOBAL_ACTION_BACK)
- }
- }
- binding.home.setOnClickListener {
- executorService.submit {
- sendGlobalActionEvent(outputStream, AccessibilityService.GLOBAL_ACTION_HOME)
- }
- }
- binding.recents.setOnClickListener {
- executorService.submit {
- sendGlobalActionEvent(outputStream, AccessibilityService.GLOBAL_ACTION_RECENTS)
- }
- }
- }
-
-
- companion object {
- private const val TAG = "ScreenPullActivity"
-
- @JvmStatic
- fun start(context: Context, host: String, port: Int) {
- val starter = Intent(context, ScreenCaptureActivity::class.java)
- .putExtra("host", host)
- .putExtra("port", port)
- context.startActivity(starter)
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/control/ScreenCaptureService.kt b/app/src/main/java/com/github/control/ScreenCaptureService.kt
deleted file mode 100644
index d477235..0000000
--- a/app/src/main/java/com/github/control/ScreenCaptureService.kt
+++ /dev/null
@@ -1,329 +0,0 @@
-package com.github.control
-
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import android.content.IntentFilter
-import android.media.projection.MediaProjection
-import android.net.nsd.NsdManager
-import android.net.nsd.NsdServiceInfo
-import android.util.Log
-import androidx.core.app.NotificationChannelCompat
-import androidx.core.app.NotificationChannelGroupCompat
-import androidx.core.app.NotificationCompat
-import androidx.core.app.NotificationManagerCompat
-import androidx.lifecycle.LifecycleService
-import androidx.lifecycle.lifecycleScope
-import com.blankj.utilcode.util.DeviceUtils
-import com.blankj.utilcode.util.ScreenUtils
-import com.blankj.utilcode.util.ServiceUtils
-import com.blankj.utilcode.util.ThreadUtils
-import com.blankj.utilcode.util.ToastUtils
-import com.github.control.gesture.Controller
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.isActive
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import org.koin.android.ext.android.inject
-import org.webrtc.DataChannel
-import org.webrtc.EglBase
-import org.webrtc.HardwareVideoDecoderFactory
-import org.webrtc.HardwareVideoEncoderFactory
-import org.webrtc.IceCandidate
-import org.webrtc.MediaConstraints
-import org.webrtc.MediaStream
-import org.webrtc.PeerConnection
-import org.webrtc.PeerConnectionFactory
-import org.webrtc.ScreenCapturerAndroid
-import org.webrtc.SdpObserver
-import org.webrtc.SessionDescription
-import org.webrtc.SurfaceTextureHelper
-import java.io.DataInputStream
-import java.io.DataOutputStream
-import java.net.ServerSocket
-import java.net.Socket
-
-
-class ScreenCaptureService : LifecycleService() {
- companion object {
- const val SCREEN_CAPTURE_INTENT = "SCREEN_CAPTURE_INTENT"
- private const val TAG = "ScreenCaptureService"
-
- @JvmStatic
- fun start(context: Context, screenCaptureIntent: Intent) {
- val starter = Intent(context, ScreenCaptureService::class.java)
- .putExtra(SCREEN_CAPTURE_INTENT, screenCaptureIntent)
- context.startService(starter)
- }
-
- @JvmStatic
- fun stop(context: Context) {
- val starter = Intent(context, ScreenCaptureService::class.java)
- context.stopService(starter)
- }
-
- @JvmStatic
- fun isServiceRunning(context: Context): Boolean {
- return ServiceUtils.isServiceRunning(ScreenCaptureService::class.java)
- }
-
- }
-
- private val context = this
- private val controller by inject()
-
- private val eglBase = EglBase.create()
- private val eglBaseContext = eglBase.getEglBaseContext()
- private val peerConnectionFactory = PeerConnectionFactory.builder()
- .setVideoEncoderFactory(HardwareVideoEncoderFactory(eglBaseContext, false, false))
- .setVideoDecoderFactory(HardwareVideoDecoderFactory(eglBaseContext))
- .createPeerConnectionFactory()
- private val surfaceTextureHelper = SurfaceTextureHelper.create("surface_texture_thread", eglBaseContext, true)
- private val videoSource = peerConnectionFactory.createVideoSource(true, true)
- private val videoTrack = peerConnectionFactory.createVideoTrack("local_video_track", videoSource)
- private val rtcConfig = PeerConnection.RTCConfiguration(emptyList())
-
-
- private lateinit var peerConnection: PeerConnection
- private var screenCapturerAndroid: ScreenCapturerAndroid? = null
- private var serverSocket: ServerSocket? = null
- private var socket: Socket? = null
- private lateinit var inputStream: DataInputStream
- private lateinit var outputStream: DataOutputStream
-
- private val receiver = object : BroadcastReceiver() {
- override fun onReceive(context: Context?, intent: Intent?) {
- ThreadUtils.getSinglePool()
- .submit {
- sendConfigurationChanged(outputStream)
- }
- }
- }
-
- override fun onCreate() {
- super.onCreate()
- Log.d(TAG, "onCreate: ")
- startForegroundService()
- startNsdService()
- registerReceiver(receiver, IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED))
-
- lifecycleScope.launch(Dispatchers.IO) {
- try {
- serverSocket = ServerSocket(40000)
- socket = serverSocket?.accept()
- inputStream = DataInputStream(socket?.inputStream)
- outputStream = DataOutputStream(socket?.outputStream)
- sendConfigurationChanged(outputStream)
- createPeerConnection()
- startReadThread()
- } catch (e: Exception) {
- stopSelf()
- }
- }
- }
-
- override fun onDestroy() {
- super.onDestroy()
- Log.d(TAG, "onDestroy: ")
- stopForegroundService()
- stopNsdService()
- unregisterReceiver(receiver)
- eglBase.release()
- screenCapturerAndroid?.stopCapture()
- screenCapturerAndroid?.dispose()
- serverSocket?.close()
- socket?.close()
- }
-
-
- private fun createPeerConnection() {
- peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, object : PeerConnection.Observer {
- override fun onSignalingChange(p0: PeerConnection.SignalingState?) {
-
- }
-
- override fun onIceConnectionChange(p0: PeerConnection.IceConnectionState?) {
-
- }
-
- override fun onIceConnectionReceivingChange(p0: Boolean) {
-
- }
-
- override fun onIceGatheringChange(p0: PeerConnection.IceGatheringState?) {
-
- }
-
- override fun onIceCandidate(iceCandidate: IceCandidate) {
- sendIceCandidate(outputStream, iceCandidate)
- }
-
- override fun onIceCandidatesRemoved(p0: Array?) {
-
- }
-
- override fun onAddStream(p0: MediaStream?) {
-
- }
-
- override fun onRemoveStream(p0: MediaStream?) {
-
- }
-
- override fun onDataChannel(p0: DataChannel?) {
-
- }
-
- override fun onRenegotiationNeeded() {
-
- }
- })!!
- peerConnection.addTrack(videoTrack)
- peerConnection.createOffer(object : SdpObserver {
- override fun onCreateSuccess(description: SessionDescription) {
- peerConnection.setLocalDescription(object : SdpObserver {
- override fun onCreateSuccess(sdp: SessionDescription) {
-
- }
-
- override fun onSetSuccess() {
-
- }
-
- override fun onCreateFailure(error: String) {
-
- }
-
- override fun onSetFailure(error: String) {
-
- }
- }, description)
- sendSessionDescription(outputStream, description)
- }
-
- override fun onSetSuccess() {
-
- }
-
- override fun onCreateFailure(error: String) {
-
- }
-
- override fun onSetFailure(error: String) {
-
- }
- }, MediaConstraints())
- }
-
-
- private fun startReadThread() {
- lifecycleScope.launch(Dispatchers.IO) {
- try {
- while (isActive) {
- val type = inputStream.readInt()
- when (type) {
- ACTION_EVENT -> receiveGlobalActionEvent(inputStream, controller)
- TOUCH_EVENT -> receiveTouchEvent(inputStream, controller)
- ICE_CANDIDATE -> receiveIceCandidate(inputStream, peerConnection)
- SESSION_DESCRIPTION -> receiveSessionDescription(inputStream, peerConnection)
- }
- }
- } catch (e: Exception) {
- ToastUtils.showLong("对端关闭")
- withContext(Dispatchers.Main) {
- stopSelf()
- }
- }
- }
- }
-
-
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- Log.d(TAG, "onStartCommand:")
- val screenCaptureIntent = intent?.getParcelableExtra(SCREEN_CAPTURE_INTENT)
- if (screenCaptureIntent != null) {
- initScreenCapturerAndroid(screenCaptureIntent)
- }
- return super.onStartCommand(intent, flags, startId)
- }
-
- private fun initScreenCapturerAndroid(screenCaptureIntent: Intent) {
- screenCapturerAndroid = ScreenCapturerAndroid(screenCaptureIntent, object : MediaProjection.Callback() {
- override fun onStop() {
- stopSelf()
- }
- }).apply {
- initialize(surfaceTextureHelper, applicationContext, videoSource.capturerObserver)
- startCapture(ScreenUtils.getScreenWidth(), ScreenUtils.getScreenHeight(), 0)
- }
- }
-
-
- /**
- * ForegroundService
- */
- private fun startForegroundService() {
- val groupId = "screenRecordingGroup"
- val channelId = "screenRecordingChannel"
- val notificationManager = NotificationManagerCompat.from(context)
- val notificationChannelGroup = NotificationChannelGroupCompat.Builder(groupId)
- .setName("录屏")
- .setDescription("录屏通知组别")
- .build()
- notificationManager.createNotificationChannelGroup(notificationChannelGroup)
- val notificationChannel = NotificationChannelCompat.Builder(channelId, NotificationManagerCompat.IMPORTANCE_LOW)
- .setName("录屏通知渠道")
- .setDescription("录屏通知渠道")
- .setGroup(groupId)
- .build()
- notificationManager.createNotificationChannelsCompat(mutableListOf(notificationChannel))
- val notification = NotificationCompat.Builder(context, channelId)
- .setSmallIcon(R.drawable.outline_screen_record_24)
- .setContentTitle("录屏服务正在运行")
- .setSilent(true)
- .setOngoing(true)
- .build()
- startForeground(1, notification)
- }
-
- private fun stopForegroundService() {
- stopForeground(STOP_FOREGROUND_REMOVE)
- }
-
- /**
- * Nsd
- */
- private val nsdManager by inject()
- private val nsdServiceInfo = NsdServiceInfo().apply {
- serviceName = DeviceUtils.getModel()
- serviceType = "_control._tcp."
- port = 40000
- }
- private val registrationListener = object : NsdManager.RegistrationListener {
- private val TAG = "NsdManager"
- override fun onServiceRegistered(nsdServiceInfo: NsdServiceInfo) {
- Log.d(TAG, "onServiceRegistered:注册成功 ")
- }
-
- override fun onRegistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
- Log.d(TAG, "onRegistrationFailed:注册失败 ")
- }
-
- override fun onServiceUnregistered(arg0: NsdServiceInfo) {
- Log.d(TAG, "onServiceUnregistered:取消注册 ")
- }
-
- override fun onUnregistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
- Log.d(TAG, "onUnregistrationFailed:取消注册失败 ")
- }
- }
-
-
- private fun startNsdService() {
- nsdManager.registerService(nsdServiceInfo, NsdManager.PROTOCOL_DNS_SD, registrationListener)
- }
-
- private fun stopNsdService() {
- nsdManager.unregisterService(registrationListener)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/control/SlaveActivity.kt b/app/src/main/java/com/github/control/SlaveActivity.kt
deleted file mode 100644
index cc1508b..0000000
--- a/app/src/main/java/com/github/control/SlaveActivity.kt
+++ /dev/null
@@ -1,119 +0,0 @@
-package com.github.control
-
-import android.media.projection.MediaProjectionManager
-import android.os.Bundle
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.rememberLauncherForActivityResult
-import androidx.activity.compose.setContent
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.appcompat.app.AppCompatActivity
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.Button
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBar
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.compose.LocalLifecycleOwner
-import androidx.lifecycle.repeatOnLifecycle
-import com.github.control.gesture.GestureControlAccessibilityService
-import org.koin.compose.koinInject
-
-class SlaveActivity : ComponentActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContent {
- SlaveScreen()
- }
- }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-private fun SlaveScreen() {
- val mediaProjectionManager = koinInject()
- val lifecycleOwner = LocalLifecycleOwner.current
- val context = LocalContext.current
- var accessibilityEnabled by remember { mutableStateOf(GestureControlAccessibilityService.isAccessibilityEnabled(context)) }
- var screenCaptureServiceEnabled by remember { mutableStateOf(ScreenCaptureService.isServiceRunning(context)) }
- fun checkState() {
- accessibilityEnabled = GestureControlAccessibilityService.isAccessibilityEnabled(context)
- screenCaptureServiceEnabled = ScreenCaptureService.isServiceRunning(context)
- }
-
-
- val screenCaptureLauncher = rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) {
- val screenCaptureIntent = it.data
- if (it.resultCode == AppCompatActivity.RESULT_OK && screenCaptureIntent != null) {
- ScreenCaptureService.start(context, screenCaptureIntent)
- checkState()
- }
- }
- LaunchedEffect(lifecycleOwner) {
- lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
- checkState()
- }
- }
-
- Scaffold(
- modifier = Modifier.fillMaxSize(),
- topBar = {
- TopAppBar(title = {
- Text(text = "受控端配置")
- })
- },
- content = { innerPadding ->
- Column(
- modifier = Modifier
- .fillMaxSize()
- .padding(innerPadding),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Text("无障碍权限", modifier = Modifier.weight(1f))
- Button(
- onClick = {
- GestureControlAccessibilityService.openAccessibilitySettings(context)
- },
- enabled = !accessibilityEnabled,
- ) {
- Text(text = "授权")
- }
- }
- Button(
- onClick = {
- screenCaptureLauncher.launch(mediaProjectionManager.createScreenCaptureIntent())
- },
- enabled = !screenCaptureServiceEnabled && accessibilityEnabled
- ) {
- Text(text = "开启服务")
- }
- Button(
- onClick = {
- ScreenCaptureService.stop(context)
- checkState()
- },
- enabled = screenCaptureServiceEnabled
- ) {
- Text(text = "停止服务")
- }
- }
- }
- )
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/control/ext.kt b/app/src/main/java/com/github/control/ext.kt
deleted file mode 100644
index 8900ffb..0000000
--- a/app/src/main/java/com/github/control/ext.kt
+++ /dev/null
@@ -1,136 +0,0 @@
-package com.github.control
-
-import android.graphics.Point
-import android.util.Size
-import android.view.MotionEvent
-import android.view.View
-import androidx.constraintlayout.widget.ConstraintLayout
-import com.blankj.utilcode.util.ScreenUtils
-import com.blankj.utilcode.util.ThreadUtils
-import com.github.control.gesture.Controller
-import org.webrtc.IceCandidate
-import org.webrtc.PeerConnection
-import org.webrtc.SdpObserver
-import org.webrtc.SessionDescription
-import org.webrtc.SurfaceViewRenderer
-import java.io.DataInputStream
-import java.io.DataOutputStream
-
-const val ACTION_EVENT = 101
-const val TOUCH_EVENT = 102
-const val ICE_CANDIDATE = 201
-const val SESSION_DESCRIPTION = 202
-const val CONFIGURATION_CHANGED = 203
-
-fun sendGlobalActionEvent(outputStream: DataOutputStream, action: Int) {
- outputStream.writeInt(ACTION_EVENT)
- outputStream.writeInt(action)
- outputStream.flush()
-}
-
-fun receiveGlobalActionEvent(inputStream: DataInputStream, controller: Controller) {
- val action = inputStream.readInt()
- controller.injectGlobalAction(action)
-}
-
-fun sendTouchEvent(outputStream: DataOutputStream, event: MotionEvent, view: View) {
- val action = event.action
- val pointerId = event.getPointerId(event.actionIndex)
- val x = (event.getX(event.actionIndex) - view.x).toInt()
- val y = (event.getY(event.actionIndex) - view.y).toInt()
- val screenWidth = view.width
- val screenHeight = view.height
- val pressure = event.pressure
- val actionButton = event.actionButton
- val buttons = event.buttonState
-
- outputStream.writeInt(TOUCH_EVENT)
- outputStream.writeInt(action)
- outputStream.writeInt(pointerId)
- outputStream.writeInt(x)
- outputStream.writeInt(y)
- outputStream.writeInt(screenWidth)
- outputStream.writeInt(screenHeight)
- outputStream.writeFloat(pressure)
- outputStream.writeInt(actionButton)
- outputStream.writeInt(buttons)
- outputStream.flush()
-}
-
-fun receiveTouchEvent(inputStream: DataInputStream, controller: Controller) {
- val action = inputStream.readInt()
- val pointerId = inputStream.readInt()
- val x = inputStream.readInt()
- val y = inputStream.readInt()
- val screenWidth = inputStream.readInt()
- val screenHeight = inputStream.readInt()
- val pressure = inputStream.readFloat()
- val actionButton = inputStream.readInt()
- val buttons = inputStream.readInt()
- controller.injectTouch(action, pointerId, Point(x, y), Size(screenWidth, screenHeight), pressure, actionButton, buttons)
-}
-
-
-fun sendIceCandidate(outputStream: DataOutputStream, iceCandidate: IceCandidate) {
- outputStream.writeInt(ICE_CANDIDATE)
- outputStream.writeUTF(iceCandidate.sdpMid)
- outputStream.writeInt(iceCandidate.sdpMLineIndex)
- outputStream.writeUTF(iceCandidate.sdp)
- outputStream.flush()
-}
-
-fun receiveIceCandidate(inputStream: DataInputStream, peerConnection: PeerConnection) {
- val sdpMid = inputStream.readUTF()
- val sdpMLineIndex = inputStream.readInt()
- val sdp = inputStream.readUTF()
- val ice = IceCandidate(sdpMid, sdpMLineIndex, sdp)
- peerConnection.addIceCandidate(ice)
-
-}
-
-fun sendSessionDescription(outputStream: DataOutputStream, sessionDescription: SessionDescription) {
- outputStream.writeInt(SESSION_DESCRIPTION)
- outputStream.writeUTF(sessionDescription.type.name)
- outputStream.writeUTF(sessionDescription.description)
- outputStream.flush()
-}
-
-fun receiveSessionDescription(inputStream: DataInputStream, peerConnection: PeerConnection) {
- val type = inputStream.readUTF()
- val description = inputStream.readUTF()
- val sdp = SessionDescription(SessionDescription.Type.valueOf(type), description)
- peerConnection.setRemoteDescription(object : SdpObserver {
- override fun onCreateSuccess(sdp: SessionDescription) {
-
- }
-
- override fun onSetSuccess() {
-
- }
-
- override fun onCreateFailure(error: String) {
-
- }
-
- override fun onSetFailure(error: String) {
-
- }
- }, sdp)
-}
-
-fun sendConfigurationChanged(outputStream: DataOutputStream) {
- outputStream.writeInt(CONFIGURATION_CHANGED)
- outputStream.writeInt(ScreenUtils.getScreenWidth())
- outputStream.writeInt(ScreenUtils.getScreenHeight())
- outputStream.flush()
-}
-
-fun receiveConfigurationChanged(inputStream: DataInputStream, renderer: SurfaceViewRenderer) {
- val screenWidth = inputStream.readInt()
- val screenHeight = inputStream.readInt()
- ThreadUtils.runOnUiThread {
- renderer.layoutParams = (renderer.layoutParams as ConstraintLayout.LayoutParams).apply {
- dimensionRatio = "${screenWidth}:${screenHeight}"
- }
- }
-}
diff --git a/app/src/main/java/com/github/control/gesture/Controller.kt b/app/src/main/java/com/github/control/gesture/Controller.kt
deleted file mode 100644
index 6c1d70e..0000000
--- a/app/src/main/java/com/github/control/gesture/Controller.kt
+++ /dev/null
@@ -1,81 +0,0 @@
-package com.github.control.gesture
-
-import android.graphics.Point
-import android.os.SystemClock
-import android.util.Size
-import android.view.InputDevice
-import android.view.MotionEvent
-import com.blankj.utilcode.util.ScreenUtils
-import org.koin.core.component.KoinComponent
-
-class Controller : KoinComponent {
- private val accessibilityService: GestureControlAccessibilityService?
- get() = getKoin().getOrNull()
-
- private var lastTouchDown: Long = 0
-
- private val pointersState = PointersState()
- private val pointerProperties = Array(PointersState.MAX_POINTERS) {
- MotionEvent.PointerProperties().apply {
- toolType = MotionEvent.TOOL_TYPE_FINGER
- }
- }
- private val pointerCoords = Array(PointersState.MAX_POINTERS) { MotionEvent.PointerCoords() }
-
-
- fun injectGlobalAction(action: Int) {
- accessibilityService?.performGlobalAction(action)
- }
-
- fun injectTouch(action: Int, pointerId: Int, inputPoint: Point, screenSize: Size, pressure: Float, actionButton: Int, buttons: Int) {
- val now = SystemClock.uptimeMillis()
- val point = mapToScreen(inputPoint, screenSize)
-
- val pointerIndex = pointersState.getPointerIndex(pointerId)
- if (pointerIndex == -1) return
-
- val pointer = pointersState[pointerIndex]
- pointer.point = point
- pointer.pressure = pressure
- pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_FINGER
- pointer.isUp = action == MotionEvent.ACTION_UP
-
- val pointerCount = pointersState.update(pointerProperties, pointerCoords)
-
- var finalAction = action
- if (pointerCount == 1 && action == MotionEvent.ACTION_DOWN) {
- lastTouchDown = now
- } else {
- finalAction = when (action) {
- MotionEvent.ACTION_UP -> MotionEvent.ACTION_POINTER_UP or (pointerIndex shl MotionEvent.ACTION_POINTER_INDEX_SHIFT)
- MotionEvent.ACTION_DOWN -> MotionEvent.ACTION_POINTER_DOWN or (pointerIndex shl MotionEvent.ACTION_POINTER_INDEX_SHIFT)
- else -> action
- }
- }
- val event = MotionEvent.obtain(lastTouchDown, now, finalAction, pointerCount, pointerProperties, pointerCoords, 0, 0, 1f, 1f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0)
- accessibilityService?.dispatchGesture(event)
- }
-
- /**
- * 将一个点从一个屏幕尺寸映射到另一个屏幕尺寸。核心步骤是计算出源屏幕和目标屏幕的坐标系转换关系,最后返回映射后的新坐标点。
- */
- private fun mapToScreen(sourcePoint: Point, targetScreenSize: Size): Point {
- val sourceX = sourcePoint.x.toDouble()
- val sourceY = sourcePoint.y.toDouble()
- val targetWidth = targetScreenSize.width.toDouble()
- val targetHeight = targetScreenSize.height.toDouble()
- val actualWidth = ScreenUtils.getScreenWidth().toDouble()
- val actualHeight = ScreenUtils.getScreenHeight().toDouble()
-
- val maxTargetWidth = minOf(targetWidth, targetHeight * actualWidth / actualHeight)
- val maxTargetHeight = minOf(targetHeight, targetWidth * actualHeight / actualWidth)
-
- val offsetX = (targetWidth - maxTargetWidth) / 2
- val offsetY = (targetHeight - maxTargetHeight) / 2
-
- val mappedX = (sourceX - offsetX) / maxTargetWidth * actualWidth
- val mappedY = (sourceY - offsetY) / maxTargetHeight * actualHeight
-
- return Point(mappedX.toInt(), mappedY.toInt())
- }
-}
diff --git a/app/src/main/java/com/github/control/gesture/GestureControlAccessibilityService.kt b/app/src/main/java/com/github/control/gesture/GestureControlAccessibilityService.kt
deleted file mode 100644
index e8330b0..0000000
--- a/app/src/main/java/com/github/control/gesture/GestureControlAccessibilityService.kt
+++ /dev/null
@@ -1,222 +0,0 @@
-package com.github.control.gesture
-
-import android.accessibilityservice.AccessibilityService
-import android.accessibilityservice.GestureDescription
-import android.content.Context
-import android.content.Intent
-import android.provider.Settings
-import android.util.Log
-import android.view.KeyEvent
-import android.view.MotionEvent
-import android.view.accessibility.AccessibilityEvent
-import org.koin.core.component.KoinComponent
-
-
-class GestureControlAccessibilityService : AccessibilityService(), KoinComponent {
- override fun onCreate() {
- super.onCreate()
- getKoin().declare(this)
- }
-
- override fun onDestroy() {
- super.onDestroy()
- }
-
- override fun onAccessibilityEvent(event: AccessibilityEvent?) {}
- override fun onInterrupt() {}
-
-
- private var isGestureActive = false // 标识当前是否有手势在进行
-
- //手势跟踪器列表,最多支持 16 个手势点
- private val trackers: List = List(16) { GestureTracker() }
-
-
- /**
- * 处理 MotionEvent 事件并转换为相应的手势操作
- *
- * @param motionEvent 触摸事件
- */
- fun dispatchGesture(motionEvent: MotionEvent) {
- try {
- val actionMasked = motionEvent.actionMasked
- val actionIndex = motionEvent.actionIndex
- val pointerId = motionEvent.getPointerId(actionIndex)
- val x = motionEvent.getX(actionIndex)
- val y = motionEvent.getY(actionIndex)
- when (actionMasked) {
- MotionEvent.ACTION_DOWN -> onActionDown(pointerId, x, y, motionEvent)
- MotionEvent.ACTION_UP -> onActionUp(pointerId, x, y, motionEvent)
- MotionEvent.ACTION_MOVE -> onActionMove(pointerId, x, y, motionEvent)
- MotionEvent.ACTION_CANCEL -> onActionCancel(pointerId, x, y, motionEvent)
- MotionEvent.ACTION_OUTSIDE -> {}
- MotionEvent.ACTION_POINTER_DOWN -> onActionPointerDown(pointerId, x, y, motionEvent)
- MotionEvent.ACTION_POINTER_UP -> onActionPointerUp(pointerId, x, y, motionEvent)
- MotionEvent.ACTION_HOVER_MOVE -> {}
- MotionEvent.ACTION_SCROLL -> {}
- MotionEvent.ACTION_HOVER_ENTER -> {}
- MotionEvent.ACTION_HOVER_EXIT -> {}
- MotionEvent.ACTION_BUTTON_PRESS -> {}
- MotionEvent.ACTION_BUTTON_RELEASE -> {}
- }
- motionEvent.recycle()
- } catch (e: Exception) {
- e.printStackTrace()
- }
- }
-
- /**
- * 处理单指触摸按下事件
- */
- private fun onActionDown(pointerId: Int, x: Float, y: Float, motionEvent: MotionEvent) {
- resetAllTrackers()
- val availableTracker = getAvailableTracker()
- if (availableTracker != null) {
- isGestureActive = true
- availableTracker.startTracking(pointerId, x, y)
- val builder = GestureDescription.Builder()
- builder.addStroke(availableTracker.stroke!!)
- dispatchGesture(builder.build(), null, null)
- }
- }
-
- /**
- * 处理手指移动事件
- */
- private fun onActionMove(pointerId: Int, x: Float, y: Float, motionEvent: MotionEvent) {
- if (isGestureActive) {
- val builder = GestureDescription.Builder()
- for (index in 0 until motionEvent.pointerCount) {
- val tracker = getTrackerByPointerId(motionEvent.getPointerId(index))
- tracker?.let {
- it.updateStroke(motionEvent.getX(index), motionEvent.getY(index), true)
- builder.addStroke(it.stroke!!)
- }
- }
- dispatchGesture(builder.build(), null, null)
- }
- }
-
- /**
- * 处理单指抬起事件
- */
- private fun onActionUp(pointerId: Int, x: Float, y: Float, motionEvent: MotionEvent) {
- val tracker = getTrackerByPointerId(pointerId)
- if (isGestureActive && tracker != null) {
- tracker.updateStroke(x, y, false)
- val builder = GestureDescription.Builder()
- builder.addStroke(tracker.stroke!!)
- isGestureActive = false
- resetAllTrackers()
- dispatchGesture(builder.build(), null, null)
- }
- }
-
- /**
- * 处理手势取消事件
- */
- private fun onActionCancel(pointerId: Int, x: Float, y: Float, motionEvent: MotionEvent) {
- isGestureActive = false
- resetAllTrackers()
- }
-
- /**
- * 处理多指抬起事件
- */
- private fun onActionPointerUp(pointerId: Int, x: Float, y: Float, motionEvent: MotionEvent) {
- val tracker = getTrackerByPointerId(pointerId)
- if (isGestureActive && tracker != null) {
- tracker.updateStroke(x, y, false)
- val builder = GestureDescription.Builder()
- builder.addStroke(tracker.stroke!!)
-
- // 处理其他仍在追踪的手指
- for (otherTracker in trackers) {
- if (otherTracker.isTracking && otherTracker != tracker) {
- otherTracker.updateStroke(otherTracker.previousX, otherTracker.previousY, true)
- builder.addStroke(otherTracker.stroke!!)
- }
- }
- tracker.reset()
- dispatchGesture(builder.build(), null, null)
- }
- }
-
- /**
- * 处理多指按下事件
- */
- private fun onActionPointerDown(pointerId: Int, x: Float, y: Float, motionEvent: MotionEvent) {
- val availableTracker = getAvailableTracker()
- if (isGestureActive && availableTracker != null) {
- val builder = GestureDescription.Builder()
-
- // 重新追踪已有的手势
- for (tracker in trackers) {
- if (tracker.isTracking) {
- tracker.startTracking(tracker.pointerId, tracker.previousX, tracker.previousY)
- builder.addStroke(tracker.stroke!!)
- }
- }
- availableTracker.startTracking(pointerId, x, y)
- builder.addStroke(availableTracker.stroke!!)
- dispatchGesture(builder.build(), null, null)
- }
- }
-
- /**
- * 获取可用的手势跟踪器
- */
- private fun getAvailableTracker(): GestureTracker? {
- return trackers.firstOrNull { !it.isTracking }
- }
-
- /**
- * 根据 pointerId 获取对应的手势跟踪器
- */
- private fun getTrackerByPointerId(pointerId: Int): GestureTracker? {
- return trackers.firstOrNull { it.isTracking && it.pointerId == pointerId }
- }
-
- /**
- * 重置所有手势跟踪器
- */
- private fun resetAllTrackers() {
- trackers.forEach { it.reset() }
- }
-
- fun performGlobalAction(keyEvent: KeyEvent) {
- val action = keyEvent.action
- val keyCode = keyEvent.keyCode
- val isShiftPressed = keyEvent.isShiftPressed
- val isCtrlPressed = keyEvent.isCtrlPressed
- when (keyCode) {
- KeyEvent.KEYCODE_HOME -> performGlobalAction(GLOBAL_ACTION_HOME)
- KeyEvent.KEYCODE_BACK -> performGlobalAction(GLOBAL_ACTION_BACK)
- KeyEvent.KEYCODE_VOLUME_UP -> {}
- KeyEvent.KEYCODE_VOLUME_DOWN -> {}
- KeyEvent.KEYCODE_POWER -> {}
- KeyEvent.KEYCODE_SPACE -> {}
- KeyEvent.KEYCODE_MENU -> {}
- KeyEvent.KEYCODE_APP_SWITCH -> performGlobalAction(GLOBAL_ACTION_RECENTS)
- }
- }
-
-
- companion object {
- private const val TAG = "GestureControlAccessibi"
-
-
- fun isAccessibilityEnabled(context: Context): Boolean {
- val enabledServices = Settings.Secure.getString(context.contentResolver, Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES)
- return enabledServices != null && enabledServices.contains(GestureControlAccessibilityService::class.java.name)
- }
-
- fun openAccessibilitySettings(context: Context) {
- val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS).apply {
- flags = Intent.FLAG_ACTIVITY_NEW_TASK
- }
- context.startActivity(intent)
- }
- }
-}
-
diff --git a/app/src/main/java/com/github/control/gesture/GestureTracker.kt b/app/src/main/java/com/github/control/gesture/GestureTracker.kt
deleted file mode 100644
index aac9b26..0000000
--- a/app/src/main/java/com/github/control/gesture/GestureTracker.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-package com.github.control.gesture;
-
-import android.accessibilityservice.GestureDescription;
-import android.graphics.Path;
-
-/**
- * GestureTracker 类用于跟踪手势轨迹并生成可用于 Android 辅助功能服务的手势描述。
- *
- * 该类主要用于记录手势的路径,并生成 GestureDescription.StrokeDescription
- * 以便在无障碍服务中模拟手势操作。
- */
-class GestureTracker {
- /** 是否正在跟踪手势 */
- var isTracking = false
-
- /** 触摸点的标识符 */
- var pointerId = 0
-
- /** 记录手势路径的 Path 对象 */
- val path = Path()
-
- /** 当前手势的笔画描述 */
- var stroke: GestureDescription.StrokeDescription? = null
-
- /** 记录上一个触摸点的 X 坐标 */
- var previousX = 0f
-
- /** 记录上一个触摸点的 Y 坐标 */
- var previousY = 0f
-
- /**
- * 开始跟踪手势
- *
- * @param pointerId 触摸点 ID
- * @param x 起始 X 坐标
- * @param y 起始 Y 坐标
- */
- fun startTracking(pointerId: Int, x: Float, y: Float) {
- this.isTracking = true
- this.pointerId = pointerId
- this.path.reset()
- this.path.moveTo(x, y)
- this.stroke = GestureDescription.StrokeDescription(path, 0L, 10L, true)
- this.previousX = x
- this.previousY = y
- }
-
- /**
- * 更新手势路径,并继续笔画
- *
- * @param x 新的 X 坐标
- * @param y 新的 Y 坐标
- * @param willContinue 是否继续跟踪手势
- */
- fun updateStroke(x: Float, y: Float, willContinue: Boolean) {
- this.path.reset()
- this.path.moveTo(previousX, previousY)
- this.path.lineTo(x, y)
- this.stroke = stroke?.continueStroke(path, 0L, 10L, willContinue)
- this.previousX = x
- this.previousY = y
- }
-
- /**
- * 复位手势跟踪器,清除所有存储的数据。
- */
- fun reset() {
- this.isTracking = false
- this.pointerId = 0
- this.path.reset()
- this.stroke = null
- this.previousX = 0f
- this.previousY = 0f
- }
-}
diff --git a/app/src/main/java/com/github/control/gesture/Pointer.kt b/app/src/main/java/com/github/control/gesture/Pointer.kt
deleted file mode 100644
index 658e090..0000000
--- a/app/src/main/java/com/github/control/gesture/Pointer.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.github.control.gesture
-
-import android.graphics.Point
-
-
-/**
- * @param id Pointer id as received from the client.
- * @param localId Local pointer id, using the lowest possible values to fill the [PointerProperties][android.view.MotionEvent.PointerProperties].
- */
-data class Pointer @JvmOverloads constructor(
- var id: Int,
- var localId: Int,
- var point: Point? = null,
- var pressure: Float = 0f,
- var isUp: Boolean = false,
-)
diff --git a/app/src/main/java/com/github/control/gesture/PointersState.kt b/app/src/main/java/com/github/control/gesture/PointersState.kt
deleted file mode 100644
index 100816f..0000000
--- a/app/src/main/java/com/github/control/gesture/PointersState.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-package com.github.control.gesture
-
-import android.view.MotionEvent
-
-class PointersState {
- companion object {
- const val MAX_POINTERS = 10
- }
-
- private val pointers = ArrayList()
-
- private fun indexOf(id: Int): Int = pointers.indexOfFirst { it.id == id }
-
- private fun isLocalIdAvailable(localId: Int): Boolean =
- pointers.none { it.localId == localId }
-
- private fun nextUnusedLocalId(): Int =
- (0 until MAX_POINTERS).firstOrNull(::isLocalIdAvailable) ?: -1
-
- operator fun get(index: Int): Pointer = pointers[index]
-
- fun getPointerIndex(id: Int): Int {
- indexOf(id).takeIf { it != -1 }?.let { return it }
-
- if (pointers.size>= MAX_POINTERS) return -1
-
- val localId = nextUnusedLocalId()
- if (localId == -1) {
- throw AssertionError("pointers.size() < maxFingers implies that a local id is available") - } - - val pointer = Pointer(id, localId) - pointers.add(pointer) - return pointers.lastIndex - } - - fun update(props: Array, coords: Array): Int {
- return pointers.size.also { count ->
- pointers.forEachIndexed { index, pointer ->
- props[index].id = pointer.localId
- coords[index].apply {
- x = pointer.point!!.x.toFloat()
- y = pointer.point!!.y.toFloat()
- pressure = pointer.pressure
- }
- }
- cleanUp()
- }
- }
-
- private fun cleanUp() {
- pointers.removeAll { it.isUp }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/webrtc/APP.kt b/app/src/main/java/com/github/webrtc/APP.kt
new file mode 100644
index 0000000..36dfea0
--- /dev/null
+++ b/app/src/main/java/com/github/webrtc/APP.kt
@@ -0,0 +1,11 @@
+package com.github.webrtc
+
+import android.app.Application
+import org.webrtc.PeerConnectionFactory
+
+class APP : Application() {
+ override fun onCreate() {
+ super.onCreate()
+ PeerConnectionFactory.initialize(PeerConnectionFactory.InitializationOptions.builder(applicationContext).createInitializationOptions())
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/webrtc/MainActivity.kt b/app/src/main/java/com/github/webrtc/MainActivity.kt
new file mode 100644
index 0000000..2834abc
--- /dev/null
+++ b/app/src/main/java/com/github/webrtc/MainActivity.kt
@@ -0,0 +1,32 @@
+package com.github.webrtc
+
+import android.content.Intent
+import android.media.projection.MediaProjectionManager
+import android.os.Bundle
+import android.widget.Button
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.getSystemService
+
+
+class MainActivity : AppCompatActivity() {
+ val registerMediaProjectionPermission = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ if (it.resultCode == RESULT_OK) {
+ ScreenCaptureService.start(this, it.data)
+ }
+ }
+
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+
+ findViewById