diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a434871
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,78 @@
+# Built application files
+*.apk
+*.aar
+*.ap_
+*.aab
+
+# Files for the ART/Dalvik VM
+*.dex
+
+# Java class files
+*.class
+
+# Generated files
+bin/
+gen/
+out/
+# Uncomment the following line in case you need and you don't have the release build type files in your app
+# release/
+
+# Gradle files
+.gradle/
+build/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# Proguard folder generated by Eclipse
+proguard/
+
+# Log Files
+*.log
+
+# Android Studio Navigation editor temp files
+.navigation/
+
+# Android Studio captures folder
+captures/
+
+# IntelliJ
+*.iml
+.idea/
+
+# Keystore files
+# Uncomment the following lines if you do not want to check your keystore files in.
+*.jks
+*.keystore
+
+# External native build folder generated in Android Studio 2.2 and later
+.externalNativeBuild
+.cxx/
+
+# Google Services (e.g. APIs or Firebase)
+# google-services.json
+
+# Freeline
+freeline.py
+freeline/
+freeline_project_description.json
+
+# fastlane
+fastlane/report.xml
+fastlane/Preview.html
+fastlane/screenshots
+fastlane/test_output
+fastlane/readme.md
+
+# Version control
+vcs.xml
+
+# lint
+lint/intermediates/
+lint/generated/
+lint/outputs/
+lint/tmp/
+# lint/reports/
+
+app/release/
+app/debug/
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..f288702
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..d163863
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+build/
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 0000000..ff34894
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,73 @@
+plugins {
+ id ("com.android.application")
+ id ("kotlin-android")
+}
+
+android {
+ namespace = "rasel.lunar.launcher"
+ compileSdk = 34
+
+ defaultConfig {
+ applicationId = "rasel.lunar.launcher"
+ minSdk = 26
+ targetSdk = 34
+ versionCode = 38
+ versionName = "2.8.2"
+ }
+
+ buildTypes {
+ getByName("debug") {
+ isMinifyEnabled = false
+ isShrinkResources = false
+ isDebuggable = true
+ applicationIdSuffix = ".debug"
+ versionNameSuffix = "-debug"
+ resValue ("string", "app_name", "Lunar Launcher Debug")
+ }
+
+ getByName("release") {
+ isMinifyEnabled = true
+ isShrinkResources = true
+ proguardFiles (getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+ resValue ("string", "app_name", "Lunar Launcher")
+ }
+ }
+
+ applicationVariants.all {
+ if (buildType.name == "release") {
+ outputs.all {
+ val output = this as? com.android.build.gradle.internal.api.BaseVariantOutputImpl
+ if (output?.outputFileName?.endsWith(".apk") == true) {
+ output.outputFileName = "${defaultConfig.applicationId}_v${defaultConfig.versionName}.apk"
+ }
+ }
+ }
+ }
+
+ buildFeatures {
+ viewBinding = true
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+}
+
+dependencies {
+ val kotlinVersion: String? by extra
+
+ implementation ("androidx.appcompat:appcompat:1.6.1")
+ implementation ("androidx.biometric:biometric-ktx:1.2.0-alpha05")
+ implementation ("androidx.browser:browser:1.8.0")
+ implementation ("androidx.core:core-ktx:1.12.0")
+ implementation ("androidx.core:core-splashscreen:1.0.1")
+ implementation ("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
+ implementation ("com.google.android.material:material:1.10.0")
+ implementation (kotlin("stdlib", version = kotlinVersion))
+ implementation ("com.github.cachapa:ExpandableLayout:2.9.2")
+}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..ff59496
--- /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.kts.
+#
+# 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
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..099a121
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/LauncherActivity.kt b/app/src/main/kotlin/rasel/lunar/launcher/LauncherActivity.kt
new file mode 100644
index 0000000..6b6b8ae
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/LauncherActivity.kt
@@ -0,0 +1,224 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher
+
+import android.Manifest
+import android.appwidget.AppWidgetManager
+import android.content.Intent
+import android.content.SharedPreferences
+import android.content.pm.PackageManager
+import android.graphics.Color
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.provider.Settings
+import android.view.WindowInsets
+import android.view.WindowManager
+import androidx.activity.OnBackPressedCallback
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
+import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updatePadding
+import androidx.recyclerview.widget.RecyclerView
+import androidx.viewpager2.widget.ViewPager2
+import com.google.android.material.color.DynamicColors
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import rasel.lunar.launcher.apps.AppDrawer
+import rasel.lunar.launcher.databinding.LauncherActivityBinding
+import rasel.lunar.launcher.feeds.Feeds
+import rasel.lunar.launcher.feeds.WidgetHost
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_APPLICATION_THEME
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_BACK_HOME
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_FIRST_LAUNCH
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_STATUS_BAR
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_WINDOW_BACKGROUND
+import rasel.lunar.launcher.helpers.Constants.Companion.PREFS_FIRST_LAUNCH
+import rasel.lunar.launcher.helpers.Constants.Companion.PREFS_SETTINGS
+import rasel.lunar.launcher.helpers.Constants.Companion.widgetHostId
+import rasel.lunar.launcher.helpers.UniUtils.Companion.getColorResId
+import rasel.lunar.launcher.helpers.ViewPagerAdapter
+import rasel.lunar.launcher.home.LauncherHome
+
+
+internal class LauncherActivity : AppCompatActivity() {
+
+ private lateinit var binding: LauncherActivityBinding
+ private lateinit var settingsPrefs: SharedPreferences
+ lateinit var viewPager: ViewPager2
+
+ companion object {
+ @JvmStatic var lActivity: LauncherActivity? = null
+ @JvmStatic var appWidgetManager: AppWidgetManager? = null
+ @JvmStatic var appWidgetHost: WidgetHost? = null
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ installSplashScreen()
+ DynamicColors.applyToActivityIfAvailable(this)
+
+ settingsPrefs = getSharedPreferences(PREFS_SETTINGS, 0)
+ AppCompatDelegate.setDefaultNightMode(settingsPrefs.getInt(KEY_APPLICATION_THEME, MODE_NIGHT_FOLLOW_SYSTEM))
+
+ super.onCreate(savedInstanceState)
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ binding = LauncherActivityBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ lActivity = this
+ appWidgetManager = AppWidgetManager.getInstance(applicationContext)
+ appWidgetHost = WidgetHost(applicationContext, widgetHostId)
+ appWidgetHost?.startListening()
+
+ /* if this is the first launch,
+ then remember the event and show the welcome dialog */
+ welcomeDialog()
+ setupView()
+
+ /* handle navigation back events */
+ handleBackPress()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ appWidgetHost?.stopListening()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ if (settingsPrefs.getBoolean(KEY_BACK_HOME, false)) viewPager.currentItem = 1
+ statusBarView()
+ setBgColor()
+ }
+
+ private fun welcomeDialog() {
+ getSharedPreferences(PREFS_FIRST_LAUNCH, 0).let {
+ if (it.getBoolean(KEY_FIRST_LAUNCH, true)) {
+ it.edit().putBoolean(KEY_FIRST_LAUNCH, false).apply()
+
+ MaterialAlertDialogBuilder(this)
+ .setTitle(R.string.welcome)
+ .setMessage(R.string.welcome_description)
+ .setPositiveButton(R.string.got_it) { dialog, _ ->
+ dialog.dismiss()
+ askPermissions()
+ }.show()
+ }
+ }
+ }
+
+ /* ask for the permissions */
+ private fun askPermissions() {
+ /* phone permission */
+ if (this.checkSelfPermission(Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
+ this.requestPermissions(arrayOf(Manifest.permission.CALL_PHONE), 1)
+ }
+ /* modify system settings */
+ if (!Settings.System.canWrite(this)) {
+ this.startActivity(
+ Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS)
+ .setData(Uri.parse("package:" + this.packageName))
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ )
+ }
+ }
+
+ /* set up viewpager2 */
+ private fun setupView() {
+ viewPager = binding.viewPager.apply {
+ adapter = ViewPagerAdapter(
+ supportFragmentManager, mutableListOf(Feeds(), LauncherHome(), AppDrawer()), lifecycle)
+ offscreenPageLimit = 1
+ setCurrentItem(1, false)
+ reduceDragSensitivity()
+ }
+ }
+
+ private fun setBgColor() {
+ binding.root.setBackgroundColor(Color.parseColor("#${
+ settingsPrefs.getString(KEY_WINDOW_BACKGROUND, getString(getColorResId(this, android.R.attr.colorBackground))
+ .replace("#", ""))}"))
+ }
+
+ private fun statusBarView() {
+ if (settingsPrefs.getBoolean(KEY_STATUS_BAR, false)) {
+ /* hide status bar */
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ window.insetsController?.hide(WindowInsets.Type.statusBars())
+ } else {
+ @Suppress("DEPRECATION")
+ window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)
+ }
+ topPadding(false)
+ } else {
+ /* show status bar */
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ window.insetsController?.show(WindowInsets.Type.statusBars())
+ } else {
+ @Suppress("DEPRECATION")
+ window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
+ }
+ topPadding(true)
+ }
+ }
+
+ /* alternative of deprecated onBackPressed method */
+ private fun handleBackPress() {
+ onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ /* while in to-do manager, go back to home screen */
+ if (supportFragmentManager.backStackEntryCount != 0) supportFragmentManager.popBackStack()
+
+ /* while in feeds or app drawer, go back to home screen */
+ if (viewPager.currentItem != 1) viewPager.currentItem = 1
+ }
+ })
+ }
+
+ private fun topPadding(topPadding: Boolean) {
+ ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
+ windowInsets.getInsets(WindowInsetsCompat.Type.systemGestures()).let {
+ val topInset = if (topPadding) {
+ if (it.top == 0) windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()).top
+ else it.top
+ } else 0
+
+ view.updatePadding(0, topInset, 0, it.bottom)
+ }
+ WindowInsetsCompat.CONSUMED
+ }
+ }
+
+ private fun ViewPager2.reduceDragSensitivity() {
+ ViewPager2::class.java.getDeclaredField("mRecyclerView").apply {
+ isAccessible = true
+ }.let { recyclerViewField ->
+ (recyclerViewField.get(this) as RecyclerView).let { recyclerView ->
+ RecyclerView::class.java.getDeclaredField("mTouchSlop").apply {
+ isAccessible = true
+ set(recyclerView, this.get(recyclerView) as Int * 8)
+ }
+ }
+ }
+ }
+
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/LunarLauncher.kt b/app/src/main/kotlin/rasel/lunar/launcher/LunarLauncher.kt
new file mode 100644
index 0000000..6b43d49
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/LunarLauncher.kt
@@ -0,0 +1,33 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher
+
+import android.app.Application
+import android.content.ComponentCallbacks2
+import android.database.sqlite.SQLiteDatabase
+
+
+internal class LunarLauncher : Application() {
+
+ override fun onTrimMemory(level: Int) {
+ super.onTrimMemory(level)
+ if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) SQLiteDatabase.releaseMemory()
+ }
+
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/apps/AlphabetScrollbar.kt b/app/src/main/kotlin/rasel/lunar/launcher/apps/AlphabetScrollbar.kt
new file mode 100644
index 0000000..043aab8
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/apps/AlphabetScrollbar.kt
@@ -0,0 +1,118 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.apps
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.util.AttributeSet
+import android.util.TypedValue
+import android.view.MotionEvent
+import android.view.View
+import androidx.core.content.ContextCompat
+import rasel.lunar.launcher.apps.AppDrawer.Companion.alphabetList
+import rasel.lunar.launcher.apps.AppDrawer.Companion.letterPreview
+import rasel.lunar.launcher.apps.AppDrawer.Companion.listenScroll
+import rasel.lunar.launcher.apps.AppDrawer.Companion.settingsPrefs
+import rasel.lunar.launcher.apps.AppsAdapter.Companion.appsSize
+import rasel.lunar.launcher.helpers.Constants
+
+
+internal class AlphabetScrollbar : View {
+
+ private var paint: Paint? = null
+ private var selectedIndex = -1
+ private val alphabet get() = alphabetList.distinct()
+
+ constructor(context: Context?) : super(context) {
+ init()
+ }
+
+ constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
+ init()
+ }
+
+ constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
+ init()
+ }
+
+ @SuppressLint("ResourceType")
+ private fun init() {
+ paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
+ color = defaultTextColor
+ textSize = 16f
+ }
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ super.onDraw(canvas)
+ val width = width
+ val height = height
+ val letterHeight: Int = height / alphabet.count()
+ alphabet.indices.forEach { i: Int ->
+ val x = width / 2f - paint!!.measureText(alphabet[i]) / 2f
+ val y = i * letterHeight + letterHeight / 2f
+ when (i) {
+ selectedIndex -> paint!!.textSize = 20f
+ else -> paint!!.textSize = 16f
+ }
+ canvas.drawText(alphabet[i], x, y, paint!!)
+ }
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ override fun onTouchEvent(event: MotionEvent): Boolean {
+ when (event.action) {
+ MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
+ val y = event.y
+ val index = (y / height * alphabet.count()).toInt()
+ if (index != selectedIndex) {
+ selectedIndex = index
+ invalidate()
+ }
+
+ if (!settingsPrefs!!.getBoolean(Constants.KEY_APPS_COUNT, true)) letterPreview?.visibility = VISIBLE
+ try { letterPreview?.text = alphabet[selectedIndex] }
+ catch (exception: Exception) { exception.printStackTrace() }
+ }
+
+ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
+ when {
+ selectedIndex < 0 -> listenScroll(alphabet[0])
+ selectedIndex > alphabet.count() - 1 -> listenScroll(alphabet[alphabet.count() - 1])
+ else -> listenScroll(alphabet[selectedIndex])
+ }
+
+ selectedIndex = -1
+ invalidate()
+ if (settingsPrefs!!.getBoolean(Constants.KEY_APPS_COUNT, true)) letterPreview?.text = appsSize.toString()
+ else letterPreview?.visibility = GONE
+ }
+ }
+ return true
+ }
+
+ private val defaultTextColor: Int get() {
+ val resolvedAttr = TypedValue()
+ context.theme.resolveAttribute(android.R.attr.textColorPrimary, resolvedAttr, true)
+ val colorRes = resolvedAttr.run { if (resourceId != 0) resourceId else data }
+ return ContextCompat.getColor(context, colorRes)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/apps/AppDrawer.kt b/app/src/main/kotlin/rasel/lunar/launcher/apps/AppDrawer.kt
new file mode 100644
index 0000000..b6cd8fc
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/apps/AppDrawer.kt
@@ -0,0 +1,335 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.apps
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.content.SharedPreferences
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.graphics.Rect
+import android.os.Build
+import android.os.Bundle
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import android.view.inputmethod.InputMethodManager
+import androidx.appcompat.app.AlertDialog
+import androidx.core.view.updateLayoutParams
+import androidx.core.widget.doOnTextChanged
+import androidx.fragment.app.Fragment
+import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.google.android.material.textview.MaterialTextView
+import rasel.lunar.launcher.BuildConfig
+import rasel.lunar.launcher.LauncherActivity.Companion.lActivity
+import rasel.lunar.launcher.R
+import rasel.lunar.launcher.databinding.AppDrawerBinding
+import rasel.lunar.launcher.helpers.Constants.Companion.DEFAULT_GRID_COLUMNS
+import rasel.lunar.launcher.helpers.Constants.Companion.DEFAULT_SCROLLBAR_HEIGHT
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_APPS_COUNT
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_APPS_LAYOUT
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_DRAW_ALIGN
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_GRID_COLUMNS
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_KEYBOARD_SEARCH
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_QUICK_LAUNCH
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_SCROLLBAR_HEIGHT
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_STATUS_BAR
+import rasel.lunar.launcher.helpers.Constants.Companion.PREFS_APP_NAMES
+import rasel.lunar.launcher.helpers.Constants.Companion.PREFS_SETTINGS
+import java.text.Normalizer
+import java.util.*
+import java.util.regex.Pattern
+
+
+internal class AppDrawer : Fragment() {
+
+ private lateinit var binding: AppDrawerBinding
+ private var layoutType: Int = 0
+ private var isSearchShown: Boolean = false
+ private var isKeyboardShowing: Boolean = false
+
+ companion object {
+ private var packageManager: PackageManager? = null
+ private var appsAdapter: AppsAdapter? = null
+ private var packageInfoList: MutableList = mutableListOf()
+ private var packageList = mutableListOf()
+ private val numberPattern = Pattern.compile("[0-9]")
+ private val alphabetPattern = Pattern.compile("[A-Z]")
+ @JvmStatic var settingsPrefs: SharedPreferences? = null
+ @JvmStatic var appNamesPrefs: SharedPreferences? = null
+ @JvmStatic var alphabetList = mutableListOf()
+ @JvmStatic var letterPreview: MaterialTextView? = null
+
+ private fun appName(resolver: ResolveInfo): String {
+ return appNamesPrefs?.getString(resolver.activityInfo.packageName, resolver.loadLabel(packageManager).toString())!!
+ }
+
+ fun listenScroll(letter: String) {
+ packageList.clear()
+ for (resolver in packageInfoList) {
+ when {
+ letter == "#" -> {
+ if (numberPattern.matcher(appName(resolver).first().uppercase()).matches()) {
+ packageList.add(Packages(resolver.activityInfo.packageName, appName(resolver)))
+ }
+ }
+ alphabetPattern.matcher(letter).matches() -> {
+ if (appName(resolver).first().uppercase() == letter) {
+ packageList.add(Packages(resolver.activityInfo.packageName, appName(resolver)))
+ }
+ }
+ letter == "⠶" -> {
+ if (!numberPattern.matcher(appName(resolver).first().uppercase()).matches() &&
+ !alphabetPattern.matcher(appName(resolver).first().uppercase()).matches()) {
+ packageList.add(Packages(resolver.activityInfo.packageName, appName(resolver)))
+ }
+ }
+ }
+ }
+ appsAdapter?.updateData(packageList.sortedBy { it.appName.lowercase() })
+ }
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ binding = AppDrawerBinding.inflate(inflater, container, false)
+
+ settingsPrefs = requireContext().getSharedPreferences(PREFS_SETTINGS, 0)
+ appNamesPrefs = requireContext().getSharedPreferences(PREFS_APP_NAMES, 0)
+ layoutType = settingsPrefs!!.getInt(KEY_APPS_LAYOUT, 0)
+ packageManager = lActivity?.packageManager
+ appsAdapter = AppsAdapter(layoutType, packageManager!!, childFragmentManager, binding.appsCount)
+ letterPreview = binding.appsCount
+
+ binding.appsCount.visibility = if (settingsPrefs!!.getBoolean(KEY_APPS_COUNT, true)) VISIBLE else GONE
+
+ setLayout()
+ fetchApps()
+ getAlphabetItems()
+ setKeyboardPadding()
+
+ return binding.root
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ binding.reset.setOnClickListener { onResume() }
+
+ binding.moveDown.setOnClickListener {
+ binding.appsList.smoothScrollToPosition(packageList.size - 1)
+ }
+
+ binding.moveUp.setOnClickListener {
+ binding.appsList.smoothScrollToPosition(0)
+ }
+
+ binding.search.setOnClickListener {
+ when (isSearchShown) {
+ true -> closeSearch()
+ false -> openSearch()
+ }
+ }
+
+ binding.searchInput.doOnTextChanged { inputText, _, _, _ ->
+ binding.searchInput.text?.let { binding.searchInput.setSelection(it.length) }
+ filterAppsList(inputText.toString())
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ fetchApps()
+ getAlphabetItems()
+
+ binding.appsCount.visibility = if (settingsPrefs!!.getBoolean(KEY_APPS_COUNT, true)) VISIBLE else GONE
+
+ if (settingsPrefs!!.getInt(KEY_APPS_LAYOUT, 0) in 0..1) {
+ appsAdapter?.updateGravity(settingsPrefs!!.getInt(KEY_DRAW_ALIGN, Gravity.CENTER))
+ }
+
+ /* pop up the keyboard */
+ if (settingsPrefs!!.getBoolean(KEY_KEYBOARD_SEARCH, false)) openSearch()
+ }
+
+ override fun onPause() {
+ super.onPause()
+ closeSearch()
+ }
+
+ private fun setLayout() {
+ when (layoutType) {
+ 0, 1 -> {
+ binding.appsList.layoutManager = LinearLayoutManager(requireContext())
+ appsAdapter!!.updateGravity(settingsPrefs!!.getInt(KEY_DRAW_ALIGN, Gravity.CENTER))
+ }
+ 2 -> binding.appsList.layoutManager = GridLayoutManager(requireContext(), settingsPrefs!!.getInt(KEY_GRID_COLUMNS, DEFAULT_GRID_COLUMNS))
+ }
+
+ /* initialize apps list adapter */
+ binding.appsList.adapter = appsAdapter
+ }
+
+ /* update app list with app and package name */
+ fun fetchApps() {
+ packageInfoList = (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ packageManager?.queryIntentActivities(
+ Intent(Intent.ACTION_MAIN, null).addCategory(Intent.CATEGORY_LAUNCHER),
+ PackageManager.ResolveInfoFlags.of(0)
+ )
+ } else {
+ (packageManager?.queryIntentActivities(
+ Intent(Intent.ACTION_MAIN, null).addCategory(Intent.CATEGORY_LAUNCHER), 0))
+ })?.apply {
+ removeIf { it.activityInfo.packageName.equals(BuildConfig.APPLICATION_ID) }
+ sortWith(ResolveInfo.DisplayNameComparator(packageManager))
+ }!!
+
+ /* add package and app names to the list */
+ packageList.clear()
+ for (resolver in packageInfoList) {
+ packageList.add(Packages(resolver.activityInfo.packageName, appName(resolver)))
+ }
+
+ when {
+ packageList.size < 1 -> return
+ else -> appsAdapter?.updateData(packageList.sortedBy { it.appName.lowercase() })
+ }
+ }
+
+ private fun getAlphabetItems() {
+// settingsPrefs!!.getInt(KEY_SCROLLBAR_HEIGHT, DEFAULT_SCROLLBAR_HEIGHT).let { height: Int ->
+// if (height == 0) { binding.alphabets.visibility = GONE }
+// else {
+// binding.alphabets.apply {
+// if (visibility == GONE) visibility = VISIBLE
+// updateLayoutParams { this.height = height }
+// }
+// alphabetList.clear()
+// for (mPackage in packageList) {
+// mPackage.appName.first().uppercase().let { firstLetter: String ->
+// when {
+// numberPattern.matcher(firstLetter).matches() -> alphabetList.add(0, "#")
+// alphabetPattern.matcher(firstLetter).matches() -> alphabetList.add(firstLetter)
+// !numberPattern.matcher(firstLetter).matches() &&
+// !alphabetPattern.matcher(firstLetter).matches() -> alphabetList.add(alphabetList.size,"⠶")
+// else -> {}
+// }
+// }
+// }
+// binding.alphabets.invalidate()
+// }
+// }
+ }
+
+ private fun filterAppsList(searchString: String) {
+ /* check each app name and add if it matches the search string */
+ packageList.clear()
+ for (resolver in packageInfoList) {
+ appName(resolver).let {
+ if (normalize(it).contains(searchString)) {
+ packageList.add(Packages(resolver.activityInfo.packageName, it))
+ }
+ }
+ }
+
+ if (packageList.size == 1 && settingsPrefs!!.getBoolean(KEY_QUICK_LAUNCH, true)) {
+ var dialog = AlertDialog.Builder(requireContext())
+ dialog.setTitle("앱 실행 확인")
+ dialog.setMessage("${searchString} 검색 결과 '${packageList[0].appName}' 준비됨")
+ dialog.setCancelable(false)
+ dialog.setOnCancelListener {
+ binding.searchInput.setText("")
+ it.dismiss()
+ }
+ dialog.setPositiveButton("실행") { s,d ->
+ startActivity(packageManager?.getLaunchIntentForPackage(packageList[0].packageName))
+ s.dismiss()
+ binding.searchInput.setText("")
+ }
+ dialog.show()
+ }
+ else appsAdapter?.updateData(packageList.sortedBy { it.appName.lowercase() })
+ }
+
+ private fun normalize(str: String): String {
+ val normalizedString =
+ Normalizer.normalize(str.replace("\\W".toRegex(), ""), Normalizer.Form.NFD)
+ val pattern = Pattern.compile("\\p{InCombiningDiacriticalMarks}+")
+ return pattern.matcher(normalizedString).replaceAll("").lowercase()
+ }
+
+ private fun openSearch() {
+ isSearchShown = true
+ binding.search.setImageResource(R.drawable.ic_close)
+ binding.searchInput.apply {
+ visibility = VISIBLE
+ requestFocus()
+ let {
+ (lActivity!!.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager)
+ .showSoftInput(it, InputMethodManager.SHOW_IMPLICIT)
+ }
+ }
+ }
+
+ /* clear search string, hide keyboard and search box */
+ private fun closeSearch() {
+ isSearchShown = false
+ binding.search.setImageResource(R.drawable.ic_search)
+ binding.searchInput.apply {
+ text?.clear()
+ visibility = GONE
+ let {
+ (lActivity!!.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager)
+ .hideSoftInputFromWindow(it.windowToken, 0)
+ }
+ }
+ }
+
+ private fun setKeyboardPadding() {
+ binding.root.viewTreeObserver.addOnGlobalLayoutListener {
+ val rect = Rect()
+ binding.root.getWindowVisibleDisplayFrame(rect)
+ val screenHeight = binding.root.height
+ val keyboardHeight = screenHeight - (rect.bottom - rect.top)
+
+ when {
+ keyboardHeight > screenHeight * 0.15 -> {
+ if (!isKeyboardShowing &&
+ !settingsPrefs!!.getBoolean(KEY_STATUS_BAR, false)) {
+ isKeyboardShowing = true
+ binding.root.setPadding(0, 0, 0, keyboardHeight)
+ }
+ }
+ else -> {
+ if (isKeyboardShowing) {
+ isKeyboardShowing = false
+ binding.root.setPadding(0, 0, 0, 0)
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/apps/AppMenu.kt b/app/src/main/kotlin/rasel/lunar/launcher/apps/AppMenu.kt
new file mode 100644
index 0000000..7d758e2
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/apps/AppMenu.kt
@@ -0,0 +1,431 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.apps
+
+import android.annotation.SuppressLint
+import android.app.ActivityOptions
+import android.content.ActivityNotFoundException
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.content.res.ColorStateList
+import android.graphics.Rect
+import android.icu.text.SimpleDateFormat
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.provider.Settings
+import android.view.KeyEvent
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.inputmethod.InputMethodManager
+import android.widget.AdapterView
+import android.widget.ArrayAdapter
+import android.widget.Toast
+import androidx.appcompat.widget.LinearLayoutCompat
+import androidx.core.content.FileProvider
+import androidx.core.content.pm.PackageInfoCompat
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import com.google.android.material.button.MaterialButton
+import com.google.android.material.button.MaterialButtonToggleGroup
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import rasel.lunar.launcher.LauncherActivity.Companion.lActivity
+import rasel.lunar.launcher.R
+import rasel.lunar.launcher.apps.AppDrawer.Companion.appNamesPrefs
+import rasel.lunar.launcher.databinding.ActivityBrowserDialogBinding
+import rasel.lunar.launcher.databinding.AppInfoDialogBinding
+import rasel.lunar.launcher.databinding.AppMenuBinding
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_APP_NO_
+import rasel.lunar.launcher.helpers.Constants.Companion.MAX_FAVORITE_APPS
+import rasel.lunar.launcher.helpers.Constants.Companion.PREFS_FAVORITE_APPS
+import rasel.lunar.launcher.helpers.UniUtils.Companion.copyToClipboard
+import rasel.lunar.launcher.helpers.UniUtils.Companion.screenHeight
+import rasel.lunar.launcher.helpers.UniUtils.Companion.screenWidth
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileOutputStream
+import java.io.IOException
+import java.util.*
+
+
+internal class AppMenu : BottomSheetDialogFragment() {
+
+ private lateinit var binding: AppMenuBinding
+ private lateinit var packageName: String
+ private lateinit var packageManager: PackageManager
+ private lateinit var appInfo: ApplicationInfo
+ private lateinit var defAppName: String
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ binding = AppMenuBinding.inflate(inflater, container, false)
+
+ /* get package name from fragment's tag */
+ packageName = tag.toString()
+ packageManager = requireContext().packageManager
+
+ /* get application info */
+ appInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ packageManager.getApplicationInfo(packageName,
+ PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA.toLong()))
+ } else {
+ packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA)
+ }
+
+ /* get default app name */
+ defAppName = packageManager.resolveActivity(Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER)
+ .setPackage(packageName), 0)?.loadLabel(packageManager).toString()
+
+ /* set application name and package name */
+ binding.appName.apply {
+ setText(appNamesPrefs?.getString(packageName, defAppName))
+ hint = defAppName
+
+ }
+ binding.appPackage.text = packageName
+ /* favorite apps */
+ favoriteApps()
+
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ (requireDialog() as BottomSheetDialog).dismissWithAnimation = true
+
+ /* copy package name */
+ binding.appPackage.setOnClickListener {
+ copyToClipboard(requireContext(), packageName)
+ }
+
+ appName()
+ binding.detailedInfo.setOnClickListener { detailedInfo() }
+ binding.activityBrowser.setOnClickListener { activityBrowser() }
+ binding.appStore.setOnClickListener { appStore() }
+ binding.appFreeform.setOnClickListener { freeform() }
+ binding.appInfo.setOnClickListener { appInfo() }
+ binding.appShare.setOnClickListener { share() }
+ binding.appUninstall.setOnClickListener { uninstall() }
+ }
+
+ /* manage initial preview and clicks for favorite apps */
+ @SuppressLint("PrivateResource")
+ private fun favoriteApps() {
+ val sharedPreferences = requireContext().getSharedPreferences(PREFS_FAVORITE_APPS, 0)
+ val enabledStroke =
+ ColorStateList.valueOf(requireContext().getColor(com.google.android.material.R.color.material_on_surface_stroke))
+ val disabledStroke =
+ ColorStateList.valueOf(requireContext().getColor(com.google.android.material.R.color.m3_chip_stroke_color))
+
+ for (position in 1..MAX_FAVORITE_APPS) {
+ val button = outlinedButton
+ val savedPackageName = sharedPreferences.getString(KEY_APP_NO_ + position, "")
+
+ /* set previews */
+ if (packageName == savedPackageName) button.isChecked = true
+ if (savedPackageName?.isNotEmpty() == true) button.strokeColor = enabledStroke
+
+ try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
+ packageManager.getPackageInfo(savedPackageName!!, PackageManager.PackageInfoFlags.of(0))
+ else
+ packageManager.getPackageInfo(savedPackageName!!, 0)
+ } catch (e: PackageManager.NameNotFoundException) {
+ requireContext().getSharedPreferences(PREFS_FAVORITE_APPS, 0)
+ .edit().remove(KEY_APP_NO_ + position).apply()
+ button.strokeColor = disabledStroke
+ e.printStackTrace()
+ }
+
+ /* listen on clicks */
+ binding.favGroup.addOnButtonCheckedListener { _: MaterialButtonToggleGroup?,
+ checkedId: Int, isChecked: Boolean ->
+ try {
+ if (checkedId == button.id) {
+ if (isChecked) {
+ requireContext().getSharedPreferences(PREFS_FAVORITE_APPS, 0)
+ .edit().putString(KEY_APP_NO_ + position, packageName).apply()
+ button.strokeColor = enabledStroke
+ } else {
+ requireContext().getSharedPreferences(PREFS_FAVORITE_APPS, 0)
+ .edit().remove(KEY_APP_NO_ + position).apply()
+ button.strokeColor = disabledStroke
+ }
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ }
+ }
+
+ private fun appName() {
+ binding.appName.setOnFocusChangeListener { _, hasFocus ->
+ if (hasFocus) binding.appName.minWidth = resources.getDimensionPixelOffset(R.dimen.twoSeventySix)
+ else {
+ (requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager)
+ .hideSoftInputFromWindow(binding.appName.windowToken, 0)
+
+ binding.appName.apply {
+ minWidth = resources.getDimensionPixelOffset(R.dimen.zero)
+
+ if (text!!.isBlank()) setText(defAppName)
+ else setText(text!!.trim())
+
+ if (text.toString() == defAppName) appNamesPrefs?.edit()!!.remove(packageName).apply()
+ else appNamesPrefs?.edit()!!.putString(packageName, text.toString()).apply()
+
+ (requireParentFragment() as AppDrawer).fetchApps()
+ }
+ }
+ }
+
+ binding.appName.setOnKeyListener { _, keyCode, event ->
+ if (event.action == KeyEvent.ACTION_DOWN) {
+ if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_BACK) {
+ binding.appName.clearFocus()
+ return@setOnKeyListener true
+ }
+ }
+ false
+ }
+ }
+
+ /* detailed info dialog */
+ @SuppressLint("SetTextI18n")
+ private fun detailedInfo() {
+ val dialogBinding = AppInfoDialogBinding.inflate(lActivity!!.layoutInflater)
+ MaterialAlertDialogBuilder(lActivity!!)
+ .setView(dialogBinding.root)
+ .setPositiveButton(android.R.string.cancel, null)
+ .show()
+
+ /* show app name */
+ dialogBinding.appName.text = packageManager.getApplicationLabel(appInfo)
+
+ /* get package info */
+ val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ packageManager.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0))
+ } else {
+ packageManager.getPackageInfo(packageName, 0)
+ }
+
+ /* show infos */
+ dialogBinding.mixed.text =
+ "${resources.getString(R.string.version)}: ${packageInfo.versionName} (${PackageInfoCompat.getLongVersionCode(packageInfo).toInt()})\n" +
+ "${resources.getString(R.string.sdk)}: ${appInfo.minSdkVersion} ~ ${appInfo.targetSdkVersion}\n" +
+ "${resources.getString(R.string.uid)}: ${appInfo.uid}\n" +
+ "${resources.getString(R.string.first_install)}: ${dateTimeFormat(packageInfo.firstInstallTime)}\n" +
+ "${resources.getString(R.string.last_update)}: ${dateTimeFormat(packageInfo.lastUpdateTime)}"
+
+ /* show permissions */
+ dialogBinding.permissions.text = permissionsList
+ }
+
+ /* activity browser dialog */
+ private fun activityBrowser() {
+ val dialogBinding = ActivityBrowserDialogBinding.inflate(lActivity!!.layoutInflater)
+ val dialogBuilder = MaterialAlertDialogBuilder(lActivity!!)
+ .setView(dialogBinding.root)
+ .setPositiveButton(android.R.string.cancel, null)
+ .show()
+
+ /* show app name */
+ dialogBinding.appName.text = packageManager.getApplicationLabel(appInfo)
+
+ /* get activity info */
+ val activityInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ packageManager.getPackageInfo(
+ packageName, PackageManager.PackageInfoFlags.of(PackageManager.GET_ACTIVITIES.toLong())
+ )
+ } else {
+ packageManager.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES)
+ }
+
+ /* show activity list */
+ val activityAdapter: ArrayAdapter =
+ ArrayAdapter(requireContext(), R.layout.list_item, R.id.itemText, ArrayList())
+ if (activityInfo.activities.isNotEmpty()) {
+ for (activity in activityInfo.activities) {
+ activityAdapter.add(
+ activity.toString().split(" ").toTypedArray()[1].replace("}", "")
+ )
+ }
+ dialogBinding.activityList.adapter = activityAdapter
+ }
+
+ /* listen item clicks */
+ dialogBinding.activityList.onItemClickListener =
+ AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, i: Int, _: Long ->
+ try {
+ /* open activity */
+ val intent = Intent()
+ intent.component = ComponentName(packageName, activityAdapter.getItem(i).toString())
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ requireContext().startActivity(intent)
+ } catch (exception: Exception) {
+ /* couldn't open activity */
+ exception.printStackTrace()
+ val exceptionShort = (exception.toString().split(": ").toTypedArray())[0]
+ Toast.makeText(requireContext(),
+ "${resources.getString(R.string.unable_to_launch)} -\n$exceptionShort", Toast.LENGTH_LONG).show()
+ }
+ dialogBuilder.dismiss()
+ }
+ }
+
+ /* open app's page in app store/market */
+ private fun appStore() {
+ try {
+ val storeIntent = Intent(Intent.ACTION_VIEW)
+ storeIntent.data = Uri.parse("market://details?id=$packageName")
+ storeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ requireContext().startActivity(storeIntent)
+ } catch (activityNotFoundException: ActivityNotFoundException) {
+ /* no app store found exception */
+ Toast.makeText(requireContext(), requireContext().getString(R.string.null_app_store_message),
+ Toast.LENGTH_SHORT).show()
+ activityNotFoundException.printStackTrace()
+ }
+ this.dismiss()
+ }
+
+ /* launch app as a freeform window */
+ private fun freeform() {
+ val freeformIntent = requireContext().packageManager.getLaunchIntentForPackage(packageName)
+ freeformIntent!!.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT or
+ Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
+ val rect = Rect(0, screenHeight / 2, screenWidth, screenHeight)
+ var activityOptions = activityOptions
+ activityOptions = activityOptions.setLaunchBounds(rect)
+ requireContext().startActivity(freeformIntent, activityOptions.toBundle())
+ this.dismiss()
+ }
+
+ /* open android's app info screen */
+ private fun appInfo() {
+ val infoIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ infoIntent.data = Uri.parse("package:$packageName")
+ infoIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ requireContext().startActivity(infoIntent)
+ this.dismiss()
+ }
+
+ private fun share() {
+ try {
+ // Create a temporary file to copy the APK
+ val apkLabel = packageManager.getApplicationLabel(appInfo).toString().lowercase().replace(" ", "_")
+ val tempApkFile = File(requireContext().externalCacheDir, "$apkLabel.apk")
+
+ // Copy the APK file
+ FileInputStream(File(appInfo.sourceDir)).use { `in` ->
+ FileOutputStream(tempApkFile).use { out ->
+ val buffer = ByteArray(1024)
+ var length: Int
+ while (`in`.read(buffer).also { length = it } > 0) {
+ out.write(buffer, 0, length)
+ }
+ }
+ }
+
+ // Generate a content URI using FileProvider
+ val contentUri =
+ FileProvider.getUriForFile(requireContext(), "${requireContext().packageName}.fileprovider", tempApkFile)
+
+ //requireContext().grantUriPermission(receivers.package.name, contentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
+
+ // Create a Share Intent
+ Intent(Intent.ACTION_SEND).apply {
+ type = "application/vnd.android.package-archive"
+ putExtra(Intent.EXTRA_STREAM, contentUri)
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ }.let {
+ // Start the chooser activity
+ startActivity(Intent.createChooser(it, getString(R.string.share_apk_message)))
+ }
+ }
+ catch (e: PackageManager.NameNotFoundException) { e.printStackTrace() }
+ catch (e: IOException) { e.printStackTrace() }
+ this.dismiss()
+ }
+
+ /* uninstall the app */
+ private fun uninstall() {
+ val uninstallIntent = Intent(Intent.ACTION_DELETE)
+ uninstallIntent.data = Uri.parse("package:$packageName")
+ uninstallIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
+ requireContext().startActivity(uninstallIntent)
+ this.dismiss()
+ }
+
+ /* create and add an outlined button to the toggle group */
+ private val outlinedButton: MaterialButton get() {
+ val style = com.google.android.material.R.attr.materialButtonOutlinedStyle
+ val button = MaterialButton(requireContext(), null, style)
+ button.layoutParams = LinearLayoutCompat.LayoutParams(
+ LinearLayoutCompat.LayoutParams.WRAP_CONTENT,
+ LinearLayoutCompat.LayoutParams.WRAP_CONTENT, 1F
+ )
+ binding.favGroup.addView(button)
+ return button
+ }
+
+ /* long value to local date-time format */
+ private fun dateTimeFormat(long: Long) : String = SimpleDateFormat.getDateTimeInstance().format(Date(long))
+
+ /* get and arrange all the permissions for an application */
+ private val permissionsList : String get() {
+ val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ packageManager.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong()))
+ } else {
+ packageManager.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS)
+ }
+
+ return if (packageInfo.requestedPermissions.isNotEmpty()) {
+ val stringBuilder = StringBuilder()
+ packageInfo.requestedPermissions.indices.forEach { i: Int ->
+ if (i != packageInfo.requestedPermissions.size - 1)
+ stringBuilder.append("${packageInfo.requestedPermissions[i]}\n\n")
+ /* don't add any new line after the last entry */
+ else
+ stringBuilder.append(packageInfo.requestedPermissions[i])
+ }
+ stringBuilder.toString()
+ } else {
+ ""
+ }
+ }
+
+ /* get activity options for launching app in freeform mode */
+ private val activityOptions: ActivityOptions get() {
+ val activityOptions = ActivityOptions.makeBasic()
+ try {
+ val method =
+ ActivityOptions::class.java.getMethod("setLaunchWindowingMode", Int::class.javaPrimitiveType)
+ method.invoke(activityOptions, 5)
+ } catch (exception: Exception) {
+ exception.printStackTrace()
+ }
+ return activityOptions
+ }
+
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/apps/AppsAdapter.kt b/app/src/main/kotlin/rasel/lunar/launcher/apps/AppsAdapter.kt
new file mode 100644
index 0000000..cf14fb7
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/apps/AppsAdapter.kt
@@ -0,0 +1,159 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.apps
+
+import android.annotation.SuppressLint
+import android.content.pm.PackageManager
+import android.util.TypedValue
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.view.updatePadding
+import androidx.fragment.app.FragmentManager
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.textview.MaterialTextView
+import rasel.lunar.launcher.LauncherActivity.Companion.lActivity
+import rasel.lunar.launcher.R
+import rasel.lunar.launcher.apps.IconPackManager.Companion.getDrawableIconForPackage
+import rasel.lunar.launcher.databinding.AppsChildBinding
+import rasel.lunar.launcher.helpers.UniUtils.Companion.dpToPx
+
+
+internal class AppsAdapter(
+ private val layoutType: Int,
+ private val packageManager: PackageManager,
+ private val fragmentManager: FragmentManager,
+ private val appsCount: MaterialTextView) : RecyclerView.Adapter() {
+
+ private var oldList = mutableListOf()
+ private var appGravity: Int = Gravity.CENTER
+
+ companion object {
+ @JvmStatic var appsSize: Int? = null
+ }
+
+ override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): AppsViewHolder =
+ AppsViewHolder(AppsChildBinding.inflate(LayoutInflater.from(viewGroup.context), viewGroup, false))
+
+ override fun onBindViewHolder(holder: AppsViewHolder, i: Int) {
+ val item = oldList[i]
+ val fourDp = dpToPx(lActivity!!, R.dimen.four)
+ val eightDp = dpToPx(lActivity!!, R.dimen.eight)
+ val twelveDp = dpToPx(lActivity!!, R.dimen.twelve)
+ val sixteenDp = dpToPx(lActivity!!, R.dimen.sixteen)
+
+ holder.view.apply {
+ childTextview.text = item.appName
+
+ when (layoutType) {
+ 0 -> {
+ appIcon.visibility = View.GONE
+ appIconTwo.visibility = View.GONE
+ childTextview.apply {
+ gravity = appGravity
+ setTextSize(TypedValue.COMPLEX_UNIT_PX, lActivity!!.resources.getDimension(R.dimen.twentyTwo))
+ }
+ root.setPadding(sixteenDp, fourDp, sixteenDp, fourDp)
+ }
+ 1 -> {
+ appIcon.visibility = View.GONE
+ appIconTwo.setImageDrawable(getDrawableIconForPackage(item.packageName, packageManager.getApplicationIcon(item.packageName)))
+ childTextview.apply {
+ gravity = appGravity or Gravity.CENTER_VERTICAL
+ setTextSize(TypedValue.COMPLEX_UNIT_PX, lActivity!!.resources.getDimension(R.dimen.twenty))
+ updatePadding(left = twelveDp)
+ }
+ root.setPadding(sixteenDp, eightDp, sixteenDp, eightDp)
+ }
+ 2 -> {
+ appIconTwo.visibility = View.GONE
+ appIcon.setImageDrawable(getDrawableIconForPackage(item.packageName, packageManager.getApplicationIcon(item.packageName)))
+ childTextview.apply {
+ gravity = Gravity.CENTER
+ setTextSize(TypedValue.COMPLEX_UNIT_PX, lActivity!!.resources.getDimension(R.dimen.twelve))
+ }
+ root.setPadding(eightDp, eightDp, eightDp, eightDp)
+ }
+ }
+ }
+
+ holder.view.root.apply {
+ /* on click - open app */
+ setOnClickListener {
+ context.startActivity(packageManager.getLaunchIntentForPackage(item.packageName))
+ }
+
+ /* on long click - open app menu */
+ setOnLongClickListener {
+ AppMenu().show(fragmentManager, item.packageName)
+ true
+ }
+ }
+ }
+
+ override fun getItemCount(): Int = oldList.size
+
+ inner class AppsViewHolder(var view: AppsChildBinding) : RecyclerView.ViewHolder(view.root)
+
+ /* update app list */
+ fun updateData(newList: List) {
+ val diffUtilResult = DiffUtil.calculateDiff(AppsDiffUtil(oldList, newList))
+
+ oldList.clear()
+ oldList.addAll(newList)
+ diffUtilResult.dispatchUpdatesTo(this)
+
+ newList.size.let {
+ appsCount.text = it.toString()
+ appsSize = it
+ }
+ }
+
+ /* update text gravity (alignment) */
+ @SuppressLint("RtlHardcoded", "NotifyDataSetChanged")
+ fun updateGravity(gravity: Int){
+ /* the first check is to avoid calling notifyDataSetChanged() everytime */
+ if (gravity != appGravity &&
+ (gravity == Gravity.LEFT || gravity == Gravity.CENTER || gravity == Gravity.RIGHT)) {
+ appGravity = gravity
+ notifyDataSetChanged()
+ }
+ }
+}
+
+internal data class Packages (
+ val packageName: String,
+ val appName: String
+)
+
+internal class AppsDiffUtil(
+ private val oldList: List, private val newList: List
+) : DiffUtil.Callback() {
+
+ override fun getOldListSize(): Int = oldList.size
+ override fun getNewListSize(): Int = newList.size
+
+ override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
+ oldList[oldItemPosition].packageName == newList[newItemPosition].packageName
+
+ override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
+ oldList[oldItemPosition] == newList[newItemPosition]
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/apps/IconPackManager.kt b/app/src/main/kotlin/rasel/lunar/launcher/apps/IconPackManager.kt
new file mode 100644
index 0000000..d967b4a
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/apps/IconPackManager.kt
@@ -0,0 +1,181 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.apps
+
+import android.annotation.SuppressLint
+import android.content.pm.PackageManager
+import android.content.res.Resources
+import android.graphics.Bitmap
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import android.util.Log
+import androidx.core.content.res.ResourcesCompat
+import org.xmlpull.v1.XmlPullParser
+import org.xmlpull.v1.XmlPullParserException
+import org.xmlpull.v1.XmlPullParserFactory
+import rasel.lunar.launcher.LauncherActivity.Companion.lActivity
+import rasel.lunar.launcher.helpers.Constants.Companion.DEFAULT_ICON_PACK
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_ICON_PACK
+import rasel.lunar.launcher.helpers.Constants.Companion.PREFS_SETTINGS
+import rasel.lunar.launcher.utils.BLog
+import java.io.IOException
+import java.util.Locale
+
+
+internal class IconPackManager {
+
+ @SuppressLint("DiscouragedApi")
+ companion object {
+
+ private val settingsPrefs = lActivity!!.getSharedPreferences(PREFS_SETTINGS, 0)
+ private val packageName = settingsPrefs.getString(KEY_ICON_PACK, DEFAULT_ICON_PACK)
+ private var loaded = false
+ private val packagesDrawables = HashMap()
+ private val backImages: MutableList = ArrayList()
+ private var maskImage: Bitmap? = null
+ private var frontImage: Bitmap? = null
+ private var factor = 1.0f
+ private var totalIcons = 0
+ private var iconPackRes: Resources? = null
+
+ private fun load() {
+ /* load appfilter.xml from the icon pack package */
+ try {
+ var xpp: XmlPullParser? = null
+ iconPackRes = lActivity!!.packageManager.getResourcesForApplication(packageName!!)
+ val appFilterId = iconPackRes!!.getIdentifier("appfilter", "xml", packageName)
+ if (appFilterId > 0) {
+ xpp = iconPackRes!!.getXml(appFilterId)
+ } else {
+ /* no resource found, try to open it from assets folder */
+ try {
+ xpp = XmlPullParserFactory.newInstance().apply { isNamespaceAware = true }
+ .newPullParser().apply { setInput(iconPackRes!!.assets.open("appfilter.xml"), "utf-8") }
+ } catch (e: IOException) {
+ e.printStackTrace()
+ BLog.w("", "Couldn't find the appfilter.xml file")
+ }
+ }
+ if (xpp != null) {
+ var eventType = xpp.eventType
+ while (eventType != XmlPullParser.END_DOCUMENT) {
+ if (eventType == XmlPullParser.START_TAG) {
+ when (xpp.name) {
+ "iconback" -> {
+ for (i in 0 until xpp.attributeCount) {
+ if (xpp.getAttributeName(i).startsWith("img")) {
+ loadBitmap(xpp.getAttributeValue(i))?.let { backImages.add(it) }
+ }
+ }
+ }
+ "iconmask" -> {
+ if (xpp.attributeCount > 0 && xpp.getAttributeName(0) == "img1") {
+ maskImage = loadBitmap(xpp.getAttributeValue(0))
+ }
+ }
+ "iconupon" -> {
+ if (xpp.attributeCount > 0 && xpp.getAttributeName(0) == "img1") {
+ frontImage = loadBitmap(xpp.getAttributeValue(0))
+ }
+ }
+ "scale" -> {
+ if (xpp.attributeCount > 0 && xpp.getAttributeName(0) == "factor") {
+ factor = java.lang.Float.valueOf(xpp.getAttributeValue(0))
+ }
+ }
+ "item" -> {
+ var componentName: String? = null
+ var drawableName: String? = null
+ for (i in 0 until xpp.attributeCount) {
+ when (xpp.getAttributeName(i)) {
+ "component" -> componentName = xpp.getAttributeValue(i)
+ "drawable" -> drawableName = xpp.getAttributeValue(i)
+ }
+ }
+ if (!packagesDrawables.containsKey(componentName)) {
+ packagesDrawables[componentName] = drawableName
+ totalIcons += 1
+ }
+ }
+ }
+ }
+ eventType = xpp.next()
+ }
+ }
+ loaded = true
+ } catch (e: PackageManager.NameNotFoundException) {
+ BLog.w("", "Failed to load the icon pack")
+ } catch (e: XmlPullParserException) {
+ BLog.w("", "Failed to parse the appfilter.xml file")
+ } catch (e: IOException) {
+ e.printStackTrace()
+ }
+ }
+
+ private fun loadBitmap(drawableName: String): Bitmap? {
+ iconPackRes!!.getIdentifier(drawableName, "drawable", packageName).let { id ->
+ if (id > 0) {
+ ResourcesCompat.getDrawable(iconPackRes!!, id, null).let {
+ if (it is BitmapDrawable) return it.bitmap
+ }
+ }
+ }
+ return null
+ }
+
+ private fun loadDrawable(drawableName: String): Drawable? {
+ iconPackRes!!.getIdentifier(drawableName, "drawable", packageName).let {
+ return if (it > 0) ResourcesCompat.getDrawable(iconPackRes!!, it, null)
+ else null
+ }
+ }
+
+ fun getDrawableIconForPackage(appPackageName: String?, defaultDrawable: Drawable?): Drawable? {
+ when (packageName) {
+ DEFAULT_ICON_PACK -> return defaultDrawable
+ else -> {
+ if (!loaded) load()
+ var componentName: String? = null
+ if (lActivity!!.packageManager.getLaunchIntentForPackage(appPackageName!!) != null) {
+ componentName = lActivity!!.packageManager.getLaunchIntentForPackage(appPackageName)!!.component.toString()
+ }
+ var drawable = packagesDrawables[componentName]
+ if (!drawable.isNullOrEmpty()) return loadDrawable(drawable)
+ else {
+ /* try to get a resource with the component filename */
+ if (!componentName.isNullOrEmpty()) {
+ val start = componentName.indexOf("{") + 1
+ val end = componentName.indexOf("}", start)
+ if (end > start) {
+ drawable = componentName.substring(start, end).lowercase(Locale.getDefault()).replace(".", "_").replace("/", "_")
+ try {
+ if (iconPackRes!!.getIdentifier(drawable, "drawable", packageName) > 0) return loadDrawable(drawable)
+ } catch (e: NullPointerException) {
+ settingsPrefs.edit().putString(KEY_ICON_PACK, DEFAULT_ICON_PACK).apply()
+ }
+ }
+ }
+ }
+ return defaultDrawable
+ }
+ }
+ }
+
+ }
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/apps/SimpleGesture.kt b/app/src/main/kotlin/rasel/lunar/launcher/apps/SimpleGesture.kt
new file mode 100644
index 0000000..f5b8a66
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/apps/SimpleGesture.kt
@@ -0,0 +1,705 @@
+package rasel.lunar.launcher.apps
+
+import android.os.SystemClock
+import android.util.Log
+import android.view.MotionEvent
+import android.view.View
+import android.view.View.OnTouchListener
+import kotlin.math.abs
+import kotlin.math.pow
+import kotlin.math.sqrt
+
+class GestureAnalyser @JvmOverloads constructor(
+ swipeSlopeIntolerance: Int = 3,
+ doubleTapMaxDelayMillis: Int = 500,
+ doubleTapMaxDownMillis: Int = 100
+) {
+ private val initialX = DoubleArray(5)
+ private val initialY = DoubleArray(5)
+ private val finalX = DoubleArray(5)
+ private val finalY = DoubleArray(5)
+ private val currentX = DoubleArray(5)
+ private val currentY = DoubleArray(5)
+ private val delX = DoubleArray(5)
+ private val delY = DoubleArray(5)
+
+ private var numFingers = 0
+ private var initialT: Long = 0
+ private var finalT: Long = 0
+ private var currentT: Long = 0
+
+ private var prevInitialT: Long = 0
+ private var prevFinalT: Long = 0
+
+ private var swipeSlopeIntolerance = 3
+
+ private val doubleTapMaxDelayMillis: Long
+ private val doubleTapMaxDownMillis: Long
+
+ init {
+ this.swipeSlopeIntolerance = swipeSlopeIntolerance
+ this.doubleTapMaxDownMillis = doubleTapMaxDownMillis.toLong()
+ this.doubleTapMaxDelayMillis = doubleTapMaxDelayMillis.toLong()
+ }
+
+ fun trackGesture(ev: MotionEvent) {
+ val n = ev.pointerCount
+ for (i in 0 until n) {
+ initialX[i] = ev.getX(i).toDouble()
+ initialY[i] = ev.getY(i).toDouble()
+ }
+ numFingers = n
+ initialT = SystemClock.uptimeMillis()
+ }
+
+ fun untrackGesture() {
+ numFingers = 0
+ prevFinalT = SystemClock.uptimeMillis()
+ prevInitialT = initialT
+ }
+
+ fun getGesture(ev: MotionEvent): GestureType {
+ var averageDistance = 0.0
+ for (i in 0 until numFingers) {
+ finalX[i] = ev.getX(i).toDouble()
+ finalY[i] = ev.getY(i).toDouble()
+ delX[i] = finalX[i] - initialX[i]
+ delY[i] = finalY[i] - initialY[i]
+
+ averageDistance += sqrt(
+ (finalX[i] - initialX[i]).pow(2.0) + (finalY[i] - initialY[i]).pow(
+ 2.0
+ )
+ )
+ }
+ averageDistance /= numFingers.toDouble()
+
+ finalT = SystemClock.uptimeMillis()
+ val gt = GestureType()
+ gt.gestureFlag = calcGesture()
+ gt.gestureDuration = finalT - initialT
+ gt.gestureDistance = averageDistance
+ return gt
+ }
+
+ fun getOngoingGesture(ev: MotionEvent): Int {
+ for (i in 0 until numFingers) {
+ currentX[i] = ev.getX(i).toDouble()
+ currentY[i] = ev.getY(i).toDouble()
+ delX[i] = finalX[i] - initialX[i]
+ delY[i] = finalY[i] - initialY[i]
+ }
+ currentT = SystemClock.uptimeMillis()
+ return calcGesture()
+ }
+
+ private fun calcGesture(): Int {
+ if (isDoubleTap) {
+ return DOUBLE_TAP_1
+ }
+
+ if (numFingers == 1) {
+ if ((-(delY[0])) > (swipeSlopeIntolerance * (abs(
+ delX[0]
+ )))
+ ) {
+ return SWIPE_1_UP
+ }
+
+ if (((delY[0])) > (swipeSlopeIntolerance * (abs(
+ delX[0]
+ )))
+ ) {
+ return SWIPE_1_DOWN
+ }
+
+ if ((-(delX[0])) > (swipeSlopeIntolerance * (abs(
+ delY[0]
+ )))
+ ) {
+ return SWIPE_1_LEFT
+ }
+
+ if (((delX[0])) > (swipeSlopeIntolerance * (abs(
+ delY[0]
+ )))
+ ) {
+ return SWIPE_1_RIGHT
+ }
+ }
+ if (numFingers == 2) {
+ if (((-delY[0]) > (swipeSlopeIntolerance * abs(
+ delX[0]
+ ))) && ((-delY[1]) > (swipeSlopeIntolerance * abs(
+ delX[1]
+ )))
+ ) {
+ return SWIPE_2_UP
+ }
+ if (((delY[0]) > (swipeSlopeIntolerance * abs(
+ delX[0]
+ ))) && ((delY[1]) > (swipeSlopeIntolerance * abs(
+ delX[1]
+ )))
+ ) {
+ return SWIPE_2_DOWN
+ }
+ if (((-delX[0]) > (swipeSlopeIntolerance * abs(
+ delY[0]
+ ))) && ((-delX[1]) > (swipeSlopeIntolerance * abs(
+ delY[1]
+ )))
+ ) {
+ return SWIPE_2_LEFT
+ }
+ if (((delX[0]) > (swipeSlopeIntolerance * abs(
+ delY[0]
+ ))) && ((delX[1]) > (swipeSlopeIntolerance * abs(
+ delY[1]
+ )))
+ ) {
+ return SWIPE_2_RIGHT
+ }
+ if (finalFingDist(0, 1) > 2 * (initialFingDist(0, 1))) {
+ return UNPINCH_2
+ }
+ if (finalFingDist(0, 1) < 0.5 * (initialFingDist(0, 1))) {
+ return PINCH_2
+ }
+ }
+ if (numFingers == 3) {
+ if (((-delY[0]) > (swipeSlopeIntolerance * abs(
+ delX[0]
+ )))
+ && ((-delY[1]) > (swipeSlopeIntolerance * abs(
+ delX[1]
+ )))
+ && ((-delY[2]) > (swipeSlopeIntolerance * abs(
+ delX[2]
+ )))
+ ) {
+ return SWIPE_3_UP
+ }
+ if (((delY[0]) > (swipeSlopeIntolerance * abs(
+ delX[0]
+ )))
+ && ((delY[1]) > (swipeSlopeIntolerance * abs(
+ delX[1]
+ )))
+ && ((delY[2]) > (swipeSlopeIntolerance * abs(
+ delX[2]
+ )))
+ ) {
+ return SWIPE_3_DOWN
+ }
+ if (((-delX[0]) > (swipeSlopeIntolerance * abs(
+ delY[0]
+ )))
+ && ((-delX[1]) > (swipeSlopeIntolerance * abs(
+ delY[1]
+ )))
+ && ((-delX[2]) > (swipeSlopeIntolerance * abs(
+ delY[2]
+ )))
+ ) {
+ return SWIPE_3_LEFT
+ }
+ if (((delX[0]) > (swipeSlopeIntolerance * abs(
+ delY[0]
+ )))
+ && ((delX[1]) > (swipeSlopeIntolerance * abs(
+ delY[1]
+ )))
+ && ((delX[2]) > (swipeSlopeIntolerance * abs(
+ delY[2]
+ )))
+ ) {
+ return SWIPE_3_RIGHT
+ }
+
+ if ((finalFingDist(0, 1) > 1.75 * (initialFingDist(0, 1)))
+ && (finalFingDist(1, 2) > 1.75 * (initialFingDist(1, 2)))
+ && (finalFingDist(2, 0) > 1.75 * (initialFingDist(2, 0)))
+ ) {
+ return UNPINCH_3
+ }
+ if ((finalFingDist(0, 1) < 0.66 * (initialFingDist(0, 1)))
+ && (finalFingDist(1, 2) < 0.66 * (initialFingDist(1, 2)))
+ && (finalFingDist(2, 0) < 0.66 * (initialFingDist(2, 0)))
+ ) {
+ return PINCH_3
+ }
+ }
+ if (numFingers == 4) {
+ if (((-delY[0]) > (swipeSlopeIntolerance * abs(
+ delX[0]
+ )))
+ && ((-delY[1]) > (swipeSlopeIntolerance * abs(
+ delX[1]
+ )))
+ && ((-delY[2]) > (swipeSlopeIntolerance * abs(
+ delX[2]
+ )))
+ && ((-delY[3]) > (swipeSlopeIntolerance * abs(
+ delX[3]
+ )))
+ ) {
+ return SWIPE_4_UP
+ }
+ if (((delY[0]) > (swipeSlopeIntolerance * abs(
+ delX[0]
+ )))
+ && ((delY[1]) > (swipeSlopeIntolerance * abs(
+ delX[1]
+ )))
+ && ((delY[2]) > (swipeSlopeIntolerance * abs(
+ delX[2]
+ )))
+ && ((delY[3]) > (swipeSlopeIntolerance * abs(
+ delX[3]
+ )))
+ ) {
+ return SWIPE_4_DOWN
+ }
+ if (((-delX[0]) > (swipeSlopeIntolerance * abs(
+ delY[0]
+ )))
+ && ((-delX[1]) > (swipeSlopeIntolerance * abs(
+ delY[1]
+ )))
+ && ((-delX[2]) > (swipeSlopeIntolerance * abs(
+ delY[2]
+ )))
+ && ((-delX[3]) > (swipeSlopeIntolerance * abs(
+ delY[3]
+ )))
+ ) {
+ return SWIPE_4_LEFT
+ }
+ if (((delX[0]) > (swipeSlopeIntolerance * abs(
+ delY[0]
+ )))
+ && ((delX[1]) > (swipeSlopeIntolerance * abs(
+ delY[1]
+ )))
+ && ((delX[2]) > (swipeSlopeIntolerance * abs(
+ delY[2]
+ )))
+ && ((delX[3]) > (swipeSlopeIntolerance * abs(
+ delY[3]
+ )))
+ ) {
+ return SWIPE_4_RIGHT
+ }
+ if ((finalFingDist(0, 1) > 1.5 * (initialFingDist(0, 1)))
+ && (finalFingDist(1, 2) > 1.5 * (initialFingDist(1, 2)))
+ && (finalFingDist(2, 3) > 1.5 * (initialFingDist(2, 3)))
+ && (finalFingDist(3, 0) > 1.5 * (initialFingDist(3, 0)))
+ ) {
+ return UNPINCH_4
+ }
+ if ((finalFingDist(0, 1) < 0.8 * (initialFingDist(0, 1)))
+ && (finalFingDist(1, 2) < 0.8 * (initialFingDist(1, 2)))
+ && (finalFingDist(2, 3) < 0.8 * (initialFingDist(2, 3)))
+ && (finalFingDist(3, 0) < 0.8 * (initialFingDist(3, 0)))
+ ) {
+ return PINCH_4
+ }
+ }
+ return 0
+ }
+
+ private fun initialFingDist(fingNum1: Int, fingNum2: Int): Double {
+ return sqrt(
+ (initialX[fingNum1] - initialX[fingNum2]).pow(2.0) + (initialY[fingNum1] - initialY[fingNum2]).pow(
+ 2.0
+ )
+ )
+ }
+
+ private fun finalFingDist(fingNum1: Int, fingNum2: Int): Double {
+ return sqrt(
+ (finalX[fingNum1] - finalX[fingNum2]).pow(2.0) + (finalY[fingNum1] - finalY[fingNum2]).pow(
+ 2.0
+ )
+ )
+ }
+
+ val isDoubleTap: Boolean
+ get() = if (initialT - prevFinalT < doubleTapMaxDelayMillis && finalT - initialT < doubleTapMaxDownMillis && prevFinalT - prevInitialT < doubleTapMaxDownMillis) {
+ true
+ } else {
+ false
+ }
+
+ inner class GestureType {
+ var gestureFlag: Int = 0
+ var gestureDuration: Long = 0
+
+ var gestureDistance: Double = 0.0
+ }
+
+
+ companion object {
+ const val DEBUG: Boolean = true
+
+ // Finished gestures flags
+ const val SWIPE_1_UP: Int = 11
+ const val SWIPE_1_DOWN: Int = 12
+ const val SWIPE_1_LEFT: Int = 13
+ const val SWIPE_1_RIGHT: Int = 14
+ const val SWIPE_2_UP: Int = 21
+ const val SWIPE_2_DOWN: Int = 22
+ const val SWIPE_2_LEFT: Int = 23
+ const val SWIPE_2_RIGHT: Int = 24
+ const val SWIPE_3_UP: Int = 31
+ const val SWIPE_3_DOWN: Int = 32
+ const val SWIPE_3_LEFT: Int = 33
+ const val SWIPE_3_RIGHT: Int = 34
+ const val SWIPE_4_UP: Int = 41
+ const val SWIPE_4_DOWN: Int = 42
+ const val SWIPE_4_LEFT: Int = 43
+ const val SWIPE_4_RIGHT: Int = 44
+ const val PINCH_2: Int = 25
+ const val UNPINCH_2: Int = 26
+ const val PINCH_3: Int = 35
+ const val UNPINCH_3: Int = 36
+ const val PINCH_4: Int = 45
+ const val UNPINCH_4: Int = 46
+
+ const val DOUBLE_TAP_1: Int = 107
+
+ //Ongoing gesture flags
+ const val SWIPING_1_UP: Int = 101
+ const val SWIPING_1_DOWN: Int = 102
+ const val SWIPING_1_LEFT: Int = 103
+ const val SWIPING_1_RIGHT: Int = 104
+ const val SWIPING_2_UP: Int = 201
+ const val SWIPING_2_DOWN: Int = 202
+ const val SWIPING_2_LEFT: Int = 203
+ const val SWIPING_2_RIGHT: Int = 204
+ const val PINCHING: Int = 205
+ const val UNPINCHING: Int = 206
+ private const val TAG = "GestureAnalyser"
+ }
+}
+
+class SimpleFingerGestures : OnTouchListener {
+ private var debug = true
+ var consumeTouchEvents: Boolean = false
+
+ protected var tracking: BooleanArray = booleanArrayOf(false, false, false, false, false)
+ private var ga: GestureAnalyser
+ private var onFingerGestureListener: OnFingerGestureListener? = null
+
+
+ /**
+ * Constructor that creates an internal [in.championswimmer.sfg.lib.GestureAnalyser] object as well
+ */
+ constructor() {
+ ga = GestureAnalyser()
+ }
+
+ constructor(
+ swipeSlopeIntolerance: Int,
+ doubleTapMaxDelayMillis: Int,
+ doubleTapMaxDownMillis: Int
+ ) {
+ ga = GestureAnalyser(swipeSlopeIntolerance, doubleTapMaxDelayMillis, doubleTapMaxDownMillis)
+ }
+
+ fun setDebug(debug: Boolean) {
+ this.debug = debug
+ }
+
+ constructor(omfgl: OnFingerGestureListener?) {
+ ga = GestureAnalyser()
+ setOnFingerGestureListener(omfgl)
+ }
+
+ /**
+ * Register a callback to be invoked when multi-finger gestures take place
+ *
+ *
+ *
+ *
+ *
+ * For the callbacks implemented via this, check the interface [in.championswimmer.sfg.lib.SimpleFingerGestures.OnFingerGestureListener]
+ *
+ *
+ * @param omfgl The callback that will run
+ */
+ fun setOnFingerGestureListener(omfgl: OnFingerGestureListener?) {
+ onFingerGestureListener = omfgl
+ }
+
+
+ override fun onTouch(view: View, ev: MotionEvent): Boolean {
+ if (debug) Log.d(TAG, "onTouch")
+ when (ev.action and MotionEvent.ACTION_MASK) {
+ MotionEvent.ACTION_DOWN -> {
+ if (debug) Log.d(TAG, "ACTION_DOWN")
+ startTracking(0)
+ ga.trackGesture(ev)
+ return consumeTouchEvents
+ }
+
+ MotionEvent.ACTION_UP -> {
+ if (debug) Log.d(TAG, "ACTION_UP")
+ if (tracking[0]) {
+ doCallBack(ga.getGesture(ev))
+ }
+ stopTracking(0)
+ ga.untrackGesture()
+ return consumeTouchEvents
+ }
+
+ MotionEvent.ACTION_POINTER_DOWN -> {
+ if (debug) Log.d(TAG, "ACTION_POINTER_DOWN" + " " + "num" + ev.pointerCount)
+ startTracking(ev.pointerCount - 1)
+ ga.trackGesture(ev)
+ return consumeTouchEvents
+ }
+
+ MotionEvent.ACTION_POINTER_UP -> {
+ if (debug) Log.d(TAG, "ACTION_POINTER_UP" + " " + "num" + ev.pointerCount)
+ if (tracking[1]) {
+ doCallBack(ga.getGesture(ev))
+ }
+ stopTracking(ev.pointerCount - 1)
+ ga.untrackGesture()
+ return consumeTouchEvents
+ }
+
+ MotionEvent.ACTION_CANCEL -> {
+ if (debug) Log.d(TAG, "ACTION_CANCEL")
+ return true
+ }
+
+ MotionEvent.ACTION_MOVE -> {
+ if (debug) Log.d(TAG, "ACTION_MOVE")
+ return consumeTouchEvents
+ }
+ }
+ return consumeTouchEvents
+ }
+
+ private fun doCallBack(mGt: GestureAnalyser.GestureType) {
+ when (mGt.gestureFlag) {
+ GestureAnalyser.SWIPE_1_UP -> onFingerGestureListener!!.onSwipeUp(
+ 1,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.SWIPE_1_DOWN -> onFingerGestureListener!!.onSwipeDown(
+ 1,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.SWIPE_1_LEFT -> onFingerGestureListener!!.onSwipeLeft(
+ 1,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.SWIPE_1_RIGHT -> onFingerGestureListener!!.onSwipeRight(
+ 1,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.SWIPE_2_UP -> onFingerGestureListener!!.onSwipeUp(
+ 2,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.SWIPE_2_DOWN -> onFingerGestureListener!!.onSwipeDown(
+ 2,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.SWIPE_2_LEFT -> onFingerGestureListener!!.onSwipeLeft(
+ 2,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.SWIPE_2_RIGHT -> onFingerGestureListener!!.onSwipeRight(
+ 2,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.PINCH_2 -> onFingerGestureListener!!.onPinch(
+ 2,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.UNPINCH_2 -> onFingerGestureListener!!.onUnpinch(
+ 2,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.SWIPE_3_UP -> onFingerGestureListener!!.onSwipeUp(
+ 3,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.SWIPE_3_DOWN -> onFingerGestureListener!!.onSwipeDown(
+ 3,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.SWIPE_3_LEFT -> onFingerGestureListener!!.onSwipeLeft(
+ 3,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.SWIPE_3_RIGHT -> onFingerGestureListener!!.onSwipeRight(
+ 3,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.PINCH_3 -> onFingerGestureListener!!.onPinch(
+ 3,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.UNPINCH_3 -> onFingerGestureListener!!.onUnpinch(
+ 3,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.SWIPE_4_UP -> onFingerGestureListener!!.onSwipeUp(
+ 4,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.SWIPE_4_DOWN -> onFingerGestureListener!!.onSwipeDown(
+ 4,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.SWIPE_4_LEFT -> onFingerGestureListener!!.onSwipeLeft(
+ 4,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.SWIPE_4_RIGHT -> onFingerGestureListener!!.onSwipeRight(
+ 4,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.PINCH_4 -> onFingerGestureListener!!.onPinch(
+ 4,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.UNPINCH_4 -> {
+ onFingerGestureListener!!.onUnpinch(4, mGt.gestureDuration, mGt.gestureDistance)
+ onFingerGestureListener!!.onDoubleTap(1)
+ }
+
+ GestureAnalyser.DOUBLE_TAP_1 -> onFingerGestureListener!!.onDoubleTap(1)
+ }
+ }
+
+ private fun startTracking(nthPointer: Int) {
+ for (i in 0..nthPointer) {
+ tracking[i] = true
+ }
+ }
+
+ private fun stopTracking(nthPointer: Int) {
+ for (i in nthPointer until tracking.size) {
+ tracking[i] = false
+ }
+ }
+
+
+ /**
+ * Interface definition for the callback to be invoked when 2-finger gestures are performed
+ */
+ interface OnFingerGestureListener {
+ /**
+ * Called when user swipes **up** with two fingers
+ *
+ * @param fingers number of fingers involved in this gesture
+ * @param gestureDuration duration in milliSeconds
+ * @return
+ */
+ fun onSwipeUp(fingers: Int, gestureDuration: Long, gestureDistance: Double): Boolean
+
+ /**
+ * Called when user swipes **down** with two fingers
+ *
+ * @param fingers number of fingers involved in this gesture
+ * @param gestureDuration duration in milliSeconds
+ * @return
+ */
+ fun onSwipeDown(fingers: Int, gestureDuration: Long, gestureDistance: Double): Boolean
+
+ /**
+ * Called when user swipes **left** with two fingers
+ *
+ * @param fingers number of fingers involved in this gesture
+ * @param gestureDuration duration in milliSeconds
+ * @return
+ */
+ fun onSwipeLeft(fingers: Int, gestureDuration: Long, gestureDistance: Double): Boolean
+
+ /**
+ * Called when user swipes **right** with two fingers
+ *
+ * @param fingers number of fingers involved in this gesture
+ * @param gestureDuration duration in milliSeconds
+ * @return
+ */
+ fun onSwipeRight(fingers: Int, gestureDuration: Long, gestureDistance: Double): Boolean
+
+ /**
+ * Called when user **pinches** with two fingers (bring together)
+ *
+ * @param fingers number of fingers involved in this gesture
+ * @param gestureDuration duration in milliSeconds
+ * @return
+ */
+ fun onPinch(fingers: Int, gestureDuration: Long, gestureDistance: Double): Boolean
+
+ /**
+ * Called when user **un-pinches** with two fingers (take apart)
+ *
+ * @param fingers number of fingers involved in this gesture
+ * @param gestureDuration duration in milliSeconds
+ * @return
+ */
+ fun onUnpinch(fingers: Int, gestureDuration: Long, gestureDistance: Double): Boolean
+
+ fun onDoubleTap(fingers: Int): Boolean
+ }
+
+ companion object {
+ // Will see if these need to be used. For now just returning duration in milliS
+ const val GESTURE_SPEED_SLOW: Long = 1500
+ const val GESTURE_SPEED_MEDIUM: Long = 1000
+ const val GESTURE_SPEED_FAST: Long = 500
+ private const val TAG = "SimpleFingerGestures"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/feeds/Feeds.kt b/app/src/main/kotlin/rasel/lunar/launcher/feeds/Feeds.kt
new file mode 100644
index 0000000..d6119a2
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/feeds/Feeds.kt
@@ -0,0 +1,356 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.feeds
+
+import android.R.attr.*
+import android.app.Activity.RESULT_CANCELED
+import android.app.Activity.RESULT_OK
+import android.appwidget.AppWidgetManager
+import android.content.Intent
+import android.content.SharedPreferences
+import android.os.*
+import android.view.*
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.widget.LinearLayoutCompat.LayoutParams
+import androidx.appcompat.widget.PopupMenu
+import androidx.core.app.JobIntentService.enqueueWork
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.google.android.material.button.MaterialButtonToggleGroup
+import kotlinx.coroutines.*
+import rasel.lunar.launcher.LauncherActivity.Companion.appWidgetHost
+import rasel.lunar.launcher.LauncherActivity.Companion.appWidgetManager
+import rasel.lunar.launcher.LauncherActivity.Companion.lActivity
+import rasel.lunar.launcher.R
+import rasel.lunar.launcher.databinding.FeedsBinding
+import rasel.lunar.launcher.feeds.rss.Rss
+import rasel.lunar.launcher.feeds.rss.RssAdapter
+import rasel.lunar.launcher.feeds.rss.RssService
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_RSS_URL
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_WIDGET_HEIGHTS
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_WIDGET_IDS
+import rasel.lunar.launcher.helpers.Constants.Companion.PREFS_SETTINGS
+import rasel.lunar.launcher.helpers.Constants.Companion.PREFS_WIDGETS
+import rasel.lunar.launcher.helpers.Constants.Companion.RSS_ITEMS
+import rasel.lunar.launcher.helpers.Constants.Companion.RSS_RECEIVER
+import rasel.lunar.launcher.helpers.Constants.Companion.SEPARATOR
+import rasel.lunar.launcher.helpers.Constants.Companion.requestCreateWidget
+import rasel.lunar.launcher.helpers.Constants.Companion.requestPickWidget
+import rasel.lunar.launcher.helpers.Constants.Companion.rssJobId
+import rasel.lunar.launcher.helpers.UniUtils.Companion.isNetworkAvailable
+import java.util.*
+
+
+internal class Feeds : Fragment() {
+
+ private lateinit var binding: FeedsBinding
+ private val requestCodeString = "requestCode"
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ binding = FeedsBinding.inflate(inflater, container, false)
+
+ updateWidgets()
+
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ expandCollapse()
+ systemInfo()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ registerForContextMenu(binding.widgetContainer)
+ }
+
+ override fun onPause() {
+ super.onPause()
+ unregisterForContextMenu(binding.widgetContainer)
+ }
+
+ override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) {
+ super.onCreateContextMenu(menu, v, menuInfo)
+ menu.clearHeader()
+ lActivity!!.menuInflater.inflate(R.menu.add_widget, menu)
+ }
+
+ override fun onContextItemSelected(item: MenuItem): Boolean {
+ if (item.itemId == R.id.add_widget) selectWidget()
+ return super.onContextItemSelected(item)
+ }
+
+ /* control view's expand-collapse actions */
+ private fun expandCollapse() {
+ binding.expandableButtons.addOnButtonCheckedListener { _: MaterialButtonToggleGroup?, checkedId: Int, isChecked: Boolean ->
+ if (isChecked) {
+ when (checkedId) {
+ binding.expandRss.id -> {
+ binding.feedsSysInfos.expandableSystemInfo.collapse()
+ binding.feedsRss.expandableRss.expand()
+ startService()
+ }
+ binding.expandSystemInfo.id -> {
+ binding.feedsRss.expandableRss.collapse()
+ binding.feedsSysInfos.expandableSystemInfo.expand()
+ }
+ }
+ } else {
+ when (checkedId) {
+ binding.expandRss.id -> binding.feedsRss.expandableRss.collapse()
+ binding.expandSystemInfo.id -> binding.feedsSysInfos.expandableSystemInfo.collapse()
+ }
+ }
+ }
+ }
+
+ /* start rss service if network is active and rss url is not empty */
+ private fun startService() {
+ val rssUrl = lActivity!!.getSharedPreferences(PREFS_SETTINGS, 0)
+ .getString(KEY_RSS_URL, "")
+ when {
+ isNetworkAvailable && !rssUrl.isNullOrEmpty() -> {
+ Intent(lActivity!!, RssService::class.java)
+ .putExtra(RSS_RECEIVER, resultReceiver).let {
+ enqueueWork(lActivity!!, RssService::class.java, rssJobId, it)
+ }
+ }
+ else -> resumeService()
+ }
+ }
+
+ /* retry to start rss service */
+ private fun resumeService() {
+ binding.feedsRss.apply {
+ rss.visibility = View.GONE
+ loading.visibility = View.GONE
+ refresh.visibility = View.VISIBLE
+ refresh.setOnClickListener { startService() }
+ }
+ }
+
+ /* rss service's result receiver */
+ @Suppress("UNCHECKED_CAST")
+ private val resultReceiver: ResultReceiver = object : ResultReceiver(Handler(Looper.getMainLooper())) {
+ override fun onReceiveResult(resultCode: Int, resultData: Bundle) {
+ when (val items = resultData.getSerializable(RSS_ITEMS) as List?) {
+ null -> resumeService()
+ else -> {
+ binding.feedsRss.apply {
+ rss.adapter = RssAdapter(items, requireContext())
+ refresh.visibility = View.GONE
+ loading.visibility = View.GONE
+ rss.visibility = View.VISIBLE
+ }
+ }
+ }
+ }
+ }
+
+ private fun systemInfo() {
+ viewLifecycleOwner.lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ SystemStats().apply {
+ intStorage(binding.feedsSysInfos.intParent)
+ extStorage(binding.feedsSysInfos.extParent)
+ while (isActive) {
+ ram(binding.feedsSysInfos.ramParent)
+ cpu(binding.feedsSysInfos.cpuParent)
+ misc(binding.feedsSysInfos.misc)
+ delay(1000)
+ }
+ }
+ }
+ }
+ }
+
+ private fun selectWidget() {
+ Intent(AppWidgetManager.ACTION_APPWIDGET_PICK).apply {
+ putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetHost?.allocateAppWidgetId())
+ putParcelableArrayListExtra(AppWidgetManager.EXTRA_CUSTOM_INFO, ArrayList())
+ putParcelableArrayListExtra(AppWidgetManager.EXTRA_CUSTOM_EXTRAS, ArrayList())
+ putExtra(requestCodeString, requestPickWidget)
+ }.let { widgetPicker.launch(it) }
+ }
+
+ private val widgetPicker =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+ val data = result.data
+ val appWidgetId = data?.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
+ if (result.resultCode == RESULT_OK) {
+ when (data?.getIntExtra(requestCodeString, requestPickWidget)) {
+ requestPickWidget -> configureWidget(appWidgetId!!)
+ requestCreateWidget -> createWidget(appWidgetId!!, null)
+ }
+ } else if (result.resultCode == RESULT_CANCELED && data != null) {
+ if (appWidgetId != -1) appWidgetHost?.deleteAppWidgetId(appWidgetId!!)
+ }
+ }
+
+ private fun configureWidget(appWidgetId: Int) {
+ when (val appWidgetConfig = appWidgetManager!!.getAppWidgetInfo(appWidgetId).configure) {
+ null -> createWidget(appWidgetId, null)
+ else -> {
+ Intent(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE).apply {
+ component = appWidgetConfig
+ putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
+ putExtra(requestCodeString, requestCreateWidget)
+ }.let {
+ try { widgetPicker.launch(it) }
+ catch (e: Exception) { e.printStackTrace() }
+ }
+ }
+ }
+ }
+
+ private fun createWidget(appWidgetId: Int, height: Int?) {
+ if (appWidgetId == -1) return
+
+ val appWidgetInfo = appWidgetManager!!.getAppWidgetInfo(appWidgetId)
+ val params: LayoutParams?
+
+ when (height) {
+ null -> {
+ params = LayoutParams(LayoutParams.MATCH_PARENT, appWidgetInfo.minHeight)
+ val updatedIds = splitWidgetIds.plus("$appWidgetId")
+ val updatedHeights = splitWidgetHeights.plus("${appWidgetInfo.minHeight}")
+ saveWidgetData(updatedIds, updatedHeights)
+ }
+ else -> params = LayoutParams(LayoutParams.MATCH_PARENT, height)
+ }
+
+ (appWidgetHost?.createView(lActivity!!.applicationContext, appWidgetId, appWidgetInfo) as WidgetHostView)
+ .apply {
+ setAppWidget(appWidgetId, appWidgetInfo)
+ }.let {
+ binding.widgetContainer.addView(it, params)
+ widgetMenu(it)
+ }
+ }
+
+ private fun updateWidgets() {
+ if (splitWidgetIds.size > 0) {
+ viewLifecycleOwner.lifecycleScope.launch {
+ binding.widgetContainer.removeAllViews()
+ splitWidgetIds.indices.forEach { i: Int ->
+ createWidget(splitWidgetIds[i]!!.int(), splitWidgetHeights[i]!!.int())
+ }
+ }
+ }
+ }
+
+ private fun widgetMenu(hostView: WidgetHostView) {
+ val appWidgetId = hostView.appWidgetId
+ hostView.setOnLongClickListener {
+ PopupMenu(requireContext(), it, Gravity.END).apply {
+ menuInflater.inflate(R.menu.widget_menu, this.menu)
+ show()
+ setOnMenuItemClickListener { menuItem ->
+ when (menuItem.itemId) {
+ R.id.move_up -> moveWidget(appWidgetId, true)
+ R.id.move_down -> moveWidget(appWidgetId, false)
+ R.id.increase_height -> resizeWidget(appWidgetId, true)
+ R.id.decrease_height -> resizeWidget(appWidgetId, false)
+ R.id.delete_widget -> removeWidget(it as WidgetHostView)
+ }
+ false
+ }
+ }
+ true
+ }
+ }
+
+ private fun moveWidget(widgetId: Int, moveUp: Boolean) {
+ val tempIds = splitWidgetIds
+ val tempHeights = splitWidgetHeights
+
+ splitWidgetIds.indexOf(widgetId.toString()).let { i ->
+ when {
+ moveUp && i > 0 -> {
+ tempIds.swap(i-1, i)
+ tempHeights.swap(i-1, i)
+ }
+ !moveUp && i < splitWidgetIds.size - 1 -> {
+ tempIds.swap(i, i+1)
+ tempHeights.swap(i, i+1)
+ }
+ else -> return
+ }
+ }
+
+ saveWidgetData(tempIds, tempHeights)
+ updateWidgets()
+ }
+
+ private fun resizeWidget(widgetId: Int, shouldAdd: Boolean) {
+ val tempList = splitWidgetHeights
+
+ splitWidgetIds.indexOf(widgetId.toString()).let { i ->
+ tempList[i] = when (shouldAdd) {
+ true -> (splitWidgetHeights[i]!!.int().plus(50)).toString()
+ false -> (splitWidgetHeights[i]!!.int().minus(50)).toString()
+ }
+ }
+
+ widgetPref.edit().putString(KEY_WIDGET_HEIGHTS, tempList.joinToString(separator = SEPARATOR)).apply()
+ updateWidgets()
+ }
+
+ private fun removeWidget(hostView: WidgetHostView) {
+ hostView.let { v ->
+ appWidgetHost?.deleteAppWidgetId(v.appWidgetId)
+ binding.widgetContainer.removeView(v)
+
+ splitWidgetIds.indexOf(v.appWidgetId.toString()).let { i ->
+ saveWidgetData(splitWidgetIds.minus(splitWidgetIds[i]), splitWidgetHeights.minus(splitWidgetHeights[i]))
+ }
+ }
+ }
+
+ private fun saveWidgetData(idList: List, heightList: List) {
+ widgetPref.edit()
+ .putString(KEY_WIDGET_IDS, idList.joinToString(separator = SEPARATOR))
+ .putString(KEY_WIDGET_HEIGHTS, heightList.joinToString(separator = SEPARATOR))
+ .apply()
+ }
+
+ private val widgetPref: SharedPreferences get() = lActivity!!.getSharedPreferences(PREFS_WIDGETS, 0)
+ private val widgetIds: String? get() = widgetPref.getString(KEY_WIDGET_IDS, "")
+ private val widgetHeights: String? get() = widgetPref.getString(KEY_WIDGET_HEIGHTS, "")
+ private val splitWidgetIds: MutableList get() = widgetIds!!.split(SEPARATOR).toMutableList()
+ private val splitWidgetHeights: MutableList get() = widgetHeights!!.split(SEPARATOR).toMutableList()
+
+ private fun MutableList.swap(index1: Int, index2: Int){
+ val temp = this[index1]
+ this[index1] = this[index2]
+ this[index2] = temp
+ }
+
+ private fun String.int() : Int {
+ return try {
+ this.toInt()
+ } catch (e: Exception) {
+ -1
+ }
+ }
+
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/feeds/SystemStats.kt b/app/src/main/kotlin/rasel/lunar/launcher/feeds/SystemStats.kt
new file mode 100644
index 0000000..de4de5b
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/feeds/SystemStats.kt
@@ -0,0 +1,297 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.feeds
+
+import android.annotation.SuppressLint
+import android.app.ActivityManager
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.*
+import android.text.Html
+import android.view.LayoutInflater
+import android.view.View
+import androidx.appcompat.widget.LinearLayoutCompat
+import androidx.core.content.ContextCompat
+import com.google.android.material.progressindicator.LinearProgressIndicator
+import com.google.android.material.textview.MaterialTextView
+import rasel.lunar.launcher.LauncherActivity.Companion.lActivity
+import rasel.lunar.launcher.R
+import rasel.lunar.launcher.databinding.ChildSysInfoBinding
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_TEMP_UNIT
+import rasel.lunar.launcher.helpers.Constants.Companion.PREFS_SETTINGS
+import rasel.lunar.launcher.helpers.UniUtils.Companion.isNetworkAvailable
+import java.io.BufferedReader
+import java.io.File
+import java.io.InputStreamReader
+import java.io.RandomAccessFile
+import java.net.NetworkInterface
+import java.util.*
+import java.util.concurrent.TimeUnit
+import kotlin.math.roundToInt
+
+
+internal class SystemStats {
+
+ private val toGb = 1.07374182E9f
+ private fun string(id: Int) : String { return lActivity!!.getString(id) }
+ private val inflater : LayoutInflater get() { return lActivity!!.layoutInflater }
+
+ /* ram info */
+ fun ram(ramParent: LinearLayoutCompat) {
+ val parent = ramParent.findViewById(R.id.childSysInfo)
+ val indicator = parent.findViewById(R.id.indicator)
+ val textView = parent.findViewById(R.id.textView)
+
+ val totalMem = memoryInfo.totalMem / toGb
+ val availMem = memoryInfo.availMem / toGb
+ val usedMem = totalMem - availMem
+
+ indicator.progress = (usedMem * 100 / totalMem).toInt()
+ textView.text = Html.fromHtml(
+ "${string(R.string.ram)}
" +
+ "${string(R.string.total)}: ${String.format("%.03f", totalMem)} GB | " +
+ "${string(R.string.used)}: ${String.format("%.03f", usedMem)} GB | " +
+ "${string(R.string.free)}: ${String.format("%.03f", availMem)} GB",
+ Html.FROM_HTML_MODE_COMPACT)
+ }
+
+
+ /* cpu and battery info */
+ fun cpu(cpuParent: LinearLayoutCompat) {
+ val parent = cpuParent.findViewById(R.id.childSysInfo)
+ val indicator = parent.findViewById(R.id.indicator)
+ val textView = parent.findViewById(R.id.textView)
+
+ var cpuTemp = 0.0f
+ try {
+ val cpuTempProcess = Runtime.getRuntime().exec("cat sys/class/thermal/thermal_zone0/temp")
+ cpuTempProcess.waitFor()
+ val cpuTempReader = BufferedReader(InputStreamReader(cpuTempProcess.inputStream))
+ cpuTemp = cpuTempReader.readLine().toFloat() / 1000.0f
+ } catch (exception: Exception) {
+ exception.printStackTrace()
+ }
+
+ val finalCpuTemp = when (tempUnit) {
+ 0 -> "$cpuTemp ºC"
+ 1 -> "${String.format("%.01f", cpuTemp * 1.8 + 32)} ºF"
+ else -> "$cpuTemp ºC"
+ }
+
+ val cpuFreq = "${String.format("%.02f", minCpuFrequency.toFloat() / 1000)} - " +
+ "${String.format("%.02f", maxCpuFrequency.toFloat() / 1000)} GHz"
+
+ indicator.progress = when (maxCpuFrequency) {
+ 0 -> 30
+ else -> frequencyOfCore * 100 / maxCpuFrequency
+ }
+ textView.text = Html.fromHtml(
+ "${string(R.string.cpu)}
" +
+ "${string(R.string.temperature)}: $finalCpuTemp | " +
+ "${string(R.string.frequency)}: $cpuFreq",
+ Html.FROM_HTML_MODE_COMPACT)
+ }
+
+
+ /* internal storage */
+ fun intStorage(intParent: LinearLayoutCompat) {
+ val parent = intParent.findViewById(R.id.childSysInfo)
+ val indicator = parent.findViewById(R.id.indicator)
+ val textView = parent.findViewById(R.id.textView)
+
+ val intPath = Environment.getExternalStorageDirectory().absolutePath
+ val statFs = StatFs(intPath)
+ val totalStorage = statFs.blockCountLong * statFs.blockSizeLong / toGb
+ val availStorage = statFs.availableBlocksLong * statFs.blockSizeLong / toGb
+ val usedStorage = totalStorage - availStorage
+
+ indicator.progress = (usedStorage * 100 / totalStorage).toInt()
+ textView.text = Html.fromHtml(
+ "${intPath + File.separator}
" +
+ "${string(R.string.total)}: ${String.format("%.03f", totalStorage)} GB | " +
+ "${string(R.string.used)}: ${String.format("%.03f", usedStorage)} GB | " +
+ "${string(R.string.free)}: ${String.format("%.03f", availStorage)} GB",
+ Html.FROM_HTML_MODE_COMPACT)
+ }
+
+
+ /* external storage */
+ fun extStorage(extParent: LinearLayoutCompat) {
+ val extStorages = ContextCompat.getExternalFilesDirs(lActivity!!, null)
+ /* sd card is available */
+ if (extStorages.size > 1) {
+ extParent.removeAllViews()
+ for (extStorage in extStorages) {
+ if (extStorage != null) {
+ val binding = ChildSysInfoBinding.inflate(inflater)
+ extParent.addView(binding.root)
+
+ val statFs = StatFs(extStorage.path)
+ val blockSize = statFs.blockSizeLong
+ val totalStorage = statFs.blockCountLong * blockSize / toGb
+ val availStorage = statFs.availableBlocksLong * blockSize / toGb
+ val usedStorage = totalStorage - availStorage
+
+ val sdcardPaths = extStorage.path.split(File.separator).toTypedArray()
+ val sdPath = File.separator + sdcardPaths[1] + File.separator + sdcardPaths[2] + File.separator
+
+ binding.indicator.progress = (usedStorage * 100 / totalStorage).toInt()
+ binding.textView.text = Html.fromHtml(
+ "$sdPath
" +
+ "${string(R.string.total)}: ${String.format("%.03f", totalStorage)} GB | " +
+ "${string(R.string.used)}: ${String.format("%.03f", usedStorage)} GB | " +
+ "${string(R.string.free)}: ${String.format("%.03f", availStorage)} GB",
+ Html.FROM_HTML_MODE_COMPACT)
+ }
+ }
+ } else {
+ extParent.visibility = View.GONE
+ }
+ }
+
+
+ @SuppressLint("SetTextI18n")
+ fun misc(misc: MaterialTextView) {
+ val totalRootStorage = StatFs(Environment.getRootDirectory().path).blockCountLong *
+ StatFs(Environment.getRootDirectory().path).blockSizeLong / toGb
+
+ val batteryIntent = lActivity!!.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
+ val batteryTemp = batteryIntent!!.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0).toFloat() / 10
+ val voltage = batteryIntent.getIntExtra(BatteryManager.EXTRA_VOLTAGE, 0).toFloat() / 1000
+
+ val finalBatteryTemp = when (tempUnit) {
+ 0 -> "$batteryTemp ºC"
+ 1 -> "${String.format("%.01f", batteryTemp * 1.8 + 32)} ºF"
+ else -> "$batteryTemp ºC"
+ }
+
+ misc.text =
+ "${longToString(SystemClock.elapsedRealtime())}\n" +
+ "${longToString(SystemClock.uptimeMillis())}\n" +
+ "${String.format("%.02f", memoryInfo.threshold / 1048576f)} MB\n" +
+ "$finalBatteryTemp\n" +
+ "$voltage V\n" +
+ "${String.format("%.03f", totalRootStorage)} GB\n" +
+ "${getIpAddress(true)}\n" +
+ getIpAddress(false)
+ }
+
+
+ private val memoryInfo: ActivityManager.MemoryInfo get() {
+ val memoryInfo = ActivityManager.MemoryInfo()
+ val activityManager = lActivity!!.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
+ activityManager.getMemoryInfo(memoryInfo)
+ return memoryInfo
+ }
+
+ private val tempUnit: Int get() =
+ lActivity!!.getSharedPreferences(PREFS_SETTINGS, 0).getInt(KEY_TEMP_UNIT, 0)
+
+ private fun longToString(long: Long) : String {
+ var seconds = (long.toDouble() / 1000).roundToInt()
+ val hours = TimeUnit.SECONDS.toHours(seconds.toLong())
+ if (hours > 0) seconds -= TimeUnit.HOURS.toSeconds(hours).toInt()
+ val minutes = if (seconds > 0) TimeUnit.SECONDS.toMinutes(seconds.toLong()) else 0
+ if (minutes > 0) seconds -= TimeUnit.MINUTES.toSeconds(minutes).toInt()
+ return if (hours > 0) String.format("%02d:%02d:%02d", hours, minutes, seconds)
+ else String.format("%02d:%02d", minutes, seconds)
+ }
+
+ /* frequency of core */
+ private val frequencyOfCore: Int get() {
+ var currentFReq = 0
+ try {
+ val currentFreq: Double
+ val readerCurFreq =
+ RandomAccessFile("/sys/devices/system/cpu/cpu" + 0 + "/cpufreq/scaling_cur_freq", "r")
+ val curFreq = readerCurFreq.readLine()
+ currentFreq = curFreq.toDouble() / 1000
+ readerCurFreq.close()
+ currentFReq = currentFreq.toInt()
+ println("$currentFReq----------------------------------------------------")
+ } catch (ex: java.lang.Exception) {
+ ex.printStackTrace()
+ }
+ return currentFReq
+ }
+
+ /* minimum cpu frequency */
+ private val minCpuFrequency: Int get() {
+ var minFreq = -1
+ try {
+ val randomAccessFile =
+ RandomAccessFile("/sys/devices/system/cpu/cpu" + 0 + "/cpufreq/cpuinfo_min_freq", "r")
+ while (true) {
+ val line = randomAccessFile.readLine() ?: break
+ val timeInState = line.toInt()
+ if (timeInState > 0) {
+ val freq = timeInState / 1000
+ if (freq > minFreq) {
+ minFreq = freq
+ }
+ }
+ }
+ } catch (exception: java.lang.Exception) {
+ exception.printStackTrace()
+ }
+ return minFreq
+ }
+
+ /* maximum cpu frequency */
+ private val maxCpuFrequency: Int get() {
+ var currentFReq = 0
+ try {
+ val currentFreq: Double
+ val readerCurFreq =
+ RandomAccessFile("/sys/devices/system/cpu/cpu" + 0 + "/cpufreq/cpuinfo_max_freq", "r")
+ val curFreq = readerCurFreq.readLine()
+ currentFreq = curFreq.toDouble() / 1000
+ readerCurFreq.close()
+ currentFReq = currentFreq.toInt()
+ } catch (exception: java.lang.Exception) {
+ exception.printStackTrace()
+ }
+ return currentFReq
+ }
+
+ private fun getIpAddress(getIPv4: Boolean): String {
+ try {
+ for (interFace in Collections.list(NetworkInterface.getNetworkInterfaces())) {
+ for (address in Collections.list(interFace.inetAddresses)) {
+ if (!address.isLoopbackAddress) {
+ val addressStr = address.hostAddress
+ val isIPv4 = addressStr!!.indexOf(':') < 0
+ if (getIPv4) {
+ if (isIPv4) return addressStr
+ } else {
+ if (!isIPv4 && isNetworkAvailable) {
+ val endIndex = addressStr.indexOf('%')
+ return if (endIndex < 0) addressStr
+ else addressStr.substring(0, endIndex)
+ }
+ }
+ }
+ }
+ }
+ } catch (e: java.lang.Exception) { e.printStackTrace() }
+ return string(R.string.na)
+ }
+
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/feeds/WidgetHost.kt b/app/src/main/kotlin/rasel/lunar/launcher/feeds/WidgetHost.kt
new file mode 100644
index 0000000..cf9fe9d
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/feeds/WidgetHost.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package rasel.lunar.launcher.feeds
+
+import android.appwidget.AppWidgetHost
+import android.appwidget.AppWidgetHostView
+import android.appwidget.AppWidgetProviderInfo
+import android.content.Context
+
+
+internal class WidgetHost(context: Context, hostId: Int) : AppWidgetHost(context, hostId) {
+
+ override fun onCreateView(context: Context?, appWidgetId: Int, appWidget: AppWidgetProviderInfo?
+ ): AppWidgetHostView = WidgetHostView(context!!)
+
+ override fun stopListening() {
+ super.stopListening()
+ clearViews()
+ }
+
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/feeds/WidgetHostView.kt b/app/src/main/kotlin/rasel/lunar/launcher/feeds/WidgetHostView.kt
new file mode 100644
index 0000000..847f2d6
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/feeds/WidgetHostView.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package rasel.lunar.launcher.feeds
+
+import android.appwidget.AppWidgetHostView
+import android.content.Context
+import android.view.MotionEvent
+import android.view.ViewConfiguration
+import kotlin.math.abs
+
+
+internal class WidgetHostView(context: Context) : AppWidgetHostView(context) {
+
+ private var hasPerformedLongPress = false
+ private var pendingCheckForLongPress: CheckForLongPress? = null
+ private var xPos = 0f
+ private var yPos = 0f
+
+ override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
+ // Consume any touch events for ourselves after longpress is triggered
+ if (hasPerformedLongPress) {
+ hasPerformedLongPress = false
+ return true
+ }
+
+ // Watch for long press events at this level to make sure
+ // users can always pick up this widget
+ when (ev.action) {
+ MotionEvent.ACTION_DOWN -> {
+ postCheckForLongClick()
+ xPos = ev.x
+ yPos = ev.y
+ }
+ MotionEvent.ACTION_MOVE -> {
+ if (abs(ev.x - xPos) > 5 || abs(ev.y - yPos) > 5) {
+ hasPerformedLongPress = false
+ if (pendingCheckForLongPress != null) removeCallbacks(pendingCheckForLongPress)
+ }
+ }
+ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
+ hasPerformedLongPress = false
+ if (pendingCheckForLongPress != null) removeCallbacks(pendingCheckForLongPress)
+ }
+ }
+
+ // Otherwise continue letting touch events fall through to children
+ return false
+ }
+
+ internal inner class CheckForLongPress : Runnable {
+ private var originalWindowAttachCount = 0
+ override fun run() {
+ if (parent != null && hasWindowFocus()
+ && originalWindowAttachCount == windowAttachCount && !hasPerformedLongPress
+ ) {
+ if (performLongClick()) hasPerformedLongPress = true
+ }
+ }
+
+ fun rememberWindowAttachCount() { originalWindowAttachCount = windowAttachCount }
+ }
+
+ private fun postCheckForLongClick() {
+ hasPerformedLongPress = false
+ if (pendingCheckForLongPress == null) pendingCheckForLongPress = CheckForLongPress()
+ pendingCheckForLongPress!!.rememberWindowAttachCount()
+ postDelayed(pendingCheckForLongPress, ViewConfiguration.getLongPressTimeout().toLong())
+ }
+
+ override fun cancelLongPress() {
+ super.cancelLongPress()
+ hasPerformedLongPress = false
+ if (pendingCheckForLongPress != null) removeCallbacks(pendingCheckForLongPress)
+ }
+
+ override fun getDescendantFocusability(): Int = FOCUS_BLOCK_DESCENDANTS
+
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/feeds/rss/RSS.kt b/app/src/main/kotlin/rasel/lunar/launcher/feeds/rss/RSS.kt
new file mode 100644
index 0000000..d665515
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/feeds/rss/RSS.kt
@@ -0,0 +1,25 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.feeds.rss
+
+
+internal class Rss(
+ val title: String,
+ val link: String
+)
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/feeds/rss/RssAdapter.kt b/app/src/main/kotlin/rasel/lunar/launcher/feeds/rss/RssAdapter.kt
new file mode 100644
index 0000000..d76a0a0
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/feeds/rss/RssAdapter.kt
@@ -0,0 +1,85 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.feeds.rss
+
+import androidx.recyclerview.widget.RecyclerView
+import android.view.ViewGroup
+import android.view.LayoutInflater
+import android.annotation.SuppressLint
+import android.content.Context
+import android.view.Gravity
+import androidx.core.content.ContextCompat
+import android.graphics.Typeface
+import android.util.TypedValue
+import android.content.res.ColorStateList
+import android.net.Uri
+import androidx.browser.customtabs.CustomTabsIntent
+import rasel.lunar.launcher.databinding.ListItemBinding
+import rasel.lunar.launcher.helpers.UniUtils.Companion.getColorResId
+
+
+internal class RssAdapter(private val items: List, private val context: Context) :
+ RecyclerView.Adapter() {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RssViewHolder {
+ val binding = ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ return RssViewHolder(binding)
+ }
+
+ override fun getItemCount(): Int = items.size
+
+ @SuppressLint("SetTextI18n")
+ override fun onBindViewHolder(holder: RssViewHolder, position: Int) {
+ /* customize the first item */
+ if (position == 0) {
+ holder.view.itemText.apply {
+ text = "\u22B6 " + items[position].title + " \u22B7"
+ gravity = Gravity.CENTER
+ setTextColor(ContextCompat.getColor(context,
+ getColorResId(context, com.google.android.material.R.attr.colorPrimary)))
+ setTypeface(null, Typeface.BOLD)
+ textSize = 18f
+ }
+ /* reset customization for rest */
+ } else {
+ holder.view.itemText.apply {
+ text = items[position].title
+ gravity = holder.gravity
+ setTextColor(holder.color)
+ typeface = holder.typeface
+ setTextSize(TypedValue.COMPLEX_UNIT_PX, holder.size)
+ }
+ }
+
+ /* on click - open in browser */
+ holder.view.itemText.setOnClickListener {
+ val customTabsIntent = CustomTabsIntent.Builder().setUrlBarHidingEnabled(true).build()
+ customTabsIntent.launchUrl(context, Uri.parse(items[position].link))
+ }
+ }
+
+ inner class RssViewHolder(var view: ListItemBinding) : RecyclerView.ViewHolder(view.root) {
+ /* store previous styles for resetting */
+ var gravity: Int = view.itemText.gravity
+ var color: ColorStateList = view.itemText.textColors
+ var typeface: Typeface = view.itemText.typeface
+ var size: Float = view.itemText.textSize
+ }
+
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/feeds/rss/RssParser.kt b/app/src/main/kotlin/rasel/lunar/launcher/feeds/rss/RssParser.kt
new file mode 100644
index 0000000..da1e656
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/feeds/rss/RssParser.kt
@@ -0,0 +1,98 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.feeds.rss
+
+import kotlin.Throws
+import org.xmlpull.v1.XmlPullParserException
+import org.xmlpull.v1.XmlPullParser
+import android.util.Xml
+import java.io.IOException
+import java.io.InputStream
+import java.util.ArrayList
+
+
+internal class RssParser {
+
+ @Throws(XmlPullParserException::class, IOException::class)
+ fun parse(inputStream: InputStream): List {
+ return inputStream.use { stream ->
+ val parser = Xml.newPullParser()
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
+ parser.setInput(stream, null)
+ parser.nextTag()
+ readFeed(parser)
+ }
+ }
+
+ @Throws(XmlPullParserException::class, IOException::class)
+ private fun readFeed(parser: XmlPullParser): List {
+ parser.require(XmlPullParser.START_TAG, null, "rss")
+ var title: String? = null
+ var link: String? = null
+ val items: MutableList = ArrayList()
+
+ while (parser.next() != XmlPullParser.END_DOCUMENT) {
+ if (parser.eventType != XmlPullParser.START_TAG) {
+ continue
+ }
+
+ val name = parser.name
+ if (name == "title") {
+ title = readTitle(parser)
+ } else if (name == "link") {
+ link = readLink(parser)
+ }
+
+ if (title != null && link != null) {
+ val item = Rss(title, link)
+ items.add(item)
+ title = null
+ link = null
+ }
+ }
+ return items
+ }
+
+ @Throws(XmlPullParserException::class, IOException::class)
+ private fun readLink(parser: XmlPullParser): String {
+ parser.require(XmlPullParser.START_TAG, null, "link")
+ val link = readText(parser)
+ parser.require(XmlPullParser.END_TAG, null, "link")
+ return link
+ }
+
+ @Throws(XmlPullParserException::class, IOException::class)
+ private fun readTitle(parser: XmlPullParser): String {
+ parser.require(XmlPullParser.START_TAG, null, "title")
+ val title = readText(parser)
+ parser.require(XmlPullParser.END_TAG, null, "title")
+ return title
+ }
+
+ @Throws(IOException::class, XmlPullParserException::class)
+ private fun readText(parser: XmlPullParser): String {
+ var result = ""
+ if (parser.next() == XmlPullParser.TEXT) {
+ result = parser.text
+ parser.nextTag()
+ }
+ return result
+ }
+
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/feeds/rss/RssService.kt b/app/src/main/kotlin/rasel/lunar/launcher/feeds/rss/RssService.kt
new file mode 100644
index 0000000..d7e90b5
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/feeds/rss/RssService.kt
@@ -0,0 +1,72 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.feeds.rss
+
+import android.content.Intent
+import android.os.Build
+import android.os.Bundle
+import android.os.ResultReceiver
+import androidx.core.app.JobIntentService
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_RSS_URL
+import rasel.lunar.launcher.helpers.Constants.Companion.PREFS_SETTINGS
+import rasel.lunar.launcher.helpers.Constants.Companion.RSS_ITEMS
+import rasel.lunar.launcher.helpers.Constants.Companion.RSS_RECEIVER
+import java.io.IOException
+import java.io.InputStream
+import java.io.Serializable
+import java.net.URL
+
+
+internal class RssService : JobIntentService() {
+
+ override fun onHandleWork(intent: Intent) {
+ val settingsPrefs = getSharedPreferences(PREFS_SETTINGS, 0)
+ val rssUrl = settingsPrefs.getString(KEY_RSS_URL, "")
+ var rssItems: List? = null
+
+ try {
+ val parser = RssParser()
+ rssItems = getInputStream(rssUrl)?.let { parser.parse(it) }
+ } catch (exception: Exception) {
+ exception.printStackTrace()
+ }
+
+ val bundle = Bundle()
+ bundle.putSerializable(RSS_ITEMS, rssItems as? Serializable)
+
+ val receiver = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ intent.getParcelableExtra(RSS_RECEIVER, ResultReceiver::class.java)
+ } else {
+ @Suppress("DEPRECATION") intent.getParcelableExtra(RSS_RECEIVER)
+ }
+
+ receiver?.send(0, bundle)
+ }
+
+ private fun getInputStream(link: String?): InputStream? {
+ return try {
+ val url = URL(link)
+ url.openConnection().getInputStream()
+ } catch (ioException: IOException) {
+ ioException.printStackTrace()
+ null
+ }
+ }
+
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/helpers/AdminReceiver.kt b/app/src/main/kotlin/rasel/lunar/launcher/helpers/AdminReceiver.kt
new file mode 100644
index 0000000..0679c17
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/helpers/AdminReceiver.kt
@@ -0,0 +1,43 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.helpers
+
+import android.app.admin.DeviceAdminReceiver
+import android.content.Context
+import android.content.Intent
+import androidx.localbroadcastmanager.content.LocalBroadcastManager
+
+
+internal class AdminReceiver : DeviceAdminReceiver() {
+
+ override fun onDisabled(context: Context, intent: Intent) {
+ super.onDisabled(context, intent)
+ LocalBroadcastManager.getInstance(context).sendBroadcast(
+ Intent("device_admin_action_disabled")
+ )
+ }
+
+ override fun onEnabled(context: Context, intent: Intent) {
+ super.onEnabled(context, intent)
+ LocalBroadcastManager.getInstance(context).sendBroadcast(
+ Intent("device_admin_action_enabled")
+ )
+ }
+
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/helpers/ColorPicker.kt b/app/src/main/kotlin/rasel/lunar/launcher/helpers/ColorPicker.kt
new file mode 100644
index 0000000..2ceeef1
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/helpers/ColorPicker.kt
@@ -0,0 +1,100 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.helpers
+
+import android.annotation.SuppressLint
+import android.graphics.Color
+import android.text.Editable
+import android.text.TextWatcher
+import androidx.constraintlayout.widget.ConstraintLayout
+import com.google.android.material.slider.Slider
+import com.google.android.material.textfield.TextInputEditText
+
+
+internal class ColorPicker(private val initialColor: String,
+ private val editText: TextInputEditText, private val sliderA: Slider, private val sliderR: Slider,
+ private val sliderG: Slider, private val sliderB: Slider, private val colorPreview: ConstraintLayout) {
+
+ @SuppressLint("SetTextI18n")
+ fun pickColor() {
+ editText.setText(initialColor)
+ stringToSlider(initialColor)
+ colorPreview.setBackgroundColor(Color.parseColor("#$initialColor"))
+
+ editText.addTextChangedListener(object : TextWatcher {
+ override fun afterTextChanged(s: Editable) {
+ if (s.length == 6){
+ sliderA.value = 255F
+ sliderR.value = Integer.parseInt(s.substring(0..1), 16).toFloat()
+ sliderG.value = Integer.parseInt(s.substring(2..3), 16).toFloat()
+ sliderB.value = Integer.parseInt(s.substring(4..5), 16).toFloat()
+ } else if (s.length == 8){
+ stringToSlider(s.toString())
+ } else if (s.isEmpty()) {
+ sliderA.value = 0F
+ sliderR.value = 0F
+ sliderG.value = 0F
+ sliderB.value = 0F
+ }
+ }
+ override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
+ override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
+ })
+
+ sliderA.addOnChangeListener(Slider.OnChangeListener { _: Slider?, _: Float, _: Boolean ->
+ editText.setText(colorString.uppercase())
+ colorPreview.setBackgroundColor(Color.parseColor("#$colorString"))
+ })
+
+ sliderR.addOnChangeListener(Slider.OnChangeListener { _: Slider?, _: Float, _: Boolean ->
+ editText.setText(colorString.uppercase())
+ colorPreview.setBackgroundColor(Color.parseColor("#$colorString"))
+ })
+
+ sliderG.addOnChangeListener(Slider.OnChangeListener { _: Slider?, _: Float, _: Boolean ->
+ editText.setText(colorString.uppercase())
+ colorPreview.setBackgroundColor(Color.parseColor("#$colorString"))
+ })
+
+ sliderB.addOnChangeListener(Slider.OnChangeListener { _: Slider?, _: Float, _: Boolean ->
+ editText.setText(colorString.uppercase())
+ colorPreview.setBackgroundColor(Color.parseColor("#$colorString"))
+ })
+ }
+
+ private fun stringToSlider(s: String) {
+ sliderA.value = Integer.parseInt(s.substring(0..1), 16).toFloat()
+ sliderR.value = Integer.parseInt(s.substring(2..3), 16).toFloat()
+ sliderG.value = Integer.parseInt(s.substring(4..5), 16).toFloat()
+ sliderB.value = Integer.parseInt(s.substring(6..7), 16).toFloat()
+ }
+
+ private val colorString: String get() {
+ var a = Integer.toHexString((((255*sliderA.value)/sliderA.valueTo).toInt()))
+ if(a.length==1) a = "0$a"
+ var r = Integer.toHexString((((255*sliderR.value)/sliderR.valueTo).toInt()))
+ if(r.length==1) r = "0$r"
+ var g = Integer.toHexString((((255*sliderG.value)/sliderG.valueTo).toInt()))
+ if(g.length==1) g = "0$g"
+ var b = Integer.toHexString((((255*sliderB.value)/sliderB.valueTo).toInt()))
+ if(b.length==1) b = "0$b"
+ return "$a$r$g$b"
+ }
+
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/helpers/Constants.kt b/app/src/main/kotlin/rasel/lunar/launcher/helpers/Constants.kt
new file mode 100644
index 0000000..a63cc3a
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/helpers/Constants.kt
@@ -0,0 +1,110 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.helpers
+
+import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
+import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
+
+
+internal class Constants {
+
+ companion object {
+
+ /* first launch */
+ const val PREFS_FIRST_LAUNCH = "rasel.lunar.launcher.FIRST_LAUNCH"
+ const val KEY_FIRST_LAUNCH = "first_launch"
+
+ /* widgets */
+ const val PREFS_WIDGETS = "rasel.lunar.launcher.WIDGETS"
+ const val KEY_WIDGET_IDS = "widget_ids"
+ const val KEY_WIDGET_HEIGHTS = "widget_heights"
+
+ /* settings */
+ const val PREFS_SETTINGS = "rasel.lunar.launcher.SETTINGS"
+ const val KEY_TIME_FORMAT = "time_format"
+ const val KEY_DATE_FORMAT = "date_format"
+ const val KEY_CITY_NAME = "city_name"
+ const val KEY_OWM_API = "owm_api"
+ const val KEY_TEMP_UNIT = "temp_unit"
+ const val KEY_SHOW_CITY = "show_city"
+ const val KEY_TODO_COUNTS = "todo_count"
+ const val KEY_TODO_LOCK = "todo_lock"
+ const val KEY_KEYBOARD_SEARCH = "keyboard_search"
+ const val KEY_QUICK_LAUNCH = "quick_launch"
+ const val KEY_APPS_LAYOUT = "apps_layout"
+ const val KEY_APPS_COUNT = "apps_count"
+ const val KEY_DRAW_ALIGN = "drawer_alignment"
+ const val KEY_ICON_PACK = "icon_pack"
+ const val KEY_GRID_COLUMNS = "grid_columns"
+ const val KEY_SCROLLBAR_HEIGHT = "scrollbar_height"
+ const val KEY_WINDOW_BACKGROUND = "window_background"
+ const val KEY_APPLICATION_THEME = "application_theme"
+ const val KEY_STATUS_BAR = "status_bar"
+ const val KEY_BACK_HOME = "back_home"
+ const val KEY_SHORTCUT_COUNT = "shortcut_count"
+ const val KEY_ICON_SIZE = "icon_size"
+ const val KEY_RSS_URL = "rss_url"
+ const val KEY_LOCK_METHOD = "lock_method"
+
+ /* --- */
+ const val DEFAULT_DATE_FORMAT = "EEE dx MMM, yyyy"
+ const val DEFAULT_ICON_SIZE = 44
+ const val DEFAULT_ICON_PACK = "default_icon_pack"
+ const val DEFAULT_GRID_COLUMNS = 4
+ const val DEFAULT_SCROLLBAR_HEIGHT = 400
+ const val MAX_SHORTCUTS = 6
+ const val MAX_FAVORITE_APPS = 6
+
+ const val BOTTOM_SHEET_TAG = "rasel.lunar.launcher.TAG"
+ const val SEPARATOR = "||"
+ const val ACCESSIBILITY_SERVICE_LOCK_SCREEN = "rasel.lunar.launcher.LOCK_SCREEN_SERVICE"
+ const val AUTHENTICATOR_TYPE = BIOMETRIC_WEAK or DEVICE_CREDENTIAL
+
+ const val rssJobId = 101
+ const val widgetHostId = 102
+ const val requestPickWidget = 103
+ const val requestCreateWidget = 104
+
+ /* app names */
+ const val PREFS_APP_NAMES = "rasel.lunar.launcher.APP_NAMES"
+
+ /* favorite apps */
+ const val PREFS_FAVORITE_APPS = "rasel.lunar.launcher.FAVORITE_APPS"
+ const val KEY_APP_NO_ = "app_no_"
+
+ /* phone and url shortcuts */
+ const val PREFS_SHORTCUTS = "rasel.lunar.launcher.SHORTCUTS"
+ const val KEY_SHORTCUT_NO_ = "shortcut_no_"
+ const val SHORTCUT_TYPE_URL = "shortcut_type_url"
+ const val SHORTCUT_TYPE_PHONE = "shortcut_type_phone"
+
+ /* to-do database */
+ const val TODO_DATABASE_NAME = "rasel.lunar.launcher.TODOS"
+ const val TODO_DATABASE_VERSION = 1
+ const val TODO_TABLE_NAME = "todo_table"
+ const val TODO_COLUMN_ID = "todo_column_id"
+ const val TODO_COLUMN_NAME = "todo_column_name"
+ const val TODO_COLUMN_CREATED = "todo_column_created"
+
+ /* rss feed */
+ const val RSS_ITEMS = "rss_items"
+ const val RSS_RECEIVER = "rss_receiver"
+ }
+
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/helpers/LockService.kt b/app/src/main/kotlin/rasel/lunar/launcher/helpers/LockService.kt
new file mode 100644
index 0000000..9033955
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/helpers/LockService.kt
@@ -0,0 +1,56 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.helpers
+
+import android.accessibilityservice.AccessibilityService
+import android.accessibilityservice.AccessibilityServiceInfo
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.view.accessibility.AccessibilityEvent
+import android.view.accessibility.AccessibilityManager
+
+
+internal class LockService : AccessibilityService() {
+
+ override fun onAccessibilityEvent(event: AccessibilityEvent?) {}
+ override fun onInterrupt() {}
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ performGlobalAction(GLOBAL_ACTION_LOCK_SCREEN)
+ }
+ return super.onStartCommand(intent, flags, startId)
+ }
+
+ /* check whether accessibility service is enabled */
+ fun isAccessibilityServiceEnabled(context: Context): Boolean {
+ val accessibilityManager =
+ context.getSystemService(ACCESSIBILITY_SERVICE) as AccessibilityManager
+ val accessibilityServiceInfoList: List =
+ accessibilityManager.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK)
+
+ for (enabledService in accessibilityServiceInfoList) {
+ val enabledServiceInfo = enabledService.resolveInfo.serviceInfo
+ if (enabledServiceInfo.packageName == context.packageName && enabledServiceInfo.name == LockService::class.java.name) return true
+ }
+ return false
+ }
+
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/helpers/SwipeTouchListener.kt b/app/src/main/kotlin/rasel/lunar/launcher/helpers/SwipeTouchListener.kt
new file mode 100644
index 0000000..8375e1b
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/helpers/SwipeTouchListener.kt
@@ -0,0 +1,101 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.helpers
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.view.GestureDetector
+import android.view.GestureDetector.SimpleOnGestureListener
+import android.view.MotionEvent
+import android.view.View
+import android.view.View.OnTouchListener
+import android.view.ViewParent
+import kotlin.math.abs
+
+
+internal open class SwipeTouchListener(c: Context?) : OnTouchListener {
+
+ private val gestureDetector: GestureDetector = GestureDetector(c, GestureListener())
+ private lateinit var viewParent: ViewParent
+
+ @SuppressLint("ClickableViewAccessibility")
+ override fun onTouch(view: View, motionEvent: MotionEvent): Boolean {
+ viewParent = view.parent
+ return gestureDetector.onTouchEvent(motionEvent)
+ }
+
+ private inner class GestureListener : SimpleOnGestureListener() {
+
+ override fun onDown(e: MotionEvent): Boolean {
+ return true
+ }
+
+ override fun onSingleTapUp(e: MotionEvent): Boolean {
+ onClick()
+ return super.onSingleTapUp(e)
+ }
+
+ override fun onDoubleTap(e: MotionEvent): Boolean {
+ onDoubleClick()
+ return super.onDoubleTap(e)
+ }
+
+ override fun onLongPress(e: MotionEvent) {
+ onLongClick()
+ viewParent.requestDisallowInterceptTouchEvent(true)
+ super.onLongPress(e)
+ }
+
+ override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
+ try {
+ val diffY = e2.y - e1!!.y
+ val diffX = e2.x - e1.x
+ val swipeThreshold = 15
+ val swipeVelocityThreshold = 90
+ if (abs(diffX) > abs(diffY)) {
+ if (abs(diffX) > swipeThreshold && abs(velocityX) > swipeVelocityThreshold) {
+ when {
+ diffX > 0 -> onSwipeRight()
+ else -> onSwipeLeft()
+ }
+ }
+ } else {
+ if (abs(diffY) > swipeThreshold && abs(velocityY) > swipeVelocityThreshold) {
+ when {
+ diffY > 0 -> onSwipeDown()
+ else -> onSwipeUp()
+ }
+ }
+ }
+ } catch (exception: Exception) {
+ exception.printStackTrace()
+ }
+ return false
+ }
+ }
+
+ fun onSwipeRight() {}
+ fun onSwipeLeft() {}
+ open fun onSwipeUp() {}
+ open fun onSwipeDown() {}
+ open fun onClick() {}
+ open fun onDoubleClick() {}
+ open fun onLongClick() {}
+
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/helpers/UniUtils.kt b/app/src/main/kotlin/rasel/lunar/launcher/helpers/UniUtils.kt
new file mode 100644
index 0000000..e6d785a
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/helpers/UniUtils.kt
@@ -0,0 +1,270 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.helpers
+
+import android.annotation.SuppressLint
+import android.app.admin.DevicePolicyManager
+import android.content.*
+import android.content.pm.PackageManager
+import android.net.ConnectivityManager
+import android.os.Build
+import android.os.PowerManager
+import android.provider.Settings
+import android.util.DisplayMetrics
+import android.util.TypedValue
+import android.view.View
+import android.view.WindowInsets
+import android.widget.Toast
+import androidx.appcompat.widget.LinearLayoutCompat
+import androidx.biometric.BiometricManager
+import androidx.biometric.BiometricManager.BIOMETRIC_SUCCESS
+import androidx.biometric.BiometricPrompt
+import androidx.core.view.isVisible
+import com.google.android.material.imageview.ShapeableImageView
+import rasel.lunar.launcher.LauncherActivity.Companion.lActivity
+import rasel.lunar.launcher.R
+import rasel.lunar.launcher.apps.IconPackManager.Companion.getDrawableIconForPackage
+import rasel.lunar.launcher.helpers.Constants.Companion.ACCESSIBILITY_SERVICE_LOCK_SCREEN
+import rasel.lunar.launcher.helpers.Constants.Companion.AUTHENTICATOR_TYPE
+import rasel.lunar.launcher.helpers.Constants.Companion.DEFAULT_ICON_SIZE
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_APPS_LAYOUT
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_APP_NO_
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_ICON_SIZE
+import rasel.lunar.launcher.helpers.Constants.Companion.MAX_FAVORITE_APPS
+import rasel.lunar.launcher.helpers.Constants.Companion.PREFS_FAVORITE_APPS
+import rasel.lunar.launcher.helpers.Constants.Companion.PREFS_SETTINGS
+import java.io.DataOutputStream
+
+
+internal class UniUtils {
+
+ companion object {
+
+ /* get display width */
+ val screenWidth: Int get() {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ val windowMetrics = lActivity!!.windowManager.currentWindowMetrics
+ val insets = windowMetrics.windowInsets
+ .getInsetsIgnoringVisibility(WindowInsets.Type.systemBars())
+ windowMetrics.bounds.width() - insets.left - insets.right
+ } else {
+ val displayMetrics = DisplayMetrics()
+ @Suppress("DEPRECATION") lActivity!!.windowManager.defaultDisplay.getMetrics(displayMetrics)
+ displayMetrics.widthPixels
+ }
+ }
+
+ /* get display height */
+ val screenHeight: Int get() {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ val windowMetrics = lActivity!!.windowManager.currentWindowMetrics
+ val insets = windowMetrics.windowInsets
+ .getInsetsIgnoringVisibility(WindowInsets.Type.systemBars())
+ windowMetrics.bounds.height() - insets.top - insets.bottom
+ } else {
+ val displayMetrics = DisplayMetrics()
+ @Suppress("DEPRECATION") lActivity!!.windowManager.defaultDisplay.getMetrics(displayMetrics)
+ displayMetrics.heightPixels
+ }
+ }
+
+ /* copy texts to clipboard */
+ fun copyToClipboard(context: Context, copiedString: String?) {
+ val clipBoard =
+ lActivity!!.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ clipBoard.setPrimaryClip(ClipData.newPlainText("", copiedString))
+ Toast.makeText(context, context.getString(R.string.copied_message), Toast.LENGTH_SHORT).show()
+ }
+
+ /* expand notification panel */
+ @SuppressLint("WrongConstant")
+ fun expandNotificationPanel(context: Context) {
+ try {
+ Class.forName("android.app.StatusBarManager")
+ .getMethod("expandNotificationsPanel")
+ .invoke(context.getSystemService("statusbar"))
+ } catch (exception: Exception) {
+ exception.printStackTrace()
+ }
+ }
+
+ /* lock using preferred method */
+ fun lockMethod(lockMethodValue: Int, context: Context, linearLayoutCompat: LinearLayoutCompat) {
+ when (lockMethodValue) {
+ 0 -> populateFavApps(context, linearLayoutCompat)
+ 1 -> lockAccessibility()
+ 2 -> lockDeviceAdmin(context)
+ 3 -> lockRoot()
+ }
+ }
+
+ /* check if the device is rooted */
+ val isRooted: Boolean get() {
+ var process: Process? = null
+ return try {
+ process = Runtime.getRuntime().exec("su")
+ true
+ } catch (exception: Exception) {
+ exception.printStackTrace()
+ false
+ } finally {
+ if (process != null) {
+ try {
+ process.destroy()
+ } catch (exception: Exception) {
+ exception.printStackTrace()
+ }
+ }
+ }
+ }
+
+ /* check if the device is connected to the internet */
+ val isNetworkAvailable: Boolean get() {
+ val connectivityManager =
+ lActivity!!.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+ @Suppress("DEPRECATION") val activeNetworkInfo = connectivityManager.activeNetworkInfo
+ @Suppress("DEPRECATION") return activeNetworkInfo != null && activeNetworkInfo.isConnectedOrConnecting
+ }
+
+ /* check if authenticator available */
+ fun canAuthenticate(context: Context): Boolean {
+ val biometricManager = BiometricManager.from(context)
+ return biometricManager.canAuthenticate(AUTHENTICATOR_TYPE) == BIOMETRIC_SUCCESS
+ }
+
+ /* show device authenticator */
+ fun biometricPromptInfo(title: String): BiometricPrompt.PromptInfo {
+ return BiometricPrompt.PromptInfo.Builder()
+ .setTitle(title)
+ .setSubtitle(lActivity!!.getString(R.string.authentication_subtitle))
+ .setConfirmationRequired(true)
+ .setAllowedAuthenticators(AUTHENTICATOR_TYPE)
+ .build()
+ }
+
+ /* get color red id from attribute */
+ fun getColorResId(context: Context, colorAttr: Int) : Int {
+ val typedValue = TypedValue()
+ context.theme.resolveAttribute(colorAttr, typedValue, true)
+ return typedValue.resourceId
+ }
+
+ /* convert dp value to px */
+ fun dpToPx(context: Context, id: Int) : Int = (context.resources.getDimension(id) * context.resources.displayMetrics.density).toInt()
+
+ /* lock screen using device admin */
+ private fun lockDeviceAdmin(context: Context) {
+ val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
+ if (powerManager.isInteractive) {
+ val policy =
+ context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
+ try {
+ policy.lockNow()
+ } catch (exception: SecurityException) {
+ /* open device admin manager screen */
+ lActivity!!.startActivity(
+ Intent().setComponent(
+ ComponentName(
+ "com.android.settings",
+ "com.android.settings.DeviceAdminSettings"
+ )
+ )
+ )
+ exception.printStackTrace()
+ }
+ }
+ }
+
+ /* favorite apps */
+ private fun populateFavApps(context: Context, linearLayoutCompat: LinearLayoutCompat) {
+ val prefsFavApps = context.getSharedPreferences(PREFS_FAVORITE_APPS, 0)
+ if (linearLayoutCompat.isVisible || prefsFavApps.all.toString().length < 3) {
+ linearLayoutCompat.visibility = View.GONE
+ } else {
+ linearLayoutCompat.removeAllViews()
+ linearLayoutCompat.visibility = View.VISIBLE
+ val iconSize = context.getSharedPreferences(PREFS_SETTINGS, 0).getInt(KEY_ICON_SIZE, DEFAULT_ICON_SIZE)
+
+ for (position in 1..MAX_FAVORITE_APPS) {
+ val packageName = prefsFavApps.getString(KEY_APP_NO_ + position.toString(), "").toString()
+ /* package name is not empty for a specific position */
+ if (packageName.isNotEmpty()) {
+ try {
+ ShapeableImageView(context).apply {
+ layoutParams = LinearLayoutCompat.LayoutParams(
+ (iconSize * resources.displayMetrics.density).toInt(),
+ (iconSize * resources.displayMetrics.density).toInt(), 1F)
+ }.let { sImageView ->
+ context.packageManager.getApplicationIcon(packageName).let { defaultIcon ->
+ sImageView.setImageDrawable(
+ if (context.getSharedPreferences(PREFS_SETTINGS, 0).getInt(KEY_APPS_LAYOUT, 0) != 0)
+ getDrawableIconForPackage(packageName, defaultIcon)
+ else defaultIcon
+ )
+ }
+ sImageView.setOnClickListener {
+ context.startActivity(context.packageManager.getLaunchIntentForPackage(packageName))
+ }
+ linearLayoutCompat.addView(sImageView)
+ }
+ } catch (nameNotFoundException: PackageManager.NameNotFoundException) {
+ context.getSharedPreferences(PREFS_FAVORITE_APPS, 0)
+ .edit().remove(KEY_APP_NO_ + position).apply()
+ }
+ }
+ }
+ }
+ }
+
+ /* lock screen using accessibility service */
+ private fun lockAccessibility() {
+ if (LockService().isAccessibilityServiceEnabled(lActivity!!.applicationContext)) {
+ try {
+ lActivity!!.startService(
+ Intent(lActivity!!.applicationContext, LockService::class.java)
+ .setAction(ACCESSIBILITY_SERVICE_LOCK_SCREEN)
+ )
+ } catch (exception: Exception) {
+ exception.printStackTrace()
+ }
+ } else {
+ /* open accessibility service screen */
+ lActivity!!.startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
+ }
+ }
+
+ /* lock screen using root */
+ private fun lockRoot() {
+ try {
+ val process = Runtime.getRuntime().exec("su")
+ val dataOutputStream = DataOutputStream(process.outputStream)
+ dataOutputStream.writeBytes("input keyevent \${KeyEvent.KEYCODE_POWER}\n")
+ dataOutputStream.writeBytes("exit\n")
+ dataOutputStream.flush()
+ dataOutputStream.close()
+ process.waitFor()
+ process.destroy()
+ } catch (exception: Exception) {
+ exception.printStackTrace()
+ }
+ }
+
+ }
+
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/helpers/ViewPagerAdapter.kt b/app/src/main/kotlin/rasel/lunar/launcher/helpers/ViewPagerAdapter.kt
new file mode 100644
index 0000000..2184b3f
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/helpers/ViewPagerAdapter.kt
@@ -0,0 +1,34 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.helpers
+
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+import androidx.lifecycle.Lifecycle
+import androidx.viewpager2.adapter.FragmentStateAdapter
+
+
+internal class ViewPagerAdapter(
+ fragmentManager: FragmentManager, private val fragments: MutableList, lifecycle: Lifecycle) :
+ FragmentStateAdapter(fragmentManager, lifecycle) {
+
+ override fun getItemCount(): Int = fragments.size
+ override fun createFragment(position: Int): Fragment = fragments[position]
+
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/home/BatteryReceiver.kt b/app/src/main/kotlin/rasel/lunar/launcher/home/BatteryReceiver.kt
new file mode 100644
index 0000000..0ff32f5
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/home/BatteryReceiver.kt
@@ -0,0 +1,67 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.home
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.os.BatteryManager
+import android.provider.Settings
+import android.view.animation.AnimationUtils
+import com.google.android.material.progressindicator.CircularProgressIndicator
+import rasel.lunar.launcher.R
+
+
+internal class BatteryReceiver(private val progressBar: CircularProgressIndicator) : BroadcastReceiver() {
+
+ /* get current battery percentage */
+ private fun batteryPercentage(intent: Intent): Int {
+ val percentage = (intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)) /
+ (intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)).toFloat()
+ return (percentage * 100).toInt()
+ }
+
+ /* get current charging status */
+ private fun chargingStatus(intent: Intent): Int = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
+
+ override fun onReceive(context: Context?, intent: Intent?) {
+ val animationDuration = try {
+ Settings.Global.getFloat(context?.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE)
+ } catch (e: Settings.SettingNotFoundException) {
+ e.printStackTrace()
+ }
+
+ /* set battery percentage value to the circular progress bar */
+ progressBar.progress = batteryPercentage(intent!!)
+
+ /* progress bar animation */
+ if (chargingStatus(intent) == BatteryManager.BATTERY_STATUS_CHARGING ||
+ chargingStatus(intent) == BatteryManager.BATTERY_STATUS_FULL) {
+ if (progressBar.animation == null && animationDuration != 0f) {
+ progressBar.startAnimation(
+ AnimationUtils.loadAnimation(context, R.anim.rotate_clockwise)
+ )
+ }
+ } else if (chargingStatus(intent) == BatteryManager.BATTERY_STATUS_DISCHARGING ||
+ chargingStatus(intent) == BatteryManager.BATTERY_STATUS_NOT_CHARGING) {
+ progressBar.clearAnimation()
+ }
+ }
+
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/home/LauncherHome.kt b/app/src/main/kotlin/rasel/lunar/launcher/home/LauncherHome.kt
new file mode 100644
index 0000000..5dc2b28
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/home/LauncherHome.kt
@@ -0,0 +1,406 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.home
+
+import android.annotation.SuppressLint
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.SharedPreferences
+import android.os.Bundle
+import android.provider.AlarmClock
+import android.text.format.DateFormat
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+import androidx.biometric.BiometricPrompt
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+import rasel.lunar.launcher.LauncherActivity.Companion.lActivity
+import rasel.lunar.launcher.R
+import rasel.lunar.launcher.databinding.LauncherHomeBinding
+import rasel.lunar.launcher.helpers.Constants.Companion.BOTTOM_SHEET_TAG
+import rasel.lunar.launcher.helpers.Constants.Companion.DEFAULT_DATE_FORMAT
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_DATE_FORMAT
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_LOCK_METHOD
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_TIME_FORMAT
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_TODO_LOCK
+import rasel.lunar.launcher.helpers.Constants.Companion.PREFS_SETTINGS
+import rasel.lunar.launcher.helpers.SwipeTouchListener
+import rasel.lunar.launcher.helpers.UniUtils.Companion.biometricPromptInfo
+import rasel.lunar.launcher.helpers.UniUtils.Companion.canAuthenticate
+import rasel.lunar.launcher.helpers.UniUtils.Companion.expandNotificationPanel
+import rasel.lunar.launcher.helpers.UniUtils.Companion.lockMethod
+import rasel.lunar.launcher.home.weather.WeatherExecutor
+import rasel.lunar.launcher.qaccess.QuickAccess
+import rasel.lunar.launcher.settings.SettingsActivity
+import rasel.lunar.launcher.todos.TodoAdapter
+import rasel.lunar.launcher.todos.TodoManager
+import rasel.lunar.launcher.utils.SimpleFingerGestures
+import java.util.*
+
+
+internal class LauncherHome : Fragment() {
+
+ private lateinit var binding: LauncherHomeBinding
+ private lateinit var fragManager: FragmentManager
+ private lateinit var settingsPrefs: SharedPreferences
+ private lateinit var batteryReceiver: BatteryReceiver
+ private var shouldResume = true
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+
+ binding = LauncherHomeBinding.inflate(inflater, container, false)
+ fragManager = lActivity!!.supportFragmentManager
+ settingsPrefs = requireContext().getSharedPreferences(PREFS_SETTINGS, 0)
+ batteryReceiver = BatteryReceiver(binding.batteryProgress)
+
+ binding.favAppsGroup.visibility = View.GONE
+
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ /* handle gesture events */
+ rootViewGestures()
+ batteryProgressGestures()
+ todosGestures()
+
+ /* refresh the to-do list after getting back from TodoManager */
+ fragManager.addOnBackStackChangedListener {
+ shouldResume = if (fragManager.backStackEntryCount == 0) {
+ binding.root.visibility = View.VISIBLE
+ showTodoList()
+ true
+ } else {
+ binding.root.visibility = View.GONE
+ false
+ }
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ if (shouldResume) {
+ /* register battery changes */
+ requireContext().registerReceiver(batteryReceiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
+
+ /* time and date */
+ if (DateFormat.is24HourFormat(requireContext())) {
+ binding.time.format24Hour = timeFormat
+ binding.date.format24Hour = dateFormat
+ } else {
+ binding.time.format12Hour = timeFormat
+ binding.date.format12Hour = dateFormat
+ }
+
+ /* show weather */
+ WeatherExecutor(settingsPrefs).generateWeatherString(binding.weather)
+ /* show to-do list */
+ showTodoList()
+ }
+ }
+
+ override fun onPause() {
+ super.onPause()
+ /* unregister battery changes */
+ if (shouldResume) requireContext().unregisterReceiver(batteryReceiver)
+ }
+
+
+ var mFingerGestureListener = object : SimpleFingerGestures.OnFingerGestureListener {
+ override fun onSwipeUp(
+ targetView: View,
+ fingers: Int,
+ gestureDuration: Long,
+ gestureDistance: Double
+ ): Boolean {
+ when(fingers) {
+ 1 ->
+ if (targetView?.equals(binding.batteryProgress) ?: false) {
+ QuickAccess().show(fragManager, BOTTOM_SHEET_TAG)
+ } else {
+ QuickAccess().show(fragManager, BOTTOM_SHEET_TAG)
+ }
+ else -> {}
+ }
+ return false
+ }
+
+ override fun onSwipeDown(
+ targetView: View,
+ fingers: Int,
+ gestureDuration: Long,
+ gestureDistance: Double
+ ): Boolean {
+ when(fingers) {
+ 1 ->
+ if (targetView?.equals(binding.batteryProgress) ?: false) {
+ expandNotificationPanel(requireContext())
+ } else {
+ expandNotificationPanel(requireContext())
+ }
+ else -> {}
+ }
+ return false
+ }
+
+ override fun onSwipeLeft(
+ targetView: View,
+ fingers: Int,
+ gestureDuration: Long,
+ gestureDistance: Double
+ ): Boolean {
+ return false
+ }
+
+ override fun onSwipeRight(
+ targetView: View,
+ fingers: Int,
+ gestureDuration: Long,
+ gestureDistance: Double
+ ): Boolean {
+ return false
+ }
+
+ override fun onPinch(
+ targetView: View,
+ fingers: Int,
+ gestureDuration: Long,
+ gestureDistance: Double
+ ): Boolean {
+ return false
+ }
+
+ override fun onUnpinch(
+ targetView: View,
+ fingers: Int,
+ gestureDuration: Long,
+ gestureDistance: Double
+ ): Boolean {
+ return false
+ }
+
+ override fun onDoubleTap(targetView: View,fingers: Int): Boolean {
+
+ when(fingers) {
+ 1 -> if (targetView?.equals(binding.batteryProgress) ?: false) {
+ lockMethod(settingsPrefs.getInt(KEY_LOCK_METHOD, 0), requireContext(), binding.favAppsGroup)
+ } else {
+ lockMethod(settingsPrefs.getInt(KEY_LOCK_METHOD, 0), requireContext(), binding.favAppsGroup)
+ }
+ else -> {}
+ }
+ return false
+ }
+
+ override fun onLongPress(targetView: View): Boolean {
+ if (view?.equals(binding.batteryProgress) ?: false) {
+ lActivity!!.startActivity(Intent(requireContext(), SettingsActivity::class.java))
+ } else if (view?.equals(binding.notes) ?: false) {
+ when (settingsPrefs.getBoolean(KEY_TODO_LOCK, false)) {
+ false -> launchTodoManager()
+ /* show authentication screen if lock is on */
+ true -> {
+ if (canAuthenticate(requireContext())) {
+ val biometricPrompt = BiometricPrompt(lActivity!!, authenticationCallback)
+ try {
+ biometricPrompt.authenticate(biometricPromptInfo(lActivity!!.getString(R.string.todo_manager)))
+ } catch (exception: Exception) {
+ exception.printStackTrace()
+ }
+ }
+ }
+ }
+ }
+ return false
+ }
+
+ override fun onClick(targetView: View): Boolean {
+ if (view?.equals(binding.batteryProgress) ?: false) {
+ requireContext().startActivity(
+ Intent(AlarmClock.ACTION_SHOW_ALARMS).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ )
+ } else if (view?.equals(binding.batteryProgress) ?: false) {
+
+ }
+ return false
+ }
+
+ }
+
+ /* gestures on root view */
+ @SuppressLint("ClickableViewAccessibility")
+ private fun rootViewGestures() {
+
+ binding.root.setOnTouchListener(SimpleFingerGestures(context = requireContext(), binding.root , mFingerGestureListener))
+// {
+
+ /* open quick access panel on swipe up */
+// override fun onSwipeUp() {
+// super.onSwipeUp()
+// QuickAccess().show(fragManager, BOTTOM_SHEET_TAG)
+// }
+// /* expand notification panel on swipe down */
+// override fun onSwipeDown() {
+// super.onSwipeDown()
+// expandNotificationPanel(requireContext())
+// }
+// /* lock the screen on double tap (optional) */
+// override fun onDoubleClick() {
+// super.onDoubleClick()
+// lockMethod(settingsPrefs.getInt(KEY_LOCK_METHOD, 0), requireContext(), binding.favAppsGroup)
+// }
+// })
+ }
+
+
+
+ /* gestures on battery progress indicator area */
+ @SuppressLint("ClickableViewAccessibility")
+ private fun batteryProgressGestures() {
+ binding.batteryProgress.setOnTouchListener(SimpleFingerGestures(context = requireContext(), binding.batteryProgress , mFingerGestureListener))
+// binding.batteryProgress.setOnTouchListener(object : SwipeTouchListener(requireContext()) {
+// /* open alarms list with default clock app */
+// override fun onClick() {
+// super.onClick()
+// requireContext().startActivity(
+// Intent(AlarmClock.ACTION_SHOW_ALARMS).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+// )
+// }
+// /* open settings activity on long click */
+// override fun onLongClick() {
+// super.onLongClick()
+// lActivity!!.startActivity(Intent(requireContext(), SettingsActivity::class.java))
+// }
+// /* expand notification panel on swipe down */
+// override fun onSwipeDown() {
+// super.onSwipeDown()
+// expandNotificationPanel(requireContext())
+// }
+// /* lock the screen on double tap (optional) */
+// override fun onDoubleClick() {
+// super.onDoubleClick()
+// lockMethod(settingsPrefs.getInt(KEY_LOCK_METHOD, 0), requireContext(), binding.favAppsGroup)
+// }
+// })
+ }
+
+ /* gestures on to-do area */
+ @SuppressLint("ClickableViewAccessibility")
+ private fun todosGestures() {
+ binding.notes.setOnTouchListener(SimpleFingerGestures(context = requireContext(), binding.notes , mFingerGestureListener))
+// binding.notes.setOnTouchListener(object : SwipeTouchListener(requireContext()) {
+// /* open TodoManager on long click */
+// override fun onLongClick() {
+// super.onLongClick()
+// when (settingsPrefs.getBoolean(KEY_TODO_LOCK, false)) {
+// false -> launchTodoManager()
+// /* show authentication screen if lock is on */
+// true -> {
+// if (canAuthenticate(requireContext())) {
+// val biometricPrompt = BiometricPrompt(lActivity!!, authenticationCallback)
+// try {
+// biometricPrompt.authenticate(biometricPromptInfo(lActivity!!.getString(R.string.todo_manager)))
+// } catch (exception: Exception) {
+// exception.printStackTrace()
+// }
+// }
+// }
+// }
+// }
+// /* open quick access panel on swipe up */
+// override fun onSwipeUp() {
+// super.onSwipeUp()
+// QuickAccess().show(fragManager, BOTTOM_SHEET_TAG)
+// }
+// /* expand notification panel on swipe down */
+// override fun onSwipeDown() {
+// super.onSwipeDown()
+// expandNotificationPanel(requireContext())
+// }
+// /* lock the screen on double tap (optional) */
+// override fun onDoubleClick() {
+// super.onDoubleClick()
+// lockMethod(settingsPrefs.getInt(KEY_LOCK_METHOD, 0), requireContext(), binding.favAppsGroup)
+// }
+// })
+ }
+
+ /* authentication callback for TodoManager lock */
+ private val authenticationCallback = object : BiometricPrompt.AuthenticationCallback() {
+ override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
+ launchTodoManager()
+ }
+ override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
+ Toast.makeText(requireContext(), lActivity!!.getString(R.string.authentication_error), Toast.LENGTH_SHORT).show()
+ }
+ override fun onAuthenticationFailed() {
+ Toast.makeText(requireContext(), lActivity!!.getString(R.string.authentication_failed), Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ /* launch TodoManager fragment */
+ private fun launchTodoManager() {
+ fragManager.beginTransaction().replace(R.id.mainFragmentsContainer, TodoManager())
+ .addToBackStack("").commit()
+ }
+
+ /* to-do list */
+ private fun showTodoList() {
+ binding.notes.adapter = TodoAdapter(null, requireContext())
+ }
+
+ /* get time format string */
+ private val timeFormat: String? get() {
+ when (settingsPrefs.getInt(KEY_TIME_FORMAT, 0)) {
+ 0 -> return if (DateFormat.is24HourFormat(requireContext())) {
+ "kk:mm"
+ } else {
+ "h:mm a"
+ }
+ 1 -> return "h:mm a"
+ 2 -> return "kk:mm"
+ }
+ return null
+ }
+
+ /* get date number suffix */
+ private val dateNumberSuffix: String get() {
+ return when (Calendar.getInstance()[Calendar.DAY_OF_MONTH]) {
+ 1, 21, 31 -> "ˢᵗ"
+ 2, 22 -> "ⁿᵈ"
+ 3, 23 -> "ʳᵈ"
+ else -> "ᵗʰ"
+ }
+ }
+
+ /* get date format string */
+ private val dateFormat: String get() {
+ settingsPrefs.getString(KEY_DATE_FORMAT, DEFAULT_DATE_FORMAT).let {
+ return if (it!!.contains("x")) {
+ it.replace("x", dateNumberSuffix)
+ } else {
+ it
+ }
+ }
+ }
+
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/home/weather/JsonParser.kt b/app/src/main/kotlin/rasel/lunar/launcher/home/weather/JsonParser.kt
new file mode 100644
index 0000000..745eaaa
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/home/weather/JsonParser.kt
@@ -0,0 +1,52 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.home.weather
+
+import org.json.JSONException
+import org.json.JSONObject
+
+
+internal class JsonParser {
+
+ fun getMyWeather(jsonStr: String): Weather {
+ val weather = Weather()
+
+ try {
+ val jsonObject = JSONObject(jsonStr)
+
+ /* Get weather condition */
+ val weatherArray = jsonObject.getJSONArray("weather")
+ val weatherObject = weatherArray.getJSONObject(0)
+ weather.weatherCondition = weatherObject.getString("main")
+ weather.weatherDescription = weatherObject.getString("description")
+ weather.weatherIconId = weatherObject.getString("icon")
+
+ /* Get temperature */
+ val mainObject = jsonObject.getJSONObject("main")
+ weather.temperature = mainObject.getDouble("temp").toFloat()
+
+ weather.cityName = jsonObject.getString("name")
+ } catch (jsonException: JSONException) {
+ jsonException.printStackTrace()
+ }
+
+ return weather
+ }
+
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/home/weather/Weather.kt b/app/src/main/kotlin/rasel/lunar/launcher/home/weather/Weather.kt
new file mode 100644
index 0000000..e146213
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/home/weather/Weather.kt
@@ -0,0 +1,28 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.home.weather
+
+
+internal class Weather {
+ var weatherCondition: String? = null
+ var weatherDescription: String? = null
+ var weatherIconId: String? = null
+ var temperature = 0f
+ var cityName: String? = null
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/home/weather/WeatherClient.kt b/app/src/main/kotlin/rasel/lunar/launcher/home/weather/WeatherClient.kt
new file mode 100644
index 0000000..5869de1
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/home/weather/WeatherClient.kt
@@ -0,0 +1,63 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.home.weather
+
+import java.io.BufferedReader
+import java.io.InputStreamReader
+import java.lang.Exception
+import java.lang.StringBuilder
+import java.net.HttpURLConnection
+import java.net.URL
+
+
+internal class WeatherClient {
+
+ fun fetchWeather(wUrl: String?): String? {
+ var httpURLConnection: HttpURLConnection? = null
+ var bufferedReader: BufferedReader? = null
+
+ try {
+ httpURLConnection = URL(wUrl).openConnection() as HttpURLConnection
+ httpURLConnection.connect()
+ bufferedReader = BufferedReader(InputStreamReader(httpURLConnection.inputStream))
+
+ val stringBuilder = StringBuilder()
+ var line: String?
+ while (bufferedReader.readLine().also { line = it } != null) {
+ stringBuilder.append("$line\n")
+ }
+
+ if (stringBuilder.isNotEmpty()) return stringBuilder.toString()
+ } catch (exception: Exception) {
+ exception.printStackTrace()
+ } finally {
+ httpURLConnection?.disconnect()
+ if (bufferedReader != null) {
+ try {
+ bufferedReader.close()
+ } catch (exception: Exception) {
+ exception.printStackTrace()
+ }
+ }
+ }
+
+ return null
+ }
+
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/home/weather/WeatherExecutor.kt b/app/src/main/kotlin/rasel/lunar/launcher/home/weather/WeatherExecutor.kt
new file mode 100644
index 0000000..657fbed
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/home/weather/WeatherExecutor.kt
@@ -0,0 +1,83 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.home.weather
+
+import android.annotation.SuppressLint
+import android.content.SharedPreferences
+import android.os.Handler
+import android.os.Looper
+import android.view.View
+import com.google.android.material.textview.MaterialTextView
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_CITY_NAME
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_OWM_API
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_SHOW_CITY
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_TEMP_UNIT
+import rasel.lunar.launcher.helpers.UniUtils.Companion.isNetworkAvailable
+import java.net.URLEncoder
+import java.util.concurrent.Executors
+
+
+internal class WeatherExecutor(sharedPreferences: SharedPreferences) {
+
+ private val cityName: String
+ private val owmApi: String
+ private val weatherUrl: String
+ private val tempUnit: Int
+ private val showCity: Boolean
+
+ @SuppressLint("SetTextI18n")
+ fun generateWeatherString(materialTextView: MaterialTextView) {
+ materialTextView.visibility = View.GONE
+
+ /* run the executor if network is available,
+ and city name and owm api values are not empty */
+ if (isNetworkAvailable && cityName.isNotEmpty() && owmApi.isNotEmpty()) {
+ try {
+ Executors.newSingleThreadExecutor().execute {
+ var weather: Weather? = null
+ WeatherClient().fetchWeather(weatherUrl).let {
+ if (!it.isNullOrEmpty()) weather = JsonParser().getMyWeather(it)
+ }
+
+ Handler(Looper.getMainLooper()).post {
+ if (weather != null) {
+ materialTextView.apply {
+ visibility = View.VISIBLE
+ text = weather!!.temperature.toString().substringBefore(".") +
+ (if (tempUnit == 0) "ºC" else "ºF") +
+ (if (showCity) " at ${weather!!.cityName}" else "")
+ }
+ }
+ }
+ }
+ } catch (exception: Exception) {
+ exception.printStackTrace()
+ }
+ }
+ }
+
+ init {
+ cityName = sharedPreferences.getString(KEY_CITY_NAME, "").toString()
+ owmApi = sharedPreferences.getString(KEY_OWM_API, "").toString()
+ tempUnit = sharedPreferences.getInt(KEY_TEMP_UNIT, 0)
+ showCity = sharedPreferences.getBoolean(KEY_SHOW_CITY, false)
+ weatherUrl = URLEncoder.encode("https://api.openweathermap.org/data/2.5/weather?q=$cityName&APPID=$owmApi&units=" + if (tempUnit == 0) "metric" else "imperial","utf-8")
+ }
+
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/qaccess/QuickAccess.kt b/app/src/main/kotlin/rasel/lunar/launcher/qaccess/QuickAccess.kt
new file mode 100644
index 0000000..c5b1463
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/qaccess/QuickAccess.kt
@@ -0,0 +1,384 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.qaccess
+
+import android.Manifest
+import android.app.AlertDialog
+import android.content.Context
+import android.content.Intent
+import android.content.SharedPreferences
+import android.content.pm.PackageManager
+import android.graphics.*
+import android.media.AudioManager
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.os.PowerManager
+import android.provider.Settings
+import android.text.InputType
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.RelativeLayout
+import androidx.appcompat.widget.LinearLayoutCompat
+import androidx.core.content.ContextCompat
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import com.google.android.material.button.MaterialButtonToggleGroup
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.android.material.slider.Slider
+import com.google.android.material.textview.MaterialTextView
+import rasel.lunar.launcher.LauncherActivity.Companion.lActivity
+import rasel.lunar.launcher.R
+import rasel.lunar.launcher.databinding.QuickAccessBinding
+import rasel.lunar.launcher.databinding.ShortcutMakerBinding
+import rasel.lunar.launcher.helpers.ColorPicker
+import rasel.lunar.launcher.helpers.Constants.Companion.DEFAULT_ICON_SIZE
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_ICON_SIZE
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_SHORTCUT_COUNT
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_SHORTCUT_NO_
+import rasel.lunar.launcher.helpers.Constants.Companion.MAX_SHORTCUTS
+import rasel.lunar.launcher.helpers.Constants.Companion.PREFS_SETTINGS
+import rasel.lunar.launcher.helpers.Constants.Companion.PREFS_SHORTCUTS
+import rasel.lunar.launcher.helpers.Constants.Companion.SEPARATOR
+import rasel.lunar.launcher.helpers.Constants.Companion.SHORTCUT_TYPE_PHONE
+import rasel.lunar.launcher.helpers.Constants.Companion.SHORTCUT_TYPE_URL
+import java.util.*
+import kotlin.properties.Delegates
+
+
+internal class QuickAccess : BottomSheetDialogFragment() {
+
+ private lateinit var binding: QuickAccessBinding
+ private lateinit var sharedPreferences: SharedPreferences
+ private var iconSize by Delegates.notNull()
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ binding = QuickAccessBinding.inflate(inflater, container, false)
+
+ sharedPreferences = requireContext().getSharedPreferences(PREFS_SHORTCUTS, 0)
+ iconSize = requireContext().getSharedPreferences(PREFS_SETTINGS, 0).getInt(KEY_ICON_SIZE, DEFAULT_ICON_SIZE)
+
+ /* set up volume sliders, brightness slider and favorite apps */
+ volumeControllers()
+ controlBrightness()
+
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ /* enable dismiss animation */
+ (requireDialog() as BottomSheetDialog).dismissWithAnimation = true
+ }
+
+ override fun onResume() {
+ super.onResume()
+ /* repopulate shortcuts and apps */
+ shortcuts()
+ }
+
+ /* control the volumes */
+ private fun volumeControllers() {
+ val audioManager = lActivity!!.getSystemService(Context.AUDIO_SERVICE) as AudioManager
+ /* max value */
+ binding.notification.valueTo = audioManager.getStreamMaxVolume(AudioManager.STREAM_NOTIFICATION).toFloat()
+ binding.alarm.valueTo = audioManager.getStreamMaxVolume(AudioManager.STREAM_ALARM).toFloat()
+ binding.media.valueTo = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC).toFloat()
+ binding.voice.valueTo = audioManager.getStreamMaxVolume(AudioManager.STREAM_VOICE_CALL).toFloat()
+ binding.ring.valueTo = audioManager.getStreamMaxVolume(AudioManager.STREAM_RING).toFloat()
+ /* current value */
+ binding.notification.value = audioManager.getStreamVolume(AudioManager.STREAM_NOTIFICATION).toFloat()
+ binding.alarm.value = audioManager.getStreamVolume(AudioManager.STREAM_ALARM).toFloat()
+ binding.media.value = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC).toFloat()
+ binding.voice.value = audioManager.getStreamVolume(AudioManager.STREAM_VOICE_CALL).toFloat()
+ binding.ring.value = audioManager.getStreamVolume(AudioManager.STREAM_RING).toFloat()
+
+ /* slider change listener for alarm volume */
+ binding.alarm.addOnChangeListener(Slider.OnChangeListener { _: Slider?, value: Float, _: Boolean ->
+ audioManager.setStreamVolume(AudioManager.STREAM_ALARM, value.toInt(), 0)
+ })
+
+ /* slider change listener for media volume */
+ binding.media.addOnChangeListener(Slider.OnChangeListener { _: Slider?, value: Float, _: Boolean ->
+ audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, value.toInt(), 0)
+ })
+
+ /* slider change listener for voice call volume */
+ binding.voice.addOnChangeListener(Slider.OnChangeListener { _: Slider?, value: Float, _: Boolean ->
+ audioManager.setStreamVolume(AudioManager.STREAM_VOICE_CALL, value.toInt(), 0)
+ })
+
+ /* notify and ring volume sliders will work only if
+ the device isn't in dnd or silent mode */
+ if (Settings.Global.getInt(lActivity!!.contentResolver, "zen_mode") == 0 &&
+ audioManager.ringerMode != AudioManager.RINGER_MODE_SILENT) {
+ /* slider change listener for notify volume */
+ binding.notification.addOnChangeListener(Slider.OnChangeListener { _: Slider?, value: Float, _: Boolean ->
+ audioManager.setStreamVolume(AudioManager.STREAM_NOTIFICATION, value.toInt(), 0)
+ })
+ /* slider change listener for ring volume */
+ binding.ring.addOnChangeListener(Slider.OnChangeListener { _: Slider?, value: Float, _: Boolean ->
+ audioManager.setStreamVolume(AudioManager.STREAM_RING, value.toInt(), 0)
+ })
+ } else {
+ binding.notification.isEnabled = false
+ binding.ring.isEnabled = false
+ }
+ }
+
+ /* set up contact and url shortcuts */
+ private fun shortcuts() {
+ binding.shortcutsGroup.removeAllViews()
+ val shortcutCount =
+ requireContext().getSharedPreferences(PREFS_SETTINGS, 0).getInt(KEY_SHORTCUT_COUNT, MAX_SHORTCUTS)
+ if (shortcutCount == 0) binding.shortcutsGroup.visibility = View.GONE
+
+ for (position in 1..shortcutCount) {
+ val shortcutValue = sharedPreferences.getString(KEY_SHORTCUT_NO_ + position.toString(), "").toString()
+ val splitShortcutValue = shortcutValue.split(SEPARATOR).toTypedArray()
+
+ var shortcutType = ""
+ var intentString = ""
+ var thumbLetter = ""
+ var color = ""
+
+ try {
+ if (splitShortcutValue.size >= 4) {
+ shortcutType = splitShortcutValue[0]
+ intentString = splitShortcutValue[1]
+ thumbLetter = splitShortcutValue[2]
+ color = splitShortcutValue[3]
+ }
+ } catch (exception : Exception) {
+ exception.printStackTrace()
+ }
+
+ shortcutsUtil(textView, shortcutType, intentString, thumbLetter, color, position)
+ }
+ }
+
+ /* control the brightness */
+ private fun controlBrightness() {
+ val resolver = lActivity!!.contentResolver
+ /* set max value */
+ binding.brightness.valueTo = maxBrightness
+
+ /* set slider value to current brightness value */
+ try {
+ binding.brightness.value = Settings.System.getInt(resolver, Settings.System.SCREEN_BRIGHTNESS).toFloat()
+ } catch (settingNotFoundException: Settings.SettingNotFoundException) {
+ settingNotFoundException.printStackTrace()
+ }
+
+ /* listen slider value changes */
+ binding.brightness.addOnChangeListener(Slider.OnChangeListener { _: Slider?, value: Float, _: Boolean ->
+ /* if write settings permission is not allowed already,
+ again ask for it to be granted */
+ if (!Settings.System.canWrite(lActivity!!)) {
+ lActivity!!.startActivity(
+ Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS)
+ .setData(Uri.parse("package:" + lActivity!!.packageName))
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ )
+ /* set the brightness according to the slider value */
+ } else {
+ Settings.System.putInt(
+ resolver,
+ Settings.System.SCREEN_BRIGHTNESS_MODE,
+ Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL
+ )
+ Settings.System.putInt(resolver, Settings.System.SCREEN_BRIGHTNESS, value.toInt())
+ }
+ })
+ }
+
+ /* contact/url shortcuts */
+ private fun shortcutsUtil(textView: MaterialTextView, shortcutType: String, intentString: String,
+ thumbLetter: String, color: String, position: Int) {
+ /* show plus sign for empty positions and set click listener */
+ if (intentString.isEmpty()) {
+ textView.text = "+"
+ textView.setOnClickListener {
+ shortcutsSaverDialog(position, "00000000", "", "", "")
+ }
+ } else {
+ /* show thumbnail letter */
+ textView.text = thumbLetter
+ /* set background color */
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ textView.background.colorFilter =
+ BlendModeColorFilter(Color.parseColor("#$color"), BlendMode.MULTIPLY)
+ } else {
+ @Suppress("DEPRECATION")
+ textView.background.setColorFilter(Color.parseColor("#$color"), PorterDuff.Mode.MULTIPLY)
+ }
+
+ /* on normal click */
+ textView.setOnClickListener {
+ /* type is url */
+ if (shortcutType == SHORTCUT_TYPE_URL) {
+ var url = intentString
+ /* add http before the url if it doesn't have http/https prefix */
+ if (!url.startsWith("http://") && !url.startsWith("https://")) {
+ url = "http://$intentString"
+ }
+ /* open the url */
+ lActivity!!.startActivity(
+ Intent(Intent.ACTION_VIEW, Uri.parse(url)).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ )
+ /* type is contact */
+ } else if (shortcutType == SHORTCUT_TYPE_PHONE) {
+ /* if the necessary permission is not granted already,
+ ask for it again */
+ if (lActivity!!.checkSelfPermission(Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
+ lActivity!!.requestPermissions(arrayOf(Manifest.permission.CALL_PHONE), 1)
+ } else {
+ /* make phone call */
+ lActivity!!.startActivity(
+ Intent(Intent.ACTION_CALL, Uri.parse("tel:$intentString"))
+ )
+ }
+ }
+ this.dismiss()
+ }
+
+ /* reset the shortcut on long click */
+ textView.setOnLongClickListener {
+ shortcutsSaverDialog(position, color, thumbLetter, shortcutType, intentString)
+ true
+ }
+ }
+ }
+
+ /* dialog for creating shortcuts */
+ private fun shortcutsSaverDialog(
+ position: Int, color: String, thumbLetter: String, shortcutType: String, intentString: String) {
+ val dialogBinding = ShortcutMakerBinding.inflate(lActivity!!.layoutInflater)
+ val dialogBuilder = MaterialAlertDialogBuilder(lActivity!!)
+ .setView(dialogBinding.root)
+ .setNeutralButton(R.string.delete, null)
+ .setNegativeButton(android.R.string.cancel, null)
+ .setPositiveButton(android.R.string.ok, null)
+ .show()
+
+ dialogBinding.thumbField.setText(thumbLetter)
+ dialogBinding.inputField.setText(intentString)
+ when (shortcutType) {
+ SHORTCUT_TYPE_PHONE -> dialogBinding.shortcutType.check(dialogBinding.contact.id)
+ SHORTCUT_TYPE_URL -> dialogBinding.shortcutType.check(dialogBinding.url.id)
+ }
+
+ /* set up color picker section */
+ ColorPicker(color, dialogBinding.colorPicker.colorInput, dialogBinding.colorPicker.colorA,
+ dialogBinding.colorPicker.colorR, dialogBinding.colorPicker.colorG,
+ dialogBinding.colorPicker.colorB, dialogBinding.root).pickColor()
+
+ /* shortcut type chooser - contact/url */
+ var updatedShortcutType = shortcutType
+ dialogBinding.shortcutType.addOnButtonCheckedListener {
+ _: MaterialButtonToggleGroup?, checkedId: Int, isChecked: Boolean ->
+ if (isChecked) {
+ when (checkedId) {
+ dialogBinding.contact.id -> {
+ updatedShortcutType = SHORTCUT_TYPE_PHONE
+ dialogBinding.inputField.inputType = InputType.TYPE_CLASS_PHONE
+ }
+ dialogBinding.url.id -> {
+ updatedShortcutType = SHORTCUT_TYPE_URL
+ dialogBinding.inputField.inputType = InputType.TYPE_TEXT_VARIATION_URI
+ }
+ }
+ }
+ }
+
+ dialogBuilder.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener {
+ sharedPreferences.edit().remove(KEY_SHORTCUT_NO_ + position).apply()
+ dialogBuilder.dismiss()
+ this.onResume()
+ }
+
+ /* save the shortcut values */
+ dialogBuilder.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
+ /* get shortcut value */
+ val updatedIntentString =
+ Objects.requireNonNull(dialogBinding.inputField.text).toString().trim { it <= ' ' }
+ /* get thumbnail letter */
+ val updatedThumbLetter =
+ Objects.requireNonNull(dialogBinding.thumbField.text).toString().trim { it <= ' ' }.uppercase()
+ /* get color value */
+ val updatedColor =
+ Objects.requireNonNull(dialogBinding.colorPicker.colorInput.text).toString().trim { it <= ' ' }
+
+ /* save the values if every field is filled */
+ if (updatedShortcutType.isNotEmpty() && updatedIntentString.isNotEmpty() &&
+ updatedThumbLetter.isNotEmpty() && updatedColor.isNotEmpty()) {
+ sharedPreferences.edit().putString(KEY_SHORTCUT_NO_ + position,
+ "$updatedShortcutType$SEPARATOR$updatedIntentString$SEPARATOR" +
+ "$updatedThumbLetter$SEPARATOR$updatedColor").apply()
+ dialogBuilder.dismiss()
+ this.onResume()
+ }
+ }
+ }
+
+ /* create text view for shortcut thumbnails */
+ private val textView: MaterialTextView get() {
+ val relativeLayout = RelativeLayout(lActivity!!)
+ relativeLayout.apply {
+ layoutParams = LinearLayoutCompat.LayoutParams(
+ LinearLayoutCompat.LayoutParams.WRAP_CONTENT,
+ LinearLayoutCompat.LayoutParams.WRAP_CONTENT, 1F)
+ gravity = Gravity.CENTER
+ }
+ binding.shortcutsGroup.addView(relativeLayout)
+
+ MaterialTextView(requireContext()).apply {
+ layoutParams = LinearLayoutCompat.LayoutParams(
+ (iconSize * resources.displayMetrics.density).toInt(),
+ (iconSize * resources.displayMetrics.density).toInt())
+ gravity = Gravity.CENTER
+ textSize = (iconSize / 4) * resources.displayMetrics.density
+ setTypeface(null, Typeface.BOLD)
+ background = ContextCompat.getDrawable(requireContext(), R.drawable.rounded_bg)
+ }.let {
+ relativeLayout.addView(it)
+ return it
+ }
+ }
+
+ /* returns maximum brightness value of the device */
+ private val maxBrightness: Float get() {
+ val powerManager = requireContext().getSystemService(Context.POWER_SERVICE) as PowerManager
+ var value = 255f
+ for (f in powerManager.javaClass.declaredFields) {
+ if (f.name.equals("BRIGHTNESS_ON")) {
+ f.isAccessible = true
+ value = try {
+ f.getInt(powerManager).toFloat()
+ } catch (e: IllegalAccessException) {
+ 255f
+ }
+ }
+ }
+ return value
+ }
+
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/settings/SettingsActivity.kt b/app/src/main/kotlin/rasel/lunar/launcher/settings/SettingsActivity.kt
new file mode 100644
index 0000000..ac73576
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/settings/SettingsActivity.kt
@@ -0,0 +1,148 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.settings
+
+import android.annotation.SuppressLint
+import android.content.Intent
+import android.content.SharedPreferences
+import android.content.res.Resources
+import android.net.Uri
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import com.google.android.material.color.DynamicColors
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import rasel.lunar.launcher.BuildConfig
+import rasel.lunar.launcher.R
+import rasel.lunar.launcher.databinding.AboutBinding
+import rasel.lunar.launcher.databinding.SettingsActivityBinding
+import rasel.lunar.launcher.helpers.Constants.Companion.BOTTOM_SHEET_TAG
+import rasel.lunar.launcher.helpers.Constants.Companion.PREFS_SETTINGS
+import rasel.lunar.launcher.settings.childs.*
+
+
+internal class SettingsActivity : AppCompatActivity() {
+
+ private lateinit var binding: SettingsActivityBinding
+ private val sourceCode = "https://github.com/iamrasel/lunar-launcher"
+
+ companion object {
+ @JvmStatic var settingsPrefs: SharedPreferences? = null
+ }
+
+ @SuppressLint("SetTextI18n")
+ override fun onCreate(savedInstanceState: Bundle?) {
+ DynamicColors.applyToActivityIfAvailable(this)
+ super.onCreate(savedInstanceState)
+
+ /* set up view */
+ binding = SettingsActivityBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ settingsPrefs = this.getSharedPreferences(PREFS_SETTINGS, 0)
+
+ /* launch child settings dialogs on button clicks */
+ binding.timeDate.setOnClickListener {
+ TimeDate().show(supportFragmentManager, BOTTOM_SHEET_TAG)
+ }
+
+ binding.weather.setOnClickListener {
+ WeatherSettings().show(supportFragmentManager, BOTTOM_SHEET_TAG)
+ }
+
+ binding.todo.setOnClickListener {
+ TodoSettings().show(supportFragmentManager, BOTTOM_SHEET_TAG)
+ }
+
+ binding.apps.setOnClickListener {
+ Apps().show(supportFragmentManager, BOTTOM_SHEET_TAG)
+ }
+
+ binding.appearances.setOnClickListener {
+ Appearances().show(supportFragmentManager, BOTTOM_SHEET_TAG)
+ }
+
+ binding.misc.setOnClickListener {
+ Misc().show(supportFragmentManager, BOTTOM_SHEET_TAG)
+ }
+
+ binding.advance.setOnClickListener {
+ Advance().show(supportFragmentManager, BOTTOM_SHEET_TAG)
+ }
+
+ /* about and support dialogs */
+ binding.about.setOnClickListener { aboutDialog() }
+ binding.support.setOnClickListener { supportDialog() }
+
+ /* show app version name */
+ binding.version.text = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})"
+ }
+
+ override fun getTheme(): Resources.Theme {
+ val theme = super.getTheme()
+ theme.applyStyle(R.style.SettingsNavBar, true)
+ return theme
+ }
+
+ /* about dialog */
+ private fun aboutDialog() {
+ val bottomSheetDialog = BottomSheetDialog(this)
+ val aboutBinding = AboutBinding.inflate(this.layoutInflater)
+ bottomSheetDialog.setContentView(aboutBinding.root)
+ bottomSheetDialog.show()
+ bottomSheetDialog.dismissWithAnimation = true
+
+ /* source code at github */
+ aboutBinding.sourceCode.setOnClickListener {
+ bottomSheetDialog.dismiss()
+ startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(sourceCode)))
+ }
+ /* wiki at github */
+ aboutBinding.wiki.setOnClickListener {
+ bottomSheetDialog.dismiss()
+ startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("$sourceCode/wiki")))
+ }
+ /* telegram community */
+ aboutBinding.telegramGroup.setOnClickListener {
+ bottomSheetDialog.dismiss()
+ startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://t.me/LunarLauncher_chats")))
+ }
+ }
+
+ /* support dialog */
+ private fun supportDialog() {
+ MaterialAlertDialogBuilder(this)
+ .setTitle(R.string.support)
+ .setMessage(R.string.support_message)
+ /* star button */
+ .setNeutralButton(R.string.star) { _, _ ->
+ startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(sourceCode)))
+ }
+ /* affiliate button */
+ .setNegativeButton(R.string.amazon) { _, _ ->
+ startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://amzn.to/44krAw9")))
+ }
+ /* donate button */
+ .setPositiveButton(R.string.donate) { _, _ ->
+ startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://iamrasel.github.io/donate")))
+ }
+ .show()
+ }
+
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/settings/childs/Advance.kt b/app/src/main/kotlin/rasel/lunar/launcher/settings/childs/Advance.kt
new file mode 100644
index 0000000..df74de4
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/settings/childs/Advance.kt
@@ -0,0 +1,72 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.settings.childs
+
+import android.content.Intent
+import android.os.Bundle
+import android.provider.Settings
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import rasel.lunar.launcher.R
+import rasel.lunar.launcher.databinding.SettingsAdvanceBinding
+import kotlin.system.exitProcess
+
+
+internal class Advance : BottomSheetDialogFragment() {
+
+ private lateinit var binding : SettingsAdvanceBinding
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ binding = SettingsAdvanceBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ (requireDialog() as BottomSheetDialog).dismissWithAnimation = true
+
+ /* open Default Home App screen from device settings */
+ binding.chooseLauncher.setOnClickListener {
+ requireContext().startActivity(Intent(Settings.ACTION_HOME_SETTINGS))
+ this.dismiss()
+ }
+
+ /* reset and restart button click listeners */
+ binding.reset.setOnClickListener { reset() }
+ binding.restart.setOnClickListener { exitProcess(0) }
+ }
+
+ /* reset app data */
+ private fun reset() {
+ MaterialAlertDialogBuilder(requireActivity())
+ .setTitle(R.string.reset)
+ .setMessage(R.string.reset_message)
+ .setPositiveButton(R.string.proceed) { dialog, _ ->
+ dialog.dismiss()
+ Runtime.getRuntime().exec("pm clear " + requireContext().packageName)
+ }
+ .setNeutralButton(android.R.string.cancel) { dialog, _ -> dialog.dismiss() }
+ .show()
+ }
+
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/settings/childs/Appearances.kt b/app/src/main/kotlin/rasel/lunar/launcher/settings/childs/Appearances.kt
new file mode 100644
index 0000000..74f185c
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/settings/childs/Appearances.kt
@@ -0,0 +1,204 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.settings.childs
+
+import android.Manifest.permission.READ_EXTERNAL_STORAGE
+import android.Manifest.permission.READ_MEDIA_IMAGES
+import android.app.Activity.RESULT_OK
+import android.app.AlertDialog
+import android.app.WallpaperManager
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.res.ColorStateList
+import android.graphics.BitmapFactory
+import android.graphics.Color
+import android.graphics.Matrix
+import android.os.Build
+import android.os.Bundle
+import android.provider.MediaStore
+import android.text.SpannableStringBuilder
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
+import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO
+import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import rasel.lunar.launcher.R
+import rasel.lunar.launcher.databinding.ColorPickerBinding
+import rasel.lunar.launcher.databinding.SettingsAppearancesBinding
+import rasel.lunar.launcher.helpers.ColorPicker
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_APPLICATION_THEME
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_STATUS_BAR
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_WINDOW_BACKGROUND
+import rasel.lunar.launcher.helpers.UniUtils.Companion.getColorResId
+import rasel.lunar.launcher.settings.SettingsActivity.Companion.settingsPrefs
+import java.io.IOException
+import java.util.*
+
+
+internal class Appearances : BottomSheetDialogFragment() {
+
+ private lateinit var binding : SettingsAppearancesBinding
+ private lateinit var windowBackground : String
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ binding = SettingsAppearancesBinding.inflate(inflater, container, false)
+
+ /* initialize views according to the saved values */
+ when (settingsPrefs!!.getInt(KEY_APPLICATION_THEME, MODE_NIGHT_FOLLOW_SYSTEM)) {
+ MODE_NIGHT_FOLLOW_SYSTEM -> binding.followSystemTheme.isChecked = true
+ MODE_NIGHT_YES -> binding.selectDarkTheme.isChecked = true
+ MODE_NIGHT_NO -> binding.selectLightTheme.isChecked = true
+ }
+
+ when (settingsPrefs!!.getBoolean(KEY_STATUS_BAR, false)) {
+ false -> binding.hideStatusNegative.isChecked = true
+ true -> binding.hideStatusPositive.isChecked = true
+ }
+
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ (requireDialog() as BottomSheetDialog).dismissWithAnimation = true
+
+ /* change theme */
+ binding.themeGroup.setOnCheckedStateChangeListener { group, _ ->
+ when (group.checkedChipId) {
+ binding.followSystemTheme.id -> {
+ settingsPrefs!!.edit().putInt(KEY_APPLICATION_THEME, MODE_NIGHT_FOLLOW_SYSTEM).apply()
+ AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_FOLLOW_SYSTEM)
+ }
+ binding.selectDarkTheme.id -> {
+ settingsPrefs!!.edit().putInt(KEY_APPLICATION_THEME, MODE_NIGHT_YES).apply()
+ AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_YES)
+ }
+ binding.selectLightTheme.id -> {
+ settingsPrefs!!.edit().putInt(KEY_APPLICATION_THEME, MODE_NIGHT_NO).apply()
+ AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_NO)
+ }
+ }
+ }
+
+ binding.background.setOnClickListener { selectBackground() }
+ binding.changeWallpaper.setOnClickListener { selectWallpaper() }
+
+ binding.hideStatusGroup.setOnCheckedStateChangeListener { group, _ ->
+ when (group.checkedChipId) {
+ binding.hideStatusNegative.id -> settingsPrefs!!.edit().putBoolean(KEY_STATUS_BAR, false).apply()
+ binding.hideStatusPositive.id -> settingsPrefs!!.edit().putBoolean(KEY_STATUS_BAR, true).apply()
+ }
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ windowBackground = settingsPrefs!!.getString(KEY_WINDOW_BACKGROUND, defaultColorString).toString()
+ binding.background.iconTint = ColorStateList.valueOf(Color.parseColor("#$windowBackground"))
+ }
+
+ private fun selectBackground() {
+ val colorPickerBinding = ColorPickerBinding.inflate(requireActivity().layoutInflater)
+ val dialogBuilder = MaterialAlertDialogBuilder(requireActivity())
+ .setView(colorPickerBinding.root)
+ .setNeutralButton(R.string.default_, null)
+ .setNegativeButton(android.R.string.cancel, null)
+ .setPositiveButton(android.R.string.ok) { _, _ ->
+ settingsPrefs!!.edit().putString(KEY_WINDOW_BACKGROUND,
+ Objects.requireNonNull(colorPickerBinding.colorInput.text).toString().trim { it <= ' ' }).apply()
+ this.onResume()
+ }
+ .show()
+
+ /* set up color picker section */
+ ColorPicker(windowBackground, colorPickerBinding.colorInput,
+ colorPickerBinding.colorA, colorPickerBinding.colorR, colorPickerBinding.colorG,
+ colorPickerBinding.colorB, colorPickerBinding.root).pickColor()
+
+ dialogBuilder.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener {
+ colorPickerBinding.colorInput.text =
+ SpannableStringBuilder(defaultColorString)
+ }
+ }
+
+ private fun selectWallpaper() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ // only for TIRAMISU and newer versions
+ if (requireActivity().checkSelfPermission(READ_MEDIA_IMAGES) != PackageManager.PERMISSION_GRANTED) {
+ requireActivity().requestPermissions(arrayOf(READ_MEDIA_IMAGES), 1)
+ } else {
+ wallpaperChangeLauncher.launch(Intent(Intent.ACTION_PICK).setType("image/*"))
+ }
+ } else {
+ if (requireActivity().checkSelfPermission(READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+ requireActivity().requestPermissions(arrayOf(READ_EXTERNAL_STORAGE), 1)
+ } else {
+ wallpaperChangeLauncher.launch(Intent(Intent.ACTION_PICK).setType("image/*"))
+ }
+ }
+ }
+
+ private val defaultColorString: String get() =
+ requireActivity().getString(getColorResId(requireContext(), android.R.attr.colorBackground))
+ .replace("#", "")
+
+ private var wallpaperChangeLauncher =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+ if (result.resultCode == RESULT_OK) {
+ try {
+ val uri = result.data?.data
+ val projection = arrayOf(MediaStore.Images.Media.DATA)
+ val cursor = requireContext().contentResolver.query(
+ uri!!, projection, null, null, null
+ )
+ cursor?.moveToFirst()
+ val index = cursor!!.getColumnIndex(projection[0])
+ val filePath = cursor.getString(index)
+ cursor.close()
+ val bitmap = BitmapFactory.decodeFile(filePath)
+ val matrix = Matrix()
+ matrix.postRotate(0F)
+ try {
+ if (bitmap != null) {
+ WallpaperManager.getInstance(requireContext()).setBitmap(bitmap)
+ Toast.makeText(requireContext(),
+ requireActivity().getString(R.string.wallpaper_change_success), Toast.LENGTH_SHORT).show()
+ } else {
+ Toast.makeText(requireContext(),
+ requireActivity().getString(R.string.image_pick_failed), Toast.LENGTH_SHORT).show()
+ }
+ } catch (e: IOException) {
+ e.printStackTrace()
+ Toast.makeText(requireContext(),
+ requireActivity().getString(R.string.something_went_wrong), Toast.LENGTH_SHORT).show()
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ }
+
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/settings/childs/Apps.kt b/app/src/main/kotlin/rasel/lunar/launcher/settings/childs/Apps.kt
new file mode 100644
index 0000000..6ffad63
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/settings/childs/Apps.kt
@@ -0,0 +1,319 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.settings.childs
+
+import android.annotation.SuppressLint
+import android.app.Dialog
+import android.content.Intent
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.os.Build
+import android.os.Bundle
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import android.widget.Toast
+import androidx.appcompat.widget.LinearLayoutCompat
+import androidx.core.view.children
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import com.google.android.material.chip.Chip
+import com.google.android.material.chip.ChipDrawable
+import com.google.android.material.chip.ChipGroup
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.android.material.slider.Slider
+import rasel.lunar.launcher.R
+import rasel.lunar.launcher.databinding.SettingsAppsBinding
+import rasel.lunar.launcher.helpers.Constants.Companion.DEFAULT_GRID_COLUMNS
+import rasel.lunar.launcher.helpers.Constants.Companion.DEFAULT_ICON_PACK
+import rasel.lunar.launcher.helpers.Constants.Companion.DEFAULT_SCROLLBAR_HEIGHT
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_APPS_COUNT
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_APPS_LAYOUT
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_DRAW_ALIGN
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_GRID_COLUMNS
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_ICON_PACK
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_KEYBOARD_SEARCH
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_QUICK_LAUNCH
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_SCROLLBAR_HEIGHT
+import rasel.lunar.launcher.helpers.UniUtils.Companion.dpToPx
+import rasel.lunar.launcher.settings.SettingsActivity.Companion.settingsPrefs
+import kotlin.system.exitProcess
+
+
+internal class Apps : BottomSheetDialogFragment() {
+
+ private lateinit var binding: SettingsAppsBinding
+ private var settingsChanged: Boolean = false
+ private var packageManager: PackageManager? = null
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val bottomSheetView = View.inflate(context, R.layout.settings_apps, null)
+ val dialog = super.onCreateDialog(savedInstanceState).apply {
+ setContentView(bottomSheetView)
+ }
+
+ // Set the height to wrap_content
+ BottomSheetBehavior.from(bottomSheetView.parent as View)
+ .peekHeight = resources.displayMetrics.heightPixels // Adjust this if needed
+
+ return dialog
+ }
+
+ @SuppressLint("RtlHardcoded")
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ binding = SettingsAppsBinding.inflate(inflater, container, false)
+ packageManager = requireActivity().packageManager
+
+ /* initialize views according to the saved values */
+ when (settingsPrefs!!.getBoolean(KEY_KEYBOARD_SEARCH, false)) {
+ false -> binding.keyboardAutoNegative.isChecked = true
+ true -> binding.keyboardAutoPositive.isChecked = true
+ }
+
+ when (settingsPrefs!!.getBoolean(KEY_QUICK_LAUNCH, true)) {
+ true -> binding.quickLaunchPositive.isChecked = true
+ false -> binding.quickLaunchNegative.isChecked = true
+ }
+
+ when (settingsPrefs!!.getBoolean(KEY_APPS_COUNT, true)) {
+ true -> binding.appsCountPositive.isChecked = true
+ false -> binding.appsCountNegative.isChecked = true
+ }
+
+ when (settingsPrefs!!.getInt(KEY_APPS_LAYOUT, 0)) {
+ 0 -> {
+ binding.drawerLayoutList.isChecked = true
+ binding.appAlignmentGroup.children.forEach { it.isEnabled = true }
+ binding.iconPackChooser.isEnabled = false
+ binding.columnsCount.isEnabled = false
+ }
+ 1 -> {
+ binding.drawerLayoutListIcon.isChecked = true
+ binding.appAlignmentGroup.children.forEach { it.isEnabled = true }
+ binding.iconPackChooser.isEnabled = true
+ binding.columnsCount.isEnabled = false
+ }
+ 2 -> {
+ binding.drawerLayoutGrid.isChecked = true
+ binding.appAlignmentGroup.children.forEach { it.isEnabled = false }
+ binding.iconPackChooser.isEnabled = true
+ binding.columnsCount.isEnabled = true
+ }
+ }
+
+ when (settingsPrefs!!.getInt(KEY_DRAW_ALIGN, Gravity.CENTER)) {
+ Gravity.CENTER -> binding.appAlignmentCenter.isChecked = true
+ Gravity.LEFT -> binding.appAlignmentLeft.isChecked = true
+ Gravity.RIGHT -> binding.appAlignmentRight.isChecked = true
+ }
+
+ binding.columnsCount.value = settingsPrefs!!.getInt(KEY_GRID_COLUMNS, DEFAULT_GRID_COLUMNS).toFloat()
+ binding.scrollbarHeight.value = settingsPrefs!!.getInt(KEY_SCROLLBAR_HEIGHT, DEFAULT_SCROLLBAR_HEIGHT).toFloat()
+
+ return binding.root
+ }
+
+ @SuppressLint("RtlHardcoded")
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ (requireDialog() as BottomSheetDialog).dismissWithAnimation = true
+
+ /* change search with keyboard value */
+ binding.keyboardAutoGroup.setOnCheckedStateChangeListener { group, _ ->
+ when (group.checkedChipId) {
+ binding.keyboardAutoPositive.id -> settingsPrefs!!.edit().putBoolean(KEY_KEYBOARD_SEARCH, true).apply()
+ binding.keyboardAutoNegative.id -> settingsPrefs!!.edit().putBoolean(KEY_KEYBOARD_SEARCH, false).apply()
+ }
+ }
+
+ /* change settings for quick launch */
+ binding.quickLaunchGroup.setOnCheckedStateChangeListener { group, _ ->
+ when (group.checkedChipId) {
+ binding.quickLaunchPositive.id -> settingsPrefs!!.edit().putBoolean(KEY_QUICK_LAUNCH, true).apply()
+ binding.quickLaunchNegative.id -> settingsPrefs!!.edit().putBoolean(KEY_QUICK_LAUNCH, false).apply()
+ }
+ }
+
+ binding.appsCountGroup.setOnCheckedStateChangeListener { group, _ ->
+ when (group.checkedChipId) {
+ binding.appsCountPositive.id -> settingsPrefs!!.edit().putBoolean(KEY_APPS_COUNT, true).apply()
+ binding.appsCountNegative.id -> settingsPrefs!!.edit().putBoolean(KEY_APPS_COUNT, false).apply()
+ }
+ }
+
+ binding.drawerLayoutGroup.setOnCheckedStateChangeListener { group, _ ->
+ settingsChanged = true
+ when (group.checkedChipId) {
+ binding.drawerLayoutList.id -> {
+ settingsPrefs!!.edit().putInt(KEY_APPS_LAYOUT, 0).apply()
+ binding.appAlignmentGroup.children.forEach { if (!it.isEnabled) it.isEnabled = true }
+ binding.iconPackChooser.let { if (it.isEnabled) it.isEnabled = false }
+ binding.columnsCount.let { if (it.isEnabled) it.isEnabled = false }
+ }
+ binding.drawerLayoutListIcon.id -> {
+ settingsPrefs!!.edit().putInt(KEY_APPS_LAYOUT, 1).apply()
+ binding.appAlignmentGroup.children.forEach { if (!it.isEnabled) it.isEnabled = true }
+ binding.iconPackChooser.let { if (!it.isEnabled) it.isEnabled = true }
+ binding.columnsCount.let { if (it.isEnabled) it.isEnabled = false }
+ }
+ binding.drawerLayoutGrid.id -> {
+ settingsPrefs!!.edit().putInt(KEY_APPS_LAYOUT, 2).apply()
+ binding.appAlignmentGroup.children.forEach { if (it.isEnabled) it.isEnabled = false }
+ binding.iconPackChooser.let { if (!it.isEnabled) it.isEnabled = true }
+ binding.columnsCount.let { if (!it.isEnabled) it.isEnabled = true }
+ }
+ }
+ }
+
+ binding.appAlignmentGroup.setOnCheckedStateChangeListener { group, _ ->
+ when (group.checkedChipId) {
+ binding.appAlignmentLeft.id -> settingsPrefs!!.edit().putInt(KEY_DRAW_ALIGN, Gravity.LEFT).apply()
+ binding.appAlignmentCenter.id -> settingsPrefs!!.edit().putInt(KEY_DRAW_ALIGN, Gravity.CENTER).apply()
+ binding.appAlignmentRight.id -> settingsPrefs!!.edit().putInt(KEY_DRAW_ALIGN, Gravity.RIGHT).apply()
+ }
+ }
+
+ binding.iconPackChooser.setOnClickListener { iconPackChooser() }
+
+ binding.columnsCount.addOnChangeListener(Slider.OnChangeListener { _, value, _ ->
+ settingsChanged = true
+ settingsPrefs!!.edit().putInt(KEY_GRID_COLUMNS, value.toInt()).apply()
+ })
+
+ binding.scrollbarHeight.addOnChangeListener(Slider.OnChangeListener { _, value, _ ->
+ settingsPrefs!!.edit().putInt(KEY_SCROLLBAR_HEIGHT, value.toInt()).apply()
+ })
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ if (settingsChanged) {
+ MaterialAlertDialogBuilder(requireActivity())
+ .setTitle(R.string.restart_now)
+ .setMessage(R.string.restart_message)
+ .setPositiveButton(R.string.restart) { _, _ ->
+ exitProcess(0)
+ }
+ .setNeutralButton(R.string.later, null)
+ .show()
+ }
+ }
+
+ private fun iconPackChooser() {
+ if (installedIconPacks.isNotEmpty()) {
+ var selectedIconPack: String? = null
+
+ val chipGroup = ChipGroup(requireContext()).apply {
+ layoutParams = LinearLayoutCompat.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
+ isSingleSelection = true
+ isSelectionRequired = true
+ setOnCheckedStateChangeListener { group, _ ->
+ selectedIconPack = group.findViewById(group.checkedChipId).tag as String
+ }
+ }
+
+ installedIconPacks.indices.forEach { i ->
+ Chip(requireContext()).apply {
+ layoutParams = LinearLayoutCompat.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
+ setChipDrawable(ChipDrawable.createFromAttributes(requireContext(), null, 0,
+ com.google.android.material.R.style.Widget_Material3_Chip_Filter_Elevated))
+
+ text = packageManager?.getApplicationLabel(appInfo(installedIconPacks[i])!!)
+ tag = installedIconPacks[i]
+
+ if (settingsPrefs!!.getString(KEY_ICON_PACK, DEFAULT_ICON_PACK).equals(tag as String)) {
+ isChecked = true
+ }
+ }.let { chipGroup.addView(it) }
+ }
+
+ val eightDp = dpToPx(requireContext(), R.dimen.eight)
+ val linearLayoutCompat = LinearLayoutCompat(requireContext()).apply {
+ layoutParams = LinearLayoutCompat.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
+ gravity = Gravity.CENTER
+ setPadding(eightDp, eightDp, eightDp, eightDp)
+ addView(chipGroup)
+ }
+
+ MaterialAlertDialogBuilder(requireActivity()).apply {
+ setTitle(R.string.choose_icon_pack)
+ setView(linearLayoutCompat)
+ setPositiveButton(android.R.string.ok) { dialog, _ ->
+ when (selectedIconPack) {
+ null -> dialog.dismiss()
+ else -> {
+ if (!selectedIconPack.equals(settingsPrefs!!.getString(KEY_ICON_PACK, DEFAULT_ICON_PACK))) {
+ settingsChanged = true
+ settingsPrefs!!.edit().putString(KEY_ICON_PACK, selectedIconPack).apply()
+ } else { dialog.dismiss() }
+
+ }
+ }
+ }
+ setNeutralButton(R.string.default_) { dialog, _ ->
+ if (DEFAULT_ICON_PACK != settingsPrefs!!.getString(KEY_ICON_PACK, DEFAULT_ICON_PACK)) {
+ settingsChanged = true
+ settingsPrefs!!.edit().putString(KEY_ICON_PACK, DEFAULT_ICON_PACK).apply()
+ } else { dialog.dismiss() }
+ }
+ show()
+ }
+ } else {
+ Toast.makeText(requireContext(), R.string.icon_pack_not_found, Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ private val installedIconPacks: ArrayList get() {
+ val iconPacks = ArrayList()
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ packageManager?.queryIntentActivities(
+ Intent("org.adw.launcher.THEMES"),
+ PackageManager.ResolveInfoFlags.of(PackageManager.GET_META_DATA.toLong())
+ )
+ } else {
+ @Suppress("DEPRECATION")
+ (packageManager?.queryIntentActivities(
+ Intent("org.adw.launcher.THEMES"), PackageManager.GET_META_DATA))
+ }.let {
+ it?.indices?.forEach { i ->
+ it[i].activityInfo.packageName.let { packageName: String? ->
+ iconPacks.add(packageName!!)
+ }
+ }
+ }
+
+ return iconPacks
+ }
+
+ private fun appInfo(packageName: String) : ApplicationInfo? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ packageManager?.getApplicationInfo(packageName,
+ PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA.toLong()))
+ } else {
+ @Suppress("DEPRECATION")
+ packageManager?.getApplicationInfo(packageName, PackageManager.GET_META_DATA)
+ }
+ }
+
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/settings/childs/Misc.kt b/app/src/main/kotlin/rasel/lunar/launcher/settings/childs/Misc.kt
new file mode 100644
index 0000000..910cadc
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/settings/childs/Misc.kt
@@ -0,0 +1,120 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.settings.childs
+
+import android.content.DialogInterface
+import android.os.Build
+import android.os.Bundle
+import android.text.SpannableStringBuilder
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import com.google.android.material.slider.Slider
+import rasel.lunar.launcher.databinding.SettingsMiscBinding
+import rasel.lunar.launcher.helpers.Constants.Companion.DEFAULT_ICON_SIZE
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_BACK_HOME
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_ICON_SIZE
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_LOCK_METHOD
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_RSS_URL
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_SHORTCUT_COUNT
+import rasel.lunar.launcher.helpers.Constants.Companion.MAX_SHORTCUTS
+import rasel.lunar.launcher.helpers.UniUtils.Companion.isRooted
+import rasel.lunar.launcher.settings.SettingsActivity.Companion.settingsPrefs
+import java.util.*
+
+
+internal class Misc : BottomSheetDialogFragment() {
+
+ private lateinit var binding : SettingsMiscBinding
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ binding = SettingsMiscBinding.inflate(inflater, container, false)
+
+ /* initialize views according to the saved values */
+ when (settingsPrefs!!.getBoolean(KEY_BACK_HOME, false)) {
+ true -> binding.backHomePositive.isChecked = true
+ false -> binding.backHomeNegative.isChecked = true
+ }
+
+ binding.shortcutCount.valueTo = MAX_SHORTCUTS.toFloat()
+ binding.shortcutCount.value = settingsPrefs!!.getInt(KEY_SHORTCUT_COUNT, MAX_SHORTCUTS).toFloat()
+ binding.iconSize.value = settingsPrefs!!.getInt(KEY_ICON_SIZE, DEFAULT_ICON_SIZE).toFloat()
+ binding.inputFeedUrl.text = SpannableStringBuilder(settingsPrefs!!.getString(KEY_RSS_URL, ""))
+
+ when (settingsPrefs!!.getInt(KEY_LOCK_METHOD, 0)) {
+ 0 -> binding.selectLockNegative.isChecked = true
+ 1 -> binding.selectLockAccessibility.isChecked = true
+ 2 -> binding.selectLockAdmin.isChecked = true
+ 3 -> binding.selectLockRoot.isChecked = true
+ }
+
+ /* disable accessibility button for devices below android 9 */
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
+ binding.selectLockAccessibility.isEnabled = false
+ }
+
+ /* disable root button for non-rooted devices */
+ if (!isRooted) {
+ binding.selectLockRoot.isEnabled = false
+ }
+
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ (requireDialog() as BottomSheetDialog).dismissWithAnimation = true
+
+ binding.backHomeGroup.setOnCheckedStateChangeListener { group, _ ->
+ when (group.checkedChipId) {
+ binding.backHomePositive.id -> settingsPrefs!!.edit().putBoolean(KEY_BACK_HOME, true).apply()
+ binding.backHomeNegative.id -> settingsPrefs!!.edit().putBoolean(KEY_BACK_HOME, false).apply()
+ }
+ }
+
+ /* change shortcut count value */
+ binding.shortcutCount.addOnChangeListener(Slider.OnChangeListener { _: Slider?, value: Float, _: Boolean ->
+ settingsPrefs!!.edit().putInt(KEY_SHORTCUT_COUNT, value.toInt()).apply()
+ })
+
+ binding.iconSize.addOnChangeListener(Slider.OnChangeListener { _: Slider?, value: Float, _: Boolean ->
+ settingsPrefs!!.edit().putInt(KEY_ICON_SIZE, value.toInt()).apply()
+ })
+
+ /* change lock method value */
+ binding.lockGroup.setOnCheckedStateChangeListener { group, _ ->
+ when (group.checkedChipId) {
+ binding.selectLockNegative.id -> settingsPrefs!!.edit().putInt(KEY_LOCK_METHOD, 0).apply()
+ binding.selectLockAccessibility.id -> settingsPrefs!!.edit().putInt(KEY_LOCK_METHOD, 1).apply()
+ binding.selectLockAdmin.id -> settingsPrefs!!.edit().putInt(KEY_LOCK_METHOD, 2).apply()
+ binding.selectLockRoot.id -> settingsPrefs!!.edit().putInt(KEY_LOCK_METHOD, 3).apply()
+ }
+ }
+ }
+
+ /* save input field value while closing the dialog */
+ override fun onDismiss(dialog: DialogInterface) {
+ super.onDismiss(dialog)
+ settingsPrefs!!.edit().putString(KEY_RSS_URL,
+ Objects.requireNonNull(binding.inputFeedUrl.text).toString().trim { it <= ' ' }).apply()
+ }
+
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/settings/childs/TimeDate.kt b/app/src/main/kotlin/rasel/lunar/launcher/settings/childs/TimeDate.kt
new file mode 100644
index 0000000..1d35c56
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/settings/childs/TimeDate.kt
@@ -0,0 +1,79 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.settings.childs
+
+import android.content.DialogInterface
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import rasel.lunar.launcher.databinding.SettingsTimeDateBinding
+import rasel.lunar.launcher.helpers.Constants.Companion.DEFAULT_DATE_FORMAT
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_DATE_FORMAT
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_TIME_FORMAT
+import rasel.lunar.launcher.settings.SettingsActivity.Companion.settingsPrefs
+import java.util.*
+
+
+internal class TimeDate : BottomSheetDialogFragment() {
+
+ private lateinit var binding : SettingsTimeDateBinding
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ binding = SettingsTimeDateBinding.inflate(inflater, container, false)
+
+ /* initialize views according to the saved values */
+ when (settingsPrefs!!.getInt(KEY_TIME_FORMAT, 0)) {
+ 0 -> binding.followSystemTime.isChecked = true
+ 1 -> binding.selectTwelve.isChecked = true
+ 2 -> binding.selectTwentyFour.isChecked = true
+ }
+
+ binding.dateFormat
+ .setText(settingsPrefs!!.getString(KEY_DATE_FORMAT, DEFAULT_DATE_FORMAT).toString())
+
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ (requireDialog() as BottomSheetDialog).dismissWithAnimation = true
+
+ /* change time format value */
+ binding.timeGroup.setOnCheckedStateChangeListener { group, _ ->
+ when (group.checkedChipId) {
+ binding.followSystemTime.id -> settingsPrefs!!.edit().putInt(KEY_TIME_FORMAT, 0).apply()
+ binding.selectTwelve.id -> settingsPrefs!!.edit().putInt(KEY_TIME_FORMAT, 1).apply()
+ binding.selectTwentyFour.id -> settingsPrefs!!.edit().putInt(KEY_TIME_FORMAT, 2).apply()
+ }
+ }
+ }
+
+ /* if the input field is empty, then save the default value.
+ else save the value from input field while closing the dialog */
+ override fun onDismiss(dialog: DialogInterface) {
+ super.onDismiss(dialog)
+ val dateFormat = Objects.requireNonNull(binding.dateFormat.text).toString().trim { it <= ' ' }
+ if (dateFormat.isEmpty()) settingsPrefs!!.edit().putString(KEY_DATE_FORMAT, DEFAULT_DATE_FORMAT).apply()
+ else settingsPrefs!!.edit().putString(KEY_DATE_FORMAT, dateFormat).apply()
+ }
+
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/settings/childs/TodoSettings.kt b/app/src/main/kotlin/rasel/lunar/launcher/settings/childs/TodoSettings.kt
new file mode 100644
index 0000000..f3db893
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/settings/childs/TodoSettings.kt
@@ -0,0 +1,70 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.settings.childs
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import com.google.android.material.slider.Slider
+import rasel.lunar.launcher.databinding.SettingsTodoBinding
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_TODO_COUNTS
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_TODO_LOCK
+import rasel.lunar.launcher.settings.SettingsActivity.Companion.settingsPrefs
+
+
+internal class TodoSettings : BottomSheetDialogFragment() {
+
+ private lateinit var binding : SettingsTodoBinding
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ binding = SettingsTodoBinding.inflate(inflater, container, false)
+
+ /* initialize views according to the saved values */
+ binding.showTodos.value = settingsPrefs!!.getInt(KEY_TODO_COUNTS, 3).toFloat()
+
+ when (settingsPrefs!!.getBoolean(KEY_TODO_LOCK, false)) {
+ false -> binding.todoLockNegative.isChecked = true
+ true -> binding.todoLockPositive.isChecked = true
+ }
+
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ (requireDialog() as BottomSheetDialog).dismissWithAnimation = true
+
+ /* change to-do count value */
+ binding.showTodos.addOnChangeListener(Slider.OnChangeListener { _: Slider?, value: Float, _: Boolean ->
+ settingsPrefs!!.edit().putInt(KEY_TODO_COUNTS, value.toInt()).apply()
+ })
+
+ /* change to-do lock state value */
+ binding.todoLockGroup.setOnCheckedStateChangeListener { group, _ ->
+ when (group.checkedChipId) {
+ binding.todoLockPositive.id -> settingsPrefs!!.edit().putBoolean(KEY_TODO_LOCK, true).apply()
+ binding.todoLockNegative.id -> settingsPrefs!!.edit().putBoolean(KEY_TODO_LOCK, false).apply()
+ }
+ }
+ }
+
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/settings/childs/WeatherSettings.kt b/app/src/main/kotlin/rasel/lunar/launcher/settings/childs/WeatherSettings.kt
new file mode 100644
index 0000000..576a381
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/settings/childs/WeatherSettings.kt
@@ -0,0 +1,91 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.settings.childs
+
+import android.content.DialogInterface
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import rasel.lunar.launcher.databinding.SettingsWeatherBinding
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_CITY_NAME
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_OWM_API
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_SHOW_CITY
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_TEMP_UNIT
+import rasel.lunar.launcher.settings.SettingsActivity.Companion.settingsPrefs
+import java.util.*
+
+
+internal class WeatherSettings : BottomSheetDialogFragment() {
+
+ private lateinit var binding : SettingsWeatherBinding
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ binding = SettingsWeatherBinding.inflate(inflater, container, false)
+
+ /* initialize views according to the saved values */
+ binding.inputCity.setText(settingsPrefs!!.getString(KEY_CITY_NAME, "").toString())
+ binding.inputOwm.setText(settingsPrefs!!.getString(KEY_OWM_API, "").toString())
+
+ when (settingsPrefs!!.getInt(KEY_TEMP_UNIT, 0)) {
+ 0 -> binding.selectCelsius.isChecked = true
+ 1 -> binding.selectFahrenheit.isChecked = true
+ }
+
+ when (settingsPrefs!!.getBoolean(KEY_SHOW_CITY, false)) {
+ false -> binding.showCityNegative.isChecked = true
+ true -> binding.showCityPositive.isChecked = true
+ }
+
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ (requireDialog() as BottomSheetDialog).dismissWithAnimation = true
+
+ /* change temperature unit value */
+ binding.tempGroup.setOnCheckedStateChangeListener { group, _ ->
+ when (group.checkedChipId) {
+ binding.selectCelsius.id -> settingsPrefs!!.edit().putInt(KEY_TEMP_UNIT, 0).apply()
+ binding.selectFahrenheit.id -> settingsPrefs!!.edit().putInt(KEY_TEMP_UNIT, 1).apply()
+ }
+ }
+
+ /* change show city value */
+ binding.cityGroup.setOnCheckedStateChangeListener { group, _ ->
+ when (group.checkedChipId) {
+ binding.showCityNegative.id -> settingsPrefs!!.edit().putBoolean(KEY_SHOW_CITY, false).apply()
+ binding.showCityPositive.id -> settingsPrefs!!.edit().putBoolean(KEY_SHOW_CITY, true).apply()
+ }
+ }
+ }
+
+ /* save input field values while closing the dialog */
+ override fun onDismiss(dialog: DialogInterface) {
+ super.onDismiss(dialog)
+ settingsPrefs!!.edit().putString(KEY_CITY_NAME,
+ Objects.requireNonNull(binding.inputCity.text).toString().trim { it <= ' ' }).apply()
+ settingsPrefs!!.edit().putString(KEY_OWM_API,
+ Objects.requireNonNull(binding.inputOwm.text).toString().trim { it <= ' ' }).apply()
+ }
+
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/todos/DatabaseHandler.kt b/app/src/main/kotlin/rasel/lunar/launcher/todos/DatabaseHandler.kt
new file mode 100644
index 0000000..4b08daf
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/todos/DatabaseHandler.kt
@@ -0,0 +1,107 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.todos
+
+import android.database.sqlite.SQLiteOpenHelper
+import android.database.sqlite.SQLiteDatabase
+import android.content.ContentValues
+import android.annotation.SuppressLint
+import android.content.Context
+import android.database.DatabaseUtils
+import rasel.lunar.launcher.helpers.Constants.Companion.TODO_COLUMN_CREATED
+import rasel.lunar.launcher.helpers.Constants.Companion.TODO_COLUMN_ID
+import rasel.lunar.launcher.helpers.Constants.Companion.TODO_COLUMN_NAME
+import rasel.lunar.launcher.helpers.Constants.Companion.TODO_DATABASE_NAME
+import rasel.lunar.launcher.helpers.Constants.Companion.TODO_DATABASE_VERSION
+import rasel.lunar.launcher.helpers.Constants.Companion.TODO_TABLE_NAME
+import java.util.ArrayList
+
+
+internal class DatabaseHandler(context: Context?) :
+ SQLiteOpenHelper(context, TODO_DATABASE_NAME, null, TODO_DATABASE_VERSION) {
+
+ /* create database */
+ override fun onCreate(database: SQLiteDatabase) {
+ val createTodoTable = "CREATE TABLE " + TODO_TABLE_NAME + " (" +
+ TODO_COLUMN_ID + " integer PRIMARY KEY AUTOINCREMENT," +
+ TODO_COLUMN_CREATED + " datetime DEFAULT CURRENT_TIMESTAMP," +
+ TODO_COLUMN_NAME + " varchar)"
+ database.execSQL(createTodoTable)
+ }
+
+ override fun onUpgrade(sqLiteDatabase: SQLiteDatabase, i: Int, i1: Int) {}
+
+ /* add new to-do entry */
+ fun addTodo(todo: Todo) {
+ val database = writableDatabase
+ val contentValues = ContentValues()
+ contentValues.put(TODO_COLUMN_NAME, todo.name)
+ database.insert(TODO_TABLE_NAME, null, contentValues)
+ }
+
+ /* update or edit existing to-do */
+ fun updateTodo(todo: Todo) {
+ val database = writableDatabase
+ val contentValues = ContentValues()
+ contentValues.put(TODO_COLUMN_NAME, todo.name)
+ database.update(
+ TODO_TABLE_NAME,
+ contentValues,
+ "$TODO_COLUMN_ID=?",
+ arrayOf(todo.id.toString())
+ )
+ }
+
+ /* delete a single to-do */
+ fun deleteTodo(todoId: Long) {
+ writableDatabase.delete(TODO_TABLE_NAME,
+ "$TODO_COLUMN_ID=?", arrayOf(todoId.toString()))
+ }
+
+ /* delete all existing todos at once */
+ fun deleteAll() {
+ writableDatabase.delete(TODO_TABLE_NAME, null, null)
+ }
+
+ @get:SuppressLint("Range")
+ val todos: ArrayList
+ get() {
+ val todoList = ArrayList()
+ val queryResult =
+ readableDatabase.rawQuery("SELECT * from $TODO_TABLE_NAME", null)
+
+ if (queryResult.moveToFirst()) {
+ do {
+ val todo = Todo()
+ todo.id = queryResult.getLong(queryResult.getColumnIndex(TODO_COLUMN_ID))
+ todo.name = queryResult.getString(queryResult.getColumnIndex(TODO_COLUMN_NAME))
+ todoList.add(todo)
+ } while (queryResult.moveToNext())
+ }
+
+ queryResult.close()
+ return todoList
+ }
+
+ /* check if any item exists in the database */
+ val isTodoExists: Boolean get() {
+ return DatabaseUtils.queryNumEntries(readableDatabase, TODO_TABLE_NAME, 1.toString()) > 0
+ }
+
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/todos/Todo.kt b/app/src/main/kotlin/rasel/lunar/launcher/todos/Todo.kt
new file mode 100644
index 0000000..84d2a4e
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/todos/Todo.kt
@@ -0,0 +1,25 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.todos
+
+
+internal class Todo {
+ var id: Long = -1
+ var name = ""
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/todos/TodoAdapter.kt b/app/src/main/kotlin/rasel/lunar/launcher/todos/TodoAdapter.kt
new file mode 100644
index 0000000..8ef5387
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/todos/TodoAdapter.kt
@@ -0,0 +1,127 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.todos
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import rasel.lunar.launcher.LauncherActivity.Companion.lActivity
+import rasel.lunar.launcher.R
+import rasel.lunar.launcher.databinding.ListItemBinding
+import rasel.lunar.launcher.databinding.TodoDialogBinding
+import rasel.lunar.launcher.helpers.Constants.Companion.KEY_TODO_COUNTS
+import rasel.lunar.launcher.helpers.Constants.Companion.PREFS_SETTINGS
+import rasel.lunar.launcher.helpers.UniUtils.Companion.copyToClipboard
+import java.util.*
+
+
+internal class TodoAdapter(
+ private val todoManager: TodoManager?,
+ private val context: Context) : RecyclerView.Adapter() {
+
+ private val currentFragment = lActivity!!.supportFragmentManager.findFragmentById(R.id.mainFragmentsContainer)
+ private val todoList = DatabaseHandler(context).todos
+
+ override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): TodoViewHolder {
+ val binding = ListItemBinding.inflate(LayoutInflater.from(viewGroup.context), viewGroup, false)
+ return TodoViewHolder(binding)
+ }
+
+ override fun getItemCount(): Int {
+ /* if current fragment is LauncherHome,
+ then return size following the settings value */
+ val sharedPreferences = context.getSharedPreferences(PREFS_SETTINGS, 0)
+ val numberOfTodos = sharedPreferences.getInt(KEY_TODO_COUNTS, 3)
+ return if (currentFragment !is TodoManager) {
+ todoList.size.coerceAtMost(numberOfTodos)
+ } else {
+ todoList.size
+ }
+ }
+
+ @SuppressLint("SetTextI18n")
+ override fun onBindViewHolder(holder: TodoViewHolder, position: Int) {
+ val todo = todoList[position]
+
+ holder.view.itemText.text = "\u25CF ${todo.name}"
+
+ if (currentFragment is TodoManager) {
+ /* multiline texts are enabled for TodoManager */
+ holder.view.itemText.isSingleLine = false
+ /* launch edit or update dialog on item click */
+ holder.view.itemText.setOnClickListener { updateDialog(position) }
+ /* copy texts on long click */
+ holder.view.itemText.setOnLongClickListener {
+ copyToClipboard(context, todo.name)
+ true
+ }
+ } else {
+ /* single line text for home screen */
+ holder.view.itemText.isSingleLine = true
+ }
+ }
+
+ inner class TodoViewHolder(var view: ListItemBinding) : RecyclerView.ViewHolder(view.root)
+
+ /* update dialog */
+ private fun updateDialog(position: Int) {
+ val bottomSheetDialog = BottomSheetDialog(lActivity!!, R.style.BottomSheetDialog)
+ val dialogBinding = TodoDialogBinding.inflate(LayoutInflater.from(context))
+ bottomSheetDialog.setContentView(dialogBinding.root)
+ bottomSheetDialog.show()
+ bottomSheetDialog.dismissWithAnimation = true
+
+ val databaseHandler = DatabaseHandler(context)
+ val todo = databaseHandler.todos[position]
+
+ dialogBinding.apply {
+ deleteAllConfirmation.visibility = View.GONE
+ todoInput.setText(todo.name)
+ todoCancel.text = context.getString(R.string.delete)
+ todoCancel.setTextColor(ContextCompat.getColor(context, android.R.color.holo_red_light))
+ todoOk.text = context.getString(R.string.update)
+ }
+
+ /* delete the item */
+ dialogBinding.todoCancel.setOnClickListener {
+ databaseHandler.deleteTodo(todo.id)
+ bottomSheetDialog.dismiss()
+ todoManager?.refreshList()
+ }
+
+ /* update the item */
+ dialogBinding.todoOk.setOnClickListener {
+ val updatedTodoString = Objects.requireNonNull(dialogBinding.todoInput.text).toString().trim { it <= ' ' }
+ if (updatedTodoString.isNotEmpty()) {
+ todo.name = updatedTodoString
+ databaseHandler.updateTodo(todo)
+ bottomSheetDialog.dismiss()
+ todoManager?.refreshList()
+ } else {
+ dialogBinding.todoInput.error = context.getString(R.string.empty_text_field)
+ }
+ }
+ }
+
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/todos/TodoManager.kt b/app/src/main/kotlin/rasel/lunar/launcher/todos/TodoManager.kt
new file mode 100644
index 0000000..9153e42
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/todos/TodoManager.kt
@@ -0,0 +1,129 @@
+/*
+ * Lunar Launcher
+ * Copyright (C) 2022 Md Rasel Hossain
+ *
+ * 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 rasel.lunar.launcher.todos
+
+import android.content.Context
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.inputmethod.InputMethodManager
+import androidx.core.content.ContextCompat
+import androidx.fragment.app.Fragment
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import rasel.lunar.launcher.LauncherActivity.Companion.lActivity
+import rasel.lunar.launcher.R
+import rasel.lunar.launcher.databinding.TodoDialogBinding
+import rasel.lunar.launcher.databinding.TodoManagerBinding
+import java.util.*
+
+
+internal class TodoManager : Fragment() {
+
+ private lateinit var binding: TodoManagerBinding
+ private lateinit var databaseHandler: DatabaseHandler
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+
+ binding = TodoManagerBinding.inflate(inflater, container, false)
+ databaseHandler = DatabaseHandler(requireContext())
+
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ /* click listeners for add new and delete all buttons */
+ binding.addNew.setOnClickListener { addNewDialog() }
+ binding.deleteAll.setOnClickListener { deleteAllDialog() }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ refreshList()
+ lActivity!!.viewPager.isUserInputEnabled = false
+ }
+
+ override fun onPause() {
+ super.onPause()
+ lActivity!!.viewPager.isUserInputEnabled = true
+ }
+
+ fun refreshList() {
+ binding.todos.adapter = TodoAdapter(this, requireContext())
+ }
+
+ /* add new dialog */
+ private fun addNewDialog() {
+ val bottomSheetDialog = BottomSheetDialog(lActivity!!, R.style.BottomSheetDialog)
+ val dialogBinding = TodoDialogBinding.inflate(LayoutInflater.from(requireContext()))
+ bottomSheetDialog.setContentView(dialogBinding.root)
+ bottomSheetDialog.show()
+ bottomSheetDialog.dismissWithAnimation = true
+
+ dialogBinding.deleteAllConfirmation.visibility = View.GONE
+ /* automatic keyboard popup */
+ dialogBinding.todoInput.requestFocus()
+ val inputMethodManager = lActivity!!.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
+ inputMethodManager.showSoftInput(dialogBinding.todoInput, InputMethodManager.SHOW_IMPLICIT)
+
+ /* dismiss the dialog on cancel button click */
+ dialogBinding.todoCancel.setOnClickListener { bottomSheetDialog.dismiss() }
+ /* add new item to the database */
+ dialogBinding.todoOk.setOnClickListener {
+ val todo = Todo()
+ val todoString = Objects.requireNonNull(dialogBinding.todoInput.text).toString().trim { it <= ' ' }
+ if (todoString.isNotEmpty()) {
+ todo.name = todoString
+ databaseHandler.addTodo(todo)
+ bottomSheetDialog.dismiss()
+ refreshList()
+ } else {
+ dialogBinding.todoInput.error = getString(R.string.empty_text_field)
+ }
+ }
+ }
+
+ /* delete all dialog */
+ private fun deleteAllDialog() {
+ val bottomSheetDialog = BottomSheetDialog(lActivity!!)
+ val dialogBinding = TodoDialogBinding.inflate(LayoutInflater.from(requireContext()))
+ bottomSheetDialog.setContentView(dialogBinding.root)
+ bottomSheetDialog.show()
+ bottomSheetDialog.dismissWithAnimation = true
+
+ /* if any item does not exist, then disable the ok button */
+ if (!databaseHandler.isTodoExists) {
+ dialogBinding.todoOk.isEnabled = false
+ }
+
+ dialogBinding.todoInput.visibility = View.GONE
+ dialogBinding.todoOk.setTextColor(ContextCompat.getColor(requireContext(), android.R.color.holo_red_light))
+
+ /* dismiss the dialog on cancel button click */
+ dialogBinding.todoCancel.setOnClickListener { bottomSheetDialog.dismiss() }
+ /* delete all the existing items from the database */
+ dialogBinding.todoOk.setOnClickListener {
+ databaseHandler.deleteAll()
+ bottomSheetDialog.dismiss()
+ refreshList()
+ }
+ }
+
+}
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/utils/BLog.kt b/app/src/main/kotlin/rasel/lunar/launcher/utils/BLog.kt
new file mode 100644
index 0000000..d46efba
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/utils/BLog.kt
@@ -0,0 +1,56 @@
+package rasel.lunar.launcher.utils
+
+import android.util.Log
+import rasel.lunar.launcher.BuildConfig
+import java.lang.Exception
+
+object BLog {
+ val DEFAULT_TAG = "MyEBook_TAG"
+ enum class BLogType {
+ D,I,E
+ }
+ fun w(tag : String = DEFAULT_TAG, log: String){
+ LOG(BLogType.D,tag,log)
+ }
+
+ fun LOGD(tag : String = DEFAULT_TAG, log: String){
+ LOG(BLogType.D,tag,log)
+ }
+
+ fun LOGI(tag : String = DEFAULT_TAG, log: String){
+ LOG(BLogType.I,tag,log)
+ }
+
+ fun LOGE(tag : String = DEFAULT_TAG, log: Throwable){
+ LOG(BLogType.E,tag,log.toString())
+ }
+
+ fun LOGE(tag : String = DEFAULT_TAG, log: Exception){
+ LOG(BLogType.E,tag,log.toString())
+ }
+
+ fun LOGE(tag : String = DEFAULT_TAG, log: String){
+ LOG(BLogType.E,tag,log)
+ }
+
+ fun LOGE(log: String){
+ LOG(BLogType.E,DEFAULT_TAG,log)
+ }
+
+ private fun LOG(type : BLogType, tag : String, log : String) {
+ if (BuildConfig.DEBUG || BuildConfig.BUILD_TYPE.contains("debug")) {
+ when(type) {
+ BLogType.D -> {
+ Log.d(tag,log)
+ }
+ BLogType.I -> {
+ Log.i(tag,log)
+ }
+ BLogType.E -> {
+ Log.e(tag,log)
+ }
+ else -> {}
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/rasel/lunar/launcher/utils/SimpleGesture.kt b/app/src/main/kotlin/rasel/lunar/launcher/utils/SimpleGesture.kt
new file mode 100644
index 0000000..7604a6b
--- /dev/null
+++ b/app/src/main/kotlin/rasel/lunar/launcher/utils/SimpleGesture.kt
@@ -0,0 +1,781 @@
+package rasel.lunar.launcher.utils
+
+import android.content.Context
+import android.os.SystemClock
+import android.util.DisplayMetrics
+import android.util.Log
+import android.view.MotionEvent
+import android.view.View
+import android.view.View.OnTouchListener
+import kotlin.math.abs
+import kotlin.math.pow
+import kotlin.math.sqrt
+
+
+class GestureAnalyser @JvmOverloads constructor(
+ swipeSlopeIntolerance: Int = 2,
+ doubleTapMaxDelayMillis: Int = 500,
+ doubleTapMaxDownMillis: Int = 100
+) {
+ var minValue = 100
+ private val initialX = DoubleArray(5)
+ private val initialY = DoubleArray(5)
+ private val finalX = DoubleArray(5)
+ private val finalY = DoubleArray(5)
+ private val currentX = DoubleArray(5)
+ private val currentY = DoubleArray(5)
+ private val delX = DoubleArray(5)
+ private val delY = DoubleArray(5)
+
+ private var numFingers = 0
+ private var initialT: Long = 0
+ private var finalT: Long = 0
+ private var currentT: Long = 0
+
+ private var prevInitialT: Long = 0
+ private var prevFinalT: Long = 0
+
+ private var swipeSlopeIntolerance = 2
+
+ private val doubleTapMaxDelayMillis: Long
+ private val doubleTapMaxDownMillis: Long
+
+ init {
+ this.swipeSlopeIntolerance = swipeSlopeIntolerance
+ this.doubleTapMaxDownMillis = doubleTapMaxDownMillis.toLong()
+ this.doubleTapMaxDelayMillis = doubleTapMaxDelayMillis.toLong()
+ }
+
+ fun trackGesture(ev: MotionEvent) {
+ val n = ev.pointerCount
+ for (i in 0 until n) {
+ initialX[i] = ev.getX(i).toDouble()
+ initialY[i] = ev.getY(i).toDouble()
+ }
+ numFingers = n
+ initialT = SystemClock.uptimeMillis()
+ }
+
+ fun untrackGesture() {
+ numFingers = 0
+ prevFinalT = SystemClock.uptimeMillis()
+ prevInitialT = initialT
+ }
+
+ fun getGesture(ev: MotionEvent): GestureType {
+ var averageDistance = 0.0
+ for (i in 0 until numFingers) {
+ finalX[i] = ev.getX(i).toDouble()
+ finalY[i] = ev.getY(i).toDouble()
+ delX[i] = finalX[i] - initialX[i]
+ delY[i] = finalY[i] - initialY[i]
+
+ averageDistance += sqrt(
+ (finalX[i] - initialX[i]).pow(2.0) + (finalY[i] - initialY[i]).pow(
+ 2.0
+ )
+ )
+ }
+ averageDistance /= numFingers.toDouble()
+
+ finalT = SystemClock.uptimeMillis()
+ val gt = GestureType()
+ gt.gestureFlag = calcGesture()
+ gt.gestureDuration = finalT - initialT
+ gt.gestureDistance = averageDistance
+ return gt
+ }
+
+ fun getOngoingGesture(ev: MotionEvent): Int {
+ for (i in 0 until numFingers) {
+ currentX[i] = ev.getX(i).toDouble()
+ currentY[i] = ev.getY(i).toDouble()
+ delX[i] = finalX[i] - initialX[i]
+ delY[i] = finalY[i] - initialY[i]
+ }
+ currentT = SystemClock.uptimeMillis()
+ return calcGesture()
+ }
+
+ private fun calcGesture(): Int {
+ if (isDoubleTap) {
+ return DOUBLE_TAP_1
+ }
+
+ if (numFingers == 1) {
+ if ((-(delY[0])) > (swipeSlopeIntolerance * (abs(
+ delX[0]
+ ))) && abs(delY[0]) > minValue
+ ) {
+ return SWIPE_1_UP
+ }
+
+ if (((delY[0])) > (swipeSlopeIntolerance * (abs(
+ delX[0]
+ ))) && abs(delY[0]) > minValue
+ ) {
+ return SWIPE_1_DOWN
+ }
+
+ if ((-(delX[0])) > (swipeSlopeIntolerance * (abs(
+ delY[0]
+ ))) && abs(delX[0]) > minValue
+ ) {
+ return SWIPE_1_LEFT
+ }
+
+ if (((delX[0])) > (swipeSlopeIntolerance * (abs(
+ delY[0]
+ ))) && abs(delX[0]) > minValue
+ ) {
+ return SWIPE_1_RIGHT
+ }
+ BLog.LOGE("initialT = ${initialT} , finalT = ${finalT} , result = ${finalT - initialT}")
+ if (finalT - initialT < 300) {
+ return CLICK_1
+ } else if(finalT - initialT > 600) {
+ return LONG_CLICK_1
+ }
+ }
+ if (numFingers == 2) {
+ if (((-delY[0]) > (swipeSlopeIntolerance * abs(
+ delX[0]
+ ))) && ((-delY[1]) > (swipeSlopeIntolerance * abs(
+ delX[1]
+ ))) && (abs(delY[0]) > minValue || abs(delY[1]) > minValue)
+ ) {
+ return SWIPE_2_UP
+ }
+ if (((delY[0]) > (swipeSlopeIntolerance * abs(
+ delX[0]
+ ))) && ((delY[1]) > (swipeSlopeIntolerance * abs(
+ delX[1]
+ ))) && (abs(delY[0]) > minValue || abs(delY[1]) > minValue)
+ ) {
+ return SWIPE_2_DOWN
+ }
+ if (((-delX[0]) > (swipeSlopeIntolerance * abs(
+ delY[0]
+ ))) && ((-delX[1]) > (swipeSlopeIntolerance * abs(
+ delY[1]
+ ))) && (abs(delX[0]) > minValue || abs(delX[1]) > minValue)
+ ) {
+ return SWIPE_2_LEFT
+ }
+ if (((delX[0]) > (swipeSlopeIntolerance * abs(
+ delY[0]
+ ))) && ((delX[1]) > (swipeSlopeIntolerance * abs(
+ delY[1]
+ ))) && (abs(delX[0]) > minValue || abs(delX[1]) > minValue)
+ ) {
+ return SWIPE_2_RIGHT
+ }
+ if (finalFingDist(0, 1) > 2 * (initialFingDist(0, 1))) {
+ return UNPINCH_2
+ }
+ if (finalFingDist(0, 1) < 0.5 * (initialFingDist(0, 1))) {
+ return PINCH_2
+ }
+ }
+ if (numFingers == 3) {
+ if (((-delY[0]) > (swipeSlopeIntolerance * abs(
+ delX[0]
+ )))
+ && ((-delY[1]) > (swipeSlopeIntolerance * abs(
+ delX[1]
+ )))
+ && ((-delY[2]) > (swipeSlopeIntolerance * abs(
+ delX[2]
+ )))
+ ) {
+ return SWIPE_3_UP
+ }
+ if (((delY[0]) > (swipeSlopeIntolerance * abs(
+ delX[0]
+ )))
+ && ((delY[1]) > (swipeSlopeIntolerance * abs(
+ delX[1]
+ )))
+ && ((delY[2]) > (swipeSlopeIntolerance * abs(
+ delX[2]
+ )))
+ ) {
+ return SWIPE_3_DOWN
+ }
+ if (((-delX[0]) > (swipeSlopeIntolerance * abs(
+ delY[0]
+ )))
+ && ((-delX[1]) > (swipeSlopeIntolerance * abs(
+ delY[1]
+ )))
+ && ((-delX[2]) > (swipeSlopeIntolerance * abs(
+ delY[2]
+ )))
+ ) {
+ return SWIPE_3_LEFT
+ }
+ if (((delX[0]) > (swipeSlopeIntolerance * abs(
+ delY[0]
+ )))
+ && ((delX[1]) > (swipeSlopeIntolerance * abs(
+ delY[1]
+ )))
+ && ((delX[2]) > (swipeSlopeIntolerance * abs(
+ delY[2]
+ )))
+ ) {
+ return SWIPE_3_RIGHT
+ }
+
+ if ((finalFingDist(0, 1) > 1.75 * (initialFingDist(0, 1)))
+ && (finalFingDist(1, 2) > 1.75 * (initialFingDist(1, 2)))
+ && (finalFingDist(2, 0) > 1.75 * (initialFingDist(2, 0)))
+ ) {
+ return UNPINCH_3
+ }
+ if ((finalFingDist(0, 1) < 0.66 * (initialFingDist(0, 1)))
+ && (finalFingDist(1, 2) < 0.66 * (initialFingDist(1, 2)))
+ && (finalFingDist(2, 0) < 0.66 * (initialFingDist(2, 0)))
+ ) {
+ return PINCH_3
+ }
+ }
+ if (numFingers == 4) {
+ if (((-delY[0]) > (swipeSlopeIntolerance * abs(
+ delX[0]
+ )))
+ && ((-delY[1]) > (swipeSlopeIntolerance * abs(
+ delX[1]
+ )))
+ && ((-delY[2]) > (swipeSlopeIntolerance * abs(
+ delX[2]
+ )))
+ && ((-delY[3]) > (swipeSlopeIntolerance * abs(
+ delX[3]
+ )))
+ ) {
+ return SWIPE_4_UP
+ }
+ if (((delY[0]) > (swipeSlopeIntolerance * abs(
+ delX[0]
+ )))
+ && ((delY[1]) > (swipeSlopeIntolerance * abs(
+ delX[1]
+ )))
+ && ((delY[2]) > (swipeSlopeIntolerance * abs(
+ delX[2]
+ )))
+ && ((delY[3]) > (swipeSlopeIntolerance * abs(
+ delX[3]
+ )))
+ ) {
+ return SWIPE_4_DOWN
+ }
+ if (((-delX[0]) > (swipeSlopeIntolerance * abs(
+ delY[0]
+ )))
+ && ((-delX[1]) > (swipeSlopeIntolerance * abs(
+ delY[1]
+ )))
+ && ((-delX[2]) > (swipeSlopeIntolerance * abs(
+ delY[2]
+ )))
+ && ((-delX[3]) > (swipeSlopeIntolerance * abs(
+ delY[3]
+ )))
+ ) {
+ return SWIPE_4_LEFT
+ }
+ if (((delX[0]) > (swipeSlopeIntolerance * abs(
+ delY[0]
+ )))
+ && ((delX[1]) > (swipeSlopeIntolerance * abs(
+ delY[1]
+ )))
+ && ((delX[2]) > (swipeSlopeIntolerance * abs(
+ delY[2]
+ )))
+ && ((delX[3]) > (swipeSlopeIntolerance * abs(
+ delY[3]
+ )))
+ ) {
+ return SWIPE_4_RIGHT
+ }
+ if ((finalFingDist(0, 1) > 1.5 * (initialFingDist(0, 1)))
+ && (finalFingDist(1, 2) > 1.5 * (initialFingDist(1, 2)))
+ && (finalFingDist(2, 3) > 1.5 * (initialFingDist(2, 3)))
+ && (finalFingDist(3, 0) > 1.5 * (initialFingDist(3, 0)))
+ ) {
+ return UNPINCH_4
+ }
+ if ((finalFingDist(0, 1) < 0.8 * (initialFingDist(0, 1)))
+ && (finalFingDist(1, 2) < 0.8 * (initialFingDist(1, 2)))
+ && (finalFingDist(2, 3) < 0.8 * (initialFingDist(2, 3)))
+ && (finalFingDist(3, 0) < 0.8 * (initialFingDist(3, 0)))
+ ) {
+ return PINCH_4
+ }
+ }
+ return 0
+ }
+
+ private fun initialFingDist(fingNum1: Int, fingNum2: Int): Double {
+ return sqrt(
+ (initialX[fingNum1] - initialX[fingNum2]).pow(2.0) + (initialY[fingNum1] - initialY[fingNum2]).pow(
+ 2.0
+ )
+ )
+ }
+
+ private fun finalFingDist(fingNum1: Int, fingNum2: Int): Double {
+ return sqrt(
+ (finalX[fingNum1] - finalX[fingNum2]).pow(2.0) + (finalY[fingNum1] - finalY[fingNum2]).pow(
+ 2.0
+ )
+ )
+ }
+
+ val isDoubleTap: Boolean
+ get() = if (initialT - prevFinalT < doubleTapMaxDelayMillis && finalT - initialT < doubleTapMaxDownMillis && prevFinalT - prevInitialT < doubleTapMaxDownMillis) {
+ true
+ } else {
+ false
+ }
+
+ inner class GestureType {
+ var gestureFlag: Int = 0
+ var gestureDuration: Long = 0
+
+ var gestureDistance: Double = 0.0
+ }
+
+
+ companion object {
+ const val DEBUG: Boolean = true
+
+ // Finished gestures flags
+ const val SWIPE_1_UP: Int = 11
+ const val SWIPE_1_DOWN: Int = 12
+ const val SWIPE_1_LEFT: Int = 13
+ const val SWIPE_1_RIGHT: Int = 14
+ const val SWIPE_2_UP: Int = 21
+ const val SWIPE_2_DOWN: Int = 22
+ const val SWIPE_2_LEFT: Int = 23
+ const val SWIPE_2_RIGHT: Int = 24
+ const val SWIPE_3_UP: Int = 31
+ const val SWIPE_3_DOWN: Int = 32
+ const val SWIPE_3_LEFT: Int = 33
+ const val SWIPE_3_RIGHT: Int = 34
+ const val SWIPE_4_UP: Int = 41
+ const val SWIPE_4_DOWN: Int = 42
+ const val SWIPE_4_LEFT: Int = 43
+ const val SWIPE_4_RIGHT: Int = 44
+ const val PINCH_2: Int = 25
+ const val UNPINCH_2: Int = 26
+ const val PINCH_3: Int = 35
+ const val UNPINCH_3: Int = 36
+ const val PINCH_4: Int = 45
+ const val UNPINCH_4: Int = 46
+
+ const val DOUBLE_TAP_1: Int = 107
+
+ const val CLICK_1 = 19001
+ const val CLICK_2 = 19002
+ const val CLICK_3 = 19003
+
+
+
+ const val LONG_CLICK_1 = 29001
+ const val LONG_CLICK_2 = 29002
+ const val LONG_CLICK_3 = 29003
+
+ //Ongoing gesture flags
+ const val SWIPING_1_UP: Int = 101
+ const val SWIPING_1_DOWN: Int = 102
+ const val SWIPING_1_LEFT: Int = 103
+ const val SWIPING_1_RIGHT: Int = 104
+ const val SWIPING_2_UP: Int = 201
+ const val SWIPING_2_DOWN: Int = 202
+ const val SWIPING_2_LEFT: Int = 203
+ const val SWIPING_2_RIGHT: Int = 204
+ const val PINCHING: Int = 205
+ const val UNPINCHING: Int = 206
+ private const val TAG = "GestureAnalyser"
+ }
+}
+
+class SimpleFingerGestures : OnTouchListener {
+ private var debug = true
+ var consumeTouchEvents: Boolean = false
+ var screenHeight : Int = 100
+ protected var tracking: BooleanArray = booleanArrayOf(false, false, false, false, false)
+ private var ga: GestureAnalyser
+ private var onFingerGestureListener: OnFingerGestureListener? = null
+ var targetView : View? = null
+ var mContext : Context? = null
+ constructor(context : Context,targetView : View, onFingerGestureListener: OnFingerGestureListener) {
+ this.mContext = context
+
+
+ this.targetView = targetView
+ ga = GestureAnalyser()
+ this.mContext?.resources?.displayMetrics?.let {
+ screenHeight = (it.heightPixels * 0.18).toInt()
+ ga.minValue = Math.max(screenHeight, 100)
+ }
+ this.onFingerGestureListener = onFingerGestureListener
+ this.targetView?.setOnClickListener { onFingerGestureListener.onClick(it) }
+ this.targetView?.setOnLongClickListener { onFingerGestureListener.onLongPress(it) }
+ }
+ /**
+ * Constructor that creates an internal [in.championswimmer.sfg.lib.GestureAnalyser] object as well
+ */
+ constructor() {
+ ga = GestureAnalyser()
+ }
+
+ constructor(
+ swipeSlopeIntolerance: Int,
+ doubleTapMaxDelayMillis: Int,
+ doubleTapMaxDownMillis: Int
+ ) {
+ ga = GestureAnalyser(swipeSlopeIntolerance, doubleTapMaxDelayMillis, doubleTapMaxDownMillis)
+ }
+
+ fun setDebug(debug: Boolean) {
+ this.debug = debug
+ }
+
+ constructor(omfgl: OnFingerGestureListener?) {
+ ga = GestureAnalyser()
+ setOnFingerGestureListener(omfgl)
+ }
+
+ /**
+ * Register a callback to be invoked when multi-finger gestures take place
+ *
+ *
+ *
+ *
+ *
+ * For the callbacks implemented via this, check the interface [in.championswimmer.sfg.lib.SimpleFingerGestures.OnFingerGestureListener]
+ *
+ *
+ * @param omfgl The callback that will run
+ */
+ fun setOnFingerGestureListener(omfgl: OnFingerGestureListener?) {
+ onFingerGestureListener = omfgl
+ }
+
+
+ override fun onTouch(view: View, ev: MotionEvent): Boolean {
+ if (debug) Log.d(TAG, "onTouch")
+ when (ev.action and MotionEvent.ACTION_MASK) {
+ MotionEvent.ACTION_DOWN -> {
+ if (debug) Log.d(TAG, "ACTION_DOWN")
+ startTracking(0)
+ ga.trackGesture(ev)
+ return consumeTouchEvents
+ }
+
+ MotionEvent.ACTION_UP -> {
+ if (debug) Log.d(TAG, "ACTION_UP")
+ if (tracking[0]) {
+ doCallBack(view,ga.getGesture(ev))
+ }
+ stopTracking(0)
+ ga.untrackGesture()
+ return consumeTouchEvents
+ }
+
+ MotionEvent.ACTION_POINTER_DOWN -> {
+ if (debug) Log.d(TAG, "ACTION_POINTER_DOWN" + " " + "num" + ev.pointerCount)
+ startTracking(ev.pointerCount - 1)
+ ga.trackGesture(ev)
+ return consumeTouchEvents
+ }
+
+ MotionEvent.ACTION_POINTER_UP -> {
+ if (debug) Log.d(TAG, "ACTION_POINTER_UP" + " " + "num" + ev.pointerCount)
+ if (tracking[1]) {
+ doCallBack(view,ga.getGesture(ev))
+ }
+ stopTracking(ev.pointerCount - 1)
+ ga.untrackGesture()
+ return consumeTouchEvents
+ }
+
+ MotionEvent.ACTION_CANCEL -> {
+ if (debug) Log.d(TAG, "ACTION_CANCEL")
+ return true
+ }
+
+ MotionEvent.ACTION_MOVE -> {
+ if (debug) Log.d(TAG, "ACTION_MOVE")
+ return consumeTouchEvents
+ }
+ }
+ return consumeTouchEvents
+ }
+
+ private fun doCallBack(targetView : View, mGt: GestureAnalyser.GestureType) {
+ when (mGt.gestureFlag) {
+ GestureAnalyser.SWIPE_1_UP -> onFingerGestureListener!!.onSwipeUp(
+ targetView,
+ 1,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.SWIPE_1_DOWN -> onFingerGestureListener!!.onSwipeDown(
+ targetView,
+ 1,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.SWIPE_1_LEFT -> onFingerGestureListener!!.onSwipeLeft(
+ targetView,
+ 1,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.SWIPE_1_RIGHT -> onFingerGestureListener!!.onSwipeRight(
+ targetView,
+ 1,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.SWIPE_2_UP -> onFingerGestureListener!!.onSwipeUp(
+ targetView,
+ 2,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.SWIPE_2_DOWN -> onFingerGestureListener!!.onSwipeDown(
+ targetView,
+ 2,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.SWIPE_2_LEFT -> onFingerGestureListener!!.onSwipeLeft(
+ targetView,
+ 2,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.SWIPE_2_RIGHT -> onFingerGestureListener!!.onSwipeRight(
+ targetView,
+ 2,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.PINCH_2 -> onFingerGestureListener!!.onPinch(
+ targetView,
+ 2,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.UNPINCH_2 -> onFingerGestureListener!!.onUnpinch(
+ targetView,
+ 2,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.SWIPE_3_UP -> onFingerGestureListener!!.onSwipeUp(
+ targetView,
+ 3,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.SWIPE_3_DOWN -> onFingerGestureListener!!.onSwipeDown(
+ targetView,
+ 3,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.SWIPE_3_LEFT -> onFingerGestureListener!!.onSwipeLeft(
+ targetView,
+ 3,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.SWIPE_3_RIGHT -> onFingerGestureListener!!.onSwipeRight(
+ targetView,
+ 3,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.PINCH_3 -> onFingerGestureListener!!.onPinch(
+ targetView,
+ 3,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.UNPINCH_3 -> onFingerGestureListener!!.onUnpinch(
+ targetView,
+ 3,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.SWIPE_4_UP -> onFingerGestureListener!!.onSwipeUp(
+ targetView,
+ 4,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.SWIPE_4_DOWN -> onFingerGestureListener!!.onSwipeDown(
+ targetView,
+ 4,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.SWIPE_4_LEFT -> onFingerGestureListener!!.onSwipeLeft(
+ targetView,
+ 4,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.SWIPE_4_RIGHT -> onFingerGestureListener!!.onSwipeRight(
+ targetView,
+ 4,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.PINCH_4 -> onFingerGestureListener!!.onPinch(
+ targetView,
+ 4,
+ mGt.gestureDuration,
+ mGt.gestureDistance
+ )
+
+ GestureAnalyser.UNPINCH_4 -> {
+ onFingerGestureListener!!.onUnpinch(targetView,4, mGt.gestureDuration, mGt.gestureDistance)
+// onFingerGestureListener!!.onDoubleTap(1)
+ }
+ GestureAnalyser.CLICK_1 -> {
+ BLog.LOGE("GestureAnalyser.CLICK_1")
+ onFingerGestureListener!!.onClick(targetView)
+// onFingerGestureListener!!.onDoubleTap(1)
+ }
+// GestureAnalyser.CLICK_2 -> {
+// onFingerGestureListener!!.onUnpinch(targetView,4, mGt.gestureDuration, mGt.gestureDistance)
+//// onFingerGestureListener!!.onDoubleTap(1)
+// }
+// GestureAnalyser.CLICK_3 -> {
+// onFingerGestureListener!!.onUnpinch(targetView,4, mGt.gestureDuration, mGt.gestureDistance)
+//// onFingerGestureListener!!.onDoubleTap(1)
+// }
+
+ GestureAnalyser.LONG_CLICK_1 -> {
+ BLog.LOGE("GestureAnalyser.LONG_CLICK_1")
+ onFingerGestureListener!!.onLongPress(targetView)
+ }
+
+
+ GestureAnalyser.DOUBLE_TAP_1 -> onFingerGestureListener!!.onDoubleTap(targetView,1)
+ }
+ }
+
+ private fun startTracking(nthPointer: Int) {
+ for (i in 0..nthPointer) {
+ tracking[i] = true
+ }
+ }
+
+ private fun stopTracking(nthPointer: Int) {
+ for (i in nthPointer until tracking.size) {
+ tracking[i] = false
+ }
+ }
+
+
+ /**
+ * Interface definition for the callback to be invoked when 2-finger gestures are performed
+ */
+ interface OnFingerGestureListener {
+ /**
+ * Called when user swipes **up** with two fingers
+ *
+ * @param fingers number of fingers involved in this gesture
+ * @param gestureDuration duration in milliSeconds
+ * @return
+ */
+ fun onSwipeUp(targetView : View, fingers: Int, gestureDuration: Long, gestureDistance: Double): Boolean
+
+ /**
+ * Called when user swipes **down** with two fingers
+ *
+ * @param fingers number of fingers involved in this gesture
+ * @param gestureDuration duration in milliSeconds
+ * @return
+ */
+ fun onSwipeDown(targetView : View,fingers: Int, gestureDuration: Long, gestureDistance: Double): Boolean
+
+ /**
+ * Called when user swipes **left** with two fingers
+ *
+ * @param fingers number of fingers involved in this gesture
+ * @param gestureDuration duration in milliSeconds
+ * @return
+ */
+ fun onSwipeLeft(targetView : View,fingers: Int, gestureDuration: Long, gestureDistance: Double): Boolean
+
+ /**
+ * Called when user swipes **right** with two fingers
+ *
+ * @param fingers number of fingers involved in this gesture
+ * @param gestureDuration duration in milliSeconds
+ * @return
+ */
+ fun onSwipeRight(targetView : View,fingers: Int, gestureDuration: Long, gestureDistance: Double): Boolean
+
+ /**
+ * Called when user **pinches** with two fingers (bring together)
+ *
+ * @param fingers number of fingers involved in this gesture
+ * @param gestureDuration duration in milliSeconds
+ * @return
+ */
+ fun onPinch(targetView : View,fingers: Int, gestureDuration: Long, gestureDistance: Double): Boolean
+
+ /**
+ * Called when user **un-pinches** with two fingers (take apart)
+ *
+ * @param fingers number of fingers involved in this gesture
+ * @param gestureDuration duration in milliSeconds
+ * @return
+ */
+ fun onUnpinch(targetView : View,fingers: Int, gestureDuration: Long, gestureDistance: Double): Boolean
+
+ fun onDoubleTap(targetView : View,fingers: Int): Boolean
+ fun onLongPress(targetView : View): Boolean
+ fun onClick(targetView : View): Boolean
+ }
+
+ companion object {
+ // Will see if these need to be used. For now just returning duration in milliS
+ const val GESTURE_SPEED_SLOW: Long = 1500
+ const val GESTURE_SPEED_MEDIUM: Long = 1000
+ const val GESTURE_SPEED_FAST: Long = 500
+ private const val TAG = "SimpleFingerGestures"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/anim/rotate_clockwise.xml b/app/src/main/res/anim/rotate_clockwise.xml
new file mode 100644
index 0000000..afd45f6
--- /dev/null
+++ b/app/src/main/res/anim/rotate_clockwise.xml
@@ -0,0 +1,11 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/apps_bg.xml b/app/src/main/res/drawable/apps_bg.xml
new file mode 100644
index 0000000..67a71d5
--- /dev/null
+++ b/app/src/main/res/drawable/apps_bg.xml
@@ -0,0 +1,26 @@
+
+
+ -
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_activity.xml b/app/src/main/res/drawable/ic_activity.xml
new file mode 100644
index 0000000..82bd496
--- /dev/null
+++ b/app/src/main/res/drawable/ic_activity.xml
@@ -0,0 +1,8 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml
new file mode 100644
index 0000000..e3ca1f4
--- /dev/null
+++ b/app/src/main/res/drawable/ic_add.xml
@@ -0,0 +1,8 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_alarm.xml b/app/src/main/res/drawable/ic_alarm.xml
new file mode 100644
index 0000000..b321714
--- /dev/null
+++ b/app/src/main/res/drawable/ic_alarm.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_brightness.xml b/app/src/main/res/drawable/ic_brightness.xml
new file mode 100644
index 0000000..c597dc2
--- /dev/null
+++ b/app/src/main/res/drawable/ic_brightness.xml
@@ -0,0 +1,8 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml
new file mode 100644
index 0000000..7dccce9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_close.xml
@@ -0,0 +1,8 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_contact.xml b/app/src/main/res/drawable/ic_contact.xml
new file mode 100644
index 0000000..59904c6
--- /dev/null
+++ b/app/src/main/res/drawable/ic_contact.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_delete.xml b/app/src/main/res/drawable/ic_delete.xml
new file mode 100644
index 0000000..8b5f27e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_delete.xml
@@ -0,0 +1,8 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_down.xml b/app/src/main/res/drawable/ic_down.xml
new file mode 100644
index 0000000..bb60629
--- /dev/null
+++ b/app/src/main/res/drawable/ic_down.xml
@@ -0,0 +1,8 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_info.xml b/app/src/main/res/drawable/ic_info.xml
new file mode 100644
index 0000000..cfc78d1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_info.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_info2.xml b/app/src/main/res/drawable/ic_info2.xml
new file mode 100644
index 0000000..bebaf27
--- /dev/null
+++ b/app/src/main/res/drawable/ic_info2.xml
@@ -0,0 +1,10 @@
+
+
+
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..acb5509
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
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..44ca518
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_link.xml b/app/src/main/res/drawable/ic_link.xml
new file mode 100644
index 0000000..21036fc
--- /dev/null
+++ b/app/src/main/res/drawable/ic_link.xml
@@ -0,0 +1,8 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_media.xml b/app/src/main/res/drawable/ic_media.xml
new file mode 100644
index 0000000..5d88d57
--- /dev/null
+++ b/app/src/main/res/drawable/ic_media.xml
@@ -0,0 +1,8 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_notification.xml b/app/src/main/res/drawable/ic_notification.xml
new file mode 100644
index 0000000..3509f1f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_notification.xml
@@ -0,0 +1,8 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_pip.xml b/app/src/main/res/drawable/ic_pip.xml
new file mode 100644
index 0000000..c4802d2
--- /dev/null
+++ b/app/src/main/res/drawable/ic_pip.xml
@@ -0,0 +1,8 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_refresh.xml b/app/src/main/res/drawable/ic_refresh.xml
new file mode 100644
index 0000000..6067f64
--- /dev/null
+++ b/app/src/main/res/drawable/ic_refresh.xml
@@ -0,0 +1,8 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_ring.xml b/app/src/main/res/drawable/ic_ring.xml
new file mode 100644
index 0000000..df04c14
--- /dev/null
+++ b/app/src/main/res/drawable/ic_ring.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_search.xml b/app/src/main/res/drawable/ic_search.xml
new file mode 100644
index 0000000..16f6d75
--- /dev/null
+++ b/app/src/main/res/drawable/ic_search.xml
@@ -0,0 +1,8 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_share.xml b/app/src/main/res/drawable/ic_share.xml
new file mode 100644
index 0000000..44d9c13
--- /dev/null
+++ b/app/src/main/res/drawable/ic_share.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_store.xml b/app/src/main/res/drawable/ic_store.xml
new file mode 100644
index 0000000..026df8c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_store.xml
@@ -0,0 +1,8 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_up.xml b/app/src/main/res/drawable/ic_up.xml
new file mode 100644
index 0000000..fa6743c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_up.xml
@@ -0,0 +1,8 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_voice.xml b/app/src/main/res/drawable/ic_voice.xml
new file mode 100644
index 0000000..c7b37c8
--- /dev/null
+++ b/app/src/main/res/drawable/ic_voice.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/rounded_bg.xml b/app/src/main/res/drawable/rounded_bg.xml
new file mode 100644
index 0000000..cf65c69
--- /dev/null
+++ b/app/src/main/res/drawable/rounded_bg.xml
@@ -0,0 +1,22 @@
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/rounded_bg_top.xml b/app/src/main/res/drawable/rounded_bg_top.xml
new file mode 100644
index 0000000..15492e6
--- /dev/null
+++ b/app/src/main/res/drawable/rounded_bg_top.xml
@@ -0,0 +1,6 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/splash_icon.xml b/app/src/main/res/drawable/splash_icon.xml
new file mode 100644
index 0000000..ee24a46
--- /dev/null
+++ b/app/src/main/res/drawable/splash_icon.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/about.xml b/app/src/main/res/layout/about.xml
new file mode 100644
index 0000000..667d12d
--- /dev/null
+++ b/app/src/main/res/layout/about.xml
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_browser_dialog.xml b/app/src/main/res/layout/activity_browser_dialog.xml
new file mode 100644
index 0000000..bc3aae1
--- /dev/null
+++ b/app/src/main/res/layout/activity_browser_dialog.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/app_drawer.xml b/app/src/main/res/layout/app_drawer.xml
new file mode 100644
index 0000000..947afea
--- /dev/null
+++ b/app/src/main/res/layout/app_drawer.xml
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/app_info_dialog.xml b/app/src/main/res/layout/app_info_dialog.xml
new file mode 100644
index 0000000..b2c5849
--- /dev/null
+++ b/app/src/main/res/layout/app_info_dialog.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/app_menu.xml b/app/src/main/res/layout/app_menu.xml
new file mode 100644
index 0000000..cfa9d22
--- /dev/null
+++ b/app/src/main/res/layout/app_menu.xml
@@ -0,0 +1,152 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/apps_child.xml b/app/src/main/res/layout/apps_child.xml
new file mode 100644
index 0000000..dca3e35
--- /dev/null
+++ b/app/src/main/res/layout/apps_child.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/child_sys_info.xml b/app/src/main/res/layout/child_sys_info.xml
new file mode 100644
index 0000000..0126031
--- /dev/null
+++ b/app/src/main/res/layout/child_sys_info.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/color_picker.xml b/app/src/main/res/layout/color_picker.xml
new file mode 100644
index 0000000..d146295
--- /dev/null
+++ b/app/src/main/res/layout/color_picker.xml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/feeds.xml b/app/src/main/res/layout/feeds.xml
new file mode 100644
index 0000000..d083b91
--- /dev/null
+++ b/app/src/main/res/layout/feeds.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/feeds_rss.xml b/app/src/main/res/layout/feeds_rss.xml
new file mode 100644
index 0000000..3ac6a49
--- /dev/null
+++ b/app/src/main/res/layout/feeds_rss.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/feeds_sys_infos.xml b/app/src/main/res/layout/feeds_sys_infos.xml
new file mode 100644
index 0000000..649b8a4
--- /dev/null
+++ b/app/src/main/res/layout/feeds_sys_infos.xml
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/launcher_activity.xml b/app/src/main/res/layout/launcher_activity.xml
new file mode 100644
index 0000000..2349793
--- /dev/null
+++ b/app/src/main/res/layout/launcher_activity.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/launcher_home.xml b/app/src/main/res/layout/launcher_home.xml
new file mode 100644
index 0000000..f656e74
--- /dev/null
+++ b/app/src/main/res/layout/launcher_home.xml
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/list_item.xml b/app/src/main/res/layout/list_item.xml
new file mode 100644
index 0000000..563c4b4
--- /dev/null
+++ b/app/src/main/res/layout/list_item.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/quick_access.xml b/app/src/main/res/layout/quick_access.xml
new file mode 100644
index 0000000..1aca443
--- /dev/null
+++ b/app/src/main/res/layout/quick_access.xml
@@ -0,0 +1,186 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/settings_activity.xml b/app/src/main/res/layout/settings_activity.xml
new file mode 100644
index 0000000..399a8dc
--- /dev/null
+++ b/app/src/main/res/layout/settings_activity.xml
@@ -0,0 +1,134 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/settings_advance.xml b/app/src/main/res/layout/settings_advance.xml
new file mode 100644
index 0000000..c803579
--- /dev/null
+++ b/app/src/main/res/layout/settings_advance.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/settings_appearances.xml b/app/src/main/res/layout/settings_appearances.xml
new file mode 100644
index 0000000..56a2dcf
--- /dev/null
+++ b/app/src/main/res/layout/settings_appearances.xml
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/settings_apps.xml b/app/src/main/res/layout/settings_apps.xml
new file mode 100644
index 0000000..bd40b19
--- /dev/null
+++ b/app/src/main/res/layout/settings_apps.xml
@@ -0,0 +1,261 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/settings_misc.xml b/app/src/main/res/layout/settings_misc.xml
new file mode 100644
index 0000000..92cf84d
--- /dev/null
+++ b/app/src/main/res/layout/settings_misc.xml
@@ -0,0 +1,154 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/settings_time_date.xml b/app/src/main/res/layout/settings_time_date.xml
new file mode 100644
index 0000000..f006235
--- /dev/null
+++ b/app/src/main/res/layout/settings_time_date.xml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/settings_todo.xml b/app/src/main/res/layout/settings_todo.xml
new file mode 100644
index 0000000..a301aaf
--- /dev/null
+++ b/app/src/main/res/layout/settings_todo.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/settings_weather.xml b/app/src/main/res/layout/settings_weather.xml
new file mode 100644
index 0000000..cb0de7f
--- /dev/null
+++ b/app/src/main/res/layout/settings_weather.xml
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/shortcut_maker.xml b/app/src/main/res/layout/shortcut_maker.xml
new file mode 100644
index 0000000..10c7c7a
--- /dev/null
+++ b/app/src/main/res/layout/shortcut_maker.xml
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/todo_dialog.xml b/app/src/main/res/layout/todo_dialog.xml
new file mode 100644
index 0000000..f521ad3
--- /dev/null
+++ b/app/src/main/res/layout/todo_dialog.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/todo_manager.xml b/app/src/main/res/layout/todo_manager.xml
new file mode 100644
index 0000000..c342ef0
--- /dev/null
+++ b/app/src/main/res/layout/todo_manager.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/add_widget.xml b/app/src/main/res/menu/add_widget.xml
new file mode 100644
index 0000000..4aab182
--- /dev/null
+++ b/app/src/main/res/menu/add_widget.xml
@@ -0,0 +1,6 @@
+
+
diff --git a/app/src/main/res/menu/widget_menu.xml b/app/src/main/res/menu/widget_menu.xml
new file mode 100644
index 0000000..989b092
--- /dev/null
+++ b/app/src/main/res/menu/widget_menu.xml
@@ -0,0 +1,18 @@
+
+
\ 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..bbd3e02
--- /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/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml
new file mode 100644
index 0000000..a6b3dae
--- /dev/null
+++ b/app/src/main/res/values-bn-rBD/strings.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
new file mode 100644
index 0000000..fd6a044
--- /dev/null
+++ b/app/src/main/res/values-de/strings.xml
@@ -0,0 +1,148 @@
+
+
+ OWM API-Schlüssel
+ Center
+ Warnung! Diese Operation löscht alle Daten von dieser App! Dies kann nicht rückgängig gemacht werden.
+ Zeit — Datum
+ Sie haben leider kein Icon-Pack installiert.
+ Standart
+ Zurücksetzen
+ Temperatur
+ Erlauben, bei Doppeltippen in den Ruhezustand zu wechseln
+ Neu Hinzufügen
+ In Zwischenablage kopiert
+ Willkommen
+ Zahl der To-Do einträge auf dem Startbildschirm
+ APK-Datei teilen über
+ Widget hinzufügen
+ App-Thema wählen
+ App-Ausrichtung
+ Letzte Aktualisierung
+ Erleichterte Bedienung
+ Kein AppStore auf diesem Gerät.
+ Danksagungen
+\n https://github.com/cachapa/ExpandableLayout
+ Hintergrund geändert
+ Negativ
+ Nicht ausführbar
+ Anzeigezeitpunkt
+ Weiteres
+ Die Änderungen werden erst nach Neustart der App übernommen. Jetzt neu starten\?
+ Willkommen bei Lunar Launcher.
+\n
+\n Nachdem Sie dieses Dialogfeld geschlossen haben, werden Sie aufgefordert, die erforderlichen Berechtigungen zu erteilen, damit die App wie erwartet funktioniert.
+\n
+\n Systemeinstellungen ändern – Um die Helligkeit mit einem Schieberegler zu steuern
+\n Telefon – Zum Tätigen von Anrufen über Verknüpfungen
+\n
+\n Es handelt sich um eine mit Copyleft versehene Libre-Software. \u0020Überprüfen, ändern und mit allen teilen.
+\n
+\n Gesten:
+\n Nach oben wischen – Schnellzugriffsdialog
+\n Nach unten wischen – Benachrichtigungsfeld
+\n Nach rechts wischen – Feeds
+\n Nach links wischen – App-Schublade
+\n Tippen und halten Sie die Batterieanzeige – Launcher-Einstellungen
+\n Tippen und halten Sie den unteren Teil des Bildschirms – To-Do-Manager
+\n Doppeltippen – Gerätesperre/Ruhezustand (in den Einstellungen aktivieren)
+\n Tippen Sie auf den Favoriten und halten Sie es gedrückt – Favorit entfernen
+\n
+\n Weitere Informationen auf der Wiki-Seite auf GitHub.
+ Total
+ Liste mit Symbolen
+ Statusleiste verstecken
+ 12-Stunden
+ Das Eingabefeld ist leer
+ Standart-Launcher wählen
+ RSS-Feed
+ Jetzt neu starten\?
+ Rechts
+ Sperrservice
+ Bild konnte nicht geladen werden
+ Aktualisieren
+ Verknüpfung & App-Icon Größe
+ Erweitert
+ Icon-Pack wählen
+ Aussehen des App-Drawers
+ Fortfahren
+ App-Info-Fenster öffnen
+ Stadtname mit Wetter zeigen
+ RSS-Feed-URL
+ Quellcode
+ Geräteadministratoren
+ To-Do Manager Sperren
+ In pip Modus öffnen
+ Löschen
+ Höhe der A bis Z Scroll-Leiste
+ Authentifizierungsfehler
+ Support
+ Version
+ URL
+ Wiederholungszahl
+ Alle To-Do Eingaben löschen\?
+ Wetter
+ Leider ist etwas schief gelaufen
+ Kontakte
+ Favorit
+ App deinstallieren
+ Auf App-Market anzeigen
+ Entwickelt von Md. Rasel Hossian
+ Hochgefahren seit
+\nSystem Aktiv
+\nVerwendeter Arbeitsspeicher
+\nAkkutemperatur
+\nAkku-Voltzahl
+\nSpeicherplatz
+\nIPv4
+\nIPv6
+ Verwendet
+ Über
+ Neustart
+ App-Drawer
+ Telegramgruppe
+ Erste Installation
+ Detailinfos anzeigen
+ Authentifizierungsfehler
+ Links
+ Lunar-Einstellungen
+ Root
+ Kostenlos
+ 24-Stunden
+ Hintergrund
+ Hochbewegen
+ Dunkelmodus
+ To-Do
+ Runterbewegen
+ Lunar Launcher erlauben, das Gerät in den Ruhezustand zu versetzen (API 28+).
+ Fortfahren
+ System folgen
+ Mit Tastatur starten
+ Hilf dem Entwickler die App Kostenlos und am leben zu halten
+\n
+\nSpenden: Wenn möglich, direkt Spenden.
+\nAmazon: Nutze den Link um den Entwickler beim Shoppen zu unterstützen.
+\nStar: Star auf GitHub um deine Unterstützung zu zeigen.
+ Liste
+ Tabelle
+ Spenden
+ Alle löschen
+ Datumsformat
+ Höhe erhöhen
+ Keine Tabellenspalten
+ Positiv
+ Schnellstart
+ Hintergrund ändern
+ Stadtname
+ APK-Datei teilen
+ Höhe verkleinern
+ Zeitformat
+ Immer zum Home-Bildschirm zurückkehren
+ To-Do Manager
+ Zahl der Verknüpfungen (URL/Kontakte)
+ Licht
+ Temperatureinheit
+ Später
+ Mit ihrem Gerätanmeldedaten anmelden, um fortzufahren.
+ Aktivitäten durchsuchen
+ Systemstatus
+
\ No newline at end of file
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
new file mode 100644
index 0000000..b7e0618
--- /dev/null
+++ b/app/src/main/res/values-es/strings.xml
@@ -0,0 +1,142 @@
+
+
+ Acerca de
+ Accesibilidad
+ Deja que Lunar Launcher ponga el dispositivo en reposo (API 28+).
+ Bloquear servicio
+
+ Agradecimientos\n
+ https://github.com/cachapa/ExpandableLayout
+
+ Añadir nuevo
+ Añadir widget
+ Avanzado
+ Apariencia
+ Elegir el tema de la aplicación
+ Alineamiento de la app
+ Cajón de aplicaciones
+ Abrir la ventana con la información de la aplicación
+ Visitar en la Tienda de aplicaciones
+ Error de autenticación
+ Autenticación fallida
+ Identifícate con las credenciales del dispositivo para proceder.
+ Fondo
+ Siempre volver a la pantalla de inicio
+ Centro
+ Cambiar fondo de pantalla
+ Escoger launcher predeterminado
+ Ciudad
+ Contactar
+ Copiado al portapapeles
+ Oscuro
+ Formato de fecha
+ Reducir altura
+ Predeterminado
+ Borrar
+ Borrar todo
+ ¿Borrar todas las tareas existentes\?
+ Desarrollado por Md. Rasel Hossain
+ Administrador del dispositivo
+ Permite poner el dispositivo en reposo con un doble toque
+ Donar
+ Método de bloqueo de doble toque
+ El campo de texto está vacío
+ URL de la fuente RSS
+ Primera instalación
+ Seguir al sistema
+ Libre
+ Abrir en modo pip
+ Frecuencias
+ Entendido
+ Ocultar barra de estado
+ Tamaño de los accesos directos e iconos de las aplicaciones
+ Error al buscar imagen
+ Aumentar altura
+ Última actualización
+ Izquierda
+ Claro
+ Ajustes de Lunar
+ Miscelánea
+
+ Tiempo encendido\nSistema activo\nUmbral de memoria\nTemperatura de la batería\nVoltaje de la bateria\nRaíz de almacenamiento\nIPv4\nIPv6
+
+ Mover hacia arriba
+ Mover hacia abajo
+ Negativo
+ No se pude encontrar ninguna tienda de aplicaciones en este dispositivo.
+ Clave de la API de OWM
+ Positivo
+ Proceder
+ Lanzado rápido
+ Resetear
+ Esta operación borrará todos los datos almacenados para esta aplicación, y no se puede deshacer.
+ Reiniciar
+ Derecha
+ Raíz
+ Feed RSS
+ Altura de la barra alfabética
+ Comienza con el teclado
+ Número de accesos directos (URL/Contacto)
+ Mostrar nombre de la ciudad en los datos meteorológicos
+ Algo ha ido mal
+ Código fuente
+ Estrella
+ Soporte
+ Apoya al desarrollador para mantener esta aplicación gratis y viva.
+ \nDonar: si es posible, haga donaciones directas para ayudar y alentar al desarrollador.
+ \nAmazon: use el enlace de afiliado mientras compra en Amazon para recompensar al desarrollador.
+ \nEstrella: Destaca en GitHub para mostrar tu apoyo.
+
+ Información del sistema
+ Grupo de Telegram
+ Temperatura
+ Unidad de temperatura
+ Hora — Fecha
+ Formato de hora
+ Tareas pendientes
+ Número de entradas de tareas pendientes en la pantalla de inicio
+ Administrador de tareas pendientes
+ Bloquear el gestor de tareas pendientes
+ Total
+ 12 Horas
+ 24 Horas
+ Fallo al abrir
+ Desinstalar la aplicación
+ Actualizar
+ URL
+ Usado
+ Versión
+ Fondo de pantalla cambiado
+ Tiempo
+ Bienvenide
+ Bienvenide a Lunar Launcher. Esta es la primera vez que abres la aplicación; si es la primera vez que la usas, lee estos textos antes de proceder.
+ \n\nPrimero, tras cerrar este mensaje, se te pedirá conceder los siguientes permisos. Si decides no hacerlo, esta aplicación no funcionará como se espera.
+ \n\nModificar ajustes del sistema - Para controlar el brillo usando una barra deslizable
+ \nTeléfono - Para poder hacer llamadas desde los accesos directos a contactos
+ \n\n Si sigues teniendo cualquier duda, puedes comprobar el código fuente de la aplicación, este es una aplicación completamente de código abierto.
+ \n\nGestos:
+ \nDeslizar hacia arriba - acceso rápido
+ \nDeslizar hacia abajo - panel de notificaciones
+ \nDeslizar a la derecha - Feed
+ \nDeslizar a la izquierda - Cajón de aplicaciones
+ \nMantener pulsado en el reloj circular - Ajustes de Lunar launcher
+ \nMantener pulsado en la parte inferior de la pantalla - Administrador de tareas
+ \nDoble toque - Bloquear dispositivo (activar en ajustes)
+ \nMantener pulsado un elemento favorito - Eliminar favorito
+ \n\nPara más información, visite la página de Wiki en Github.
+
+ Lo sentimos, no tienes ningún paquete de iconos instalado.
+ Comparte el APK a través de
+ Los cambios que acabas de realizar requieren reiniciar la aplicación para que surtan efecto. ¿Quieres reiniciarla ahora\?
+ ¿Reiniciar ahora\?
+ Elige un paquete de iconos
+ Diseño del cajón de las aplicaciones
+ Ver la información detallada
+ Lista
+ Cuadrícula
+ Número de columnas de la cuadrícula
+ Compartir el archivo apk
+ Más tarde
+ Navegar por las actividades
+ Lista con iconos
+
\ No newline at end of file
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
new file mode 100644
index 0000000..493de38
--- /dev/null
+++ b/app/src/main/res/values-fr/strings.xml
@@ -0,0 +1,148 @@
+
+
+ Centrer
+ Ajouter nouveau
+ Copié dans le presse-papier
+ Ajouter widget
+ Sélection du thème de l’application
+ Alignement des applications
+ Accessibilité
+ Remerciements
+\nhttps://github.com/cachapa/ExpandableLayout
+ Apparence
+ Sélectionner le lanceur par défaut
+ Service de verrouillage
+ Avancé
+ Sélectionner le pack d’icônes
+ Contact
+ À propos
+ Arrière-plan
+ Sombre
+ Laissez Lunar Launcher mettre l’appareil en sommeil (API 28+).
+ Modifier le fond d’écran
+ Nom de la ville
+ Parcourir les activités
+ agencement du tiroir d\'applications
+ Fenêtre des informations sur l\'application
+ Visiter sur l\'app market
+ tiroir d\'applications
+ Erreur d\'authentification
+ Par défaut
+ Permettre de mettre l\'appareil en mode veille en tapant deux fois
+ Administrateur de l\'appareil
+ Supprimer
+ Échec d\'authentification
+ Supprimer toutes les tâches\?
+ Développé par Md. Rasel Hossain
+ Voir l\'information en détail
+ Faire un don
+ Tout supprimer
+ Format de la date
+ Diminuer la hauteur
+ Toujours revenir à l\'écran d\'accueil
+ Connectez-vous à l\'aide de votre code
+ Clé d\'API chez OWM
+ Cette opération supprimera toutes les données stockées pour cette application, sans retour possible.
+ Heure — Date
+ Désolé, vous n\'avez aucun pack d\'icônes installé.
+ Réinitialiser
+ Température
+ Bienvenue
+ Nombre de tâches sur la page d\'accueil
+ Partager le fichier .apk via
+ Dernière mise à jour
+ Aucun magasin d\'applications trouvé sur cet appareil.
+ Fond d\'écran changé
+ Négatif
+ Incapable de lancer
+ Autres
+ Les changements que vous venez de faire requièrent un redémarrage de l\'application pour être appliqués. Voulez-vous redémarrer maintenant ?
+ Bienvenue sur Lunar Launcher.
+\n
+\nAprès avoir fermé cette boîte de dialogue, il vous sera demandé d\'accorder les permissions nécessaires au bon fonctionnement de cette application.
+\n
+\nModifier les paramètres système — Pour contrôler la luminosité de l\'écran en utilisant un curseur
+\nTéléphone — Pour faire des appels à partir de raccourcis contact
+\n
+\nCeci est un logiciel libre ; inspectez son code source, modifiez-le et partagez-le avec le plus grand nombre.
+\n
+\nGestes :
+\nGlisser vers le haut — Fenêtre d\'accès rapide
+\nGlisser vers le bas — Panneau de notifications
+\nGlisser vers la droite — Fils RSS
+\nGlisser vers la gauche — Liste des applications
+\nRester appuyé sur l\'indicateur de batterie — Paramètres du lanceur
+\nRester appuyé dans la zone inférieure de l\'écran — Gestionnaire de tâches
+\nDouble-clic — Verrouiller l\'appareil (à activer dans les paramètres du lanceur)
+\nRester appuyé sur un favori — Supprimer cet item
+\n
+\nPlus d\'infos sur la page wiki (en anglais) sur GitHub.
+ Total
+ Liste avec icône
+ Cacher la barre d\'état
+ 12 heures
+ La zone de texte est vide
+ Flux RSS
+ Redémarrer maintenant ?
+ Droite
+ Impossible de récupérer l\'image
+ Mettre à jour
+ Raccourci et taille des icônes
+ Compris
+ Montrer la ville avec la météo
+ URL du flux RSS
+ Code source
+ Vérouiller le gestionnaire de tâches
+ Ouvrir en mode pip
+ Hauteur de la barre de défilement A-Z
+ Soutenir
+ Version
+ URL
+ Fréquence
+ Météo
+ Quelque chose s\'est mal déroulé
+ Noter
+ Désinstaller l\'application
+ Temps d\'éveil de l\'appareil
+\nSystème actif
+\nSeuil mémoire
+\nTempérature de la batterie
+\nTension de la batterie
+\nDossier de stockage racine
+\nIPv4
+\nIPv6
+ Utilisé
+ Redémarrer
+ Groupe Telegram
+ Première installation
+ Gauche
+ Paramètres de Lunar
+ Racine
+ Gratuit
+ 24 heures
+ Déplacer vers le haut
+ Tâches
+ Déplacer vers le bas
+ Continuer
+ Suivre le système
+ Démarrer avec le clavier
+ Apportez votre soutien au développeur pour garder cette application gratuite et en vie.
+\n
+\nFaire un don : Si possible, faites un don directement au développeur pour l\'aider et l\'encourager.
+\nAmazon : Utilisez le lien d\'affiliation lors de vos achats chez Amazon pour récompenser le développeur.
+\nGitHub : Ajoutez cette application à vos favoris sur GitHub pour montrer votre soutien.
+ Liste
+ Grille
+ Augmenter la hauteur
+ Nombre de colonnes dans la grille
+ Positif
+ Lancement rapide
+ Partager le fichier .apk
+ Format d\'heure
+ Gestionnaire de tâches
+ Nombre de raccourcis (URL/Contact)
+ Clair
+ Unité de température
+ Plus tard
+ Statistiques système
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ia/strings.xml b/app/src/main/res/values-ia/strings.xml
new file mode 100644
index 0000000..44ff705
--- /dev/null
+++ b/app/src/main/res/values-ia/strings.xml
@@ -0,0 +1,118 @@
+
+
+ Clave del API de OWM
+ Hora — Data
+ Predeterminate
+ Reinitialisar
+ Temperatura
+ Adder nove
+ Copiate al area de transferentia
+ Benvenite
+ Compartir le APK via
+ Adder widget
+ Seliger le thema del application
+ Alineamento del applicationes
+ Ultime actualisation
+ Accessibilitate
+ Non poteva trovar necun magazin de applicationes in iste apparato.
+ Fundo de schermo cambiate
+ Negative
+ Non pote lancear
+ Apparentia
+ Miscellanea
+ Total
+ Lista con icones
+ Celar le barra de stato
+ 12 horas
+ Seliger lanceator predefinite
+ Fluxo RSS
+ Reinitiar ora?
+ Dextra
+ Non poteva obtener le imagine
+ Actualisar
+ Dimension de icones de application e accessos directe
+ Avantiate
+ Seliger le pacchetto de icones
+ Designo de tiratorio de applicationes
+ Comprendite
+ Aperir le fenestra de information del application
+ Monstrar nomine del citate con le tempore
+ URL de fluxo RSS
+ Codice fonte
+ Administrator de apparato
+ Aperir in modo pip
+ Deler
+ Altitude del barra alphabetic A-Z
+ Falleva le authentication
+ Supporto
+ Version
+ URL
+ Frequentia
+ Tempore
+ Contacto
+ Disinstallar le application
+ Visitar sur le magazin de applicationes
+ Disvellopate per Md. Rasel Hossain
+ Usate
+ A proposito de
+ Reinitiar
+ Tiratorio de applicationes
+ Gruppo de Telegram
+ Prime installation
+ Vider le information in detalio
+ Error de authentication
+ Sinistra
+ Parametros de Lunar
+ Radice
+ Libere
+ 24 horas
+ Fundo
+ Obscur
+ Continuar
+ Sequer le systema
+ Comenciar con le claviero
+ Lista
+ Grillia
+ Facer un donation
+ Deler toto
+ Formato del data
+ Augmentar le altitude
+ Numero de columnas del grillia
+ Positive
+ Cambiar le fundo del schermo
+ Nomine del citate
+ Compartir le file apk
+ Diminuer le altitude
+ Formato de hora
+ Sempre retornar al schermo de initio
+ Numero de accessos directe (URL/Contacto)
+ Clar
+ Unitate de temperatura
+ Depost
+ Navigar per le activitates
+ Information del systema
+ Centro
+ Iste operation delera tote le datos immagazinate pro iste application e non pote esser disfacite.
+ Nos regretta, tu non ha necun pacchetto de icones installate.
+ Numero de entratas de cargas in le schermo de initio
+ \u0020Recognoscentias
+\n \u0020https://github.com/cachapa/ExpandableLayout \u0020
+ Le campo de texto es vacue
+ Blocar servicio
+ Blocar le gestor de cargas
+ Deler tote le entratas de cargas?
+ Habeva qualcosa improprie
+ Tempore de activitate del apparato
+\nSystema active
+\nLimine de memoria
+\nTemperatura de batteria
+\nVoltage de batteria
+\nImmagazinage radice
+\nIPv4
+\nIPv6
+ Mover in alto
+ Cargas pendente
+ Mover in basso
+ Lanceamento rapide
+ Gestor de cargas
+
\ No newline at end of file
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
new file mode 100644
index 0000000..1f2889c
--- /dev/null
+++ b/app/src/main/res/values-it/strings.xml
@@ -0,0 +1,147 @@
+
+
+ Maggiori informazioni
+ Accessibilità
+ Permette a Lunar Launcher di bloccare il dispositivo,(API 28 e successivi).
+ Servizio di blocco
+
+ Ringraziamenti\n
+ https://github.com/cachapa/ExpandableLayout
+
+ Aggiungi
+ Aggiungi widget
+ Avanzate
+ Stile
+ Scegli il tema dell\'applicazione
+ Allineamento applicazioni
+ Pannello applicazioni
+ Apri finestra informazioni app
+ Visualizza nel negozio app
+ Errore di autenticazione
+ Autenticazione fallita
+ Autenticati con le credenziali del dispositivo per procedere.
+ Colore di fondo
+ Torna sempre alla schermata principale
+ Centro
+ Cambia sfondo
+ Scegli il launcher predefinito
+ Nome della città
+ Contatti
+ Copiato negli appunti
+ Scuro
+ Formato Data
+ Predefinito
+ Cancella
+ Cancella tutto
+ Elimino tutte le note\?
+ Sviluppato da Md. Rasel Hossain
+ Amministratore del dispositivo
+ Permetti il blocco dello schermo con due tocchi
+ Donazioni
+ Blocco schermo con due tocchi
+ Il campo è vuoto
+ URL del flusso RSS
+ Prima installazione
+ Come da sistema
+ Libero
+ Apri in modalità PiP
+ Frequenza
+ Capito
+ Immagine non ottenuta
+ Ultimo aggiornamento
+ Sinistra
+ Chiaro
+ Impostazioni di Lunar
+ Varie
+
+ Tempo di uptime\nSistema attivo da\nSoglia della Memoria\nTemperatura della Batteria\nTensione della Batteria
+ \nArchiviazione di Root\nIPv4\nIPv6
+
+ Negativo
+ Non riesco a trovare Negozi installati in questo dispositivo.
+ Chiave API OWM
+ Affermativo
+ Procedere
+ Avvio Rapido
+ Reset
+ Questa operazione cancellerà tutti i dati salvati per questa applicazione. I dati non saranno ripristinabili.
+ Riavvia
+ Destra
+ Root
+ Flusso RSS
+ Inizia con tastiera
+ Numero di scorciatoie (URL/Contatti)
+ Mostra il nome della città con il meteo
+ Qualcosa è andato storto
+ Codice sorgente
+ Stella
+ Supporto
+ Supporti lo sviluppatore per mantenere l\'applicazione viva e gratuita:
+\n
+\nDonazioni: se possibile, può fare una donazione diretta ed incoraggiare lo sviluppatore.
+\nAmazon: può usare il link di affiliazione mentre fa acquisti su Amazon per ricompensare lo sviluppatore.
+\nMetta una stella: può mostrare il suo supporto su Github mettendo una stella.
+ Statistiche di sistema
+ Grruppo Telegram
+ Temperatura
+ Unità della temperatura
+ Data e ora
+ Formato ora
+ Note
+ Numero di note nella schermata principale
+ Gestore delle note
+ Blocco del gestore note
+ Totale
+ 12 ore
+ 24 ore
+ Impossibile avviare
+ Disinstalla la applicazione
+ Aggiorna
+ URL
+ Usata
+ Versione
+ Sfondo cambiato
+ Meteo
+ Benvenuti
+ Benvenuti su Lunar Launcher.
+\n
+\nAppena chiuso questo dialogo, le verrà chiesto di fornire le seguenti autorizzazioni. Se si rifiuta di fornirle, questa applicazione non funzionerà come previsto.
+\n
+\nModifica dei parametri di sistema - Per controllare la luminosità utilizzando il cursore
+\nTelefono - Per effettuare chiamate dalle scorciatoie
+\n
+\nSe ha ancora qualche dubbio, può verificare il codice sorgente in qualsiasi momento. Questa applicazione è completamente open-source.
+\n
+\nGesti:
+\nScorrere verso l\'alto - comparsa accesso rapido
+\nScorrere verso il basso - pannello delle notifiche
+\nScorrere a destra - pannello delle informazioni
+\nScorrere a sinistra - pannello delle applicazioni
+\nPremere e mantenere sull\'indicatore della batteria - impostazioni del launcher
+\nPremere e mantenere sulla parte inferiore dello schermo - gestore delle note
+\nDoppio tocco - blocco del dispositivo (attivare dalle impostazioni)
+\nPremere e mantenere l\'elemento dei preferiti - rimuove l\'elemento
+\n
+\nPer maggiori informazioni, visitare la wiki su Github.
+ Scusa, non hai pacchetti di icone installati.
+ Condividi APK via
+ I cambiamenti che hai fatto richiedono il riavvio della app per essere applicati. Vuoi riavviare la app adesso\?
+ Lista con icone
+ Nascondi barra di stato
+ Riavviare adesso\?
+ Dimensioni scorciatoie e icone app
+ Scegli pacchetto icone
+ Layout cassetto delle app
+ Altezza della barra A-Z
+ Visualizza dettagli
+ Muovi sù
+ Muovi giù
+ Lista
+ Griglia
+ Aumenta altezza
+ Numero colonne
+ Condividi il file apk
+ Abbassa altezza
+ Dopo
+ Scorri attività
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
new file mode 100644
index 0000000..a88a1af
--- /dev/null
+++ b/app/src/main/res/values-ja/strings.xml
@@ -0,0 +1,58 @@
+
+
+ 新しく追加
+ ウィジェットを追加
+ アプリケーションのテーマを選択
+ アプリの調整
+ アクセシビリティ
+ 感謝
+\n https://github.com/cachapa/ExpandableLayout
+ 外観
+ ロックサービス
+ 高度
+ アプリドロワーのレイアウト
+ アプリ情報ウィンドウを開く
+ アプリマーケットにアクセス
+ 詳細
+ アプリドロワー
+ Lunar Launcherでデバイスをスリープ状態にします。(API 28+)
+ アクティビティを参照
+ センター
+ デフォルトのランチャーを選択
+ アイコンパックを選択
+ 認証に失敗しました
+ 連絡先
+ 認証エラー
+ バックグラウンド
+ 壁紙を変更
+ 都市名
+ 常にホーム画面に戻る
+ 続行するには、デバイスの情報で認証してください。
+ 申し訳ありませんが、アイコンパックがインストールされていません。
+ デフォルト
+ ダブルタップのジェスチャーでデバイスをスリープ状態にします
+ クリップボードにコピーしました
+ ステータスバーを非表示にする
+ テキストフィールドが空です
+ 画像を取得出来ませんでした
+ ショートカット&アプリアイコンのサイズ
+ 分かった
+ RSSフィードURL
+ デバイス管理者
+ PIPモードで開く
+ 削除
+ 頻度
+ 全てのToDoエントリーを削除しますか?
+ 開発者:Md. Rasel Hossain
+ 最初のインストール
+ 詳細情報を見る
+ 自由
+ ダーク
+ フォローシステム
+ グリッド
+ 寄付
+ 全て削除
+ 日付
+ グリッド列の数
+ 高さを減らす
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml
new file mode 100644
index 0000000..ed86e22
--- /dev/null
+++ b/app/src/main/res/values-ml/strings.xml
@@ -0,0 +1,5 @@
+
+
+ അക്സസിബിളിറ്റി
+ വിശദാംശം
+
\ No newline at end of file
diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000..8909e04
--- /dev/null
+++ b/app/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,44 @@
+
+
+ Tilgjengelighet
+ Bakgrunn
+ Legg til ny
+ Enhetsadministrator
+ Programdrakt
+ Kopiert til utklippstavlen
+ Alltid gå tilbake til hjemmeskjermen
+ Forvalg
+ Endre bakgrunnsbilde
+ Datoformat
+ Slett
+ Slett alle
+ Doner
+ Tekstfeltet er tomt
+ Programjustering
+ Om
+ Legg til miniprogram
+ Utseeender
+ Programskuff
+ Programinfo
+ Programbutikk
+ Bynavn
+ Kontakt
+ Mørk
+ Tilbakestill
+ Temperatur
+ Del APK via
+ Ymse
+ Velg forvalgt oppstarter
+ RSS-informasjonskanal
+ Høyre
+ Låsetjeneste
+ Kildekode
+ Støtte
+ Slett alle gjøremålsoppføringer\?
+ Noe gikk galt
+ Omstart
+ Telegram-gruppe
+ Vis detaljer info
+ Tidsformat
+ Temperaturenhet
+
\ No newline at end of file
diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml
new file mode 100644
index 0000000..74e5118
--- /dev/null
+++ b/app/src/main/res/values-night/colors.xml
@@ -0,0 +1,7 @@
+
+
+ #FF1C1B1F
+ #FFFFFBFE
+ #11FFFFFF
+ #01000000
+
\ No newline at end of file
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000..978bc96
--- /dev/null
+++ b/app/src/main/res/values-night/themes.xml
@@ -0,0 +1,15 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000..cc3f491
--- /dev/null
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,5 @@
+
+
+ Sobre
+ Acessibilidade
+
\ No newline at end of file
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
new file mode 100644
index 0000000..a2e002b
--- /dev/null
+++ b/app/src/main/res/values-pt/strings.xml
@@ -0,0 +1,135 @@
+
+
+ Alterar papel de parede
+ Agradecimentos
+\n https://github.com/cachapa/ExpandableLayout
+ Adicionar Widget
+ Avançar
+ Aparências
+ Tema do aplicativo
+ Alinhamento do aplicativo
+ Autentifique-se com a credencial do seu dispositivo para prosseguir.
+ Escolher Launcher padrão
+ Nome da cidade
+ Copiado para a área de transferência
+ Formato de data
+ Diminuir altura
+ Padrão
+ Excluir
+ Desenvolvido por Md. Rasel Hossain
+ Administrador do dispositivo
+ Doar
+ Método de bloqueio de toque-duplo
+ O campo de texto está vazio
+ URL do Feed RSS
+ Primeira instalação
+ Gratuito
+ Forma livre
+ Frequência
+ Entendi
+ Ocultar a barra de status
+ Tamanho do atalho e do ícone do aplicativo
+ Não foi possível buscar a imagem
+ Aumentar a altura
+ Última atualização
+ Esquerda
+ Claro
+ Miscelâneos
+ Código-fonte
+ Estrela
+ Apoie
+ Grupo no Telegram
+ Temperatura
+ Unidade de temperatura
+ Tempo — Data
+ Formato de hora
+ Gerenciador de tarefas
+ Bloqueio do gerenciador de tarefas
+ Total
+ Bem-vindo(a)
+ 12 horas
+ 24 horas
+ Não é possível iniciar
+ Desinstalar
+ Atualizar
+ URL
+ Usado
+ Versão
+ Papel de parede alterado
+ Centro
+ Fundo
+ Sempre voltar para a tela inicial
+ Sobre
+ Acessibilidade
+ Deixar o Lunar Launcher colocar o dispositivo para dormir (no Android Pie (API 28+)).
+ Informações do aplicativo
+ Adicionar novo
+ Serviço de bloqueio
+ Gaveta de aplicativos
+ Loja de aplicativos
+ Erro de autenticação
+ Falha na autentificação
+ Contato
+ Seguir o sistema
+ Toque duplo para bloquear
+ Escuro
+ Excluir tudo
+ Excluir todas as tarefas\?
+ Clima
+ Negativo
+ Positivo
+ Bem-vindo(a) ao Luna Launcher.
+\n
+\nApós fechar este diálogo, você será pedido para dar as permissões necessárias para o aplicativo funcionar como esperado.
+\n
+\nModificar configurações do sistema — Para controlar o brilho com um deslizador
+\nTelefone — Para realizar chamadas a partir dos atalhos
+\n
+\nÉ um software livre com distribuição gratuita (Copyleft); verifique, mude e compartilhe com todos(as).
+\n
+\nGestos:
+\nDeslizar para cima — Acesso rápido
+\nDeslizar para baixo — Painel de notificações
+\nDeslizar para direita— Feeds
+\nDeslizar para esquerda — Gaveta de aplicativos
+\nToque e segure dentro do indicador de bateria — Configurações do Launcher
+\nToque e segure na parte inferior da tela — Gerenciador de tarefas
+\nToque-duplo — Bloquear dispositivo/Colocar para dormir (ativar em configurações)
+\nToque e segure no item favorito — Remover favorito
+\n
+\nMais informações na página wiki no GitHub.
+ Configurações do aplicativo
+ Direita
+ Número de entradas de tarefas na tela inicial
+ Tempo de atividade do dispositivo
+\nSistema ativo
+\nLimite de memória
+\nTemperatura da bateria
+\nTensão da bateria
+\nArmazenamento raiz
+\nIPv4
+\nIPv6
+ Mover para baixo
+ Nenhuma loja de aplicativos instalada neste dispositivo.
+ Mover para cima
+ Chave da API OWM
+ Prosseguir
+ Início rápido
+ Resetar
+ Raiz
+ Reiniciar
+ Essa ação limpa todos os dados armazenados para este aplicativo e não pode ser desfeita.
+ Altura da barra de deslocamento A-Z
+ Feed RSS
+ Tarefas
+ Procurar com o teclado
+ Algo deu errado
+ Número de atalhos (URL/Contato)
+ Mostrar nome da cidade com previsão do tempo
+ Status do sistema
+ Apoie o desenvolvedor para manter este aplicativo gratuito e vivo.
+\n
+\nDoe: Se possível, faça doações diretas para ajudar e encorajar o desenvolvedor.
+\nAmazon: Use o link afiliado enquanto compra na Amazon para recompensar o desenvolvedor.
+\nEstrela: Dê uma estrela no GitHub para mostrar o seu apoio.
+
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
new file mode 100644
index 0000000..47cce92
--- /dev/null
+++ b/app/src/main/res/values-tr/strings.xml
@@ -0,0 +1,137 @@
+
+
+ Hakkında
+ Erişilebilirlik
+
+ Lunar Launcher\'ın cihazı uyku moduna almasına izin veren erişilebilirlik servisi,
+ Android Pie (API 28) ve üstü için.
+
+ Kilit Servisi
+
+ Teşekkürler\n
+ https://github.com/cachapa/ExpandableLayout
+
+ Yeni Ekle
+ Widget Ekle
+ Gelişmiş
+ Görünüm
+ Uygulama Teması
+ Uygulama Çekmecesi
+ Uygulama Bilgisi
+ Uygulama Mağazası
+ Kimlik doğrulama hatası
+ Kimlik doğrulama başarısız oldu
+
+ Devam etmek için cihaz kimlik bilgilerinizle doğrulama yapın
+
+ Arka Plan
+ Her zaman Ana Ekrana Geri Dön
+ Duvar Kağıdı Değiştir
+ Varsayılan Başlatıcıyı Seçin
+ Şehir Adı
+ İletişim
+ Panoya kopyalandı
+ Karanlık
+ Tarih Formatı
+ Yüksekliği Azalt
+ Varsayılan
+ Sil
+ Tümünü Sil
+
+ Mevcut tüm görevleri silmek istediğinizden emin misiniz?
+
+ Geliştirici Md Rasel Hossain
+ Cihaz Yöneticisi
+
+ Çift dokunarak kilitleme özelliğini kullanmaya izin ver
+
+ Bağış Yap
+ Çift Dokunma Kilitleme Yöntemi
+ Metin alanı boş
+ RSS Besleme URL\'si
+ İlk Yükleme
+ Sistemi Takip Et
+ Ücretsiz
+ Serbest form
+ Frekans
+ Anladım
+ Durum Çubuğunu Gizle
+ Resim alınamadı
+ Yüksekliği Arttır
+ Son Güncelleme
+ Aydınlık
+ Ay Ayarları
+ Çeşitli
+
+ Cihaz Çalışma Süresi\nSistem Aktif\nBellek Eşiği\nBatarya Sıcaklığı\nBatarya Gerilimi
+ \nKök Depolama\nIPv4\nIPv6
+
+ Yukarı Taşı
+ Aşağı Taşı
+ Negatif
+ Bu cihazda yüklü hiçbir uygulama mağazası yok.
+ OWM API Anahtarı
+ Pozitif
+ Devam et
+ Hızlı Başlat
+ Sıfırla
+
+ Bu işlem, bu uygulama için depolanan tüm verileri temizleyecek ve geri alınamaz.
+
+ Yeniden Başlat
+ Kök
+ RSS Beslemesi
+ Klavyeyle Ara
+ Kısayol Sayısı (URL/Kişi)
+ Hava Durumu ile Şehir Adını Göster
+ Bir şeyler yanlış gitti
+ Kaynak Kodu
+ Yıldız
+ Destek
+ Geliştiricinin uygulamayı ücretsiz ve canlı tutmak için desteğini destekleyin.\n
+ \nBağış: Mümkünse, geliştiriciyi desteklemek ve teşvik etmek için doğrudan bağış yapın.
+ \nAmazon: Amazon\'da alışveriş yaparken ortaklık bağlantısını kullanarak geliştiriciyi ödüllendirebilirsiniz.
+ \nYıldız: Destek göstermek için Github\'da yıldız verin.
+
+ Sistem İstatistikleri
+ Telegram Grubu
+ Sıcaklık
+ Sıcaklık Birimi
+ Zaman - Tarih
+ Zaman Biçimi
+ Yapılacaklar
+ Ana Ekran\'daki Yapılacak Sayısı
+ Yapılacaklar Yöneticisi
+ Yapılacaklar Yöneticisi Kilidi
+ Toplam
+ 12 Saat
+ 24 Saat
+ Başlatılamadı
+ Kaldır
+ Güncelle
+ URL
+ Kullanılmış
+ Versiyon
+ Arkaplan başarıyla değiştirildi
+ Hava Durumu
+ Hoş Geldiniz
+ Lunar Launcher\'a hoş geldiniz. İlk açılışınız; eğer yeni bir kullanıcısınız,
+ devam etmeden önce bu metinleri okuyun.
+ \n\nİlk olarak, bu iletişim kutusunu kapattıktan sonra aşağıdaki izinleri vermeniz istenecektir. Eğer izinleri vermezseniz,
+ uygulama beklenildiği gibi çalışmaz.
+ \n\nSistem ayarlarını değiştirin - Bir kaydırıcı kullanarak parlaklığı kontrol etmek için
+ \nTelefon - Kısayollardan arama yapmak için
+ \n\nHala herhangi bir şüpheniz varsa, kaynak kodunu her zaman kontrol edebilirsiniz, tamamen açık kaynaklıdır.
+ \n\nJestler:
+ \nYukarı kaydır - hızlı erişim
+ \nAşağı kaydır - bildirim paneli
+ \nSağa kaydır - beslemeler
+ \nSola kaydır - uygulama çekmecesi
+ \nPil göstergesi içinde basılı tutun - launcher ayarları
+ \nEkrandaki alt kısımda basılı tutun - yapılacaklar yöneticisi
+ \nÇift tıklayın - cihaz kilidi/uyku (Ayarlar\'dan etkinleştirin)
+ \nFavori öğeyi basılı tutun - favoriyi kaldırın
+ \n\nDaha fazla bilgi için Github\'daki wiki sayfasını ziyaret edin.
+
+
+
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..7796124
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+ #FFFFFBFE
+ #FF1C1B1F
+ #11000000
+ #01ffffff
+ #FFFF0000
+ #FF00FF00
+ #FF0000FF
+
\ No newline at end of file
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..f814810
--- /dev/null
+++ b/app/src/main/res/values/dimens.xml
@@ -0,0 +1,23 @@
+
+
+ 0dp
+ 2dp
+ 4dp
+ 8dp
+ 12dp
+ 16dp
+ 20dp
+ 22dp
+ 36dp
+ 40dp
+ 48dp
+ 136dp
+ 196dp
+ 276dp
+ 324dp
+
+ 6sp
+ 16sp
+ 48sp
+ 216sp
+
\ 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..0387eab
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,169 @@
+
+
+ About
+ Accessibility
+
+ Let Lunar Launcher put the device to sleep (API 28+).
+
+ Lock Service
+
+ Acknowledgements\n
+ https://github.com/cachapa/ExpandableLayout
+
+ Browse activities
+ Add New
+ Add Widget
+ Advance
+ Amazon
+ Appearances
+ Choose Application Theme
+ App Alignment
+ App Drawer
+ App Drawer Layout
+ Open App Info window
+ Visit on app market
+ Apps Count
+ AARRGGBB
+ Authentication error
+ Authentication failed
+
+ Authenticate with your device credential to proceed.
+
+ Background
+ Always Go Back to Home Screen
+ Celsius
+ Center
+ Change Wallpaper
+ Choose Icon Pack
+ Choose Default Launcher
+ City Name
+ Contact
+ Copied to clipboard
+ CPU
+ Dark
+ Date Format
+ Decrease Height
+ Default
+ Delete
+ Delete All
+
+ Delete all to-do entries?
+
+ See detail information
+ Developed by Md. Rasel Hossain
+ Device Admin
+
+ Allow to put the device to sleep on double tap gesture
+
+ Donate
+ Double Tap Action
+ The text field is empty
+ Fahrenheit
+ RSS Feed URL
+ First Install
+ Follow System
+ Free
+ Open in pip mode
+ Frequency
+ Got it
+ Grid
+ No of Grid Columns
+ Hide Status Bar
+ Sorry, you don\'t have any icon pack installed.
+ Shortcut & App Icon Size
+ Could not fetch image
+ Increase Height
+ Last Update
+ Later
+ Left
+ Light
+ List
+ List with Icon
+ Lunar Settings
+ Misc
+
+ Device Uptime\nSystem Active\nMemory Threshold\nBattery Temperature\nBattery Voltage
+ \nRoot Storage\nIPv4\nIPv6
+
+ Move Up
+ Move Down
+ N/A
+ Negative
+
+ Couldn\'t find any app store on this device.
+
+ OWM API Key
+ Positive
+ Proceed
+ Quick Launch
+ RAM
+ Reset
+
+ This operation will clear all data stored for this app, and can\'t be undone.
+
+ Restart
+
+ The changes you have just made require app restart to take effect. Do you want to restart now?
+
+ Restart now?
+ Right
+ Root
+ RSS Feed
+ Height of A-Z Scrollbar
+ SDK
+ Start with Keyboard
+ Share the apk file
+ Share APK via
+ Number Of Shortcuts (URL/Contact)
+ Show City Name With Weather
+ Something went wrong
+ Source Code
+ Star
+ Support
+ Support the developer to keep this app gratis and alive.\n
+ \nDonate: If possible, make direct donations to help and encourage the developer.
+ \nAmazon: Use the affiliate link while shopping on Amazon to reward the developer.
+ \nStar: Star on GitHub to show your support.
+
+ System Stats
+ Telegram Group
+ Temperature
+ Temperature Unit
+ Time — Date
+ Time Format
+ To-do
+ Number Of To-Do Entries On Home Screen
+ To-Do Manager
+ Lock To-Do Manager
+ Total
+ 12 Hour
+ 24 Hour
+ UID
+ Unable to launch
+ Uninstall the application
+ Update
+ URL
+ Used
+ Version
+ Wallpaper changed
+ Weather
+ Welcome
+ Welcome to Lunar Launcher.
+ \n\nAfter closing this dialog, you will be asked to grant permissions needed for the app to work as expected.
+ \n\nModify system settings — To control brightness using a slider
+ \nPhone — To make call from shortcuts
+ \n\nIt\'s a copylefted libre software; check, change and share with all.
+ \n\nGestures:
+ \nSwipe up — Quick access dialog
+ \nSwipe down — Notification panel
+ \nSwipe right — Feeds
+ \nSwipe left — App drawer
+ \nTap and hold inside battery indicator — Launcher settings
+ \nTap and hold in the lower part of the screen — To-Do manager
+ \nDouble tap — Device lock/sleep (enable from settings)
+ \nTap and hold the favourite item — Remove favourite
+ \n\nMore info on the wiki page at GitHub.
+
+ Wiki
+
+
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..ed2bb2c
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..4a6b39a
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,17 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/device_admin.xml b/app/src/main/res/xml/device_admin.xml
new file mode 100644
index 0000000..43d5826
--- /dev/null
+++ b/app/src/main/res/xml/device_admin.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml
new file mode 100644
index 0000000..a8df289
--- /dev/null
+++ b/app/src/main/res/xml/file_paths.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/lock_service.xml b/app/src/main/res/xml/lock_service.xml
new file mode 100644
index 0000000..b5bd58a
--- /dev/null
+++ b/app/src/main/res/xml/lock_service.xml
@@ -0,0 +1,7 @@
+
+
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..e96e5e9
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,18 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+buildscript {
+ val kotlinVersion = "1.9.0"
+ extra ["kotlinVersion"] = kotlinVersion
+
+ dependencies {
+ classpath (kotlin("gradle-plugin", version = kotlinVersion))
+ }
+}
+
+plugins {
+ id ("com.android.application") version "8.2.1" apply false
+ id ("com.android.library") version "8.2.1" apply false
+}
+
+tasks.register("clean") {
+ delete(rootProject.buildDir)
+}
diff --git a/fastlane/metadata/android/en-US/changelogs/36.txt b/fastlane/metadata/android/en-US/changelogs/36.txt
new file mode 100644
index 0000000..3beff84
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/36.txt
@@ -0,0 +1,3 @@
+- requests consideration
+- new translations
+- improvements
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/37.txt b/fastlane/metadata/android/en-US/changelogs/37.txt
new file mode 100644
index 0000000..d29838a
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/37.txt
@@ -0,0 +1 @@
+- fixes a bug
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/38.txt b/fastlane/metadata/android/en-US/changelogs/38.txt
new file mode 100644
index 0000000..d019f1b
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/38.txt
@@ -0,0 +1,3 @@
+- startup crash fix by @ottop
+- icon pack support for favorite apps by @fs-sifat
+- app rename support by @fs-sifat
diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt
new file mode 100644
index 0000000..3af7bd0
--- /dev/null
+++ b/fastlane/metadata/android/en-US/full_description.txt
@@ -0,0 +1,19 @@
+Lunar Launcher is yet another addition to the world of android launchers. Low memory footprint and clean interface, but large number of features are awaiting. It is fully open-source and contains no ads or trackers.
+
+Features:
+
+* Material design 3
+* Day/night theme mode
+* Double tap: Lock/sleep
+* Swipe down: Expand notification panel
+* Quick app search and launch
+* Launch apps in freeform mode
+* Animated battery percentage indicator
+* 12/24 time format and date
+* Weather: celsius and fahrenheit
+* Todo manager
+* Quick actions
+* RSS feeds
+* Device stats
+
+More exciting features will be added soon. Please donate to support the development.
diff --git a/fastlane/metadata/android/en-US/images/featureGraphic.png b/fastlane/metadata/android/en-US/images/featureGraphic.png
new file mode 100644
index 0000000..256ac39
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png
new file mode 100644
index 0000000..aab6e1c
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/icon.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png
new file mode 100644
index 0000000..8fde580
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png
new file mode 100644
index 0000000..7be2faf
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png
new file mode 100644
index 0000000..ee5e85c
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png
new file mode 100644
index 0000000..72164dd
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png
new file mode 100644
index 0000000..deb0d56
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png
new file mode 100644
index 0000000..713c93f
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png
new file mode 100644
index 0000000..df24a6a
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/8.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/8.png
new file mode 100644
index 0000000..94a7966
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/8.png differ
diff --git a/fastlane/metadata/android/en-US/short_description.txt b/fastlane/metadata/android/en-US/short_description.txt
new file mode 100644
index 0000000..09ac761
--- /dev/null
+++ b/fastlane/metadata/android/en-US/short_description.txt
@@ -0,0 +1 @@
+Feature rich android home with minimal look.
diff --git a/fastlane/metadata/android/en-US/title.txt b/fastlane/metadata/android/en-US/title.txt
new file mode 100644
index 0000000..dc15035
--- /dev/null
+++ b/fastlane/metadata/android/en-US/title.txt
@@ -0,0 +1 @@
+Lunar Launcher
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..6b08247
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,26 @@
+# 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.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# 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
+# When set to true the Gradle daemon is used to run the build. For local developer builds,
+# this is a favorite property. The developer environment is optimized for speed and feedback.
+# org.gradle.daemon=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app"s APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
+android.defaults.buildfeatures.buildconfig=true
+android.nonFinalResIds=true
\ No newline at end of file
diff --git a/ic_launcher_512.png b/ic_launcher_512.png
new file mode 100644
index 0000000..dc84526
Binary files /dev/null and b/ic_launcher_512.png differ
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..b96280e
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,20 @@
+pluginManagement {
+ repositories {
+ gradlePluginPortal()
+ google()
+ mavenCentral()
+ }
+}
+
+@Suppress("UnstableApiUsage")
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ maven(url = "https://jitpack.io")
+ }
+}
+
+rootProject.name = "LunarLauncher"
+include ("app")