diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c5514c7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,35 @@
+#built application files
+*.apk
+*.ap_
+
+# files for the dex VM
+*.dex
+
+# Java class files
+*.class
+
+# generated files
+bin/
+gen/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# Windows thumbnail db
+Thumbs.db
+
+# OSX files
+.DS_Store
+
+# Android Studio
+*.iml
+.idea
+.gradle
+build/
+.navigation
+captures/
+output.json
+
+#NDK
+obj/
+.externalNativeBuild
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..f8aa10a
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1 @@
+Jorrit "Chainfire" Jongma
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..7a3b7c2
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,674 @@
+ 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
+.
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..368eeee
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,14 @@
+Copyright (C) 2019 Jorrit "Chainfire" Jongma
+
+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 .
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..cc00d2c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,165 @@
+This is the sauce for the [Holey Light](https://play.google.com/store/apps/details?id=eu.chainfire.holeylight) app.
+
+[LICENSE](./LICENSE) is GPLv3.
+
+---
+
+Holey Light is an S10 Notification LED emulation app. It animates the
+edges of the camera cut-out as replacement for the sadly missing LED.
+
+It will only light up for notifications that would have lit up the LED
+if the S10 had one.
+
+This is for S10/S10e/S10+ only! It will crash (wont-fix) on any other
+device!
+
+My personal test devices is the *plain* S10, if things are out of
+whack on the other models, let me know.
+
+### Features
+
+- Emulates notification LED
+
+It really is a one-trick pony :)
+
+### Questions you could and/or should ask
+
+#### Can I get this or that feature?
+
+Maybe. I am building this for me, and you get to use it if you want.
+If I want your feature for myself, I may add it. If not, you could
+always **pull request** :)
+
+The issue tracker is the TODO list, you might ask for it there.
+
+#### Aren't there other apps that do this?
+
+Maybe, yeah. But I got the idea for this the day I got the device, and
+started soon after. Maybe I'm not first to finish, but finish it I will.
+
+I am building this primarily for me, it will be running on my device
+every day all day. So I want to know and control exactly what it does,
+since anything untoward in the handling of this will drain your battery
+quite quickly (6-8 hours).
+
+Someone pointed me at another app but it was a monstrous ad-fest so
+I uninstalled before even trying. Nope!
+
+This is free, and you get teh sauce.
+
+#### I heard you built this entire thing just so you could call an app "Holey Light"?
+
+That is absurd. Who would even do such a thing. Certainly not me. /s
+
+#### Something something ETA?
+
+No.
+
+#### Why doesn't the animation show in the lockscreen?
+
+It is currently not possible to arbitrarily draw over the lockscreen
+for a standard third-party app, unless we replace the lockscreen in
+its entirety.
+
+It *may* be possible to do this with root.
+
+#### When I'm using the Screen off feature, my lockscreen behaves weirdly
+
+Please describe exactly what happens and what you expected to happen.
+As we're *faking* the screen being off, various events of the devices
+turning on and off have to be faked as well. It might not always work
+so well.
+
+#### So what about LED emulation when the device is on battery power?
+
+This isn't done yet. I've been trying different things to see their
+effects, but I'm not quite satisfied with how things have worked out.
+
+Currently, the animation is only shown when the screen is on, or when
+the screen is off and the device is charging.
+
+See the battery use section below.
+
+### Battery use
+
+To display an animation like the one we're doing, we obviously need
+the screen to be in some sort of ON state, or we'd just be staring
+at a blank screen.
+
+During normal (screen on) phone operation, this is not a problem. What
+follows is precisely about the situation where the screen is "off" and
+Android is not displayed.
+
+Ideally, we would be operating in the *doze* or one of the *suspended*
+states. These are power saving states, that reduce power used by the
+CPU and/or display directly. Aside from that, code running in the
+background is restricted. Unfortunately, it is not possible for
+third-party apps such as these to attain these power-saving states,
+at least not without root.
+
+The *Always On Display* mode (which is just Samsung-speak for
+Android's *Ambient Display* feature) does run in this mode, but
+decompiling AOD and several of it's plugins, I did not find a way
+to hook into it (or overlay it) properly.
+
+For the devs among us, if you were unaware, *Ambient Display* is quite
+literally just a Dream (with a few extra hidden methods to trigger *doze*
+mode). The path to the Dream that runs as *Ambient Display* is hardcoded
+in an xml in framework-res. With root and an rro overlay (or something
+similar) we might be able to replace it. I am not currently planning on
+rooting my S10, but it seems doable in theory. Typical Google to lock
+something like this down while it could so easily be used for nice
+things.
+
+Since we are not able to run in or trigger any of the power saving
+states we want to, we are left only with the standard wakelocks
+(and equivalent flags/attrs) to keep the CPU and/or screen awake.
+
+While those methods are tested and true, implementing as an *Ambient
+Dream* would be simpler logic (if it worked). And aside from not
+running the CPU and display in a lower power state, other code running
+in the background is not particularly restricted either. For all
+intents and purposes, the device is *fully* awake, even though it's
+showing a black screen with only a small animation it.
+
+Of course, since it's an AMOLED screen, the black pixels themselves
+use practically no power, but the chips are still in full power mode.
+We're still drawing relatively large amounts of power compared to
+full sleep or even the *doze*/*suspended* states. Enough so that it
+can drain your battery from full straight to zero overnight.
+
+None of this really matters while the device is charging, but it does
+when it's on battery power.
+
+I'm still thinking about and testing how to handle that case. The
+most obvious solution would be to run only for a while after a new
+notification comes in, or show the animation periodically. The best
+parameters for that take some testing, and that is slow going when
+you need to wait many hours each round to be able to compare the
+results.
+
+Of course, you can recompile it yourself, enable the feature, and
+see what happens.
+
+### Freedom!
+
+This app is free, without in-app purchases (there may be a donate button at some point), without ads, without tracking, but *with* GPLv3 [sauce](https://github.com/Chainfire/HoleyLight).
+
+### Download
+
+You can grab it from [Google Play](https://play.google.com/store/apps/details?id=eu.chainfire.holeylight).
+
+[Screenshot#1](https://lh3.googleusercontent.com/jzDVR2wFkO8rd9dgEP_Pg6PKo5EjlL-O8fjLR5Widw5b-M5sxBujj_gh8QEBcaxMfBk)
+
+### Feedback
+
+It puts it in the [XDA thread](https://forum.xda-developers.com/galaxy-s10/themes/app-holey-light-t3917675) or in the [GitHub issue tracker](https://github.com/Chainfire/HoleyLight/issues).
+
+The workings of the app are quite intricate, so describe what is happening in minute detail.
+
+### TODO
+
+You can find the TODO list in the [issue tracker](https://github.com/Chainfire/HoleyLight/issues?utf8=%E2%9C%93&q=is%3Aissue).
+
+### Enjoy!
+Or not.
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..090d1a6
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,45 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 28
+ defaultConfig {
+ applicationId "eu.chainfire.holeylight"
+ minSdkVersion 28
+ targetSdkVersion 28
+ versionCode 10
+ versionName "0.10"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ applicationVariants.all { variant ->
+ variant.outputs.all { output ->
+ if (variant.buildType.name == 'release') {
+ outputFileName = "HoleyLight" + "-v" + android.defaultConfig.versionName + "-" + (new Date()).format('yyyyMMddHHmmss') + ".apk"
+ //TODO the line below warns about deprecation, fix. Bloody Gradle and their breakage every other week.
+ variant.assemble.doLast {
+ copy {
+ from 'build/outputs/mapping/release'
+ into 'proguard'
+ include '**/mapping.txt'
+ }
+ }
+ }
+ }
+ }
+}
+
+dependencies {
+ implementation fileTree(include: ['*.jar'], dir: 'libs')
+ implementation 'androidx.appcompat:appcompat:1.1.0-alpha03'
+ implementation 'androidx.preference:preference:1.1.0-alpha03'
+ implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha3'
+ implementation 'com.airbnb.android:lottie:3.0.0'
+}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..f1b4245
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..209af23
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/eu/chainfire/holeylight/misc/Battery.java b/app/src/main/java/eu/chainfire/holeylight/misc/Battery.java
new file mode 100644
index 0000000..678a483
--- /dev/null
+++ b/app/src/main/java/eu/chainfire/holeylight/misc/Battery.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2019 Jorrit "Chainfire" Jongma
+ *
+ * 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 .
+ *
+ */
+
+package eu.chainfire.holeylight.misc;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.BatteryManager;
+
+public class Battery {
+ public static boolean isCharging(Context context) {
+ Intent batteryStatus = context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
+ if (batteryStatus != null) {
+ int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
+ return (status == BatteryManager.BATTERY_STATUS_CHARGING) || (status == BatteryManager.BATTERY_STATUS_FULL);
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/app/src/main/java/eu/chainfire/holeylight/misc/CameraCutout.java b/app/src/main/java/eu/chainfire/holeylight/misc/CameraCutout.java
new file mode 100644
index 0000000..b1d2958
--- /dev/null
+++ b/app/src/main/java/eu/chainfire/holeylight/misc/CameraCutout.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2019 Jorrit "Chainfire" Jongma
+ *
+ * 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 .
+ *
+ */
+
+package eu.chainfire.holeylight.misc;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.hardware.display.DisplayManager;
+import android.view.Display;
+
+import java.util.List;
+
+import androidx.core.view.WindowInsetsCompat;
+
+@SuppressWarnings({"WeakerAccess", "unused"})
+public class CameraCutout {
+ public static class Cutout {
+ private final Rect area;
+ private final Point resolution;
+
+ public Cutout(Rect area, Point resolution) {
+ this.area = area;
+ this.resolution = resolution;
+ }
+
+ public Cutout(Cutout src) {
+ this.area = new Rect(src.getArea());
+ this.resolution = new Point(src.getResolution());
+ }
+
+ public Rect getArea() { return area; }
+ public Point getResolution() { return resolution; }
+
+ public Cutout scaleTo(Point resolution) {
+ if (this.resolution.equals(resolution)) return this;
+ float sX = (float)resolution.x / (float)this.resolution.x;
+ float sY = (float)resolution.y / (float)this.resolution.y;
+ return new Cutout(new Rect(
+ (int)((float)area.left * sX),
+ (int)((float)area.top * sY),
+ (int)((float)area.right * sX),
+ (int)((float)area.bottom * sY)
+ ), resolution);
+ }
+
+ public boolean equalsScaled(Cutout cmp) {
+ // scaling and rounding introduces errors, allow 2 pixel discrepancy
+ if (cmp == null) return false;
+ Cutout a = this;
+ Cutout b = cmp;
+ if (!a.getResolution().equals(b.getResolution())) {
+ if (a.getResolution().x * a.getResolution().y > b.getResolution().x * b.getResolution().y) {
+ b = b.scaleTo(a.getResolution());
+ } else {
+ a = a.scaleTo(b.getResolution());
+ }
+ }
+ Rect rA = a.getArea();
+ Rect rB = b.getArea();
+ return
+ Math.abs(rA.left - rB.left) <= 2 &&
+ Math.abs(rA.top - rB.top) <= 2 &&
+ Math.abs(rA.right - rB.right) <= 2 &&
+ Math.abs(rA.bottom - rB.bottom) <= 2;
+ }
+
+ public boolean isCircular() {
+ return Math.abs(area.width() - area.height()) <= 2;
+ }
+ }
+
+ // these were determined by running on each devices, algorithm seems perfect for S10/S10E,
+ // but has a few extra pixels on the right for S10PLUS.
+ public static final Cutout CUTOUT_S10E = new Cutout(new Rect(931, 25, 1021, 116), new Point(1080, 2280));
+ public static final Cutout CUTOUT_S10 = new Cutout(new Rect(1237, 33, 1352, 149), new Point(1440, 3040));
+ public static final Cutout CUTOUT_S10PLUS = new Cutout(new Rect(1114, 32, 1378, 142), new Point(1440, 3040));
+
+ private final Display display;
+ private final int nativeMarginTop;
+ private final int nativeMarginRight;
+
+ private Cutout cutout = null;
+
+ public CameraCutout(Context context) {
+ this.display = ((DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE)).getDisplay(0);
+
+ int id;
+ Resources res = context.getResources();
+
+ // below is Samsung S10(?) specific. Newer firmwares on other Samsung devices also seem to have these values present.
+
+ id = res.getIdentifier("status_bar_camera_top_margin", "dimen", "android");
+ nativeMarginTop = id > 0 ? res.getDimensionPixelSize(id) : 0;
+
+ id = res.getIdentifier("status_bar_camera_padding", "dimen", "android");
+ nativeMarginRight = id > 0 ? res.getDimensionPixelSize(id) : 0;
+ }
+
+ public Point getNativeResolution() {
+ Point ret = null;
+
+ Display.Mode[] modes = display.getSupportedModes();
+ for (Display.Mode mode : modes) {
+ if ((ret == null) || (mode.getPhysicalWidth() > ret.x) || (mode.getPhysicalHeight() > ret.y)) {
+ ret = new Point(mode.getPhysicalWidth(), mode.getPhysicalHeight());
+ }
+ }
+
+ return ret;
+ }
+
+ public Point getCurrentResolution() {
+ Point ret = new Point();
+ display.getRealSize(ret);
+ return ret;
+ }
+
+ public void updateFromBoundingRect(Rect rect) {
+ Point nativeRes = getNativeResolution();
+ Point currentRes = getCurrentResolution();
+
+ Rect r = new Rect(rect);
+
+ // convert margins from native to current resolution, and apply to rect; without this we'd get a big notch rather than just the camera area
+ r.right -= (int)((float)nativeMarginRight * ((float)currentRes.x / (float)nativeRes.x));
+ r.top += (int)((float)nativeMarginTop * ((float)currentRes.y / (float)nativeRes.y));
+
+ cutout = new Cutout(r, currentRes);
+ }
+
+ public void updateFromAreaRect(Rect rect) {
+ cutout = new Cutout(rect, getCurrentResolution());
+ }
+
+ public void updateFromInsets(WindowInsetsCompat insets) {
+ if ((insets == null) || (insets.getDisplayCutout() == null)) return;
+ List rects = insets.getDisplayCutout().getBoundingRects();
+ if (rects.size() != 1) return;
+ updateFromBoundingRect(rects.get(0));
+ }
+
+ public Cutout getCutout() {
+ return cutout == null ? null : new Cutout(cutout);
+ }
+
+ public void applyCutout(Cutout cutout) {
+ this.cutout = cutout.scaleTo(getCurrentResolution());
+ }
+
+ public boolean isValid() {
+ return cutout != null;
+ }
+}
diff --git a/app/src/main/java/eu/chainfire/holeylight/misc/NotificationAnimation.java b/app/src/main/java/eu/chainfire/holeylight/misc/NotificationAnimation.java
new file mode 100644
index 0000000..3b0d499
--- /dev/null
+++ b/app/src/main/java/eu/chainfire/holeylight/misc/NotificationAnimation.java
@@ -0,0 +1,333 @@
+/*
+ * Copyright (C) 2019 Jorrit "Chainfire" Jongma
+ *
+ * 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 .
+ *
+ */
+
+package eu.chainfire.holeylight.misc;
+
+import android.animation.Animator;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Point;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.Rect;
+import android.os.Build;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+
+import com.airbnb.lottie.LottieAnimationView;
+import com.airbnb.lottie.LottieComposition;
+import com.airbnb.lottie.LottieCompositionFactory;
+import com.airbnb.lottie.LottieProperty;
+import com.airbnb.lottie.model.KeyPath;
+import com.airbnb.lottie.value.LottieValueCallback;
+
+import androidx.core.view.WindowInsetsCompat;
+
+@SuppressWarnings("unused")
+public class NotificationAnimation implements Settings.OnSettingsChangedListener {
+ public interface OnNotificationAnimationListener {
+ void onDimensionsApplied(LottieAnimationView view);
+ void onAnimationComplete(LottieAnimationView view);
+ }
+
+ // From SystemUI: assets/face_unlocking_cutout_ic_bX.json
+ private static final String jsonBeyond0 = "{\"v\":\"5.1.20\",\"fr\":60,\"ip\":0,\"op\":61,\"w\":132,\"h\":132,\"nm\":\"beyond_punch_cut_ani_B0\",\"ddd\":0,\"assets\":[],\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":1,\"nm\":\"L\",\"td\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":45,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.1,\"y\":1},\"o\":{\"x\":0.33,\"y\":0},\"n\":\"0p1_1_0p33_0\",\"t\":0,\"s\":[-41,66.548,0],\"e\":[170,66.548,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":60}],\"ix\":2},\"a\":{\"a\":0,\"k\":[24,125,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"sw\":48,\"sh\":250,\"sc\":\"#ffffff\",\"ip\":0,\"op\":4000,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":4,\"nm\":\"cue_02\",\"tt\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[66,66,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"hasMask\":true,\"masksProperties\":[{\"inv\":false,\"mode\":\"s\",\"pt\":{\"a\":0,\"k\":{\"i\":[[27.614,0],[0,-27.616],[-27.614,0],[0,27.616]],\"o\":[[-27.614,0],[0,27.616],[27.614,0],[0,-27.616]],\"v\":[[0,-50],[-50,0.004],[0,50.008],[50,0.004]],\"c\":true},\"ix\":1},\"o\":{\"a\":0,\"k\":100,\"ix\":3},\"x\":{\"a\":0,\"k\":0,\"ix\":4},\"nm\":\"Mask 1\"}],\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"d\":3,\"ty\":\"el\",\"s\":{\"a\":0,\"k\":[100,100],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"nm\":\"Ellipse Path 1\",\"mn\":\"ADBE Vector Shape - Ellipse\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.5],\"y\":[1]},\"o\":{\"x\":[0.33],\"y\":[0]},\"n\":[\"0p5_1_0p33_0\"],\"t\":0,\"s\":[0],\"e\":[14]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.1],\"y\":[0]},\"n\":[\"0p833_0p833_0p1_0\"],\"t\":9,\"s\":[14],\"e\":[0]},{\"t\":53,\"s\":[0],\"h\":1}],\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Ellipse 1\",\"np\":2,\"cix\":2,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":4000,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":3,\"ty\":4,\"nm\":\"cue_01\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[66,66,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"hasMask\":true,\"masksProperties\":[{\"inv\":false,\"mode\":\"s\",\"pt\":{\"a\":0,\"k\":{\"i\":[[27.614,0],[0,-27.616],[-27.614,0],[0,27.616]],\"o\":[[-27.614,0],[0,27.616],[27.614,0],[0,-27.616]],\"v\":[[0,-50],[-50,0.004],[0,50.008],[50,0.004]],\"c\":true},\"ix\":1},\"o\":{\"a\":0,\"k\":100,\"ix\":3},\"x\":{\"a\":0,\"k\":0,\"ix\":4},\"nm\":\"Mask 1\"}],\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"d\":3,\"ty\":\"el\",\"s\":{\"a\":0,\"k\":[100,100],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"nm\":\"Ellipse Path 1\",\"mn\":\"ADBE Vector Shape - Ellipse\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.5],\"y\":[1]},\"o\":{\"x\":[0.33],\"y\":[0]},\"n\":[\"0p5_1_0p33_0\"],\"t\":0,\"s\":[0],\"e\":[14]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.1],\"y\":[0]},\"n\":[\"0p833_0p833_0p1_0\"],\"t\":9,\"s\":[14],\"e\":[0]},{\"t\":53,\"s\":[0],\"h\":1}],\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Ellipse 1\",\"np\":2,\"cix\":2,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":4000,\"st\":0,\"bm\":0}],\"markers\":[]}";
+ private static final String jsonBeyond1 = "{\"v\":\"5.1.20\",\"fr\":60,\"ip\":0,\"op\":61,\"w\":138,\"h\":138,\"nm\":\"beyond_punch_cut_ani_B1\",\"ddd\":0,\"assets\":[],\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":1,\"nm\":\"L\",\"parent\":2,\"td\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":45,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.1,\"y\":1},\"o\":{\"x\":0.33,\"y\":0},\"n\":\"0p1_1_0p33_0\",\"t\":0,\"s\":[-107.5,0.548,0],\"e\":[108,0.548,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":60}],\"ix\":2},\"a\":{\"a\":0,\"k\":[24,125,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"sw\":48,\"sh\":250,\"sc\":\"#ffffff\",\"ip\":0,\"op\":4000,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":4,\"nm\":\"cue_02\",\"tt\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[69,69,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"hasMask\":true,\"masksProperties\":[{\"inv\":false,\"mode\":\"s\",\"pt\":{\"a\":0,\"k\":{\"i\":[[28.719,0],[0,-28.721],[-28.719,0],[0,28.721]],\"o\":[[-28.719,0],[0,28.721],[28.719,0],[0,-28.721]],\"v\":[[0,-52.008],[-52,-0.004],[0,52],[52,-0.004]],\"c\":true},\"ix\":1},\"o\":{\"a\":0,\"k\":100,\"ix\":3},\"x\":{\"a\":0,\"k\":0,\"ix\":4},\"nm\":\"Mask 1\"}],\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"d\":3,\"ty\":\"el\",\"s\":{\"a\":0,\"k\":[104,104],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"nm\":\"Ellipse Path 1\",\"mn\":\"ADBE Vector Shape - Ellipse\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.5],\"y\":[1]},\"o\":{\"x\":[0.33],\"y\":[0]},\"n\":[\"0p5_1_0p33_0\"],\"t\":0,\"s\":[0],\"e\":[14]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.1],\"y\":[0]},\"n\":[\"0p833_0p833_0p1_0\"],\"t\":9,\"s\":[14],\"e\":[0]},{\"t\":53,\"s\":[0],\"h\":1}],\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Ellipse 1\",\"np\":2,\"cix\":2,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":4000,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":3,\"ty\":4,\"nm\":\"cue_01\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[69,69,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"hasMask\":true,\"masksProperties\":[{\"inv\":false,\"mode\":\"s\",\"pt\":{\"a\":0,\"k\":{\"i\":[[28.719,0],[0,-28.721],[-28.719,0],[0,28.721]],\"o\":[[-28.719,0],[0,28.721],[28.719,0],[0,-28.721]],\"v\":[[0,-52.008],[-52,-0.004],[0,52],[52,-0.004]],\"c\":true},\"ix\":1},\"o\":{\"a\":0,\"k\":100,\"ix\":3},\"x\":{\"a\":0,\"k\":0,\"ix\":4},\"nm\":\"Mask 1\"}],\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"d\":3,\"ty\":\"el\",\"s\":{\"a\":0,\"k\":[104,104],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"nm\":\"Ellipse Path 1\",\"mn\":\"ADBE Vector Shape - Ellipse\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.5],\"y\":[1]},\"o\":{\"x\":[0.33],\"y\":[0]},\"n\":[\"0p5_1_0p33_0\"],\"t\":0,\"s\":[0],\"e\":[14]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.1],\"y\":[0]},\"n\":[\"0p833_0p833_0p1_0\"],\"t\":9,\"s\":[14],\"e\":[0]},{\"t\":53,\"s\":[0],\"h\":1}],\"ix\":5},\"lc\":2,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Ellipse 1\",\"np\":2,\"cix\":2,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":4000,\"st\":0,\"bm\":0}],\"markers\":[]}";
+ private static final String jsonBeyond2 = "{\"v\":\"5.1.20\",\"fr\":60,\"ip\":0,\"op\":61,\"w\":258,\"h\":132,\"nm\":\"beyond_punch_cut_ani_B2\",\"ddd\":0,\"assets\":[],\"layers\":[{\"ddd\":0,\"ind\":1,\"ty\":1,\"nm\":\"L\",\"td\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":45,\"ix\":10},\"p\":{\"a\":1,\"k\":[{\"i\":{\"x\":0.1,\"y\":1},\"o\":{\"x\":0.33,\"y\":0},\"n\":\"0p1_1_0p33_0\",\"t\":0,\"s\":[-40,70.548,0],\"e\":[288,70.548,0],\"to\":[0,0,0],\"ti\":[0,0,0]},{\"t\":60}],\"ix\":2},\"a\":{\"a\":0,\"k\":[24,125,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"sw\":48,\"sh\":250,\"sc\":\"#ffffff\",\"ip\":0,\"op\":4000,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":2,\"ty\":4,\"nm\":\"cue_02\",\"tt\":1,\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":100,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[129,66,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"hasMask\":true,\"masksProperties\":[{\"inv\":false,\"mode\":\"s\",\"pt\":{\"a\":0,\"k\":{\"i\":[[0,-27.062],[27.062,0],[0,0],[0,27.062],[-27.062,0],[0,0]],\"o\":[[0,27.062],[0,0],[-27.062,0],[0,-27.062],[0,0],[27.062,0]],\"v\":[[110,0],[61,49],[-61,49],[-110,0],[-61,-49],[61,-49]],\"c\":true},\"ix\":1},\"o\":{\"a\":0,\"k\":100,\"ix\":3},\"x\":{\"a\":0,\"k\":0,\"ix\":4},\"nm\":\"Mask 1\"}],\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[220,98],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":54,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.5],\"y\":[1]},\"o\":{\"x\":[0.33],\"y\":[0]},\"n\":[\"0p5_1_0p33_0\"],\"t\":0,\"s\":[0],\"e\":[14]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.1],\"y\":[0]},\"n\":[\"0p833_0p833_0p1_0\"],\"t\":9,\"s\":[14],\"e\":[0]},{\"t\":53,\"s\":[0],\"h\":1}],\"ix\":5},\"lc\":1,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Rectangle 1\",\"np\":2,\"cix\":2,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":4000,\"st\":0,\"bm\":0},{\"ddd\":0,\"ind\":3,\"ty\":4,\"nm\":\"cue_01\",\"sr\":1,\"ks\":{\"o\":{\"a\":0,\"k\":30,\"ix\":11},\"r\":{\"a\":0,\"k\":0,\"ix\":10},\"p\":{\"a\":0,\"k\":[129,66,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100,100],\"ix\":6}},\"ao\":0,\"hasMask\":true,\"masksProperties\":[{\"inv\":false,\"mode\":\"s\",\"pt\":{\"a\":0,\"k\":{\"i\":[[0,-27.062],[27.062,0],[0,0],[0,27.062],[-27.062,0],[0,0]],\"o\":[[0,27.062],[0,0],[-27.062,0],[0,-27.062],[0,0],[27.062,0]],\"v\":[[110,0],[61,49],[-61,49],[-110,0],[-61,-49],[61,-49]],\"c\":true},\"ix\":1},\"o\":{\"a\":0,\"k\":100,\"ix\":3},\"x\":{\"a\":0,\"k\":0,\"ix\":4},\"nm\":\"Mask 1\"}],\"shapes\":[{\"ty\":\"gr\",\"it\":[{\"ty\":\"rc\",\"d\":1,\"s\":{\"a\":0,\"k\":[220,98],\"ix\":2},\"p\":{\"a\":0,\"k\":[0,0],\"ix\":3},\"r\":{\"a\":0,\"k\":54,\"ix\":4},\"nm\":\"Rectangle Path 1\",\"mn\":\"ADBE Vector Shape - Rect\",\"hd\":false},{\"ty\":\"st\",\"c\":{\"a\":0,\"k\":[1,1,1,1],\"ix\":3},\"o\":{\"a\":0,\"k\":100,\"ix\":4},\"w\":{\"a\":1,\"k\":[{\"i\":{\"x\":[0.5],\"y\":[1]},\"o\":{\"x\":[0.33],\"y\":[0]},\"n\":[\"0p5_1_0p33_0\"],\"t\":0,\"s\":[0],\"e\":[14]},{\"i\":{\"x\":[0.833],\"y\":[0.833]},\"o\":{\"x\":[0.1],\"y\":[0]},\"n\":[\"0p833_0p833_0p1_0\"],\"t\":9,\"s\":[14],\"e\":[0]},{\"t\":53,\"s\":[0],\"h\":1}],\"ix\":5},\"lc\":1,\"lj\":1,\"ml\":4,\"nm\":\"Stroke 1\",\"mn\":\"ADBE Vector Graphic - Stroke\",\"hd\":false},{\"ty\":\"tr\",\"p\":{\"a\":0,\"k\":[0,0],\"ix\":2},\"a\":{\"a\":0,\"k\":[0,0],\"ix\":1},\"s\":{\"a\":0,\"k\":[100,100],\"ix\":3},\"r\":{\"a\":0,\"k\":0,\"ix\":6},\"o\":{\"a\":0,\"k\":100,\"ix\":7},\"sk\":{\"a\":0,\"k\":0,\"ix\":4},\"sa\":{\"a\":0,\"k\":0,\"ix\":5},\"nm\":\"Transform\"}],\"nm\":\"Rectangle 1\",\"np\":2,\"cix\":2,\"ix\":1,\"mn\":\"ADBE Vector Group\",\"hd\":false}],\"ip\":0,\"op\":4000,\"st\":0,\"bm\":0}],\"markers\":[]}";
+
+ private final OnNotificationAnimationListener onNotificationAnimationListener;
+ private final Settings settings;
+ private final CameraCutout cameraCutout;
+ private final LottieAnimationView lottieAnimationView;
+
+ private final String json;
+ private final int dpAddScaleBase;
+ private final int dpAddScaleHorizontal;
+ private final int dpShiftVertical;
+ private final int dpShiftHorizontal;
+
+ private volatile LottieComposition lottieComposition;
+ private volatile boolean play = false;
+
+ private volatile int[] colors = new int[] { Color.WHITE, Color.GREEN, Color.RED };
+ private volatile int colorIndex = 0;
+
+ private volatile int[] colorsNext = null;
+ private volatile boolean playNext = false;
+
+ public NotificationAnimation(Context context, LottieAnimationView lottie, OnNotificationAnimationListener onNotificationAnimationListener) {
+ this.onNotificationAnimationListener = onNotificationAnimationListener;
+ settings = Settings.getInstance(context);
+ cameraCutout = new CameraCutout(context);
+ this.lottieAnimationView = lottie;
+
+ String device = Build.DEVICE;
+ switch (device) {
+ case "beyond0": // s10e
+ json = jsonBeyond0;
+ dpAddScaleBase = 4;
+ dpAddScaleHorizontal = 0;
+ dpShiftVertical = 0;
+ dpShiftHorizontal = 0;
+ break;
+ case "beyond1": // s10
+ json = jsonBeyond1;
+ dpAddScaleBase = 4;
+ dpAddScaleHorizontal = 0;
+ dpShiftVertical = 0;
+ dpShiftHorizontal = 0;
+ break;
+ case "beyond2": // s10+
+ json = jsonBeyond2;
+ dpAddScaleBase = 4;
+ dpAddScaleHorizontal = 1;
+ dpShiftVertical = 0;
+ dpShiftHorizontal = -1;
+ break;
+ default:
+ json = null;
+ dpAddScaleBase = 0;
+ dpAddScaleHorizontal = 0;
+ dpShiftVertical = 0;
+ dpShiftHorizontal = 0;
+ break;
+ }
+
+ if (!isValid()) return;
+
+ LottieCompositionFactory.fromJsonString(json, null).addListener(result -> {
+ lottieComposition = result;
+ applyDimensions();
+ });
+
+ lottie.addAnimatorListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ synchronized (NotificationAnimation.this) {
+ boolean newColors = false;
+ colorIndex++;
+ if (colorIndex >= colors.length) {
+ colorIndex = 0;
+ if (colorsNext != null) {
+ colors = colorsNext;
+ colorsNext = null;
+ newColors = true;
+ }
+ }
+ if (colors.length > 0) {
+ setColor(colors[colorIndex]);
+ if (play || newColors || (colorIndex > 0)) {
+ lottie.playAnimation();
+ } else {
+ if (onNotificationAnimationListener != null) {
+ onNotificationAnimationListener.onAnimationComplete(lottieAnimationView);
+ }
+ }
+ if (newColors) {
+ play = playNext;
+ }
+ } else {
+ if (onNotificationAnimationListener != null) {
+ onNotificationAnimationListener.onAnimationComplete(lottieAnimationView);
+ }
+ }
+ }
+ }
+
+ @Override public void onAnimationStart(Animator animation) {}
+ @Override public void onAnimationCancel(Animator animation) {}
+ @Override public void onAnimationRepeat(Animator animation) {}
+ });
+
+ settings.registerOnSettingsChangedListener(this);
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ settings.unregisterOnSettingsChangedListener(this);
+ super.finalize();
+ }
+
+ @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+ private synchronized boolean isValid() {
+ return (json != null) && (lottieAnimationView != null);
+ }
+
+ @Override
+ public void onSettingsChanged() {
+ applyDimensions();
+ }
+
+ private synchronized void setColor(int color) {
+ lottieAnimationView.addValueCallback(
+ new KeyPath("**"),
+ LottieProperty.COLOR_FILTER,
+ new LottieValueCallback<>(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP))
+ );
+ }
+
+ public synchronized void updateFromInsets(WindowInsetsCompat insets) {
+ cameraCutout.updateFromInsets(insets);
+ if (cameraCutout.isValid()) {
+ settings.setCutoutAreaRect(cameraCutout.getCutout().getArea());
+ }
+ applyDimensions();
+ }
+
+ @SuppressWarnings("StatementWithEmptyBody")
+ public synchronized void applyDimensions() {
+ // most of this could just be hardcoded, but whatever
+
+ if (!isValid()) return;
+
+ Rect cutoutRect = settings.getCutoutAreaRect();
+ if (cutoutRect.left > -1) {
+ cameraCutout.updateFromAreaRect(cutoutRect);
+ }
+
+ if (cameraCutout.isValid() && (lottieComposition != null)) {
+ int rotation = ((WindowManager)lottieAnimationView.getContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getRotation();
+
+ float realDpToPx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, lottieAnimationView.getContext().getResources().getDisplayMetrics());
+
+ Point resolution = cameraCutout.getCurrentResolution();
+
+ // something weird is going on with Lottie's px->dp if current resolution doesn't match native resolution
+ float scale = (float) resolution.x / (float) cameraCutout.getNativeResolution().x;
+ float LottieDpToPx = (1.0f / scale) * realDpToPx;
+
+ Rect r = cameraCutout.getCutout().getArea();
+ Rect b = lottieComposition.getBounds();
+
+ float height = (b.height() / LottieDpToPx);
+ float width = (b.width() / LottieDpToPx);
+ float left = r.exactCenterX() - (width / 2.0f) + (getDpShiftHorizontal() * realDpToPx);
+ float top = r.exactCenterY() - (height / 2.0f) + (getDpShiftVertical() * realDpToPx);
+
+ ViewGroup.LayoutParams params = lottieAnimationView.getLayoutParams();
+ params.width = (int)width;
+ params.height = (int)height;
+ if (params instanceof WindowManager.LayoutParams) {
+ if (rotation == 0) {
+ ((WindowManager.LayoutParams)params).x = (int)left;
+ ((WindowManager.LayoutParams)params).y = (int)top;
+ } else if (rotation == 1) {
+ // not possible to reach area to render
+ } else if (rotation == 2) {
+ ((WindowManager.LayoutParams)params).x = resolution.x - (int)(left + width);
+ ((WindowManager.LayoutParams)params).y = resolution.y - (int)(top + height);
+ } else if (rotation == 3) {
+ // not possible to reach area to render
+ }
+ lottieAnimationView.setRotation(rotation * -90);
+
+ // we're only going to allow straight up landscape
+ lottieAnimationView.setVisibility(rotation == 0 ? View.VISIBLE : View.INVISIBLE);
+ } else if (params instanceof ViewGroup.MarginLayoutParams) {
+ ((ViewGroup.MarginLayoutParams)params).setMargins((int)left, (int)top, 0, 0);
+ }
+ lottieAnimationView.setLayoutParams(params);
+
+ // not sure why we need this, but we do, and lottie doesn't support out-of-aspect scaling itself
+ // you'd assume as these animations come straight from Samsung's ROMs that they'd work perfectly
+ // out of the box, but oh no...
+ float addVertical = getDpAddScaleBase() * realDpToPx;
+ float addHorizontal = (addVertical * ((float)b.width() / (float)b.height())) + (getDpAddScaleHorizontal() * realDpToPx);
+ float scaledWidth = width + addHorizontal;
+ float scaledHeight = height + addVertical;
+ lottieAnimationView.setScaleX(scaledWidth / width);
+ lottieAnimationView.setScaleY(scaledHeight / height);
+
+ if (lottieAnimationView.getComposition() == null) {
+ lottieAnimationView.setComposition(lottieComposition);
+ }
+
+ if (!lottieAnimationView.isAnimating() && play) {
+ lottieAnimationView.playAnimation();
+ }
+
+ if (onNotificationAnimationListener != null) {
+ onNotificationAnimationListener.onDimensionsApplied(lottieAnimationView);
+ }
+ }
+ }
+
+ public synchronized void play(boolean once) {
+ play = !once;
+ if (!lottieAnimationView.isAnimating()) {
+ if (colors.length > 0) {
+ colorIndex = 0;
+ setColor(colors[colorIndex]);
+ lottieAnimationView.playAnimation();
+ }
+ }
+ }
+
+ public synchronized void play(int[] colors, boolean once) {
+ if ((colors == null) || (colors.length == 0)) {
+ play = false;
+ stop(true);
+ return;
+ }
+ if (lottieAnimationView.isAnimating()) {
+ this.colorsNext = colors;
+ this.playNext = !once;
+ } else {
+ this.colors = colors;
+ play = !once;
+ colorIndex = 0;
+ setColor(colors[colorIndex]);
+ lottieAnimationView.playAnimation();
+ }
+ }
+
+ public synchronized void stop(boolean immediately) {
+ play = false;
+ if (isPlaying()) {
+ if (immediately) {
+ lottieAnimationView.cancelAnimation();
+ }
+ } else {
+ if (onNotificationAnimationListener != null) {
+ onNotificationAnimationListener.onAnimationComplete(lottieAnimationView);
+ }
+ }
+ }
+
+ public synchronized boolean isPlaying() {
+ return play || lottieAnimationView.isAnimating();
+ }
+
+ public int getDpAddScaleBase() {
+ return settings.getDpAddScaleBase(dpAddScaleBase);
+ }
+
+ public int getDpAddScaleHorizontal() {
+ return settings.getDpAddScaleHorizontal(dpAddScaleHorizontal);
+ }
+
+ public int getDpShiftVertical() {
+ return settings.getDpShiftVertical(dpShiftVertical);
+ }
+
+ public int getDpShiftHorizontal() {
+ return settings.getDpShiftHorizontal(dpShiftHorizontal);
+ }
+}
diff --git a/app/src/main/java/eu/chainfire/holeylight/misc/Settings.java b/app/src/main/java/eu/chainfire/holeylight/misc/Settings.java
new file mode 100644
index 0000000..baffd39
--- /dev/null
+++ b/app/src/main/java/eu/chainfire/holeylight/misc/Settings.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2019 Jorrit "Chainfire" Jongma
+ *
+ * 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 .
+ *
+ */
+
+package eu.chainfire.holeylight.misc;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.graphics.Rect;
+import androidx.preference.PreferenceManager;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@SuppressWarnings({"WeakerAccess", "unused", "UnusedReturnValue"})
+public class Settings implements SharedPreferences.OnSharedPreferenceChangeListener {
+ public interface OnSettingsChangedListener {
+ void onSettingsChanged();
+ }
+
+ public static final String ENABLED_MASTER = "enabled_master";
+ private static final boolean ENABLED_MASTER_DEFAULT = true;
+
+ public static final String ENABLED_SCREEN_OFF_CHARGING = "enabled_screen_off_charging";
+ private static final boolean ENABLED_SCREEN_OFF_CHARGING_DEFAULT = false;
+
+ public static final String ENABLED_SCREEN_OFF_BATTERY = "enabled_screen_off_battery";
+ private static final boolean ENABLED_SCREEN_OFF_BATTERY_DEFAULT = false;
+
+ private static final String CUTOUT_AREA_LEFT = "cutout_area_left";
+ private static final String CUTOUT_AREA_TOP = "cutout_area_top";
+ private static final String CUTOUT_AREA_RIGHT = "cutout_area_right";
+ private static final String CUTOUT_AREA_BOTTOM = "cutout_area_bottom";
+ private static final String DP_ADD_SCALE_BASE = "dp_add_scale_base";
+ private static final String DP_ADD_SCALE_HORIZONTAL = "dp_add_scale_horizontal";
+ private static final String DP_SHIFT_VERTICAL = "dp_shift_vertical";
+ private static final String DP_SHIFT_HORIZONTAL = "dp_shift_horizontal";
+
+ private static Settings instance;
+ public static Settings getInstance(Context context) {
+ synchronized (Settings.class) {
+ if (instance == null) {
+ instance = new Settings(context);
+ }
+ return instance;
+ }
+ }
+
+ private final List listeners = new ArrayList<>();
+ private final SharedPreferences prefs;
+ private volatile SharedPreferences.Editor editor = null;
+ private volatile int ref = 0;
+
+ private Settings(Context context) {
+ prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ prefs.registerOnSharedPreferenceChangeListener(this);
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ prefs.unregisterOnSharedPreferenceChangeListener(this);
+ super.finalize();
+ }
+
+ @Override
+ public synchronized void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+ if (ref == 0) notifyListeners();
+ }
+
+ public synchronized void registerOnSettingsChangedListener(OnSettingsChangedListener onSettingsChangedListener) {
+ if (!listeners.contains(onSettingsChangedListener)) {
+ listeners.add(onSettingsChangedListener);
+ }
+ }
+
+ public synchronized void unregisterOnSettingsChangedListener(OnSettingsChangedListener onSettingsChangedListener) {
+ listeners.remove(onSettingsChangedListener);
+ }
+
+ private synchronized void notifyListeners() {
+ for (OnSettingsChangedListener listener : listeners) {
+ listener.onSettingsChanged();
+ }
+ }
+
+ @SuppressLint("CommitPrefEdits")
+ public synchronized Settings edit() {
+ if (editor == null) {
+ editor = prefs.edit();
+ ref = 0;
+ }
+ ref++;
+ return this;
+ }
+
+ public synchronized void save(boolean immediately) {
+ ref--;
+ if (ref < 0) ref = 0;
+ if (ref == 0) {
+ if (immediately) {
+ editor.commit();
+ } else {
+ editor.apply();
+ }
+ notifyListeners();
+ editor = null;
+ }
+ }
+
+ public Rect getCutoutAreaRect() {
+ return new Rect(
+ prefs.getInt(CUTOUT_AREA_LEFT, -1),
+ prefs.getInt(CUTOUT_AREA_TOP, -1),
+ prefs.getInt(CUTOUT_AREA_RIGHT, -1),
+ prefs.getInt(CUTOUT_AREA_BOTTOM, -1)
+ );
+ }
+
+ public Settings setCutoutAreaRect(Rect rect) {
+ edit();
+ try {
+ editor.putInt(CUTOUT_AREA_LEFT, rect.left);
+ editor.putInt(CUTOUT_AREA_TOP, rect.top);
+ editor.putInt(CUTOUT_AREA_RIGHT, rect.right);
+ editor.putInt(CUTOUT_AREA_BOTTOM, rect.bottom);
+ } finally {
+ save(true);
+ }
+ return this;
+ }
+
+ public int getDpAddScaleBase(int defaultValue) {
+ return prefs.getInt(DP_ADD_SCALE_BASE, defaultValue);
+ }
+
+ public void setDpAddScaleBase(int value) {
+ edit();
+ try {
+ editor.putInt(DP_ADD_SCALE_BASE, value);
+ } finally {
+ save(true);
+ }
+ }
+
+ public int getDpAddScaleHorizontal(int defaultValue) {
+ return prefs.getInt(DP_ADD_SCALE_HORIZONTAL, defaultValue);
+ }
+
+ public void setDpAddScaleHorizontal(int value) {
+ edit();
+ try {
+ editor.putInt(DP_ADD_SCALE_HORIZONTAL, value);
+ } finally {
+ save(true);
+ }
+ }
+
+ public int getDpShiftVertical(int defaultValue) {
+ return prefs.getInt(DP_SHIFT_VERTICAL, defaultValue);
+ }
+
+ public void setDpShiftVertical(int value) {
+ edit();
+ try {
+ editor.putInt(DP_SHIFT_VERTICAL, value);
+ } finally {
+ save(true);
+ }
+ }
+
+ public int getDpShiftHorizontal(int defaultValue) {
+ return prefs.getInt(DP_SHIFT_HORIZONTAL, defaultValue);
+ }
+
+ public void setDpShiftHorizontal(int value) {
+ edit();
+ try {
+ editor.putInt(DP_SHIFT_HORIZONTAL, value);
+ } finally {
+ save(true);
+ }
+ }
+
+ public boolean isEnabled() {
+ return prefs.getBoolean(ENABLED_MASTER, ENABLED_MASTER_DEFAULT);
+ }
+
+ public void setEnabled(boolean enabled) {
+ edit();
+ try {
+ editor.putBoolean(ENABLED_MASTER, enabled);
+ } finally {
+ save(true);
+ }
+ }
+
+ public boolean isEnabledWhileScreenOffCharging() {
+ return isEnabled() && prefs.getBoolean(ENABLED_SCREEN_OFF_CHARGING, ENABLED_SCREEN_OFF_CHARGING_DEFAULT);
+ }
+
+ public boolean isEnabledWhileScreenOffBattery() {
+ return isEnabled() && false;
+ }
+}
diff --git a/app/src/main/java/eu/chainfire/holeylight/service/NotificationListenerService.java b/app/src/main/java/eu/chainfire/holeylight/service/NotificationListenerService.java
new file mode 100644
index 0000000..b4ee28f
--- /dev/null
+++ b/app/src/main/java/eu/chainfire/holeylight/service/NotificationListenerService.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2019 Jorrit "Chainfire" Jongma
+ *
+ * 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 .
+ *
+ */
+
+package eu.chainfire.holeylight.service;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationChannelGroup;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.Color;
+import android.hardware.display.DisplayManager;
+import android.os.Process;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.service.notification.StatusBarNotification;
+import android.util.Log;
+import android.view.Display;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+import eu.chainfire.holeylight.BuildConfig;
+import eu.chainfire.holeylight.misc.Battery;
+import eu.chainfire.holeylight.misc.Settings;
+import eu.chainfire.holeylight.ui.LockscreenActivity;
+
+public class NotificationListenerService extends android.service.notification.NotificationListenerService implements Settings.OnSettingsChangedListener {
+ private Settings settings = null;
+ private Overlay overlay = null;
+ private int[] currentColors = new int[0];
+ private boolean enabled = true;
+
+ private boolean isDisplayOn(boolean ifDoze) {
+ int state = ((DisplayManager)getSystemService(DISPLAY_SERVICE)).getDisplay(0).getState();
+ if (state == Display.STATE_OFF) return false;
+ if (state == Display.STATE_DOZE) return ifDoze;
+ if (state == Display.STATE_DOZE_SUSPEND) return ifDoze;
+ if (state == Display.STATE_ON_SUSPEND) return ifDoze;
+ return true;
+ }
+
+ private BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction() == null) return;
+
+ log("Intent: %s", intent.getAction());
+
+ if (
+ intent.getAction().equals(Intent.ACTION_SCREEN_OFF) ||
+ (intent.getAction().equals(Intent.ACTION_POWER_CONNECTED) && !isDisplayOn(false))
+ ) {
+ if (settings.isEnabledWhileScreenOffCharging() && Battery.isCharging(NotificationListenerService.this)) {
+ // this is a really bad way to detect we didn't just press the power button while
+ // our LockscreenActivity was in the foreground
+ if (SystemClock.elapsedRealtime() - LockscreenActivity.lastVisible() > 2500) {
+ log("Showing lockscreen");
+ Intent i = new Intent(NotificationListenerService.this, LockscreenActivity.class);
+ i.setFlags(
+ Intent.FLAG_ACTIVITY_NEW_TASK |
+ Intent.FLAG_ACTIVITY_NO_HISTORY |
+ Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
+ );
+ i.putExtra(BuildConfig.APPLICATION_ID + ".colors", currentColors);
+ startActivity(i);
+ }
+ }
+ }
+ }
+ };
+ private IntentFilter intentFilter = null;
+
+ private void log(String fmt, Object... args) {
+ Log.d("HoleyLight/Listener", String.format(Locale.ENGLISH, fmt, args));
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ settings = Settings.getInstance(this);
+ enabled = settings.isEnabled();
+ overlay = Overlay.getInstance(this);
+
+ intentFilter = new IntentFilter();
+ intentFilter.addAction(Intent.ACTION_SCREEN_ON);
+ intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
+ intentFilter.addAction(Intent.ACTION_USER_PRESENT);
+ intentFilter.addAction(Intent.ACTION_POWER_CONNECTED);
+ intentFilter.setPriority(998);
+
+ settings.registerOnSettingsChangedListener(this);
+ }
+
+ @Override
+ public void onDestroy() {
+ settings.unregisterOnSettingsChangedListener(this);
+ super.onDestroy();
+ }
+
+ @Override
+ public void onSettingsChanged() {
+ boolean newEnabled = settings.isEnabled();
+ if (newEnabled != enabled) {
+ enabled = newEnabled;
+ apply();
+ }
+ }
+
+ @Override
+ public void onListenerConnected() {
+ super.onListenerConnected();
+ log("onListenerConnected");
+ registerReceiver(broadcastReceiver, intentFilter);
+ handleLEDNotifications();
+ }
+
+ @Override
+ public void onListenerDisconnected() {
+ log("onListenerDisconnected");
+ unregisterReceiver(broadcastReceiver);
+ overlay.hide(true);
+ super.onListenerDisconnected();
+ }
+
+ @Override
+ public void onInterruptionFilterChanged(int interruptionFilter) {
+ super.onInterruptionFilterChanged(interruptionFilter);
+ log("onInterruptionFilterChanged");
+ handleLEDNotifications();
+ }
+
+ @Override
+ public void onNotificationPosted(StatusBarNotification sbn) {
+ super.onNotificationPosted(sbn);
+ log("onNotificationPosted");
+ handleLEDNotifications();
+ }
+
+ @Override
+ public void onNotificationRemoved(StatusBarNotification sbn) {
+ super.onNotificationRemoved(sbn);
+ log("onNotificationRemoved");
+ handleLEDNotifications();
+ }
+
+ @Override
+ public void onNotificationChannelGroupModified(String pkg, UserHandle user, NotificationChannelGroup group, int modificationType) {
+ super.onNotificationChannelGroupModified(pkg, user, group, modificationType);
+ log("onNotificationChannelGroupModified");
+ handleLEDNotifications();
+ }
+
+ @Override
+ public void onNotificationChannelModified(String pkg, UserHandle user, NotificationChannel channel, int modificationType) {
+ super.onNotificationChannelModified(pkg, user, channel, modificationType);
+ log("onNotificationChannelModified");
+ handleLEDNotifications();
+ }
+
+ @Override
+ public void onNotificationRankingUpdate(RankingMap rankingMap) {
+ super.onNotificationRankingUpdate(rankingMap);
+ log("onNotificationRankingUpdate");
+ handleLEDNotifications();
+ }
+
+ private void handleLEDNotifications() {
+ List colors = new ArrayList<>();
+
+ StatusBarNotification[] sbns = getActiveNotifications();
+ for (StatusBarNotification sbn : sbns) {
+ try {
+ Notification not = sbn.getNotification();
+ if (not.getChannelId() != null) {
+ List chans = getNotificationChannels(sbn.getPackageName(), Process.myUserHandle());
+ for (NotificationChannel chan : chans) {
+ if (chan.getId().equals(not.getChannelId())) {
+ if (chan.shouldShowLights()) {
+ int c = chan.getLightColor();
+
+ // Twitter passes black for some reason, make white
+ if ((c & 0xFFFFFF) == 0) c = 0xFFFFFF;
+
+ // There's a lot of white notifications, try using the notification accent color instead
+ if (((c & 0xFFFFFF) == 0xFFFFFF) && ((not.color & 0xFFFFFF) > 0)) {
+
+ // Set dominant channel to max brightness
+ int r = Color.red(not.color);
+ int g = Color.green(not.color);
+ int b = Color.blue(not.color);
+
+ if ((r >= g) && (r >= b)) {
+ r = 255;
+ } else if ((g >= r) && (g >= b)) {
+ g = 255;
+ } else {
+ b = 255;
+ }
+
+ c = Color.rgb(r, g, b);
+ }
+
+ // Make sure we have alpha
+ c = c | 0xFF000000;
+
+ // Log and save
+ Integer color = c;
+ log("%s --> #%08X / #%08X --> #%08X", sbn.getPackageName(), chan.getLightColor(), not.color, c);
+ if (!colors.contains(color)) {
+ colors.add(color);
+ }
+ }
+ }
+ }
+ }
+ } catch (SecurityException e) {
+ // CompanionDeviceManager.getAssociations().size() == 0
+ }
+ }
+
+ int[] sorted = new int[colors.size()];
+ for (int i = 0; i < sorted.length; i++) {
+ sorted[i] = colors.get(i);
+ }
+ Arrays.sort(sorted);
+
+ boolean changes = (sorted.length != currentColors.length);
+ if (!changes) {
+ for (int i = 0; i < currentColors.length; i++) {
+ if (currentColors[i] != sorted[i]) {
+ changes = true;
+ break;
+ }
+ }
+ }
+ if (changes) {
+ currentColors = sorted;
+ apply();
+ }
+ }
+
+ private void apply() {
+ if ((currentColors.length > 0) && (enabled)) {
+ overlay.show(currentColors);
+ } else {
+ overlay.hide(!enabled);
+ }
+ Intent intent = new Intent(BuildConfig.APPLICATION_ID + ".colors");
+ intent.putExtra(BuildConfig.APPLICATION_ID + ".colors", currentColors);
+ LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
+ }
+}
diff --git a/app/src/main/java/eu/chainfire/holeylight/service/Overlay.java b/app/src/main/java/eu/chainfire/holeylight/service/Overlay.java
new file mode 100644
index 0000000..aef357e
--- /dev/null
+++ b/app/src/main/java/eu/chainfire/holeylight/service/Overlay.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2019 Jorrit "Chainfire" Jongma
+ *
+ * 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 .
+ *
+ */
+
+package eu.chainfire.holeylight.service;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.PixelFormat;
+import android.view.Gravity;
+import android.view.WindowManager;
+
+import com.airbnb.lottie.LottieAnimationView;
+
+import eu.chainfire.holeylight.misc.NotificationAnimation;
+
+@SuppressWarnings({"WeakerAccess", "unused"})
+public class Overlay {
+ private static Overlay instance;
+ public static Overlay getInstance(Context context) {
+ synchronized (Overlay.class) {
+ if (instance == null) {
+ instance = new Overlay(context);
+ }
+ return instance;
+ }
+ }
+
+ private BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction() == null) return;
+
+ if (intent.getAction().equals(Intent.ACTION_CONFIGURATION_CHANGED)) {
+ updateParams();
+ }
+ }
+ };
+ private IntentFilter intentFilter = new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED);
+
+ private final WindowManager windowManager;
+ private final LottieAnimationView lottieAnimationView;
+ private final NotificationAnimation animation;
+
+ private boolean added = false;
+
+ private Overlay(Context context) {
+ windowManager = (WindowManager)context.getSystemService(Activity.WINDOW_SERVICE);
+ lottieAnimationView = new LottieAnimationView(context);
+ initParams();
+ animation = new NotificationAnimation(context, lottieAnimationView, new NotificationAnimation.OnNotificationAnimationListener() {
+ @Override
+ public void onDimensionsApplied(LottieAnimationView view) {
+ if (added) {
+ windowManager.updateViewLayout(lottieAnimationView, lottieAnimationView.getLayoutParams());
+ }
+ }
+
+ @Override
+ public void onAnimationComplete(LottieAnimationView view) {
+ removeOverlay();
+ }
+ });
+ }
+
+ @SuppressLint("RtlHardcoded")
+ private void initParams() {
+ WindowManager.LayoutParams params = new WindowManager.LayoutParams(
+ 0,
+ 0,
+ 0,
+ 0,
+ WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
+ | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+ | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
+ | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION
+ | WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
+ | WindowManager.LayoutParams.FLAG_FULLSCREEN
+ , PixelFormat.TRANSLUCENT);
+ params.gravity = Gravity.LEFT | Gravity.TOP;
+ params.setTitle("HoleyLight");
+ lottieAnimationView.setLayoutParams(params);
+ }
+
+ private void updateParams() {
+ animation.applyDimensions();
+ }
+
+ private void createOverlay() {
+ if (added) return;
+ try {
+ updateParams();
+ added = true; // had a case of a weird exception that caused this to run in a loop if placed after addView
+ windowManager.addView(lottieAnimationView, lottieAnimationView.getLayoutParams());
+ lottieAnimationView.getContext().registerReceiver(broadcastReceiver, intentFilter);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void updateOverlay() {
+ if (!added) return;
+ try {
+ updateParams();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void removeOverlay() {
+ if (!added) return;
+ try {
+ windowManager.removeView(lottieAnimationView);
+ lottieAnimationView.getContext().unregisterReceiver(broadcastReceiver);
+ added = false;
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void show(int[] colors) {
+ createOverlay();
+ animation.play(colors, false);
+ }
+
+ public void hide(boolean immediately) {
+ animation.stop(immediately);
+ if (immediately) removeOverlay();
+ }
+}
diff --git a/app/src/main/java/eu/chainfire/holeylight/ui/DebugActivity.java b/app/src/main/java/eu/chainfire/holeylight/ui/DebugActivity.java
new file mode 100644
index 0000000..68bd34b
--- /dev/null
+++ b/app/src/main/java/eu/chainfire/holeylight/ui/DebugActivity.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2019 Jorrit "Chainfire" Jongma
+ *
+ * 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 .
+ *
+ */
+
+package eu.chainfire.holeylight.ui;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.app.NotificationCompat;
+import androidx.core.app.NotificationManagerCompat;
+import eu.chainfire.holeylight.R;
+import eu.chainfire.holeylight.misc.NotificationAnimation;
+import eu.chainfire.holeylight.misc.Settings;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.View;
+import android.widget.TextView;
+
+import java.util.Locale;
+
+public class DebugActivity extends AppCompatActivity implements Settings.OnSettingsChangedListener {
+ private Handler handler;
+ private Settings settings = null;
+ private NotificationAnimation animation = null;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_debug);
+
+ handler = new Handler();
+ settings = Settings.getInstance(this);
+ animation = new NotificationAnimation(this,null, null);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ updateLabels();
+ settings.registerOnSettingsChangedListener(this);
+ }
+
+ @Override
+ protected void onStop() {
+ settings.unregisterOnSettingsChangedListener(this);
+ super.onStop();
+ }
+
+ @Override
+ public void onSettingsChanged() {
+ updateLabels();
+ }
+
+ public void btnClick(View view) {
+ if (view == findViewById(R.id.btnAddScaleBaseMinus)) {
+ settings.setDpAddScaleBase(animation.getDpAddScaleBase() - 1);
+ } else if (view == findViewById(R.id.btnAddScaleBasePlus)) {
+ settings.setDpAddScaleBase(animation.getDpAddScaleBase() + 1);
+ } else if (view == findViewById(R.id.btnAddScaleHorizontalMinus)) {
+ settings.setDpAddScaleBase(animation.getDpAddScaleHorizontal() - 1);
+ } else if (view == findViewById(R.id.btnAddScaleHorizontalPlus)) {
+ settings.setDpAddScaleBase(animation.getDpAddScaleHorizontal() + 1);
+ } else if (view == findViewById(R.id.btnShiftVerticalMinus)) {
+ settings.setDpAddScaleBase(animation.getDpShiftVertical() - 1);
+ } else if (view == findViewById(R.id.btnShiftVerticalPlus)) {
+ settings.setDpAddScaleBase(animation.getDpShiftVertical() + 1);
+ } else if (view == findViewById(R.id.btnShiftHorizontalMinus)) {
+ settings.setDpAddScaleBase(animation.getDpShiftHorizontal() - 1);
+ } else if (view == findViewById(R.id.btnShiftHorizontalPlus)) {
+ settings.setDpAddScaleBase(animation.getDpShiftHorizontal() + 1);
+ }
+ }
+
+ private void updateLabels() {
+ ((TextView)findViewById(R.id.tvAddScaleBase)).setText(String.format(Locale.ENGLISH, "Scale base: %ddp", animation.getDpAddScaleBase()));
+ ((TextView)findViewById(R.id.tvAddScaleHorizontal)).setText(String.format(Locale.ENGLISH, "Scale horizontal: %ddp", animation.getDpAddScaleHorizontal()));
+ ((TextView)findViewById(R.id.tvShiftVertical)).setText(String.format(Locale.ENGLISH, "Shift vertical: %ddp", animation.getDpShiftVertical()));
+ ((TextView)findViewById(R.id.tvShiftHorizontal)).setText(String.format(Locale.ENGLISH, "Shift horizontal: %ddp", animation.getDpAddScaleHorizontal()));
+ }
+
+ public void btnNotificationTestClick(View view) {
+ final NotificationManager notMan = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
+
+ NotificationManagerCompat.from(this).deleteNotificationChannel("eu.chainfire.test.1");
+ final NotificationChannel chan = new NotificationChannel("eu.chainfire.test.1", getString(R.string.app_name), NotificationManager.IMPORTANCE_LOW);
+ chan.setDescription(getString(R.string.app_name));
+ chan.enableLights(true);
+ chan.setLightColor(Color.RED);
+ NotificationManagerCompat.from(this).createNotificationChannel(chan);
+
+ handler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ final Notification not = (new NotificationCompat.Builder(DebugActivity.this, chan.getId()))
+ .setContentTitle("title")
+ .setContentText("text")
+ .setBadgeIconType(NotificationCompat.BADGE_ICON_NONE)
+ .setOngoing(true)
+ .setOnlyAlertOnce(true)
+ .setNumber(0)
+ .setSmallIcon(R.drawable.ic_launcher_vector)
+ .build();
+
+ NotificationManagerCompat.from(DebugActivity.this).notify(3, not);
+ handler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ NotificationManagerCompat.from(DebugActivity.this).cancel(3);
+ }
+ }, 30000);
+ }
+ }, 10000);
+ }
+}
diff --git a/app/src/main/java/eu/chainfire/holeylight/ui/DetectCutoutActivity.java b/app/src/main/java/eu/chainfire/holeylight/ui/DetectCutoutActivity.java
new file mode 100644
index 0000000..85c8468
--- /dev/null
+++ b/app/src/main/java/eu/chainfire/holeylight/ui/DetectCutoutActivity.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2019 Jorrit "Chainfire" Jongma
+ *
+ * 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 .
+ *
+ */
+
+package eu.chainfire.holeylight.ui;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.view.ViewCompat;
+import eu.chainfire.holeylight.R;
+import eu.chainfire.holeylight.misc.NotificationAnimation;
+
+import android.os.Bundle;
+
+public class DetectCutoutActivity extends AppCompatActivity {
+ private NotificationAnimation animation;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_detect_cutout);
+ animation = new NotificationAnimation(this,null, null);
+
+ ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.container), (view, insets) -> {
+ animation.updateFromInsets(insets);
+ finish();
+ return insets;
+ });
+ }
+}
diff --git a/app/src/main/java/eu/chainfire/holeylight/ui/LockscreenActivity.java b/app/src/main/java/eu/chainfire/holeylight/ui/LockscreenActivity.java
new file mode 100644
index 0000000..201626f
--- /dev/null
+++ b/app/src/main/java/eu/chainfire/holeylight/ui/LockscreenActivity.java
@@ -0,0 +1,362 @@
+/*
+ * Copyright (C) 2019 Jorrit "Chainfire" Jongma
+ *
+ * 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 .
+ *
+ */
+
+package eu.chainfire.holeylight.ui;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+import eu.chainfire.holeylight.BuildConfig;
+import eu.chainfire.holeylight.R;
+import eu.chainfire.holeylight.misc.NotificationAnimation;
+import eu.chainfire.holeylight.misc.Settings;
+
+import android.app.KeyguardManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.PowerManager;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.WindowManager;
+
+import com.airbnb.lottie.LottieAnimationView;
+
+import java.util.Locale;
+
+public class LockscreenActivity extends AppCompatActivity implements GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener {
+ private static long visible = 0L;
+ public static long lastVisible() {
+ return visible;
+ }
+
+ private Settings settings = null;
+ private NotificationAnimation notificationAnimation;
+ private LottieAnimationView lottieAnimationView;
+ private KeyguardManager keyguardManager;
+ private PowerManager powerManager;
+ private Handler handler;
+ private GestureDetector gestureDetector;
+ private boolean haveNotifications = false;
+
+ private PowerManager.WakeLock partialWakeLock = null;
+ private PowerManager.WakeLock screenWakeLock = null;
+ private PowerManager.WakeLock proximityWakeLock = null;
+
+ private void log(String fmt, Object... args) {
+ Log.d("HoleyLight/Lockscreen", String.format(Locale.ENGLISH, fmt, args));
+ }
+
+ private boolean broadcastReceiverRegistered = false;
+ private BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
+ @SuppressWarnings("deprecation")
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction() == null) return;
+
+ log("Intent: %s", intent.getAction());
+
+ switch (intent.getAction()) {
+ case Intent.ACTION_SCREEN_ON:
+ if (!haveNotifications && !notificationAnimation.isPlaying()) {
+ // We timed out the screen earlier, and user turned it back on again
+ finish();
+ }
+ break;
+ case Intent.ACTION_SCREEN_OFF:
+ if ((proximityWakeLock != null) && proximityWakeLock.isHeld()) {
+ // The power button was pressed while this lockscreen was displaying, instead of
+ // turning off, turn on and show the real lockscreen.
+ finish();
+ PowerManager.WakeLock wakelock = powerManager.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP, BuildConfig.APPLICATION_ID + ":Lockscreen/Exit");
+ wakelock.acquire(1000);
+ }
+ break;
+ case Intent.ACTION_POWER_DISCONNECTED:
+ if (!settings.isEnabledWhileScreenOffBattery()) {
+ finish();
+ }
+ break;
+ }
+ }
+ };
+
+ private boolean localBroadcastReceiverRegistered = false;
+ private BroadcastReceiver localBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction() == null) return;
+
+ if (intent.getAction().equals(BuildConfig.APPLICATION_ID + ".colors")) {
+ int[] colors = intent.getIntArrayExtra(BuildConfig.APPLICATION_ID + ".colors");
+ haveNotifications = (colors != null) && (colors.length > 0);
+ if (haveNotifications) {
+ wakeup();
+ notificationAnimation.play(colors, false);
+ } else {
+ notificationAnimation.stop(false);
+ }
+ }
+ }
+ };
+
+ private void wakeup() {
+ if (!partialWakeLock.isHeld()) partialWakeLock.acquire(2500);
+ if (!screenWakeLock.isHeld()) screenWakeLock.acquire(2500);
+ //TODO battery only// if (!proximityWakeLock.isHeld()) proximityWakeLock.acquire(2500);
+ //TODO battery - check order of these.. proximity before others? also check other code
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ hideSystemUI();
+ getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(visibility -> {
+ if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
+ hideSystemUI();
+ }
+ });
+
+ setShowWhenLocked(true);
+ getWindow().addFlags(
+ // In case we are shown due to screen off, but not automatically locked yet,
+ // allow the device to time-out and lock, rather than staying unlocked forever
+ // while this screen is active
+ WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
+ );
+
+ setContentView(R.layout.activity_lockscreen);
+
+ settings = Settings.getInstance(this);
+ keyguardManager = (KeyguardManager)getSystemService(KEYGUARD_SERVICE);
+ powerManager = (PowerManager)getSystemService(POWER_SERVICE);
+
+ lottieAnimationView = findViewById(R.id.lottie);
+ notificationAnimation = new NotificationAnimation(this, lottieAnimationView, new NotificationAnimation.OnNotificationAnimationListener() {
+ @Override public void onAnimationComplete(LottieAnimationView view) { }
+ @Override public void onDimensionsApplied(LottieAnimationView view) { }
+ });
+ int[] colors = getIntent().getIntArrayExtra(BuildConfig.APPLICATION_ID + ".colors");
+ haveNotifications = (colors != null) && (colors.length > 0);
+ notificationAnimation.play(colors, false);
+
+ partialWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, BuildConfig.APPLICATION_ID + ":Lockscreen/CPU");
+ partialWakeLock.setReferenceCounted(false);
+ partialWakeLock.acquire(2500);
+
+ screenWakeLock = powerManager.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP, BuildConfig.APPLICATION_ID + ":Lockscreen/Screen");
+ screenWakeLock.setReferenceCounted(false);
+
+ proximityWakeLock = powerManager.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP, BuildConfig.APPLICATION_ID + ":Lockscreen/Proximity");
+ proximityWakeLock.setReferenceCounted(false);
+
+ handler = new Handler();
+ handler.postDelayed(() -> {
+ if (haveNotifications) {
+ // doesn't work right if we do it directly from onCreate
+ wakeup();
+ }
+ }, 250);
+
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(Intent.ACTION_SCREEN_ON);
+ intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
+ intentFilter.addAction(Intent.ACTION_POWER_DISCONNECTED);
+ intentFilter.setPriority(999);
+ registerReceiver(broadcastReceiver, intentFilter);
+ broadcastReceiverRegistered = true;
+
+ gestureDetector = new GestureDetector(this, this);
+
+ handler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ // while we are not in an actually locked state, both the overlay and this screen
+ // would be visible simultaneously, with overlapping animations
+ if (!keyguardManager.isKeyguardLocked()) {
+ if (lottieAnimationView.getVisibility() != View.INVISIBLE) {
+ lottieAnimationView.setVisibility(View.INVISIBLE);
+ }
+ handler.postDelayed(this, 500);
+ } else {
+ if (lottieAnimationView.getVisibility() == View.INVISIBLE) {
+ lottieAnimationView.setVisibility(View.VISIBLE);
+ }
+ }
+ }
+ }, 0);
+
+ LocalBroadcastManager.getInstance(this).registerReceiver(localBroadcastReceiver, new IntentFilter(BuildConfig.APPLICATION_ID + ".colors"));
+ localBroadcastReceiverRegistered = true;
+ }
+
+ private void hideSystemUI() {
+ View decorView = getWindow().getDecorView();
+ decorView.setSystemUiVisibility(
+ View.SYSTEM_UI_FLAG_IMMERSIVE
+ | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_FULLSCREEN);
+ }
+
+ private Runnable repeatedWhileVisible = new Runnable() {
+ @Override
+ public void run() {
+ visible = SystemClock.elapsedRealtime();
+ partialWakeLock.acquire(15000);
+ if (haveNotifications || notificationAnimation.isPlaying()) {
+ screenWakeLock.acquire(15000);
+ //TODO battery only// proximityWakeLock.acquire(15000);
+ } else {
+ if (screenWakeLock.isHeld()) screenWakeLock.release();
+ //TODO battery only// if (proximityWakeLock.isHeld()) proximityWakeLock.release();
+ }
+ //TODO check order of proximity
+ handler.removeCallbacks(repeatedWhileVisible);
+ handler.postDelayed(this, 10000);
+ }
+ };
+
+ @Override
+ protected void onStart() {
+ log("onStart");
+ super.onStart();
+ visible = SystemClock.elapsedRealtime();
+ handler.postDelayed(repeatedWhileVisible, 0);
+ }
+
+ @Override
+ protected void onStop() {
+ log("onStop");
+ visible = SystemClock.elapsedRealtime();
+ handler.removeCallbacks(repeatedWhileVisible);
+ super.onStop();
+ }
+
+ private void unregister() {
+ try {
+ if (broadcastReceiverRegistered) {
+ unregisterReceiver(broadcastReceiver);
+ broadcastReceiverRegistered = false;
+ }
+ if (localBroadcastReceiverRegistered) {
+ LocalBroadcastManager.getInstance(this).unregisterReceiver(localBroadcastReceiver);
+ localBroadcastReceiverRegistered = false;
+ }
+ } catch (Throwable t) {
+ // the amount of grief Android will give you for trying to *not* do something...
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ unregister();
+ super.onDestroy();
+ }
+
+ @Override
+ protected void onUserLeaveHint() {
+ super.onUserLeaveHint();
+ finish();
+ }
+
+ @Override
+ public void finish() {
+ handler.removeCallbacks(repeatedWhileVisible);
+ unregister();
+ super.finish();
+ overridePendingTransition(0, 0);
+ if (proximityWakeLock.isHeld()) proximityWakeLock.release();
+ if (screenWakeLock.isHeld()) screenWakeLock.release();
+ if (partialWakeLock.isHeld()) partialWakeLock.release();
+ }
+
+ @Override
+ public void onBackPressed() {
+ finish();
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event){
+ gestureDetector.onTouchEvent(event);
+ return super.onTouchEvent(event);
+ }
+
+ private void unlock() {
+ if (keyguardManager.isKeyguardLocked() && (!keyguardManager.isDeviceLocked() || (!keyguardManager.isKeyguardSecure() && !keyguardManager.isDeviceSecure()))) {
+ // Unlock device, if we don't do this (or it fails) we end up on device lockscreen
+ // The idea is to do this when the user has performed some action, and something like
+ // SmartLock has kept the device from a secure locking, might as well treat that
+ // user actions as an immediate unlock
+ keyguardManager.requestDismissKeyguard(this, null);
+ }
+ finish();
+ }
+
+ @Override
+ public boolean onDoubleTap(MotionEvent e) {
+ unlock();
+ return true;
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ float distance = (float)Math.sqrt(Math.pow(e2.getX() - e1.getX(), 2) + Math.pow(e2.getY() - e1.getY(), 2));
+ if (distance > (float)getWindow().getDecorView().getHeight() / 4.0f) {
+ unlock();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onDown(MotionEvent e) {
+ if (e.getY() > ((float)getWindow().getDecorView().getHeight() * (5.0f/6.0f)) && keyguardManager.isDeviceLocked() && keyguardManager.isDeviceSecure()) {
+ // User pressed in fingerprint area, and device is locked, and device is secure.
+ // Disappearing now (and thus showing the lockscreen) will allow the user
+ // to fingerprint unlock in a single action.
+ finish();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onLongPress(MotionEvent e) {
+ if (e.getY() > ((float)getWindow().getDecorView().getHeight() * (5.0f/6.0f))) {
+ // User pressed and held in fingerprint area, and fingerprint detection isn't thought
+ // to be relevant (see onDown()), unlock for consistency
+ unlock();
+ }
+ }
+
+ @Override public boolean onSingleTapConfirmed(MotionEvent e) { return false; }
+ @Override public boolean onDoubleTapEvent(MotionEvent e) { return false; }
+ @Override public void onShowPress(MotionEvent e) { }
+ @Override public boolean onSingleTapUp(MotionEvent e) { return false; }
+ @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { return false; }
+}
diff --git a/app/src/main/java/eu/chainfire/holeylight/ui/MainActivity.java b/app/src/main/java/eu/chainfire/holeylight/ui/MainActivity.java
new file mode 100644
index 0000000..49f8c94
--- /dev/null
+++ b/app/src/main/java/eu/chainfire/holeylight/ui/MainActivity.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2019 Jorrit "Chainfire" Jongma
+ *
+ * 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 .
+ *
+ */
+
+package eu.chainfire.holeylight.ui;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.SwitchCompat;
+import androidx.core.app.NotificationManagerCompat;
+import eu.chainfire.holeylight.BuildConfig;
+import eu.chainfire.holeylight.R;
+import eu.chainfire.holeylight.misc.Settings;
+
+import android.annotation.SuppressLint;
+import android.companion.AssociationRequest;
+import android.companion.CompanionDeviceManager;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+
+public class MainActivity extends AppCompatActivity implements Settings.OnSettingsChangedListener {
+ private Handler handler = null;
+ private Settings settings = null;
+ private SwitchCompat switchMaster = null;
+
+ private boolean checkPermissionsOnResume = false;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ handler = new Handler();
+ settings = Settings.getInstance(this);
+ settings.registerOnSettingsChangedListener(this);
+
+ startActivity(new Intent(this, DetectCutoutActivity.class));
+ }
+
+ @Override
+ protected void onDestroy() {
+ settings.unregisterOnSettingsChangedListener(this);
+ super.onDestroy();
+ }
+
+ @Override
+ public void onSettingsChanged() {
+ if (switchMaster != null) {
+ boolean enabled = settings.isEnabled();
+ if (enabled != switchMaster.isChecked()) {
+ switchMaster.setChecked(enabled);
+ }
+ }
+ }
+
+ private void checkPermissions() {
+ if (!android.provider.Settings.canDrawOverlays(this)) {
+ (new AlertDialog.Builder(this))
+ .setTitle(getString(R.string.permission_required) + " 1/4")
+ .setMessage(R.string.permission_overlay)
+ .setPositiveButton(android.R.string.ok, (dialog, which) -> {
+ Intent intent = new Intent(android.provider.Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
+ intent.setData(Uri.parse("package:" + BuildConfig.APPLICATION_ID));
+ startActivity(intent);
+ })
+ .show();
+ } else if (((CompanionDeviceManager)getSystemService(COMPANION_DEVICE_SERVICE)).getAssociations().size() == 0) {
+ (new AlertDialog.Builder(this))
+ .setTitle(getString(R.string.permission_required) + " 2/4")
+ .setMessage(R.string.permission_associate)
+ .setPositiveButton(android.R.string.ok, (dialog, which) -> {
+ CompanionDeviceManager companionDeviceManager = (CompanionDeviceManager)getSystemService(COMPANION_DEVICE_SERVICE);
+ companionDeviceManager.associate((new AssociationRequest.Builder()).build(), new CompanionDeviceManager.Callback() {
+ @Override
+ public void onDeviceFound(IntentSender chooserLauncher) {
+ try {
+ startIntentSenderForResult(chooserLauncher, 0, null, 0, 0, 0);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override public void onFailure(CharSequence error) { }
+ }, handler);
+ })
+ .show();
+ } else if (!NotificationManagerCompat.getEnabledListenerPackages(this).contains(getPackageName())) {
+ (new AlertDialog.Builder(this))
+ .setTitle(getString(R.string.permission_required) + " 3/4")
+ .setMessage(R.string.permission_notifications)
+ .setPositiveButton(android.R.string.ok, (dialog, which) -> {
+ Intent intent = new Intent(android.provider.Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS);
+ startActivity(intent);
+ })
+ .show();
+/* //TODO temporarily disabled, we might not actually need this ?
+ } else if (!((PowerManager)getSystemService(POWER_SERVICE)).isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID)) {
+ (new AlertDialog.Builder(this))
+ .setTitle(getString(R.string.permission_required) + " 4/4")
+ .setMessage(R.string.permission_battery)
+ .setPositiveButton(android.R.string.ok, (dialog, which) -> {
+ checkPermissionsOnResume = true;
+ Intent intent = new Intent(android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
+ intent.setData(Uri.parse("package:" + BuildConfig.APPLICATION_ID));
+ startActivity(intent);
+ })
+ .show();
+*/
+ } else {
+ //TODO temp startActivity(new Intent(this, DebugActivity.class));
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (checkPermissionsOnResume) {
+ checkPermissionsOnResume = false;
+ checkPermissions();
+ }
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ checkPermissions();
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ checkPermissions();
+ }
+
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ @SuppressLint("InflateParams") View layout = getLayoutInflater().inflate(R.layout.toolbar_switch, null);
+ switchMaster = layout.findViewById(R.id.toolbar_switch);
+ switchMaster.setChecked(settings.isEnabled());
+ switchMaster.setOnCheckedChangeListener((buttonView, isChecked) -> settings.setEnabled(isChecked));
+
+ MenuItem item = menu.add("");
+ item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+ item.setActionView(layout);
+ return true;
+ }
+}
diff --git a/app/src/main/java/eu/chainfire/holeylight/ui/SettingsFragment.java b/app/src/main/java/eu/chainfire/holeylight/ui/SettingsFragment.java
new file mode 100644
index 0000000..8712b5e
--- /dev/null
+++ b/app/src/main/java/eu/chainfire/holeylight/ui/SettingsFragment.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2019 Jorrit "Chainfire" Jongma
+ *
+ * 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 .
+ *
+ */
+
+package eu.chainfire.holeylight.ui;
+
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageInfo;
+import android.net.Uri;
+import android.os.Bundle;
+
+import androidx.preference.CheckBoxPreference;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
+import androidx.preference.PreferenceFragmentCompat;
+import androidx.preference.PreferenceManager;
+import androidx.preference.PreferenceScreen;
+
+import eu.chainfire.holeylight.R;
+import eu.chainfire.holeylight.misc.Settings;
+
+@SuppressWarnings({"WeakerAccess"})
+public class SettingsFragment extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener {
+ private SharedPreferences prefs = null;
+ private Settings settings = null;
+
+ private CheckBoxPreference prefScreenOn = null;
+ private CheckBoxPreference prefScreenOffCharging = null;
+ private CheckBoxPreference prefScreenOffBattery = null;
+ private Preference prefAdviceAOD = null;
+ private Preference prefAdviceLock = null;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ settings = Settings.getInstance(getActivity());
+
+ setPreferenceScreen(createPreferenceHierarchy());
+ }
+
+ @Override
+ public void onDestroy() {
+ if (prefs != null) {
+ prefs.unregisterOnSharedPreferenceChangeListener(this);
+ }
+ super.onDestroy();
+ }
+
+ @Override
+ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+ }
+
+ @SuppressWarnings("ConstantConditions")
+ public PreferenceCategory category(PreferenceScreen root, int caption) {
+ PreferenceCategory retval = new PreferenceCategory(getContext());
+ retval.setIconSpaceReserved(false);
+ if (caption > 0) retval.setTitle(caption);
+ root.addPreference(retval);
+ return retval;
+ }
+
+ @SuppressWarnings("ConstantConditions")
+ public Preference pref(PreferenceCategory category, int caption, int summary, String key, boolean enabled, Preference.OnPreferenceClickListener clickListener) {
+ Preference retval = new Preference(getContext());
+ if (caption > 0) retval.setTitle(caption);
+ if (summary > 0) retval.setSummary(summary);
+ retval.setEnabled(enabled);
+ if (key != null) retval.setKey(key);
+ retval.setIconSpaceReserved(false);
+ if (clickListener != null) retval.setOnPreferenceClickListener(clickListener);
+ if (category != null) category.addPreference(retval);
+ return retval;
+ }
+
+ @SuppressWarnings("ConstantConditions")
+ public CheckBoxPreference check(PreferenceCategory category, int caption, int summary, String key, Object defaultValue, boolean enabled) {
+ CheckBoxPreference retval = new CheckBoxPreference(getContext());
+ if (caption > 0) retval.setTitle(caption);
+ if (summary > 0) retval.setSummary(summary);
+ retval.setEnabled(enabled);
+ retval.setKey(key);
+ retval.setDefaultValue(defaultValue);
+ retval.setIconSpaceReserved(false);
+ if (category != null) category.addPreference(retval);
+ return retval;
+ }
+
+ @SuppressWarnings("ConstantConditions")
+ private PreferenceScreen createPreferenceHierarchy() {
+ PreferenceScreen root = getPreferenceManager().createPreferenceScreen(getActivity());
+
+ prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
+
+ String title = getActivity().getString(R.string.app_name);
+ try {
+ PackageInfo pkg = getActivity().getPackageManager().getPackageInfo(getActivity().getPackageName(), 0);
+ title = title + " v" + pkg.versionName;
+ } catch (Exception e) {
+ // no action
+ }
+
+ Preference copyright = pref(null, R.string.app_name, R.string.app_details, "copyright", true, preference -> {
+ Intent i = new Intent(Intent.ACTION_VIEW);
+ i.setData(Uri.parse(getString(R.string.app_website_url)));
+ startActivity(i);
+ return false;
+ });
+ copyright.setTitle(title);
+ root.addPreference(copyright);
+
+ PreferenceCategory catOperation = category(root, R.string.settings_category_operation);
+ prefScreenOn = check(catOperation, R.string.settings_screen_on_title, R.string.settings_screen_on_description, Settings.ENABLED_MASTER, settings.isEnabled(), true);
+ prefScreenOffCharging = check(catOperation, R.string.settings_screen_off_charging_title, R.string.settings_screen_off_charging_description, Settings.ENABLED_SCREEN_OFF_CHARGING, settings.isEnabledWhileScreenOffCharging(), true);
+ prefScreenOffBattery = check(catOperation, R.string.settings_screen_off_battery_title, R.string.settings_screen_off_battery_description, Settings.ENABLED_SCREEN_OFF_BATTERY, settings.isEnabledWhileScreenOffBattery(), false);
+
+ PreferenceCategory catAdvice = category(root, R.string.settings_category_advice);
+ prefAdviceAOD = pref(catAdvice, R.string.settings_advice_aod_title, 0, null, true, null);
+ prefAdviceLock = pref(catAdvice, R.string.settings_advice_lock_title, 0, null, true, null);
+
+ PreferenceCategory catChainfire = category(root, R.string.settings_category_chainfire);
+ pref(catChainfire, R.string.settings_playstore_title, R.string.settings_playstore_description, null, true, preference -> {
+ try {
+ Intent i = new Intent(Intent.ACTION_VIEW);
+ i.setData(Uri.parse("market://search?q=pub:Chainfire"));
+ startActivity(i);
+ } catch (Exception e) {
+ // Play Store not installed
+ }
+ return false;
+ });
+ pref(catChainfire, R.string.settings_follow_twitter_title, R.string.settings_follow_twitter_description, null,true, preference -> {
+ try {
+ Intent i = new Intent(Intent.ACTION_VIEW);
+ i.setData(Uri.parse("http://www.twitter.com/ChainfireXDA"));
+ startActivity(i);
+ } catch (Exception e) {
+ // no action
+ }
+ return false;
+ });
+
+ updatePrefs(null);
+ prefs.registerOnSharedPreferenceChangeListener(this);
+ return root;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ updatePrefs(null);
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+ try {
+ updatePrefs(key);
+ } catch (Throwable t) {
+ // no action
+ }
+ }
+
+ private void updatePrefs(String key) {
+ if (prefAdviceAOD != null) {
+ try {
+ if (settings.isEnabledWhileScreenOffCharging() || settings.isEnabledWhileScreenOffBattery()) {
+ int aod_mode = android.provider.Settings.System.getInt(getContext().getContentResolver(), "aod_mode", 0);
+ prefAdviceAOD.setSummary(getString(R.string.settings_advice_aod_description, aod_mode == 1 ? getString(R.string.enabled) : getString(R.string.disabled)));
+ } else {
+ prefAdviceAOD.setSummary(R.string.settings_advice_irrelevant);
+ }
+ } catch (Exception e) {
+ // no action
+ }
+ }
+ if (prefAdviceLock != null) {
+ try {
+ if (settings.isEnabledWhileScreenOffCharging() || settings.isEnabledWhileScreenOffBattery()) {
+ int timeout = android.provider.Settings.Secure.getInt(getContext().getContentResolver(), "lock_screen_lock_after_timeout", 0);
+ prefAdviceLock.setSummary(getString(R.string.settings_advice_lock_description, (int)(timeout / 1000)));
+ } else {
+ prefAdviceLock.setSummary(R.string.settings_advice_irrelevant);
+ }
+ } catch (Exception e) {
+ // no action
+ }
+ }
+ if (prefScreenOn != null) {
+ prefScreenOn.setChecked(settings.isEnabled()); // for sync with master switch
+ prefScreenOffCharging.setEnabled(settings.isEnabled());
+ prefScreenOffBattery.setEnabled(settings.isEnabledWhileScreenOffCharging() && false);
+ }
+ }
+}
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..6ce56bd
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..e05b510
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_vector.xml b/app/src/main/res/drawable/ic_launcher_vector.xml
new file mode 100644
index 0000000..da20c7d
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_vector.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_debug.xml b/app/src/main/res/layout/activity_debug.xml
new file mode 100644
index 0000000..bc5d0c3
--- /dev/null
+++ b/app/src/main/res/layout/activity_debug.xml
@@ -0,0 +1,146 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_detect_cutout.xml b/app/src/main/res/layout/activity_detect_cutout.xml
new file mode 100644
index 0000000..e8e4289
--- /dev/null
+++ b/app/src/main/res/layout/activity_detect_cutout.xml
@@ -0,0 +1,11 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_lockscreen.xml b/app/src/main/res/layout/activity_lockscreen.xml
new file mode 100644
index 0000000..a960042
--- /dev/null
+++ b/app/src/main/res/layout/activity_lockscreen.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..fd34c36
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/toolbar_switch.xml b/app/src/main/res/layout/toolbar_switch.xml
new file mode 100644
index 0000000..646ebbc
--- /dev/null
+++ b/app/src/main/res/layout/toolbar_switch.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..7353dbd
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..7353dbd
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..2b0a195
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..15d6692
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..0551b2e
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..91701d9
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..f2fdb54
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..c93350f
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..a4838c3
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..dd92911
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..0a7eec6
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..1eb9948
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..7914fac
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #c62828
+ #8e0000
+ #c62828
+
diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000..c5d5899
--- /dev/null
+++ b/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #FFFFFF
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..a81feb4
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,35 @@
+
+ Holey Light
+ Copyright © 2019 – Chainfire\nTwitter: @ChainfireXDA\nTap to visit XDA thread
+ https://forum.xda-developers.com/galaxy-s10/themes/app-holey-light-t3917675
+
+ Permissions needed
+ On the next screen, please allow Holey Light to appear on top. This is required to show the notification animation.
+ In the next dialog, please allow Holey Light to stop optimizing battery usage. This is required to keep the notification animation running.
+ In the next dialog, please link Holey Light with a device. This is required to work around an issue in Android regarding notifications. It does not seem to matter which device you choose.
+ In the next screen, please allow Holey Light notification access. This is required to be able to detect notifications.
+
+ Enabled
+ Disabled
+
+ Operation
+ Screen on
+ Show light while the phone is in normal use
+ Screen off (Charging)
+ Experimental feature to show light while the screen is turned off
+ Screen off (Battery)
+ This feature is still under development and not available right now
+
+ Advisory
+ Always On Display
+ AOD is advised to be disabled for consistent operation. Current setting:\n[ %s ]
+ Lock Automatically
+ It is advised to lock the device immediately (0 seconds) after screen off. Current setting:\n[ %d seconds ]
+ No advice applies for the current configuration
+
+ Chainfire
+ All my apps
+ View all my apps available on Google Play
+ Follow me on Twitter
+ Stay up to date with my developments
+
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..e32dd3e
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..af70df7
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,27 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+
+ repositories {
+ google()
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.3.2'
+
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..d546dea
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,17 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+android.enableJetifier=true
+android.useAndroidX=true
+org.gradle.jvmargs=-Xmx1536m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+
+
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..f6b961f
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..a2d4a09
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Sat Mar 30 12:11:03 CET 2019
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..cccdd3d
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..f955316
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/graphics/play/empty_feature.png b/graphics/play/empty_feature.png
new file mode 100644
index 0000000..09eae7c
Binary files /dev/null and b/graphics/play/empty_feature.png differ
diff --git a/graphics/play/ic_launcher-web.png b/graphics/play/ic_launcher-web.png
new file mode 100644
index 0000000..cd66d9b
Binary files /dev/null and b/graphics/play/ic_launcher-web.png differ
diff --git a/graphics/play/screenshot1.png b/graphics/play/screenshot1.png
new file mode 100644
index 0000000..628d691
Binary files /dev/null and b/graphics/play/screenshot1.png differ
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..e7b4def
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+include ':app'