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 +\n
  • Spenden: Wenn möglich, direkt Spenden.
  • +\n
  • Amazon: Nutze den Link um den Entwickler beim Shoppen zu unterstützen.
  • +\n
  • Star: 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. + \n
  • Donar: si es posible, haga donaciones directas para ayudar y alentar al desarrollador.
  • + \n
  • Amazon: use el enlace de afiliado mientras compra en Amazon para recompensar al desarrollador.
  • + \n
  • Estrella: 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\n
  • Modificar ajustes del sistema - Para controlar el brillo usando una barra deslizable
  • + \n
  • Telé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: + \n
  • Deslizar hacia arriba - acceso rápido
  • + \n
  • Deslizar hacia abajo - panel de notificaciones
  • + \n
  • Deslizar a la derecha - Feed
  • + \n
  • Deslizar a la izquierda - Cajón de aplicaciones
  • + \n
  • Mantener pulsado en el reloj circular - Ajustes de Lunar launcher
  • + \n
  • Mantener pulsado en la parte inferior de la pantalla - Administrador de tareas
  • + \n
  • Doble toque - Bloquear dispositivo (activar en ajustes)
  • + \n
  • Mantener 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 +\n
  • Modifier les paramètres système — Pour contrôler la luminosité de l\'écran en utilisant un curseur
  • +\n
  • Té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 : +\n
  • Glisser vers le haut — Fenêtre d\'accès rapide
  • +\n
  • Glisser vers le bas — Panneau de notifications
  • +\n
  • Glisser vers la droite — Fils RSS
  • +\n
  • Glisser vers la gauche — Liste des applications
  • +\n
  • Rester appuyé sur l\'indicateur de batterie — Paramètres du lanceur
  • +\n
  • Rester appuyé dans la zone inférieure de l\'écran — Gestionnaire de tâches
  • +\n
  • Double-clic — Verrouiller l\'appareil (à activer dans les paramètres du lanceur)
  • +\n
  • Rester 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 +\n
  • Faire un don : Si possible, faites un don directement au développeur pour l\'aider et l\'encourager.
  • +\n
  • Amazon : Utilisez le lien d\'affiliation lors de vos achats chez Amazon pour récompenser le développeur.
  • +\n
  • GitHub : 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 +\n
  • Donazioni: se possibile, può fare una donazione diretta ed incoraggiare lo sviluppatore.
  • +\n
  • Amazon: può usare il link di affiliazione mentre fa acquisti su Amazon per ricompensare lo sviluppatore.
  • +\n
  • Metta 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 +\n
  • Modifica dei parametri di sistema - Per controllare la luminosità utilizzando il cursore
  • +\n
  • Telefono - 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: +\n
  • Scorrere verso l\'alto - comparsa accesso rapido
  • +\n
  • Scorrere verso il basso - pannello delle notifiche
  • +\n
  • Scorrere a destra - pannello delle informazioni
  • +\n
  • Scorrere a sinistra - pannello delle applicazioni
  • +\n
  • Premere e mantenere sull\'indicatore della batteria - impostazioni del launcher
  • +\n
  • Premere e mantenere sulla parte inferiore dello schermo - gestore delle note
  • +\n
  • Doppio tocco - blocco del dispositivo (attivare dalle impostazioni)
  • +\n
  • Premere 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 +\n
  • Modificar configurações do sistema — Para controlar o brilho com um deslizador
  • +\n
  • Telefone — 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: +\n
  • Deslizar para cima — Acesso rápido
  • +\n
  • Deslizar para baixo — Painel de notificações
  • +\n
  • Deslizar para direita— Feeds
  • +\n
  • Deslizar para esquerda — Gaveta de aplicativos
  • +\n
  • Toque e segure dentro do indicador de bateria — Configurações do Launcher
  • +\n
  • Toque e segure na parte inferior da tela — Gerenciador de tarefas
  • +\n
  • Toque-duplo — Bloquear dispositivo/Colocar para dormir (ativar em configurações)
  • +\n
  • Toque 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 +\n
  • Doe: Se possível, faça doações diretas para ajudar e encorajar o desenvolvedor.
  • +\n
  • Amazon: Use o link afiliado enquanto compra na Amazon para recompensar o desenvolvedor.
  • +\n
  • Estrela: 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 + \n
  • Bağış: Mümkünse, geliştiriciyi desteklemek ve teşvik etmek için doğrudan bağış yapın.
  • + \n
  • Amazon: Amazon\'da alışveriş yaparken ortaklık bağlantısını kullanarak geliştiriciyi ödüllendirebilirsiniz.
  • + \n
  • Yı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\n
  • Sistem ayarlarını değiştirin - Bir kaydırıcı kullanarak parlaklığı kontrol etmek için
  • + \n
  • Telefon - 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: + \n
  • Yukarı kaydır - hızlı erişim
  • + \n
  • Aşağı kaydır - bildirim paneli
  • + \n
  • Sağa kaydır - beslemeler
  • + \n
  • Sola kaydır - uygulama çekmecesi
  • + \n
  • Pil göstergesi içinde basılı tutun - launcher ayarları
  • + \n
  • Ekrandaki alt kısımda basılı tutun - yapılacaklar yöneticisi
  • + \n
  • Çift tıklayın - cihaz kilidi/uyku (Ayarlar\'dan etkinleştirin)
  • + \n
  • Favori öğ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 + \n
  • Donate: If possible, make direct donations to help and encourage the developer.
  • + \n
  • Amazon: Use the affiliate link while shopping on Amazon to reward the developer.
  • + \n
  • Star: 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\n
  • Modify system settings — To control brightness using a slider
  • + \n
  • Phone — To make call from shortcuts
  • + \n\nIt\'s a copylefted libre software; check, change and share with all. + \n\nGestures: + \n
  • Swipe up — Quick access dialog
  • + \n
  • Swipe down — Notification panel
  • + \n
  • Swipe right — Feeds
  • + \n
  • Swipe left — App drawer
  • + \n
  • Tap and hold inside battery indicator — Launcher settings
  • + \n
  • Tap and hold in the lower part of the screen — To-Do manager
  • + \n
  • Double tap — Device lock/sleep (enable from settings)
  • + \n
  • Tap 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")