commit 749e4e3073f544b9f4b13ec8c6ff7c154765c80d Author: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Sat Sep 18 23:37:52 2021 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..085797df --- /dev/null +++ b/.gitignore @@ -0,0 +1,304 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/androidstudio,android,gradle,linux,macos,windows +# Edit at https://www.toptal.com/developers/gitignore?templates=androidstudio,android,gradle,linux,macos,windows + +### Android ### +# 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/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +.idea/jarRepositories.xml +# Android Studio 3 in .gitignore file. +.idea/caches +.idea/modules.xml +# Comment next line if keeping position of elements in Navigation Editor is relevant for you +.idea/navEditor.xml + +# 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/ + +# Android Profiling +*.hprof + +### Android Patch ### +gen-external-apklibs +output.json + +# Replacement of .externalNativeBuild directories introduced +# with Android Studio 3.5. + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### Gradle ### +.gradle + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties + +### Gradle Patch ### +**/build/ + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +### AndroidStudio ### +# Covers files to be ignored for android development using Android Studio. + +# Built application files + +# Files for the ART/Dalvik VM + +# Java class files + +# Generated files + +# Gradle files + +# Signing files +.signing/ + +# Local configuration file (sdk path, etc) + +# Proguard folder generated by Eclipse + +# Log Files + +# Android Studio +/*/build/ +/*/local.properties +/*/out +/*/*/build +/*/*/production +*.ipr +*.swp + +# Keystore files +*.jks +*.keystore + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Android Patch + +# External native build folder generated in Android Studio 2.2 and later + +# NDK +obj/ + +# IntelliJ IDEA +*.iws +/out/ + +# User-specific configurations +.idea/caches/ +.idea/libraries/ +.idea/shelf/ +.idea/.name +.idea/compiler.xml +.idea/copyright/profiles_settings.xml +.idea/encodings.xml +.idea/misc.xml +.idea/scopes/scope_settings.xml +.idea/vcs.xml +.idea/jsLibraryMappings.xml +.idea/datasources.xml +.idea/dataSources.ids +.idea/sqlDataSources.xml +.idea/dynamic.xml +.idea/uiDesigner.xml + +# OS-specific files +.DS_Store? + +# Legacy Eclipse project files +.cproject +.settings/ + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.war +*.ear + +# virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) +hs_err_pid* + +## Plugin-specific files: + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Mongo Explorer plugin +.idea/mongoSettings.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### AndroidStudio Patch ### + +!/gradle/wrapper/gradle-wrapper.jar + +# End of https://www.toptal.com/developers/gitignore/api/androidstudio,android,gradle,linux,macos,windows + +*/**/output-metadata.json diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 00000000..529374ff --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 00000000..6e6eec11 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml new file mode 100644 index 00000000..03f0f267 --- /dev/null +++ b/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.properties b/.idea/gradle.properties new file mode 100644 index 00000000..e69de29b diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 00000000..d606a954 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/kotlinScripting.xml b/.idea/kotlinScripting.xml new file mode 100644 index 00000000..bc444dea --- /dev/null +++ b/.idea/kotlinScripting.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..f288702d --- /dev/null +++ b/LICENSE.txt @@ -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 00000000..796b96d1 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 00000000..a89593bd --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,153 @@ +import java.text.SimpleDateFormat +import java.util.* + +plugins { + id("com.android.application") + id("kotlin-android") + id("kotlin-android-extensions") +} + +android { + packagingOptions { + resources.excludes.add("META-INF/DEPENDENCIES") + resources.excludes.add("META-INF/LICENSE") + resources.excludes.add("META-INF/LICENSE.txt") + resources.excludes.add("META-INF/license.txt") + resources.excludes.add("META-INF/NOTICE") + resources.excludes.add("META-INF/NOTICE.txt") + resources.excludes.add("META-INF/notice.txt") + resources.excludes.add("META-INF/ASL2.0") + resources.excludes.add("META-INF/LICENSE.md") + resources.excludes.add("META-INF/NOTICE.md") + } + + compileSdk = sdk.versions.compileSdk.get().toInt() + defaultConfig { + applicationId = "de.mm20.launcher2" + minSdk = sdk.versions.minSdk.get().toInt() + targetSdk = sdk.versions.targetSdk.get().toInt() + versionCode = versionCodeDate() + versionName = "1.0.0" + multiDexEnabled = true + } + buildTypes { + release { + isMinifyEnabled = false + isShrinkResources = false + proguardFiles( + getDefaultProguardFile("proguard-android.txt"), + "proguard-rules.pro" + ) + applicationIdSuffix = ".release" + + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + + versionNameSuffix = "-" + buildTime() + } + debug { + applicationIdSuffix = ".debug" + } + } + configurations.all { + //Fixes Error: Duplicate class: com.google.common.util.concurrent.ListenableFuture + exclude(group = "com.google.guava", module = "listenablefuture") + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + + lint { + isAbortOnError = false + } +} + +fun buildTime(): String { + val df = SimpleDateFormat("yyyyMMdd") + return df.format(Date()) +} + +fun versionCodeDate(): Int { + val df = SimpleDateFormat("yyyyMMdd00") + return df.format(Date()).toInt() +} + + +dependencies { + implementation(libs.bundles.kotlin) + + //Android Jetpack + implementation(libs.androidx.appcompat) + implementation(libs.androidx.preference) + implementation(libs.androidx.cardview) + implementation(libs.androidx.browser) + implementation(libs.androidx.palette) + implementation(libs.androidx.fragment) + implementation(libs.androidx.core) + implementation(libs.androidx.exifinterface) + implementation(libs.androidx.media2) + implementation(libs.materialcomponents) + implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.gridlayout) + + implementation(libs.bundles.androidx.lifecycle) + + implementation(libs.androidx.work) + implementation(libs.androidx.multidex) + + implementation(libs.glide) + implementation(libs.glidetransformations) + + implementation(libs.lottie.core) + + implementation(libs.textdrawable) + + implementation(libs.bundles.materialdialogs) + + implementation(libs.bundles.groupie) + + implementation(libs.draglinearlayout) + implementation(libs.viewpropertyobjectanimator) + + implementation(project(":applications")) + implementation(project(":appsearch")) + implementation(project(":badges")) + implementation(project(":base")) + implementation(project(":calculator")) + implementation(project(":calendar")) + implementation(project(":contacts")) + implementation(project(":crashreporter")) + implementation(project(":favorites")) + implementation(project(":files")) + implementation(project(":g-services")) + implementation(project(":hiddenitems")) + implementation(project(":i18n")) + implementation(project(":icons")) + implementation(project(":ktx")) + implementation(project(":ms-services")) + implementation(project(":music")) + implementation(project(":nextcloud")) + implementation(project(":notifications")) + implementation(project(":owncloud")) + implementation(project(":permissions")) + implementation(project(":preferences")) + implementation(project(":rssparser")) + implementation(project(":search")) + implementation(project(":transition")) + implementation(project(":unitconverter")) + implementation(project(":ui")) + implementation(project(":weather")) + implementation(project(":websites")) + implementation(project(":widgets")) + implementation(project(":wikipedia")) + + // Uncomment this if you want annoying notifications in your debug builds yelling at you how terrible your code is + //debugImplementation(libs.leakcanary) +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 00000000..57980d1a --- /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.kts.kts.kts.kts.kts.kts.kts.kts.kts.kts.kts.kts.kts.kts.kts.kts.kts.kts.kts.kts.kts.kts.kts.kts.kts.kts.kts.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 diff --git a/app/src/debug/google-services.json b/app/src/debug/google-services.json new file mode 100644 index 00000000..5e4205a4 --- /dev/null +++ b/app/src/debug/google-services.json @@ -0,0 +1,59 @@ +{ + "project_info": { + "project_number": "1080818269583", + "firebase_url": "https://quaesitio-1523049861646.firebaseio.com", + "project_id": "quaesitio-1523049861646", + "storage_bucket": "quaesitio-1523049861646.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:1080818269583:android:c37c9bbefdd27e04", + "android_client_info": { + "package_name": "de.mm20.launcher2.debug" + } + }, + "oauth_client": [ + { + "client_id": "1080818269583-br291p34djunaf9katvhomj5u5qcvr8c.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "de.mm20.launcher2.debug", + "certificate_hash": "af1d5f4a72fbaf9fce328142d1ed4a3ea4e77574" + } + }, + { + "client_id": "1080818269583-4nll59hn87841bivhi9actpijmvpn6r0.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "1080818269583-36kh12cv92opc17pa49umltcih0qfh2q.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyDmEgLMLFnwYQviEE05mCg8kD6ik0gcWw4" + } + ], + "services": { + "analytics_service": { + "status": 1 + }, + "appinvite_service": { + "status": 2, + "other_platform_oauth_client": [ + { + "client_id": "1080818269583-4nll59hn87841bivhi9actpijmvpn6r0.apps.googleusercontent.com", + "client_type": 3 + } + ] + }, + "ads_service": { + "status": 2 + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/app/src/debug/ic_launcher-playstore.png b/app/src/debug/ic_launcher-playstore.png new file mode 100644 index 00000000..dfcf7888 Binary files /dev/null and b/app/src/debug/ic_launcher-playstore.png differ diff --git a/app/src/debug/ic_launcher-web.png b/app/src/debug/ic_launcher-web.png new file mode 100644 index 00000000..c4f8341f Binary files /dev/null and b/app/src/debug/ic_launcher-web.png differ diff --git a/app/src/debug/java/de/mm20/launcher2/debug/Debug.kt b/app/src/debug/java/de/mm20/launcher2/debug/Debug.kt new file mode 100644 index 00000000..69fac8f7 --- /dev/null +++ b/app/src/debug/java/de/mm20/launcher2/debug/Debug.kt @@ -0,0 +1,22 @@ +package de.mm20.launcher2.debug + +import android.os.StrictMode +import android.util.Log +import de.mm20.launcher2.BuildConfig + +class Debug { + init { + Log.d("MM20", "MM20Launcher2 is running in debug mode.") + StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder() + .detectAll() + .penaltyLog() + .build()) + StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder() + .detectAll() + .penaltyLog() + .build()) + } + companion object { + const val DEBUG_MODE = true + } +} \ No newline at end of file diff --git a/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..4ae7d123 --- /dev/null +++ b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..4ae7d123 --- /dev/null +++ b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/debug/res/mipmap-hdpi/ic_launcher.png b/app/src/debug/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..160f9a50 Binary files /dev/null and b/app/src/debug/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-hdpi/ic_launcher_background.png b/app/src/debug/res/mipmap-hdpi/ic_launcher_background.png new file mode 100644 index 00000000..e6897753 Binary files /dev/null and b/app/src/debug/res/mipmap-hdpi/ic_launcher_background.png differ diff --git a/app/src/debug/res/mipmap-hdpi/ic_launcher_foreground.png b/app/src/debug/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..9c13d2c9 Binary files /dev/null and b/app/src/debug/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 00000000..e95ac658 Binary files /dev/null and b/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/debug/res/mipmap-mdpi/ic_launcher.png b/app/src/debug/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..a8549f68 Binary files /dev/null and b/app/src/debug/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-mdpi/ic_launcher_background.png b/app/src/debug/res/mipmap-mdpi/ic_launcher_background.png new file mode 100644 index 00000000..72a5d9c0 Binary files /dev/null and b/app/src/debug/res/mipmap-mdpi/ic_launcher_background.png differ diff --git a/app/src/debug/res/mipmap-mdpi/ic_launcher_foreground.png b/app/src/debug/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..6ba53180 Binary files /dev/null and b/app/src/debug/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 00000000..7c699c17 Binary files /dev/null and b/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/debug/res/mipmap-xhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..d76b6755 Binary files /dev/null and b/app/src/debug/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-xhdpi/ic_launcher_background.png b/app/src/debug/res/mipmap-xhdpi/ic_launcher_background.png new file mode 100644 index 00000000..7a56ec15 Binary files /dev/null and b/app/src/debug/res/mipmap-xhdpi/ic_launcher_background.png differ diff --git a/app/src/debug/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/debug/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..41d9a6a2 Binary files /dev/null and b/app/src/debug/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 00000000..a1103926 Binary files /dev/null and b/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..a1098cb2 Binary files /dev/null and b/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-xxhdpi/ic_launcher_background.png b/app/src/debug/res/mipmap-xxhdpi/ic_launcher_background.png new file mode 100644 index 00000000..2fe8f6c3 Binary files /dev/null and b/app/src/debug/res/mipmap-xxhdpi/ic_launcher_background.png differ diff --git a/app/src/debug/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/debug/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..4ee07d1f Binary files /dev/null and b/app/src/debug/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..bf4b4ed6 Binary files /dev/null and b/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..7c53c7c7 Binary files /dev/null and b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_background.png b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_background.png new file mode 100644 index 00000000..b597dbda Binary files /dev/null and b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_background.png differ diff --git a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..2c90b976 Binary files /dev/null and b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..da772cec Binary files /dev/null and b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/debug/res/values-de/strings.xml b/app/src/debug/res/values-de/strings.xml new file mode 100644 index 00000000..e5a39862 --- /dev/null +++ b/app/src/debug/res/values-de/strings.xml @@ -0,0 +1,3 @@ + + Kvæsitso Debug + diff --git a/app/src/debug/res/values/bools.xml b/app/src/debug/res/values/bools.xml new file mode 100644 index 00000000..b73515b5 --- /dev/null +++ b/app/src/debug/res/values/bools.xml @@ -0,0 +1,4 @@ + + + true + \ No newline at end of file diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml new file mode 100644 index 00000000..e5a39862 --- /dev/null +++ b/app/src/debug/res/values/strings.xml @@ -0,0 +1,3 @@ + + Kvæsitso Debug + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..d80ac9b9 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 00000000..35adae67 Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt b/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt new file mode 100644 index 00000000..4319bee7 --- /dev/null +++ b/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt @@ -0,0 +1,84 @@ +package de.mm20.launcher2 + +import android.app.Application +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.graphics.Bitmap +import androidx.appcompat.app.AppCompatDelegate +import de.mm20.launcher2.debug.Debug +import de.mm20.launcher2.icons.IconRepository +import de.mm20.launcher2.preferences.LauncherPreferences +import de.mm20.launcher2.preferences.Themes +import de.mm20.launcher2.ui.legacy.helper.WallpaperBlur +import kotlinx.coroutines.* +import java.text.Collator +import kotlin.coroutines.CoroutineContext + +class LauncherApplication : Application(), CoroutineScope { + + override val coroutineContext: CoroutineContext + get() = Dispatchers.Main + SupervisorJob() + + var blurredWallpaper: Bitmap? = null + + + private val appReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + IconRepository.getInstance(this@LauncherApplication).requestIconPackListUpdate() + } + } + + + override fun onCreate() { + super.onCreate() + Debug() + instance = this + LauncherPreferences.initialize(this) + IconRepository.getInstance(this).requestIconPackListUpdate() + + registerReceiver(appReceiver, IntentFilter().apply { + addAction(Intent.ACTION_PACKAGE_REPLACED) + addAction(Intent.ACTION_PACKAGE_ADDED) + addAction(Intent.ACTION_PACKAGE_REMOVED) + addAction(Intent.ACTION_MY_PACKAGE_REPLACED) + addAction(Intent.ACTION_PACKAGE_CHANGED) + addDataScheme("package") + }) + val theme = LauncherPreferences.instance.theme + AppCompatDelegate.setDefaultNightMode( + when (theme) { + Themes.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO // light + Themes.DARK -> AppCompatDelegate.MODE_NIGHT_YES // dark, black + Themes.AUTO -> AppCompatDelegate.MODE_NIGHT_AUTO // auto + else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM //system + } + ) + WallpaperBlur.requestBlur(this) + @Suppress("DEPRECATION") // We need to access the wallpaper directly to blur it + registerReceiver(WallpaperReceiver(), IntentFilter(Intent.ACTION_WALLPAPER_CHANGED)) + } + + companion object { + lateinit var instance: LauncherApplication + + val collator: Collator by lazy { + Collator.getInstance().apply { strength = Collator.SECONDARY } + } + } + +} + +object PermissionRequests { + const val CALENDAR = 309 + const val LOCATION = 410 + const val ALL = 666 +} + +class WallpaperReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent?) { + WallpaperBlur.requestBlur(context) + } + +} \ No newline at end of file diff --git a/app/src/main/java/de/mm20/launcher2/activity/AddItemActivity.kt b/app/src/main/java/de/mm20/launcher2/activity/AddItemActivity.kt new file mode 100644 index 00000000..f8b0e196 --- /dev/null +++ b/app/src/main/java/de/mm20/launcher2/activity/AddItemActivity.kt @@ -0,0 +1,28 @@ +package de.mm20.launcher2.activity; + +import android.app.Activity +import android.content.Context +import android.content.pm.LauncherApps +import android.os.Build +import android.os.Bundle +import de.mm20.launcher2.favorites.FavoritesRepository +import de.mm20.launcher2.search.data.AppShortcut + +class AddItemActivity : Activity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val launcherApps = getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps + val pinRequest = launcherApps.getPinItemRequest(intent) ?: return run { finish() } + val shortcutInfo = pinRequest.shortcutInfo ?: return run { finish() } + val shortcut = AppShortcut(this.applicationContext, shortcutInfo, + packageManager.getApplicationInfo(shortcutInfo.`package`, 0) + .loadLabel(packageManager).toString()) + if (pinRequest.accept()) { + FavoritesRepository.getInstance(this).pinItem(shortcut) + } + } + finish() + } +} diff --git a/app/src/main/java/de/mm20/launcher2/activity/SettingsActivity.kt b/app/src/main/java/de/mm20/launcher2/activity/SettingsActivity.kt new file mode 100644 index 00000000..dc51b4b7 --- /dev/null +++ b/app/src/main/java/de/mm20/launcher2/activity/SettingsActivity.kt @@ -0,0 +1,86 @@ +package de.mm20.launcher2.activity + +import android.content.Intent +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import de.mm20.launcher2.R +import de.mm20.launcher2.fragment.PreferencesCalendarFragment +import de.mm20.launcher2.fragment.PreferencesMainFragment +import de.mm20.launcher2.fragment.PreferencesServicesFragment +import de.mm20.launcher2.fragment.PreferencesWeatherFragment +import de.mm20.launcher2.ui.legacy.activity.LauncherActivity + +class SettingsActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (savedInstanceState == null) { + val fragment = getStartFragment() + setupActionBar() + supportFragmentManager + .beginTransaction() + .add(android.R.id.content, fragment) + .commit() + } else if (!savedInstanceState.getBoolean("theme_change")) { + val fragment = getStartFragment() + setupActionBar() + supportFragmentManager + .beginTransaction() + .replace(android.R.id.content, fragment) + .commit() + } + findViewById(android.R.id.content)?.setBackgroundColor(getColor(R.color.settings_window_background)) + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putBoolean("theme_change", true) + } + + private fun getStartFragment(): Fragment { + return when (intent.extras?.getString(FRAGMENT, "")) { + FRAGMENT_CALENDAR -> PreferencesCalendarFragment() + FRAGMENT_WEATHER -> PreferencesWeatherFragment() + FRAGMENT_SERVICES -> PreferencesServicesFragment() + else -> PreferencesMainFragment() + } + } + + private fun setupActionBar() { + actionBar?.setDisplayHomeAsUpEnabled(true) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + val id = item.itemId + if (id == android.R.id.home) { + if (supportFragmentManager.backStackEntryCount == 0) { + finish() + startActivity(Intent(this, LauncherActivity::class.java)) + } else { + supportFragmentManager.popBackStack() + } + return true + } + return super.onOptionsItemSelected(item) + } + + override fun onBackPressed() { + if (supportFragmentManager.backStackEntryCount > 0) { + supportFragmentManager.popBackStack() + } else { + finish() + startActivity(Intent(this, LauncherActivity::class.java)) + } + } + + companion object { + const val RESULT_NEED_RESTART = 0x09 + const val FRAGMENT_WEATHER: String = "weather" + const val FRAGMENT_CALENDAR: String = "calendar" + const val FRAGMENT_SERVICES: String = "services" + const val FRAGMENT: String = "fragment" + } +} diff --git a/app/src/main/java/de/mm20/launcher2/content/GenericFileProvider.kt b/app/src/main/java/de/mm20/launcher2/content/GenericFileProvider.kt new file mode 100644 index 00000000..183f52e9 --- /dev/null +++ b/app/src/main/java/de/mm20/launcher2/content/GenericFileProvider.kt @@ -0,0 +1,5 @@ +package de.mm20.launcher2.content + +import androidx.core.content.FileProvider + +class GenericFileProvider: FileProvider() \ No newline at end of file diff --git a/app/src/main/java/de/mm20/launcher2/fragment/PreferencesAboutFragment.kt b/app/src/main/java/de/mm20/launcher2/fragment/PreferencesAboutFragment.kt new file mode 100644 index 00000000..0df25401 --- /dev/null +++ b/app/src/main/java/de/mm20/launcher2/fragment/PreferencesAboutFragment.kt @@ -0,0 +1,176 @@ +package de.mm20.launcher2.fragment + +import android.annotation.SuppressLint +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Bundle +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.preference.Preference +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceFragmentCompat +import com.afollestad.materialdialogs.MaterialDialog +import de.mm20.launcher2.R +import de.mm20.launcher2.crashreporter.CrashReporter +import de.mm20.launcher2.helper.DebugInformationDumper + + +class PreferencesAboutFragment : PreferenceFragmentCompat() { + + private var easterEggCounter = 0 + + @SuppressLint("ResourceType") + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.preferences_about) + val versionPref = findPreference("version")!! + try { + val version = requireContext().packageManager.getPackageInfo( + requireActivity().application.packageName, + 0 + ).versionName + versionPref.summary = version + } catch (e: PackageManager.NameNotFoundException) { + //Should never happen + versionPref.summary = "Ich mag Bockwurst-Bananen" + } + + versionPref.setOnPreferenceClickListener { + if (easterEggCounter in arrayOf(3, 4, 7)) Toast.makeText( + context, when (easterEggCounter) { + 3 -> R.string.easter_egg_1 + 4 -> R.string.easter_egg_2 + 7 -> R.string.easter_egg_3 + else -> 0 + }, Toast.LENGTH_SHORT + ).show() + if (easterEggCounter == 8) { + easterEggCounter = 0 + requireFragmentManager().beginTransaction() + .setCustomAnimations( + R.anim.preference_fragment_child_enter, + R.anim.preference_fragment_parent_exit, + R.anim.preference_fragment_parent_enter, + R.anim.preference_fragment_child_exit + ) + .replace( + android.R.id.content, + PreferencesEasterEggFragment() + ) + .addToBackStack(null) + .commit() + } + easterEggCounter++ + false + } + + val licenses = findPreference("category_licenses") as PreferenceCategory + for (l in LICENSES) { + val license = resources.obtainTypedArray(l) + val preference = Preference(activity, null, 0, R.style.Preference_Material) + preference.title = license.getString(0) + preference.summary = license.getString(1) + preference.onPreferenceClickListener = Preference.OnPreferenceClickListener { + parentFragmentManager.beginTransaction() + .setCustomAnimations( + R.anim.preference_fragment_child_enter, + R.anim.preference_fragment_parent_exit, + R.anim.preference_fragment_parent_enter, + R.anim.preference_fragment_child_exit + ) + .replace(android.R.id.content, + PreferencesLicenseFragment().apply { library = l }) + .addToBackStack(null) + .commit() + true + } + license.recycle() + licenses.addPreference(preference) + } + findPreference("crash_reporter")?.setOnPreferenceClickListener { + startActivity(CrashReporter.getLaunchIntent()) + true + } + findPreference("export_debug")?.setOnPreferenceClickListener { + Toast.makeText( + activity, + getString( + R.string.debug_export_information_file, + DebugInformationDumper().dump(requireContext()) + ), + Toast.LENGTH_SHORT + ).show() + true + } + findPreference("export_databases")?.setOnPreferenceClickListener { + MaterialDialog(requireContext()).show { + message(res = R.string.debug_export_databases_warning) + positiveButton(res = R.string.dialog_continue, click = { + Toast.makeText( + activity, + getString( + R.string.debug_export_information_file, + DebugInformationDumper().exportDatabases(requireContext()) + ), + Toast.LENGTH_SHORT + ).show() + it.dismiss() + }) + negativeButton(res = android.R.string.cancel, click = { + it.cancel() + }) + } + + true + } + + findPreference("license")?.setOnPreferenceClickListener { + parentFragmentManager.beginTransaction() + .setCustomAnimations( + R.anim.preference_fragment_child_enter, + R.anim.preference_fragment_parent_exit, + R.anim.preference_fragment_parent_enter, + R.anim.preference_fragment_child_exit + ) + .replace(android.R.id.content, + PreferencesLicenseFragment().apply { library = R.array.license_mm20launcher2 }) + .addToBackStack(null) + .commit() + true + } + } + + override fun onResume() { + super.onResume() + (activity as AppCompatActivity).supportActionBar?.setTitle(R.string.preference_screen_about) + } + + companion object { + + private val LICENSES = intArrayOf( + R.array.license_accompanist, + R.array.license_android_jetpack, + R.array.license_suncalc, + R.array.license_crashreporter, + R.array.license_draglinearlayout, + R.array.license_glide, + R.array.license_glide_transformations, + R.array.license_google_apiclient, + R.array.license_google_auth, + R.array.license_groupie, + R.array.license_gson, + R.array.license_jsoup, + R.array.license_kotlin_stdlib, + R.array.license_lottie, + R.array.license_mdicons, + R.array.license_material_components, + R.array.license_materialdialogs, + R.array.license_msal, + R.array.license_msgraph, + R.array.license_mxparser, + R.array.license_okhttp, + R.array.license_retrofit, + R.array.license_textdrawable, + R.array.license_viewpropertyobjectanimator + ) + } +} diff --git a/app/src/main/java/de/mm20/launcher2/fragment/PreferencesAppearanceFragment.kt b/app/src/main/java/de/mm20/launcher2/fragment/PreferencesAppearanceFragment.kt new file mode 100644 index 00000000..9d2528dc --- /dev/null +++ b/app/src/main/java/de/mm20/launcher2/fragment/PreferencesAppearanceFragment.kt @@ -0,0 +1,182 @@ +package de.mm20.launcher2.fragment + +import android.Manifest +import android.app.WallpaperManager +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.view.View +import android.widget.LinearLayout +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.app.ActivityCompat +import androidx.lifecycle.lifecycleScope +import androidx.preference.ListPreference +import androidx.preference.Preference +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceFragmentCompat +import com.afollestad.materialdialogs.MaterialDialog +import com.afollestad.materialdialogs.customview.customView +import de.mm20.launcher2.R +import de.mm20.launcher2.icons.IconPackManager +import de.mm20.launcher2.icons.IconRepository +import de.mm20.launcher2.icons.LauncherIcon +import de.mm20.launcher2.ktx.checkPermission +import de.mm20.launcher2.preferences.IconShape +import de.mm20.launcher2.preferences.LauncherPreferences +import de.mm20.launcher2.preferences.Themes +import de.mm20.launcher2.ui.legacy.view.LauncherIconView +import kotlinx.coroutines.launch + +class PreferencesAppearanceFragment : PreferenceFragmentCompat() { + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.preferences_appearance) + findPreference("theme")?.setOnPreferenceChangeListener { _, newValue -> + val theme = Themes.byValue(newValue as String) + @Suppress("DEPRECATION") // Still using MODE_NIGHT_AUTO + AppCompatDelegate.setDefaultNightMode(when (theme) { + Themes.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO + Themes.DARK -> AppCompatDelegate.MODE_NIGHT_YES + Themes.AUTO -> AppCompatDelegate.MODE_NIGHT_AUTO + else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM + }) + requireActivity().recreate() + true + } + if (WallpaperManager.getInstance(activity).wallpaperInfo != null) { + findPreference("blur_cards")?.apply { + setSummary(R.string.preference_blur_cards_summary_lwp) + isEnabled = false + } + } + findPreference("blur_cards")?.setOnPreferenceChangeListener { _, newValue -> + val newVal = newValue as? Boolean ?: return@setOnPreferenceChangeListener true + if (newVal && requireActivity().checkPermission(Manifest.permission.READ_EXTERNAL_STORAGE)) { + ActivityCompat.requestPermissions(requireActivity(), arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), 0) + } + true + } + + findPreference("wallpaper")?.setOnPreferenceClickListener { + requireContext().startActivity(Intent.createChooser(Intent(Intent.ACTION_SET_WALLPAPER), null)) + true + } + findPreference("cards")?.setOnPreferenceClickListener { + requireFragmentManager().beginTransaction() + .setCustomAnimations(R.anim.preference_fragment_child_enter, R.anim.preference_fragment_parent_exit, + R.anim.preference_fragment_parent_enter, R.anim.preference_fragment_child_exit) + .replace(android.R.id.content, PreferencesCardFragment()) + .addToBackStack(null) + .commit() + true + } + + val manager = IconPackManager.getInstance(requireContext()) + lifecycleScope.launch { + val packs = manager.getInstalledIconPacks() + findPreference("icon_pack")?.apply { + entries = packs.map { it.name }.toMutableList().apply { add(0, "System") }.toTypedArray() + entryValues = (-1 until packs.size).map { it.toString() }.toTypedArray() + if (packs.isEmpty()) { + isEnabled = false + setSummary(R.string.preference_icon_pack_summary_empty) + } else { + isEnabled = true + summary = "%s" + value = packs.indexOfFirst { it.packageName == manager.selectedIconPack }.toString() + } + setOnPreferenceChangeListener { _, newValue -> + val index = (newValue as String).toInt() + IconRepository.getInstance(requireContext()).clearCache() + if (index == -1) manager.selectIconPack("") + else { + manager.selectIconPack(packs[index].packageName) + } + true + } + } + } + + findPreference("legacy_icon_bg")?.setOnPreferenceChangeListener { _, _ -> + IconRepository.getInstance(requireContext()).clearCache() + true + } + + val shapePreference = findPreference("icon_shape")!! + shapePreference.summary = getShapeName() + shapePreference.setOnPreferenceClickListener { + val launcherIcon = LauncherIcon( + foreground = requireContext().getDrawable(R.mipmap.ic_launcher_foreground)!!, + background = requireContext().getDrawable(R.mipmap.ic_launcher_background) + ) + val iconShapeList = LinearLayout(requireContext()) + iconShapeList.orientation = LinearLayout.VERTICAL + val shapes = arrayOf( + IconShape.PLATFORM_DEFAULT to R.string.preference_icon_shape_platform, + IconShape.CIRCLE to R.string.preference_icon_shape_circle, + IconShape.ROUNDED_SQUARE to R.string.preference_icon_shape_rounded_square, + IconShape.SQUARE to R.string.preference_icon_shape_square, + IconShape.SQUIRCLE to R.string.preference_icon_shape_squircle, + IconShape.HEXAGON to R.string.preference_icon_shape_hexagon, + IconShape.TRIANGLE to R.string.preference_icon_shape_triangle, + IconShape.PENTAGON to R.string.preference_icon_shape_pentagon + ) + val layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT) + val dialog = MaterialDialog(requireContext()) + shapes.forEachIndexed { i, shape -> + val view = View.inflate(requireContext(), R.layout.preference_icon_shape_row, null) + view.findViewById(R.id.icon).also { iconView -> + iconView.icon = launcherIcon + iconView.shape = shape.first + } + view.findViewById(R.id.label).also { labelView -> + labelView.setText(shape.second) + } + view.layoutParams = layoutParams + iconShapeList.addView(view) + view.setOnClickListener { + LauncherPreferences.instance.iconShape = shape.first + shapePreference.summary = getShapeName() + dialog.dismiss() + } + } + + dialog.customView(view = iconShapeList, scrollable = true) + .title(R.string.preference_icon_shape) + .negativeButton(android.R.string.cancel) { + dialog.cancel() + } + .show() + true + } + + val systemBarsCategory = findPreference("system_bars")!! + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + systemBarsCategory.removePreference(findPreference("light_nav_bar")) + } + } + + private fun getShapeName(): String { + return requireContext().getString(when (LauncherIconView.getDefaultShape(requireContext())) { + IconShape.TRIANGLE -> R.string.preference_icon_shape_triangle + IconShape.HEXAGON -> R.string.preference_icon_shape_hexagon + IconShape.ROUNDED_SQUARE -> R.string.preference_icon_shape_rounded_square + IconShape.SQUIRCLE -> R.string.preference_icon_shape_squircle + IconShape.SQUARE -> R.string.preference_icon_shape_square + IconShape.PENTAGON -> R.string.preference_icon_shape_pentagon + IconShape.PLATFORM_DEFAULT -> R.string.preference_icon_shape_platform + else -> R.string.preference_icon_shape_circle + }) + } + + + override fun onResume() { + super.onResume() + (activity as AppCompatActivity).supportActionBar + ?.setTitle(R.string.preference_screen_appearance) + } +} \ No newline at end of file diff --git a/app/src/main/java/de/mm20/launcher2/fragment/PreferencesBadgesFragment.kt b/app/src/main/java/de/mm20/launcher2/fragment/PreferencesBadgesFragment.kt new file mode 100644 index 00000000..9e137b75 --- /dev/null +++ b/app/src/main/java/de/mm20/launcher2/fragment/PreferencesBadgesFragment.kt @@ -0,0 +1,47 @@ + package de.mm20.launcher2.fragment + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import de.mm20.launcher2.R +import de.mm20.launcher2.badges.BadgeProvider +import de.mm20.launcher2.notifications.NotificationService + +class PreferencesBadgesFragment : PreferenceFragmentCompat() { + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.preferences_badges) + findPreference("notification_badges")?.setOnPreferenceChangeListener { _, newValue -> + if (newValue as Boolean) { + de.mm20.launcher2.notifications.NotificationService.getInstance()?.generateBadges() + } else { + BadgeProvider.getInstance(requireContext()).removeNotificationBadges() + } + true + } + findPreference("suspended_badges")?.setOnPreferenceChangeListener { _, newValue -> + if (newValue as Boolean) { + BadgeProvider.getInstance(requireContext()).addSuspendBadges() + } else { + BadgeProvider.getInstance(requireContext()).removeSuspendBadges() + } + true + } + findPreference("cloud_badges")?.setOnPreferenceChangeListener { _, newValue -> + if (newValue as Boolean) { + BadgeProvider.getInstance(requireContext()).addCloudBadges() + } else { + BadgeProvider.getInstance(requireContext()).removeCloudBadges() + } + true + } + } + + + override fun onResume() { + super.onResume() + (activity as AppCompatActivity).supportActionBar + ?.setTitle(R.string.preference_screen_badges) + } +} \ No newline at end of file diff --git a/app/src/main/java/de/mm20/launcher2/fragment/PreferencesCalendarFragment.kt b/app/src/main/java/de/mm20/launcher2/fragment/PreferencesCalendarFragment.kt new file mode 100644 index 00000000..621847fa --- /dev/null +++ b/app/src/main/java/de/mm20/launcher2/fragment/PreferencesCalendarFragment.kt @@ -0,0 +1,121 @@ +package de.mm20.launcher2.fragment + +import android.Manifest +import android.content.res.ColorStateList +import android.os.Bundle +import android.widget.CheckBox +import android.widget.LinearLayout +import android.widget.ScrollView +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.core.view.setPadding +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import com.afollestad.materialdialogs.MaterialDialog +import com.afollestad.materialdialogs.bottomsheets.BottomSheet +import com.afollestad.materialdialogs.customview.customView +import de.mm20.launcher2.R +import de.mm20.launcher2.ktx.checkPermission +import de.mm20.launcher2.ktx.dp +import de.mm20.launcher2.preferences.LauncherPreferences +import de.mm20.launcher2.search.data.CalendarEvent +import de.mm20.launcher2.search.data.UserCalendar + +class PreferencesCalendarFragment : PreferenceFragmentCompat() { + + private var hasCalendarPermission = false + private val calendars = mutableListOf() + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.preferences_calendar) + init() + } + + + fun init(requestPermission: Boolean = true) { + val context = context ?: return + hasCalendarPermission = context.checkPermission(Manifest.permission.READ_CALENDAR) + if (hasCalendarPermission) { + calendars.clear() + calendars.addAll(CalendarEvent.getCalendars(context)) + val unselectedCalendars = LauncherPreferences.instance.unselectedCalendars.toMutableList() + findPreference("calendar_calendars")?.apply { + var count = calendars.size - unselectedCalendars.size + summary = resources.getQuantityString(R.plurals.preference_calendar_calendars_summary, count, count) + isEnabled = true + setOnPreferenceClickListener { + val sheetView = LinearLayout(activity) + sheetView.setPadding((8 * context.dp).toInt()) + sheetView.orientation = LinearLayout.VERTICAL + sheetView.setBackgroundColor(ContextCompat.getColor(context, R.color.bottom_sheet)) + var owner = "" + val padding = (8 * context.dp).toInt() + for (c in calendars) { + if (owner != c.owner) { + owner = c.owner + val text = TextView(activity) + text.setTextColor(ContextCompat.getColor(context, R.color.text_color_secondary_normal)) + text.setPadding(padding, 2 * padding, padding, padding) + text.text = owner + sheetView.addView(text) + } + val checkbox = CheckBox(activity) + checkbox.text = c.name + checkbox.buttonTintList = ColorStateList.valueOf(CalendarEvent.getDisplayColor(context, c.color)) + checkbox.setPadding(padding) + checkbox.isChecked = !unselectedCalendars.contains(c.id) + checkbox.setOnCheckedChangeListener { _, checked -> + if (checked) { + unselectedCalendars.remove(c.id) + } else { + unselectedCalendars.add(c.id) + } + LauncherPreferences.instance.unselectedCalendars = unselectedCalendars + count = calendars.size - unselectedCalendars.size + summary = resources.getQuantityString(R.plurals.preference_calendar_calendars_summary, count, count) + } + sheetView.addView(checkbox) + } + val scrollView = ScrollView(context) + scrollView.isNestedScrollingEnabled = true + scrollView.addView(sheetView) + MaterialDialog(context, BottomSheet()).show { + customView(view = scrollView) + title(R.string.preference_calendar_calendars) + .negativeButton(R.string.close) { + dismiss() + } + } + true + } + } + } else { + if (requestPermission) { + ActivityCompat.requestPermissions( + requireActivity(), + arrayOf(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR), + 0) + } + findPreference("calendar_calendars")?.apply { + isEnabled = false + setSummary(R.string.preference_permission_denied) + } + } + } + + + override fun onResume() { + super.onResume() + (activity as AppCompatActivity).supportActionBar?.setTitle(R.string.preference_screen_calendar) + hasCalendarPermission = requireActivity().checkPermission(Manifest.permission.READ_CALENDAR) + && requireActivity().checkPermission(Manifest.permission.WRITE_CALENDAR) + } + + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + init(false) + } +} \ No newline at end of file diff --git a/app/src/main/java/de/mm20/launcher2/fragment/PreferencesCardFragment.kt b/app/src/main/java/de/mm20/launcher2/fragment/PreferencesCardFragment.kt new file mode 100644 index 00000000..38371b77 --- /dev/null +++ b/app/src/main/java/de/mm20/launcher2/fragment/PreferencesCardFragment.kt @@ -0,0 +1,199 @@ +package de.mm20.launcher2.fragment + +import android.animation.Animator +import android.animation.ObjectAnimator +import android.app.WallpaperManager +import android.graphics.* +import android.os.Bundle +import android.view.View +import android.view.ViewOutlineProvider +import androidx.core.content.res.ResourcesCompat +import androidx.core.view.doOnNextLayout +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import de.mm20.launcher2.LauncherApplication +import de.mm20.launcher2.R +import de.mm20.launcher2.ktx.castTo +import de.mm20.launcher2.ktx.dp +import de.mm20.launcher2.ktx.translate +import de.mm20.launcher2.preferences.CardBackground +import de.mm20.launcher2.preferences.LauncherPreferences +import de.mm20.launcher2.ui.legacy.helper.WallpaperBlur +import kotlinx.android.synthetic.main.fragment_card_settings.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File +import kotlin.math.roundToInt + +class PreferencesCardFragment : Fragment(R.layout.fragment_card_settings) { + + val preferences = LauncherPreferences.instance + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + previewCard.strokeOpacity = 0xFF + previewCardBlur.clipToOutline = true + + previewCardBlur.outlineProvider = object : ViewOutlineProvider() { + override fun getOutline(view: View, outline: Outline?) { + val radius = preferences.cardRadius + outline?.setRoundRect(0, 0, view.width, view.height, radius * dp) + } + } + + val context = requireContext() + + + val previewCardBlur = previewCardBlur + val previewCard = previewCard + val prefFragment = PreferenesCardInnerFragment() + + prefFragment.onPreferencesReady = { + findPreference("card_radius")?.let { + it.summary = preferences.cardRadius.toString() + it.setOnPreferenceChangeListener { pref, newValue -> + val value = newValue as Int + previewCard.radius = value * dp + previewCardBlur.invalidateOutline() + pref.summary = value.toString() + true + } + } + findPreference("card_opacity")?.let { + it.summary = preferences.cardOpacity.toString() + it.setOnPreferenceChangeListener { pref, newValue -> + val value = newValue as Int + previewCard.backgroundOpacity = value + previewCard.cardElevation = if (value == 0xFF) resources.getDimension(R.dimen.card_elevation) else 0f + pref.summary = value.toString() + true + } + } + findPreference("card_stroke_width")?.let { + it.summary = preferences.cardRadius.toString() + it.setOnPreferenceChangeListener { pref, newValue -> + val value = newValue as Int + previewCard.strokeWidth = (value * dp).roundToInt() + pref.summary = value.toString() + true + } + } + findPreference("blur_cards")?.let { + if (WallpaperManager.getInstance(requireContext()).wallpaperInfo != null) { + it.isEnabled = false + it.setSummary(R.string.preference_blur_cards_summary_lwp) + previewCardBlur.visibility = View.INVISIBLE + } else { + previewCardBlur.visibility = if (preferences.blurCards) { + View.VISIBLE + } else { + View.INVISIBLE + } + it.setOnPreferenceChangeListener { pref, newValue -> + previewCardBlur.visibility = if (newValue as Boolean) { + View.VISIBLE + } else { + View.INVISIBLE + } + true + } + } + } + findPreference("card_background")?.let { + it.setOnPreferenceChangeListener { preference, newValue -> + val background = CardBackground.byValue(newValue as String) + var color = when (background) { + CardBackground.BLACK -> context.getColor(R.color.cardview_background_black) + else -> context.getColor(R.color.cardview_background) + } + color = color and ((previewCard.backgroundOpacity shl 24) or 0xFFFFFF) + previewCard.setCardBackgroundColor(color) + true + } + } + } + + childFragmentManager.beginTransaction() + .replace(R.id.preferencesView, prefFragment) + .commit() + + + } + + private var blurBitmap: Bitmap? = null + + private var animator: Animator? = null + override fun onStart() { + super.onStart() + val content = activity?.findViewById(android.R.id.content) ?: return + animator = ObjectAnimator.ofArgb(content, "backgroundColor", ResourcesCompat.getColor(resources, R.color.settings_window_background, null), Color.TRANSPARENT) + .apply { + duration = 200 + startDelay = resources.getInteger(android.R.integer.config_shortAnimTime).toLong() + start() + } + + if (preferences.blurCards && preferences.cardOpacity < 0xFF) { + lifecycleScope.launch { + val wallpaper = withContext(Dispatchers.IO) { + WallpaperBlur.getCachedBitmap(requireContext()) + } + LauncherApplication.instance.blurredWallpaper = wallpaper + } + } + + + content.doOnNextLayout { + WallpaperManager.getInstance(requireContext()).setWallpaperOffsets(it.windowToken, 0.5f, 0.5f) + } + + val activity = requireActivity() + + lifecycleScope.launch { + val viewPosition = intArrayOf(0, 0) + val rect = Rect(0, 0, previewCardBlur.width, previewCardBlur.height) + val screen = Point() + activity.windowManager.defaultDisplay.getRealSize(screen) + previewCardBlur.getLocationOnScreen(viewPosition) + val file = File(requireContext().cacheDir, "wallpaper") + if (!file.exists()) return@launch + blurBitmap = withContext(Dispatchers.IO) { + val wallpaperWidth: Int + val wallpaperHeight: Int + val decoder = BitmapRegionDecoder + .newInstance(file.absolutePath, false) + wallpaperHeight = decoder.height + wallpaperWidth = decoder.width + + if (wallpaperWidth >= screen.x && wallpaperHeight >= screen.y) { + val translateX = (wallpaperWidth - previewCardBlur.width) / 2f + val translateY = (wallpaperHeight - screen.y) / 2f + viewPosition[1] + rect.translate(translateX.roundToInt(), + translateY.roundToInt()) + } + + decoder.decodeRegion(rect, null) + } + previewCardBlur.setImageBitmap(blurBitmap) + } + } + + override fun onStop() { + super.onStop() + if (animator?.isRunning == true) animator?.end() + activity?.findViewById(android.R.id.content)?.setBackgroundColor(ResourcesCompat.getColor(resources, R.color.settings_window_background, null)) + } +} + +class PreferenesCardInnerFragment : PreferenceFragmentCompat() { + var onPreferencesReady: (PreferenesCardInnerFragment.() -> Unit)? = null + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.preferences_cards) + onPreferencesReady?.invoke(this) + } + +} \ No newline at end of file diff --git a/app/src/main/java/de/mm20/launcher2/fragment/PreferencesEasterEggFragment.kt b/app/src/main/java/de/mm20/launcher2/fragment/PreferencesEasterEggFragment.kt new file mode 100644 index 00000000..c7ae44f5 --- /dev/null +++ b/app/src/main/java/de/mm20/launcher2/fragment/PreferencesEasterEggFragment.kt @@ -0,0 +1,29 @@ +package de.mm20.launcher2.fragment + +import android.animation.ObjectAnimator +import android.animation.ValueAnimator +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.Toast +import android.widget.ToggleButton +import androidx.fragment.app.Fragment +import de.mm20.launcher2.R +import de.mm20.launcher2.preferences.LauncherPreferences + +class PreferencesEasterEggFragment : Fragment() { + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view = inflater.inflate(R.layout.fragment_easteregg, null, false) + val root = view.findViewById(R.id.easterEggRoot) + val toggle = view.findViewById(R.id.magicModeToggle) + toggle.isChecked = LauncherPreferences.instance.easterEggEnabled + toggle.setOnCheckedChangeListener { _, isChecked -> + LauncherPreferences.instance.easterEggEnabled = isChecked + Toast.makeText(requireContext(), if (isChecked) R.string.easter_egg_activated else R.string.easter_egg_deactivated, Toast.LENGTH_SHORT).show() + } + return view + } + +} diff --git a/app/src/main/java/de/mm20/launcher2/fragment/PreferencesLicenseFragment.kt b/app/src/main/java/de/mm20/launcher2/fragment/PreferencesLicenseFragment.kt new file mode 100644 index 00000000..d14527f3 --- /dev/null +++ b/app/src/main/java/de/mm20/launcher2/fragment/PreferencesLicenseFragment.kt @@ -0,0 +1,60 @@ +package de.mm20.launcher2.fragment + +import android.annotation.SuppressLint +import android.net.Uri +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.browser.customtabs.CustomTabColorSchemeParams +import androidx.browser.customtabs.CustomTabsIntent +import androidx.fragment.app.Fragment +import com.bumptech.glide.Glide +import de.mm20.launcher2.R + +class PreferencesLicenseFragment : Fragment() { + var library: Int = 0 + + @SuppressLint("ResourceType") + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View? { + val view = inflater.inflate(R.layout.fragment_license, null, false) + val license = resources.obtainTypedArray(library) + (activity as AppCompatActivity).supportActionBar?.title = license.getString(0) + val icon = view.findViewById(R.id.icon) + val iconUri = license.getString(2) + if (iconUri == null) icon.visibility = View.GONE + else { + Glide + .with(icon) + .load(iconUri) + .into(icon) + icon.visibility = View.VISIBLE + } + val url = license.getString(6) + val website = view.findViewById(R.id.website) + website.setOnClickListener { + val intent = CustomTabsIntent.Builder() + .setDefaultColorSchemeParams(CustomTabColorSchemeParams + .Builder() + .setToolbarColor(-0x9f8275) + .build()) + .setShowTitle(true) + .build() + intent.launchUrl(activity as AppCompatActivity, Uri.parse(url)) + } + val description = view.findViewById(R.id.description) + description.text = license.getString(1) + val licenseTitle = view.findViewById(R.id.licenseTitle) + val licenseText = view.findViewById(R.id.licenseText) + val licenseCopyright = view.findViewById(R.id.licenseCopyright) + licenseTitle.text = license.getString(3) + licenseCopyright.text = license.getString(4) + licenseText.text = resources.openRawResource(license.getResourceId(5, 0)).reader().readText() + license.recycle() + return view + } +} diff --git a/app/src/main/java/de/mm20/launcher2/fragment/PreferencesMainFragment.kt b/app/src/main/java/de/mm20/launcher2/fragment/PreferencesMainFragment.kt new file mode 100644 index 00000000..0b46bf74 --- /dev/null +++ b/app/src/main/java/de/mm20/launcher2/fragment/PreferencesMainFragment.kt @@ -0,0 +1,60 @@ +package de.mm20.launcher2.fragment + +import android.content.Context +import android.graphics.Color +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import de.mm20.launcher2.R + +class PreferencesMainFragment : PreferenceFragmentCompat() { + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.preferences_main) + findPreference("screen_appearance")?.setOnPreferenceClickListener { + setSettingsScreen(PreferencesAppearanceFragment()) + true + } + findPreference("screen_about")?.setOnPreferenceClickListener { + setSettingsScreen(PreferencesAboutFragment()) + true + } + findPreference("screen_weather")?.setOnPreferenceClickListener { + setSettingsScreen(PreferencesWeatherFragment()) + true + } + findPreference("screen_services")?.setOnPreferenceClickListener { + setSettingsScreen(PreferencesServicesFragment()) + true + } + findPreference("screen_search")?.setOnPreferenceClickListener { + setSettingsScreen(PreferencesSearchFragment()) + true + } + findPreference("screen_calendar")?.setOnPreferenceClickListener { + setSettingsScreen(PreferencesCalendarFragment()) + true + } + findPreference("screen_badges")?.setOnPreferenceClickListener { + setSettingsScreen(PreferencesBadgesFragment()) + true + } + } + + private fun setSettingsScreen(fragment: Fragment) { + parentFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.preference_fragment_child_enter, R.anim.preference_fragment_parent_exit, + R.anim.preference_fragment_parent_enter, R.anim.preference_fragment_child_exit) + .replace(android.R.id.content, fragment) + .addToBackStack(null) + .commit() + } + + override fun onResume() { + super.onResume() + (activity as AppCompatActivity).supportActionBar?.setTitle(R.string.title_activity_settings) + } +} \ No newline at end of file diff --git a/app/src/main/java/de/mm20/launcher2/fragment/PreferencesSearchFragment.kt b/app/src/main/java/de/mm20/launcher2/fragment/PreferencesSearchFragment.kt new file mode 100644 index 00000000..9681c3a4 --- /dev/null +++ b/app/src/main/java/de/mm20/launcher2/fragment/PreferencesSearchFragment.kt @@ -0,0 +1,176 @@ +package de.mm20.launcher2.fragment + +import android.Manifest +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import de.mm20.launcher2.R +import de.mm20.launcher2.gservices.GoogleApiHelper +import de.mm20.launcher2.ktx.checkPermission +import de.mm20.launcher2.msservices.MicrosoftGraphApiHelper +import de.mm20.launcher2.nextcloud.NextcloudApiHelper +import de.mm20.launcher2.owncloud.OwncloudClient +import de.mm20.launcher2.preferences.LauncherPreferences +import kotlinx.coroutines.launch + +class PreferencesSearchFragment : PreferenceFragmentCompat() { + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + lifecycleScope.launch { + viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) { + updateGoogleDrive() + updateOneDrive() + } + } + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.preferences_search) + findPreference("search_activities")?.summary = + getString( + R.string.preference_search_activities_summary, + requireActivity().componentName.flattenToShortString() + ) + findPreference("search_files")?.setOnPreferenceChangeListener { _, newValue -> + if (newValue == true && + requireContext().checkPermission(Manifest.permission.READ_EXTERNAL_STORAGE) + ) { + ActivityCompat.requestPermissions( + requireActivity(), + arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), + 0 + ) + } + true + } + findPreference("search_edit_websearch")?.setOnPreferenceClickListener { + setSettingsScreen(PreferencesWebSearchesFragment()) + true + } + } + + private suspend fun updateGoogleDrive() { + val googleApiHelper = GoogleApiHelper.getInstance(context ?: return) + val account = googleApiHelper.getAccount() + val pref = findPreference("search_gdrive")!! + if (account == null) { + pref.apply { + setSummary(R.string.preference_summary_not_logged_in) + } + } else { + pref.apply { + summary = context.getString(R.string.preference_search_gdrive_summary, account.name) + } + } + val isSignedIn = account != null + pref.setOnPreferenceChangeListener { _, value -> + val newVal = value as Boolean + if (newVal && !isSignedIn) { + googleLogin() + } + true + } + } + + private suspend fun updateOneDrive() { + val oneDrivePref = findPreference("search_onedrive")!! + val user = MicrosoftGraphApiHelper.getInstance(requireContext()).getUser() + if (user == null) { + oneDrivePref.setSummary(R.string.preference_summary_not_logged_in) + oneDrivePref.setOnPreferenceChangeListener { _, value -> + if (value as Boolean) { + lifecycleScope.launch launch2@{ + MicrosoftGraphApiHelper.getInstance(requireContext()) + .login(requireActivity()) + updateOneDrive() + } + } + true + } + } else { + oneDrivePref.summary = + context?.getString(R.string.preference_search_onedrive_summary, user.name) + } + } + + private fun updateNextcloud() { + val nextcloudPref = findPreference("search_nextcloud")!! + val client = NextcloudApiHelper(context ?: return) + lifecycleScope.launch { + val user = client.getLoggedInUser() + if (user == null) { + nextcloudPref.setSummary(R.string.preference_summary_not_logged_in) + LauncherPreferences.instance.searchNextcloud = false + nextcloudPref.setOnPreferenceChangeListener { _, value -> + if (value as Boolean) { + lifecycleScope.launch launch2@{ + updateNextcloud() + } + } + true + } + } else { + nextcloudPref.summary = context?.getString( + R.string.preference_search_nextcloud_summary, + user.displayName + ) + } + } + } + + private fun updateOwncloud() { + val owncloudPref = findPreference("search_owncloud")!! + lifecycleScope.launch { + val client = OwncloudClient(context ?: return@launch) + val user = client.getLoggedInUser() + if (user == null) { + owncloudPref.setSummary(R.string.preference_summary_not_logged_in) + LauncherPreferences.instance.searchOwncloud = false + owncloudPref.setOnPreferenceChangeListener { _, value -> + if (value as Boolean) { + lifecycleScope.launch launch2@{ + client.login(requireActivity(), 0) + updateOwncloud() + } + } + true + } + } else { + owncloudPref.summary = context?.getString( + R.string.preference_search_nextcloud_summary, + user.displayName, + ) + } + } + } + + private fun googleLogin() { + GoogleApiHelper.getInstance(requireContext()).login(requireActivity()) + } + + private fun setSettingsScreen(fragment: Fragment) { + parentFragmentManager.beginTransaction() + .setCustomAnimations( + R.anim.preference_fragment_child_enter, R.anim.preference_fragment_parent_exit, + R.anim.preference_fragment_parent_enter, R.anim.preference_fragment_child_exit + ) + .replace(android.R.id.content, fragment) + .addToBackStack(null) + .commit() + } + + override fun onResume() { + super.onResume() + (activity as AppCompatActivity).supportActionBar?.setTitle(R.string.preference_screen_search) + updateNextcloud() + updateOwncloud() + } +} \ No newline at end of file diff --git a/app/src/main/java/de/mm20/launcher2/fragment/PreferencesServicesFragment.kt b/app/src/main/java/de/mm20/launcher2/fragment/PreferencesServicesFragment.kt new file mode 100644 index 00000000..2fb04beb --- /dev/null +++ b/app/src/main/java/de/mm20/launcher2/fragment/PreferencesServicesFragment.kt @@ -0,0 +1,174 @@ +package de.mm20.launcher2.fragment + +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import de.mm20.launcher2.R +import de.mm20.launcher2.gservices.GoogleApiHelper +import de.mm20.launcher2.msservices.MicrosoftGraphApiHelper +import de.mm20.launcher2.nextcloud.NextcloudApiHelper +import de.mm20.launcher2.owncloud.OwncloudClient +import kotlinx.coroutines.launch + +class PreferencesServicesFragment : PreferenceFragmentCompat() { + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + lifecycleScope.launch { + viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) { + updateGooglePreferences() + updateMicrosoftPreferences() + updateNextcloudPreferences() + } + } + } + + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.preferences_services) + } + + private suspend fun updateGooglePreferences() { + val pref = findPreference("google_signin")!! + val googleApiHelper = GoogleApiHelper.getInstance(requireContext()) + if (!googleApiHelper.isAvailable()) { + pref.isEnabled = false + pref.summary = context?.getString(R.string.feature_not_available, context?.getString(R.string.app_name)) + return + } + val account = googleApiHelper.getAccount() + if (account == null) { + pref.apply { + setTitle(R.string.preference_google_signin) + setSummary(R.string.preference_google_signin_summary) + setOnPreferenceClickListener { + googleApiHelper.login(requireActivity()) + true + } + } + } else { + pref.apply { + title = context.getString(R.string.preference_signin_logout) + summary = context.getString(R.string.preference_signin_user, account.name) + setOnPreferenceClickListener { + googleApiHelper.logout() + lifecycleScope.launch { + updateGooglePreferences() + } + true + } + } + } + } + + private suspend fun updateMicrosoftPreferences() { + val pref = findPreference("ms_signin")!! + val msApiHelper = MicrosoftGraphApiHelper.getInstance(requireContext()) + if (!msApiHelper.isAvailable()) { + pref.isEnabled = false + pref.summary = context?.getString(R.string.feature_not_available, context?.getString(R.string.app_name)) + return + } + val user = MicrosoftGraphApiHelper.getInstance(requireContext()).getUser() + if (user == null) { + pref.setTitle(R.string.preference_ms_signin) + pref.setSummary(R.string.preference_ms_signin_summary) + pref.setOnPreferenceClickListener { + lifecycleScope.launch { + msApiHelper.login(requireActivity()) + updateMicrosoftPreferences() + } + true + } + } else { + pref.setTitle(R.string.preference_signin_logout) + pref.summary = context?.getString(R.string.preference_signin_user, user.name) + pref.setOnPreferenceClickListener { + lifecycleScope.launch { + msApiHelper.logout() + updateMicrosoftPreferences() + } + true + } + + } + } + + private suspend fun updateNextcloudPreferences() { + val nextcloud = NextcloudApiHelper(requireContext()) + val user = nextcloud.getLoggedInUser() + if (user == null) { + findPreference("nextcloud_signin")?.let { + it.setOnPreferenceClickListener { + nextcloud.login(requireActivity()) + true + } + it.setTitle(R.string.preference_nextcloud_signin) + it.setSummary(R.string.preference_nextcloud_signin_summary) + } + } else { + findPreference("nextcloud_signin")?.let { + it.setOnPreferenceClickListener { + lifecycleScope.launch { + nextcloud.logout() + updateNextcloudPreferences() + } + true + } + it.setTitle(R.string.preference_signin_logout) + it.summary = context?.getString( + R.string.preference_signin_user_nextcloud, + user.displayName + ) + } + } + } + + private fun updateOwncloudPreferences() { + val client = OwncloudClient(context ?: return) + lifecycleScope.launch { + val user = client.getLoggedInUser() + if (user == null) { + findPreference("owncloud_signin")?.let { + it.setOnPreferenceClickListener { + OwncloudClient(requireContext()).login( + requireActivity(), + REQUEST_OWNCLOUD_LOGIN + ) + true + } + it.setTitle(R.string.preference_owncloud_signin) + it.setSummary(R.string.preference_owncloud_signin_summary) + } + } else { + findPreference("owncloud_signin")?.let { + it.setOnPreferenceClickListener { + OwncloudClient(requireContext()).logout() + updateOwncloudPreferences() + true + } + it.setTitle(R.string.preference_signin_logout) + it.summary = context?.getString( + R.string.preference_signin_user_nextcloud, + user.displayName, + ) + } + } + } + } + + override fun onResume() { + super.onResume() + (activity as AppCompatActivity).supportActionBar?.setTitle(R.string.preference_screen_services) + updateOwncloudPreferences() + } + + companion object { + const val REQUEST_OWNCLOUD_LOGIN = 581 + } +} \ No newline at end of file diff --git a/app/src/main/java/de/mm20/launcher2/fragment/PreferencesWeatherFragment.kt b/app/src/main/java/de/mm20/launcher2/fragment/PreferencesWeatherFragment.kt new file mode 100644 index 00000000..17da14e2 --- /dev/null +++ b/app/src/main/java/de/mm20/launcher2/fragment/PreferencesWeatherFragment.kt @@ -0,0 +1,166 @@ +package de.mm20.launcher2.fragment + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.SwitchPreference +import com.afollestad.materialdialogs.MaterialDialog +import com.afollestad.materialdialogs.list.listItems +import com.afollestad.materialdialogs.list.listItemsSingleChoice +import de.mm20.launcher2.R +import de.mm20.launcher2.preferences.LauncherPreferences +import de.mm20.launcher2.preferences.WeatherProviders +import de.mm20.launcher2.weather.WeatherProvider +import de.mm20.launcher2.weather.WeatherViewModel +import de.mm20.launcher2.weather.here.HereProvider +import de.mm20.launcher2.weather.metno.MetNoProvider +import de.mm20.launcher2.weather.openweathermap.OpenWeatherMapProvider +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class PreferencesWeatherFragment : PreferenceFragmentCompat() { + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.preferences_weather) + findPreference("location")?.setOnPreferenceChangeListener { _, newValue -> + lifecycleScope.launch { + val locations = withContext(Dispatchers.IO) { + WeatherProvider.getInstance(requireContext()) + ?.lookupLocation(newValue as String) + } ?: return@launch + onLookupCompleted(locations) + } + false + } + /*findPreference("weather_provider")?.setOnPreferenceChangeListener { pref, newValue -> + val newProvider = WeatherProviders.byValue(newValue as String) + LauncherPreferences.instance.weatherProvider = newProvider + WeatherProvider.getInstance(requireContext())?.resetLastUpdate() + ViewModelProvider(this).get(WeatherViewModel::class.java).requestUpdate(requireContext()) + updateProviderPreferences() + true + }*/ + val providerPref = findPreference("weather_provider")!! + val context = requireContext() + + val providers = mutableListOf>() + OpenWeatherMapProvider(context).takeIf { it.isAvailable() }?.let { + providers.add(WeatherProviders.OPENWEATHERMAP to it.name) + } + HereProvider(context).takeIf { it.isAvailable() }?.let { + providers.add(WeatherProviders.HERE to it.name) + } + MetNoProvider(context).takeIf { it.isAvailable() }?.let { + providers.add(WeatherProviders.MET_NO to it.name) + } + + if (providers.isEmpty()) { + providerPref.summary = context.getString( + R.string.feature_not_available, + context.getString(R.string.app_name) + ) + providerPref.isEnabled = false + } else { + providerPref.setOnPreferenceClickListener { + MaterialDialog(context).show { + title(R.string.preference_weather_provider) + listItemsSingleChoice( + items = providers.map { it.second }, + initialSelection = providers.indexOfFirst { it.first == LauncherPreferences.instance.weatherProvider } + ) { dialog, index, text -> + LauncherPreferences.instance.weatherProvider = providers[index].first + WeatherProvider.getInstance(requireContext())?.resetLastUpdate() + ViewModelProvider(this@PreferencesWeatherFragment) + .get(WeatherViewModel::class.java) + .requestUpdate(requireContext()) + updateProviderPreferences() + dialog.dismiss() + } + } + true + } + } + findPreference("auto_location")?.setOnPreferenceChangeListener { _, newValue -> + val autoLocation = newValue as Boolean + val provider = WeatherProvider.getInstance(requireContext()) + provider?.autoLocation = autoLocation + provider?.resetLastUpdate() + provider?.setLocation(null, "") + ViewModelProvider(this).get(WeatherViewModel::class.java) + .requestUpdate(requireContext()) + updateProviderPreferences() + true + } + updateProviderPreferences() + } + + private fun updateProviderPreferences() { + val provider = WeatherProvider.getInstance(requireContext()) + val autoLocationPref = findPreference("auto_location")!! + val locationPref = findPreference("location")!! + val unitsPref = findPreference("imperial_units")!! + val providerPref = findPreference("weather_provider")!! + + + + locationPref.parent?.isVisible = provider != null + unitsPref.isVisible = provider != null + + provider ?: return + + providerPref.summary = provider.name + + if (provider.supportsAutoLocation) { + autoLocationPref.setSummary(R.string.preference_automatic_location_summary) + autoLocationPref.isChecked = provider.autoLocation + if (!provider.supportsManualLocation) { + autoLocationPref.isEnabled = false + autoLocationPref.isChecked = true + locationPref.isEnabled = false + locationPref.setSummary(R.string.preference_location_disabled_summary) + } else { + autoLocationPref.isEnabled = true + autoLocationPref.isChecked = provider.autoLocation + locationPref.isEnabled = true + locationPref.summary = provider.getLastLocation() + } + } else { + autoLocationPref.isEnabled = false + autoLocationPref.setSummary(R.string.preference_automatic_location_disabled_summary) + autoLocationPref.isChecked = false + } + } + + private fun onLookupCompleted(results: List>) { + MaterialDialog(requireContext()) + .listItems( + items = results.map { it.second }, + waitForPositiveButton = false + ) { dialog, index, _ -> + val provider = WeatherProvider.getInstance(requireContext()) + ?: return@listItems dialog.dismiss() + provider.resetLastUpdate() + provider.setLocation(results[index].first, results[index].second) + findPreference("location")?.summary = results[index].second + ViewModelProvider(this).get(WeatherViewModel::class.java) + .requestUpdate(requireContext()) + dialog.dismiss() + } + .negativeButton { + it.cancel() + } + .show() + } + + + override fun onResume() { + super.onResume() + + (activity as AppCompatActivity).supportActionBar?.setTitle(R.string.preference_screen_weather) + } + +} diff --git a/app/src/main/java/de/mm20/launcher2/fragment/PreferencesWebSearchesFragment.kt b/app/src/main/java/de/mm20/launcher2/fragment/PreferencesWebSearchesFragment.kt new file mode 100644 index 00000000..ec3d48bb --- /dev/null +++ b/app/src/main/java/de/mm20/launcher2/fragment/PreferencesWebSearchesFragment.kt @@ -0,0 +1,229 @@ +package de.mm20.launcher2.fragment + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.Intent +import android.content.res.ColorStateList +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.PorterDuff +import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.widget.EditText +import android.widget.ImageView +import androidx.appcompat.app.AppCompatActivity +import androidx.core.graphics.scale +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import com.afollestad.materialdialogs.MaterialDialog +import com.afollestad.materialdialogs.bottomsheets.BottomSheet +import com.afollestad.materialdialogs.color.colorChooser +import com.afollestad.materialdialogs.customview.customView +import com.bumptech.glide.Glide +import com.bumptech.glide.request.target.SimpleTarget +import com.bumptech.glide.request.transition.Transition +import de.mm20.launcher2.R +import de.mm20.launcher2.ktx.dp +import de.mm20.launcher2.search.SearchViewModel +import de.mm20.launcher2.search.WebsearchViewModel +import de.mm20.launcher2.search.data.Websearch +import java.io.File +import java.io.FileOutputStream +import java.lang.ref.WeakReference + +class PreferencesWebSearchesFragment : PreferenceFragmentCompat() { + + private lateinit var rootView: View + + private var sheetIcon: WeakReference? = null + + private val viewModel by lazy { + ViewModelProvider(context as AppCompatActivity)[WebsearchViewModel::class.java] + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + preferenceScreen = preferenceManager.createPreferenceScreen(activity) + val searches = viewModel.allWebsearches + searches.observe(context as AppCompatActivity, Observer { + updatePreferenceScreen(it) + }) + } + + private fun updatePreferenceScreen(searches: List) { + preferenceScreen.removeAll() + + for (search in searches) { + val pref = Preference(context) + pref.title = search.label + if (search.icon == null) { + val drawable = resources.getDrawable(R.drawable.ic_search, requireActivity().theme).mutate() + drawable.setTintMode(PorterDuff.Mode.SRC_ATOP) + drawable.setTint(search.color) + pref.icon = drawable + } else { + Glide.with(requireContext()) + .asDrawable() + .load(search.icon) + .into(object : SimpleTarget() { + override fun onResourceReady(resource: Drawable, transition: Transition?) { + pref.icon = resource + } + }) + } + pref.setOnPreferenceClickListener { + editSearch(search) + true + } + preferenceScreen.addPreference(pref) + } + + + val newPref = Preference(activity) + newPref.setTitle(R.string.preference_websearch_new) + newPref.setIcon(R.drawable.ic_preference_websearch_new) + newPref.setOnPreferenceClickListener { + editSearch(null) + true + } + preferenceScreen.addPreference(newPref) + } + + private fun editSearch(search: Websearch?) { + + val websearch = search ?: Websearch("", "", 0xFF555555.toInt(), null, null) + + val dialogView = LayoutInflater.from(activity).inflate(R.layout.dialog_websearch, null) + val nameEdit = dialogView.findViewById(R.id.websearchName) + nameEdit.setText(websearch.label) + val urlEdit = dialogView.findViewById(R.id.websearchUrl) + urlEdit.setText(websearch.urlTemplate) + val iconView = dialogView.findViewById(R.id.websearchIcon) + iconView.apply { + if (websearch.icon == null) { + setImageResource(R.drawable.ic_search) + imageTintList = ColorStateList.valueOf(websearch.color) + } else { + Glide.with(this) + .load(websearch.icon) + .into(this) + } + sheetIcon = WeakReference(this) + } + + val sheet = MaterialDialog(requireContext(), BottomSheet()) + .cornerRadius(8f) + .customView(view = dialogView) + + val radius = 8 * dialogView.dp + dialogView.background = GradientDrawable().apply { + cornerRadii = floatArrayOf( + radius, radius, // top left + radius, radius, // top right + 0f, 0f, // bottom left + 0f, 0f // bottom right + ) + } + + var newColor = websearch.color + var newIcon: String? = websearch.icon + + + sheet.noAutoDismiss() + .positiveButton(android.R.string.ok) { + val newUrl = urlEdit.text.toString() + val newName = nameEdit.text.toString() + if (!newUrl.contains("\${1}")) { + urlEdit.error = getString(R.string.websearch_dialog_url_error) + return@positiveButton + } + File(requireContext().cacheDir, "websearch-tmp").takeIf { it.exists() }?.let { + websearch.icon?.let { File(it).takeIf { it.exists() }?.delete() } + val newFile = File(requireContext().filesDir, "websearch-${System.currentTimeMillis()}") + it.copyTo(newFile, true) + it.delete() + newIcon = newFile.absolutePath + } + if (newIcon == null) { + websearch.icon?.let { File(it).takeIf { it.exists() }?.delete() } + } + websearch.urlTemplate = newUrl + websearch.label = newName + websearch.icon = newIcon + websearch.color = newColor + viewModel.insertWebsearch(websearch) + sheet.dismiss() + } + + sheet.negativeButton(android.R.string.cancel) { + sheet.cancel() + } + + @Suppress("DEPRECATION") + sheet.neutralButton(R.string.menu_delete) { + sheet.dismiss() + websearch.icon?.let { File(it).takeIf { it.exists() }?.delete() } + viewModel.deleteWebsearch(websearch) + } + + sheet.setOnCancelListener { + File(requireContext().cacheDir, "websearch-tmp").takeIf { it.exists() }?.delete() + } + + dialogView.findViewById(R.id.websearchIcon).setOnClickListener { + MaterialDialog(requireContext()).show { + @Suppress("DEPRECATION") + neutralButton(R.string.custom_icon) { + val intent = Intent(Intent.ACTION_GET_CONTENT) + intent.type = "image/*" + try { + startActivityForResult(intent, 24) + } catch (e: ActivityNotFoundException) { + } + dismiss() + } + title(R.string.websearch_dialog_choose_icon_color) + colorChooser( + colors = context.resources.getIntArray(R.array.color_chooser_presets), + allowCustomArgb = true, + showAlphaSelector = false + ) { _, color -> + iconView.setImageResource(R.drawable.ic_search) + iconView.imageTintList = ColorStateList.valueOf(color) + newColor = color + newIcon = null + File(requireContext().cacheDir, "websearch-tmp").takeIf { it.exists() }?.delete() + dismiss() + } + } + } + sheet.show() + } + + override fun onResume() { + super.onResume() + (activity as AppCompatActivity).supportActionBar + ?.setTitle(R.string.preference_search_edit_websearch) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + val dataUri = data?.data + if (requestCode == 24 && resultCode == Activity.RESULT_OK && dataUri != null) { + val stream = requireActivity().contentResolver.openInputStream(dataUri) + val icon = BitmapFactory.decodeStream(stream) + val scaledIcon = icon.scale((32 * requireContext().dp).toInt(), (32 * requireContext().dp).toInt()) + val out = FileOutputStream(File(requireContext().cacheDir, "websearch-tmp")) + scaledIcon.compress(Bitmap.CompressFormat.PNG, 100, out) + out.close() + sheetIcon?.get()?.apply { + imageTintList = null + setImageBitmap(scaledIcon) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/mm20/launcher2/helper/DebugInformationDumper.kt b/app/src/main/java/de/mm20/launcher2/helper/DebugInformationDumper.kt new file mode 100644 index 00000000..20ac7979 --- /dev/null +++ b/app/src/main/java/de/mm20/launcher2/helper/DebugInformationDumper.kt @@ -0,0 +1,45 @@ +package de.mm20.launcher2.helper + +import android.content.Context +import android.os.Build +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File +import java.text.SimpleDateFormat +import java.util.* + +class DebugInformationDumper { + + fun dump(context: Context): String { + val df = SimpleDateFormat("yyyy-MM-dd-HHmmss") + val file = File(context.getExternalFilesDir(null), "kvaesitso-log-${df.format(Date(System.currentTimeMillis()))}") + val fos = file.outputStream().writer() + fos.write("Device: ${Build.DEVICE}\n") + fos.write("SDK version: ${Build.VERSION.SDK_INT}\n") + fos.write("====================================\n") + Thread { + val input = Runtime.getRuntime().exec("/system/bin/sh -c logcat").inputStream.bufferedReader() + var line = input.readLine() + while (line != null) { + line = input.readLine() + fos.write("$line\n") + } + fos.close() + }.start() + return file.absolutePath + } + + fun exportDatabases(context: Context): String { + val df = SimpleDateFormat("yyyy-MM-dd-HHmmss") + val exportFile = File(context.getExternalFilesDir(null), "room-${df.format(Date(System.currentTimeMillis()))}.db") + GlobalScope.launch { + withContext(Dispatchers.IO) { + context.getDatabasePath("room").copyTo(exportFile) + } + } + return exportFile.absolutePath + } + +} \ No newline at end of file diff --git a/app/src/main/java/de/mm20/launcher2/ui/preferences/AppStartAnimPreference.kt b/app/src/main/java/de/mm20/launcher2/ui/preferences/AppStartAnimPreference.kt new file mode 100644 index 00000000..4be04ac1 --- /dev/null +++ b/app/src/main/java/de/mm20/launcher2/ui/preferences/AppStartAnimPreference.kt @@ -0,0 +1,99 @@ +package de.mm20.launcher2.ui.preferences + +import android.animation.Animator +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.view.postDelayed +import androidx.preference.Preference +import com.afollestad.materialdialogs.MaterialDialog +import com.afollestad.materialdialogs.customview.customView +import com.airbnb.lottie.LottieAnimationView +import de.mm20.launcher2.R +import de.mm20.launcher2.preferences.AppStartAnimation +import de.mm20.launcher2.preferences.LauncherPreferences + +class AppStartAnimPreference @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = R.attr.preferenceStyle) : Preference(context, attrs, defStyleAttr) { + + init { + summary = getNameForAnimation(LauncherPreferences.instance.appStartAnim) + + setOnPreferenceClickListener { + val anims = mutableListOf( + AppStartAnimation.M to R.raw.app_start_anim_m, + AppStartAnimation.SLIDE_BOTTOM to R.raw.app_start_anim_slide_bottom, + AppStartAnimation.FADE to R.raw.app_start_anim_fade + ) + + val dialog = MaterialDialog(context) + val layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + + val list = LinearLayout(context) + + list.orientation = LinearLayout.VERTICAL + + anims.forEachIndexed { _, anim -> + val view = View.inflate(context, R.layout.preference_start_anim_item, null) + view.findViewById(R.id.icon).also { iconView -> + iconView.setAnimation(anim.second) + iconView.addAnimatorListener(object : Animator.AnimatorListener { + override fun onAnimationRepeat(animation: Animator?) { + } + + override fun onAnimationEnd(animation: Animator) { + iconView.postDelayed(500) { + iconView.frame = 0 + } + iconView.postDelayed(1300) { + iconView.playAnimation() + } + } + + override fun onAnimationCancel(animation: Animator?) { + } + + override fun onAnimationStart(animation: Animator?) { + } + + }) + iconView.postDelayed(300) { + iconView.playAnimation() + } + } + view.findViewById(R.id.label).also { labelView -> + labelView.setText(getNameForAnimation(anim.first)) + } + view.layoutParams = layoutParams + list.addView(view) + view.setOnClickListener { + LauncherPreferences.instance.appStartAnim = anim.first + summary = getNameForAnimation(anim.first) + dialog.dismiss() + } + } + + dialog.customView(view = list, scrollable = true) + .title(R.string.preference_app_start_animation) + .negativeButton(android.R.string.cancel) { + dialog.cancel() + } + .show() + true + } + } + + private fun getNameForAnimation(anim: AppStartAnimation): String { + return when (anim) { + AppStartAnimation.FADE -> context.getString(R.string.preference_app_start_animation_fade) + AppStartAnimation.SLIDE_BOTTOM -> context.getString(R.string.preference_app_start_animation_slide_bottom) + AppStartAnimation.M -> context.getString(R.string.preference_app_start_animation_m) + else -> context.getString(R.string.preference_app_start_animation_default) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/mm20/launcher2/ui/view/PreferencesView.kt b/app/src/main/java/de/mm20/launcher2/ui/view/PreferencesView.kt new file mode 100644 index 00000000..3579c3e7 --- /dev/null +++ b/app/src/main/java/de/mm20/launcher2/ui/view/PreferencesView.kt @@ -0,0 +1,73 @@ +package de.mm20.launcher2.ui.view + +import android.content.Context +import android.os.Bundle +import android.util.AttributeSet +import android.view.View +import android.widget.FrameLayout +import androidx.annotation.XmlRes +import androidx.appcompat.app.AppCompatActivity +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import de.mm20.launcher2.R +import de.mm20.launcher2.ktx.castTo + +class PreferencesView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr) { + + private val fragment = PreferenceViewFragment() + + init { + if (id == View.NO_ID) id = View.generateViewId() + context.castTo().supportFragmentManager.beginTransaction() + .add(id, fragment) + .commit() + attrs?.let { + val ta = context.theme.obtainStyledAttributes(it, R.styleable.PreferencesView, 0, defStyleAttr) + val preferenceScreen = ta.getResourceId(R.styleable.SearchGridView_columnCount, 0) + setPreferenceResource(preferenceScreen) + ta.recycle() + } + } + + fun setPreferenceResource(@XmlRes resId: Int) { + if (resId == 0) return + fragment.setPreferenceResource(resId) + } + + fun findPreference(key: String): T? { + return fragment.findPreference(key) + } + + var onPreferencesReady: (() -> Unit)? = null + set(value) { + field = value + fragment.onPreferencesReady = value + } + +} + +class PreferenceViewFragment : PreferenceFragmentCompat() { + + private var isInitialized = false + var onPreferencesReady: (() -> Unit)? = null + + @XmlRes + private var preferenceResource = 0 + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + if (preferenceResource != 0) addPreferencesFromResource(preferenceResource) + isInitialized = true + onPreferencesReady?.invoke() + onPreferencesReady = null + } + + internal fun setPreferenceResource(@XmlRes resId: Int) { + preferenceResource = resId + if (isInitialized && resId != 0) { + addPreferencesFromResource(resId) + } + } + +} \ No newline at end of file diff --git a/app/src/main/res/anim/app_to_launcher_in.xml b/app/src/main/res/anim/app_to_launcher_in.xml new file mode 100644 index 00000000..34fcc81e --- /dev/null +++ b/app/src/main/res/anim/app_to_launcher_in.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/app_to_launcher_out.xml b/app/src/main/res/anim/app_to_launcher_out.xml new file mode 100644 index 00000000..5af47f21 --- /dev/null +++ b/app/src/main/res/anim/app_to_launcher_out.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/fast_out_extra_slow_in.xml b/app/src/main/res/anim/fast_out_extra_slow_in.xml new file mode 100644 index 00000000..b70fb29a --- /dev/null +++ b/app/src/main/res/anim/fast_out_extra_slow_in.xml @@ -0,0 +1,3 @@ + + diff --git a/app/src/main/res/anim/ic_pause_to_play_path.xml b/app/src/main/res/anim/ic_pause_to_play_path.xml new file mode 100644 index 00000000..025974cd --- /dev/null +++ b/app/src/main/res/anim/ic_pause_to_play_path.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/ic_pause_to_play_rotate.xml b/app/src/main/res/anim/ic_pause_to_play_rotate.xml new file mode 100644 index 00000000..e4174dce --- /dev/null +++ b/app/src/main/res/anim/ic_pause_to_play_rotate.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/ic_play_to_pause_path.xml b/app/src/main/res/anim/ic_play_to_pause_path.xml new file mode 100644 index 00000000..3b0c626e --- /dev/null +++ b/app/src/main/res/anim/ic_play_to_pause_path.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/ic_play_to_pause_rotate.xml b/app/src/main/res/anim/ic_play_to_pause_rotate.xml new file mode 100644 index 00000000..4f7e9603 --- /dev/null +++ b/app/src/main/res/anim/ic_play_to_pause_rotate.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/ic_skip_next_arrow1.xml b/app/src/main/res/anim/ic_skip_next_arrow1.xml new file mode 100644 index 00000000..341036d1 --- /dev/null +++ b/app/src/main/res/anim/ic_skip_next_arrow1.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/ic_skip_next_arrow2.xml b/app/src/main/res/anim/ic_skip_next_arrow2.xml new file mode 100644 index 00000000..31c62ac9 --- /dev/null +++ b/app/src/main/res/anim/ic_skip_next_arrow2.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/ic_skip_prev_arrow1.xml b/app/src/main/res/anim/ic_skip_prev_arrow1.xml new file mode 100644 index 00000000..0674c678 --- /dev/null +++ b/app/src/main/res/anim/ic_skip_prev_arrow1.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/ic_skip_prev_arrow2.xml b/app/src/main/res/anim/ic_skip_prev_arrow2.xml new file mode 100644 index 00000000..4d4029e4 --- /dev/null +++ b/app/src/main/res/anim/ic_skip_prev_arrow2.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/preference_fragment_child_enter.xml b/app/src/main/res/anim/preference_fragment_child_enter.xml new file mode 100644 index 00000000..2b84e6c0 --- /dev/null +++ b/app/src/main/res/anim/preference_fragment_child_enter.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/preference_fragment_child_exit.xml b/app/src/main/res/anim/preference_fragment_child_exit.xml new file mode 100644 index 00000000..321e7762 --- /dev/null +++ b/app/src/main/res/anim/preference_fragment_child_exit.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/preference_fragment_parent_enter.xml b/app/src/main/res/anim/preference_fragment_parent_enter.xml new file mode 100644 index 00000000..bcaf0193 --- /dev/null +++ b/app/src/main/res/anim/preference_fragment_parent_enter.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/preference_fragment_parent_exit.xml b/app/src/main/res/anim/preference_fragment_parent_exit.xml new file mode 100644 index 00000000..850ec0da --- /dev/null +++ b/app/src/main/res/anim/preference_fragment_parent_exit.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/animator/card_raise_animator.xml b/app/src/main/res/animator/card_raise_animator.xml new file mode 100644 index 00000000..c957f60b --- /dev/null +++ b/app/src/main/res/animator/card_raise_animator.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/ic_wikipedia.webp b/app/src/main/res/drawable-hdpi/ic_wikipedia.webp new file mode 100644 index 00000000..3679bc0b Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_wikipedia.webp differ diff --git a/app/src/main/res/drawable-mdpi/ic_wikipedia.webp b/app/src/main/res/drawable-mdpi/ic_wikipedia.webp new file mode 100644 index 00000000..afd78fad Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_wikipedia.webp differ diff --git a/app/src/main/res/drawable-night/ic_account_owncloud.xml b/app/src/main/res/drawable-night/ic_account_owncloud.xml new file mode 100644 index 00000000..15eaacfb --- /dev/null +++ b/app/src/main/res/drawable-night/ic_account_owncloud.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable-night/ic_permission_calendar.xml b/app/src/main/res/drawable-night/ic_permission_calendar.xml new file mode 100644 index 00000000..616d46e0 --- /dev/null +++ b/app/src/main/res/drawable-night/ic_permission_calendar.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/app/src/main/res/drawable-xhdpi/ic_wikipedia.webp b/app/src/main/res/drawable-xhdpi/ic_wikipedia.webp new file mode 100644 index 00000000..36cc5b48 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_wikipedia.webp differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_wikipedia.webp b/app/src/main/res/drawable-xxhdpi/ic_wikipedia.webp new file mode 100644 index 00000000..e6d316a7 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_wikipedia.webp differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_about_mm20.webp b/app/src/main/res/drawable-xxxhdpi/ic_about_mm20.webp new file mode 100644 index 00000000..7f8184e5 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_about_mm20.webp differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_wikipedia.webp b/app/src/main/res/drawable-xxxhdpi/ic_wikipedia.webp new file mode 100644 index 00000000..ffaa3967 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_wikipedia.webp differ diff --git a/app/src/main/res/drawable/ic_about_fdroid.xml b/app/src/main/res/drawable/ic_about_fdroid.xml new file mode 100644 index 00000000..3d750757 --- /dev/null +++ b/app/src/main/res/drawable/ic_about_fdroid.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_about_github.xml b/app/src/main/res/drawable/ic_about_github.xml new file mode 100644 index 00000000..38af1f5c --- /dev/null +++ b/app/src/main/res/drawable/ic_about_github.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_about_telegram.xml b/app/src/main/res/drawable/ic_about_telegram.xml new file mode 100644 index 00000000..be85023e --- /dev/null +++ b/app/src/main/res/drawable/ic_about_telegram.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_about_xda.xml b/app/src/main/res/drawable/ic_about_xda.xml new file mode 100644 index 00000000..907afbf1 --- /dev/null +++ b/app/src/main/res/drawable/ic_about_xda.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_account_google.xml b/app/src/main/res/drawable/ic_account_google.xml new file mode 100644 index 00000000..89d2b99d --- /dev/null +++ b/app/src/main/res/drawable/ic_account_google.xml @@ -0,0 +1,30 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_account_microsoft.xml b/app/src/main/res/drawable/ic_account_microsoft.xml new file mode 100644 index 00000000..2eed828d --- /dev/null +++ b/app/src/main/res/drawable/ic_account_microsoft.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_account_nextcloud.xml b/app/src/main/res/drawable/ic_account_nextcloud.xml new file mode 100644 index 00000000..50ae6b4a --- /dev/null +++ b/app/src/main/res/drawable/ic_account_nextcloud.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_account_owncloud.xml b/app/src/main/res/drawable/ic_account_owncloud.xml new file mode 100644 index 00000000..b61d7b3f --- /dev/null +++ b/app/src/main/res/drawable/ic_account_owncloud.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_drop_down.xml b/app/src/main/res/drawable/ic_arrow_drop_down.xml new file mode 100644 index 00000000..ce583469 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_drop_down.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_cancel.xml b/app/src/main/res/drawable/ic_cancel.xml new file mode 100644 index 00000000..7d2b57eb --- /dev/null +++ b/app/src/main/res/drawable/ic_cancel.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_clear.xml b/app/src/main/res/drawable/ic_clear.xml new file mode 100644 index 00000000..ede4b710 --- /dev/null +++ b/app/src/main/res/drawable/ic_clear.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_description.xml b/app/src/main/res/drawable/ic_description.xml new file mode 100644 index 00000000..0fcd6f4a --- /dev/null +++ b/app/src/main/res/drawable/ic_description.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_drag_handle.xml b/app/src/main/res/drawable/ic_drag_handle.xml new file mode 100644 index 00000000..68a71905 --- /dev/null +++ b/app/src/main/res/drawable/ic_drag_handle.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_expand_more.xml b/app/src/main/res/drawable/ic_expand_more.xml new file mode 100644 index 00000000..8d57dbc1 --- /dev/null +++ b/app/src/main/res/drawable/ic_expand_more.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_file_android.xml b/app/src/main/res/drawable/ic_file_android.xml new file mode 100644 index 00000000..8d3fd387 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_android.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_file_archive.xml b/app/src/main/res/drawable/ic_file_archive.xml new file mode 100644 index 00000000..f38c06fe --- /dev/null +++ b/app/src/main/res/drawable/ic_file_archive.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_file_code.xml b/app/src/main/res/drawable/ic_file_code.xml new file mode 100644 index 00000000..f7cc253e --- /dev/null +++ b/app/src/main/res/drawable/ic_file_code.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_file_document.xml b/app/src/main/res/drawable/ic_file_document.xml new file mode 100644 index 00000000..f55b73f2 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_document.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_file_folder.xml b/app/src/main/res/drawable/ic_file_folder.xml new file mode 100644 index 00000000..82347288 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_folder.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_file_form.xml b/app/src/main/res/drawable/ic_file_form.xml new file mode 100644 index 00000000..84cf107e --- /dev/null +++ b/app/src/main/res/drawable/ic_file_form.xml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_file_generic.xml b/app/src/main/res/drawable/ic_file_generic.xml new file mode 100644 index 00000000..a431a5e9 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_generic.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_file_markup.xml b/app/src/main/res/drawable/ic_file_markup.xml new file mode 100644 index 00000000..6720df48 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_markup.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_file_music.xml b/app/src/main/res/drawable/ic_file_music.xml new file mode 100644 index 00000000..69f0a3a4 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_music.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_file_pdf.xml b/app/src/main/res/drawable/ic_file_pdf.xml new file mode 100644 index 00000000..a73638b0 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_pdf.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_file_picture.xml b/app/src/main/res/drawable/ic_file_picture.xml new file mode 100644 index 00000000..0d8d5030 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_picture.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_file_presentation.xml b/app/src/main/res/drawable/ic_file_presentation.xml new file mode 100644 index 00000000..4d799702 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_presentation.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_file_spreadsheet.xml b/app/src/main/res/drawable/ic_file_spreadsheet.xml new file mode 100644 index 00000000..da1dbcc4 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_spreadsheet.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_file_video.xml b/app/src/main/res/drawable/ic_file_video.xml new file mode 100644 index 00000000..349fbf31 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_video.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_location.xml b/app/src/main/res/drawable/ic_location.xml new file mode 100644 index 00000000..e3291a94 --- /dev/null +++ b/app/src/main/res/drawable/ic_location.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_more_horiz.xml b/app/src/main/res/drawable/ic_more_horiz.xml new file mode 100644 index 00000000..da83afdb --- /dev/null +++ b/app/src/main/res/drawable/ic_more_horiz.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_open_external.xml b/app/src/main/res/drawable/ic_open_external.xml new file mode 100644 index 00000000..60b75a54 --- /dev/null +++ b/app/src/main/res/drawable/ic_open_external.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_open_in_browser.xml b/app/src/main/res/drawable/ic_open_in_browser.xml new file mode 100644 index 00000000..d597c37e --- /dev/null +++ b/app/src/main/res/drawable/ic_open_in_browser.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_permission_calendar.xml b/app/src/main/res/drawable/ic_permission_calendar.xml new file mode 100644 index 00000000..2b892e4b --- /dev/null +++ b/app/src/main/res/drawable/ic_permission_calendar.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_precipitation_none.xml b/app/src/main/res/drawable/ic_precipitation_none.xml new file mode 100644 index 00000000..a0df182d --- /dev/null +++ b/app/src/main/res/drawable/ic_precipitation_none.xml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_precipitation_rain.xml b/app/src/main/res/drawable/ic_precipitation_rain.xml new file mode 100644 index 00000000..ab227fa7 --- /dev/null +++ b/app/src/main/res/drawable/ic_precipitation_rain.xml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_precipitation_rain_snow.xml b/app/src/main/res/drawable/ic_precipitation_rain_snow.xml new file mode 100644 index 00000000..f9d9a1e0 --- /dev/null +++ b/app/src/main/res/drawable/ic_precipitation_rain_snow.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_precipitation_snow.xml b/app/src/main/res/drawable/ic_precipitation_snow.xml new file mode 100644 index 00000000..50cabe72 --- /dev/null +++ b/app/src/main/res/drawable/ic_precipitation_snow.xml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_pref_about.xml b/app/src/main/res/drawable/ic_pref_about.xml new file mode 100644 index 00000000..cfa4ef95 --- /dev/null +++ b/app/src/main/res/drawable/ic_pref_about.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_pref_account.xml b/app/src/main/res/drawable/ic_pref_account.xml new file mode 100644 index 00000000..157a0f45 --- /dev/null +++ b/app/src/main/res/drawable/ic_pref_account.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_pref_appearance.xml b/app/src/main/res/drawable/ic_pref_appearance.xml new file mode 100644 index 00000000..d0c84022 --- /dev/null +++ b/app/src/main/res/drawable/ic_pref_appearance.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_pref_badge.xml b/app/src/main/res/drawable/ic_pref_badge.xml new file mode 100644 index 00000000..0ea8130a --- /dev/null +++ b/app/src/main/res/drawable/ic_pref_badge.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_pref_calendar.xml b/app/src/main/res/drawable/ic_pref_calendar.xml new file mode 100644 index 00000000..2bd51fff --- /dev/null +++ b/app/src/main/res/drawable/ic_pref_calendar.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_pref_plugins.xml b/app/src/main/res/drawable/ic_pref_plugins.xml new file mode 100644 index 00000000..e2254d7f --- /dev/null +++ b/app/src/main/res/drawable/ic_pref_plugins.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_pref_search.xml b/app/src/main/res/drawable/ic_pref_search.xml new file mode 100644 index 00000000..69350a19 --- /dev/null +++ b/app/src/main/res/drawable/ic_pref_search.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_pref_weather.xml b/app/src/main/res/drawable/ic_pref_weather.xml new file mode 100644 index 00000000..81383eaf --- /dev/null +++ b/app/src/main/res/drawable/ic_pref_weather.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_preference_websearch_new.xml b/app/src/main/res/drawable/ic_preference_websearch_new.xml new file mode 100644 index 00000000..59514b94 --- /dev/null +++ b/app/src/main/res/drawable/ic_preference_websearch_new.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_resize_drag_handle.xml b/app/src/main/res/drawable/ic_resize_drag_handle.xml new file mode 100644 index 00000000..e9ba7540 --- /dev/null +++ b/app/src/main/res/drawable/ic_resize_drag_handle.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml new file mode 100644 index 00000000..ace746c4 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_skip_next.xml b/app/src/main/res/drawable/ic_skip_next.xml new file mode 100644 index 00000000..eddfe018 --- /dev/null +++ b/app/src/main/res/drawable/ic_skip_next.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_skip_next_anim.xml b/app/src/main/res/drawable/ic_skip_next_anim.xml new file mode 100644 index 00000000..b4f3bd5e --- /dev/null +++ b/app/src/main/res/drawable/ic_skip_next_anim.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_skip_prev.xml b/app/src/main/res/drawable/ic_skip_prev.xml new file mode 100644 index 00000000..131ab07e --- /dev/null +++ b/app/src/main/res/drawable/ic_skip_prev.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_skip_prev_anim.xml b/app/src/main/res/drawable/ic_skip_prev_anim.xml new file mode 100644 index 00000000..d57e660b --- /dev/null +++ b/app/src/main/res/drawable/ic_skip_prev_anim.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_today.xml b/app/src/main/res/drawable/ic_today.xml new file mode 100644 index 00000000..77fd98c7 --- /dev/null +++ b/app/src/main/res/drawable/ic_today.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_website.xml b/app/src/main/res/drawable/ic_website.xml new file mode 100644 index 00000000..c9f2d069 --- /dev/null +++ b/app/src/main/res/drawable/ic_website.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_widget_resize.xml b/app/src/main/res/drawable/ic_widget_resize.xml new file mode 100644 index 00000000..06a54c71 --- /dev/null +++ b/app/src/main/res/drawable/ic_widget_resize.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_wind.xml b/app/src/main/res/drawable/ic_wind.xml new file mode 100644 index 00000000..308e94bf --- /dev/null +++ b/app/src/main/res/drawable/ic_wind.xml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_websearch.xml b/app/src/main/res/layout/dialog_websearch.xml new file mode 100644 index 00000000..e7372c83 --- /dev/null +++ b/app/src/main/res/layout/dialog_websearch.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_card_settings.xml b/app/src/main/res/layout/fragment_card_settings.xml new file mode 100644 index 00000000..bbcab1cf --- /dev/null +++ b/app/src/main/res/layout/fragment_card_settings.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_easteregg.xml b/app/src/main/res/layout/fragment_easteregg.xml new file mode 100644 index 00000000..740e89aa --- /dev/null +++ b/app/src/main/res/layout/fragment_easteregg.xml @@ -0,0 +1,26 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_license.xml b/app/src/main/res/layout/fragment_license.xml new file mode 100644 index 00000000..42825317 --- /dev/null +++ b/app/src/main/res/layout/fragment_license.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/permission_view.xml b/app/src/main/res/layout/permission_view.xml new file mode 100644 index 00000000..7b9021f9 --- /dev/null +++ b/app/src/main/res/layout/permission_view.xml @@ -0,0 +1,29 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/preference_icon_shape_row.xml b/app/src/main/res/layout/preference_icon_shape_row.xml new file mode 100644 index 00000000..b27538f3 --- /dev/null +++ b/app/src/main/res/layout/preference_icon_shape_row.xml @@ -0,0 +1,30 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/preference_start_anim_item.xml b/app/src/main/res/layout/preference_start_anim_item.xml new file mode 100644 index 00000000..5fef62eb --- /dev/null +++ b/app/src/main/res/layout/preference_start_anim_item.xml @@ -0,0 +1,30 @@ + + + + + + + \ 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 00000000..4ae7d123 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..4ae7d123 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_background.png b/app/src/main/res/mipmap-hdpi/ic_launcher_background.png new file mode 100644 index 00000000..e6897753 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_background.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..f9455607 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_background.png b/app/src/main/res/mipmap-mdpi/ic_launcher_background.png new file mode 100644 index 00000000..72a5d9c0 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_background.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..679d7b1b Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png new file mode 100644 index 00000000..7a56ec15 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..48a2ebf4 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png new file mode 100644 index 00000000..2fe8f6c3 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..7ffdca08 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png new file mode 100644 index 00000000..b597dbda Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..ff5e86ac Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/raw/app_start_anim_default.json b/app/src/main/res/raw/app_start_anim_default.json new file mode 100755 index 00000000..63689c94 --- /dev/null +++ b/app/src/main/res/raw/app_start_anim_default.json @@ -0,0 +1 @@ +{"v":"5.7.1","fr":29.9700012207031,"ip":0,"op":18.000000733155,"w":216,"h":216,"nm":"app_start_animation_fg","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"app_start_animation_fg Konturen","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[100]},{"t":17.0000006924242,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[44]},{"t":17.0000006924242,"s":[107]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[41.5]},{"t":17.0000006924242,"s":[108]}],"ix":4}},"a":{"a":0,"k":[44,41.5,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":10,"s":[100,100,100]},{"t":17.0000006924242,"s":[588.571,588.571,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,56.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,36],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":18.000000733155,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Formebene 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[108,108,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[32,35],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rechteckpfad: 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Kontur 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[-64.104,-66.442],"to":[10.684,11.074],"ti":[-10.684,-11.074]},{"t":17.0000006924242,"s":[0,0]}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":10,"s":[98.504,98.274]},{"t":17.0000006924242,"s":[636.029,585.095]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Rechteck 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":18.000000733155,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"app_start_animation_bg Konturen","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[100]},{"t":17.0000006924242,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[108,108,0],"ix":2},"a":{"a":0,"k":[108,108,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":10,"s":[100,100,100]},{"t":17.0000006924242,"s":[80,80,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,187.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129,187.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[87,187.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,187.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 4","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 5","np":2,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.628,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.628,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129.333,167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 6","np":2,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[86.666,167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 7","np":2,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 8","np":2,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,143.833],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 9","np":2,"cix":2,"bm":0,"ix":9,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129,143.833],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 10","np":2,"cix":2,"bm":0,"ix":10,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[87,143.833],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 11","np":2,"cix":2,"bm":0,"ix":11,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,143.833],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 12","np":2,"cix":2,"bm":0,"ix":12,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.628],[-6.627,0]],"o":[[0,6.628],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,123.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 13","np":2,"cix":2,"bm":0,"ix":13,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.628,0],[0,6.628],[-6.627,0]],"o":[[0,6.628],[-6.627,0],[0,-6.627],[6.628,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129.333,123.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 14","np":2,"cix":2,"bm":0,"ix":14,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.628],[-6.627,0]],"o":[[0,6.628],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[86.666,123.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 15","np":2,"cix":2,"bm":0,"ix":15,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.628],[-6.627,0]],"o":[[0,6.628],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,123.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 16","np":2,"cix":2,"bm":0,"ix":16,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,100.167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 17","np":2,"cix":2,"bm":0,"ix":17,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129,100.167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 18","np":2,"cix":2,"bm":0,"ix":18,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[87,100.167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 19","np":2,"cix":2,"bm":0,"ix":19,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,100.167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 20","np":2,"cix":2,"bm":0,"ix":20,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,79.667],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 21","np":2,"cix":2,"bm":0,"ix":21,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.628,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.628,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129.333,79.667],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 22","np":2,"cix":2,"bm":0,"ix":22,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[86.666,79.667],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 23","np":2,"cix":2,"bm":0,"ix":23,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,79.667],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 24","np":2,"cix":2,"bm":0,"ix":24,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,56.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 25","np":2,"cix":2,"bm":0,"ix":25,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129,56.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 26","np":2,"cix":2,"bm":0,"ix":26,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[87,56.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 27","np":2,"cix":2,"bm":0,"ix":27,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,36],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 28","np":2,"cix":2,"bm":0,"ix":28,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.628,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.628,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129.333,36],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 29","np":2,"cix":2,"bm":0,"ix":29,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[86.666,36],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 30","np":2,"cix":2,"bm":0,"ix":30,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[4.42,0],[0,0],[0,4.42],[0,0],[-4.421,0],[0,0],[0,-4.421],[0,0]],"o":[[0,0],[-4.421,0],[0,0],[0,-4.421],[0,0],[4.42,0],[0,0],[0,4.42]],"v":[[83.996,92],[-83.996,92],[-92,83.996],[-92,-83.996],[-83.996,-92],[83.996,-92],[92,-83.996],[92,83.996]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[108,108],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 31","np":2,"cix":2,"bm":0,"ix":31,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":18.000000733155,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/src/main/res/raw/app_start_anim_fade.json b/app/src/main/res/raw/app_start_anim_fade.json new file mode 100644 index 00000000..1b5dc720 --- /dev/null +++ b/app/src/main/res/raw/app_start_anim_fade.json @@ -0,0 +1 @@ +{"v":"5.5.10","fr":29.9700012207031,"ip":0,"op":18.000000733155,"w":216,"h":216,"nm":"app_start_animation_fg","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Formebene 1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":17.0000006924242,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[66.653,96.216,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[216,216],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rechteckpfad: 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[41.347,11.784],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Rechteck 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":18.000000733155,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"app_start_animation_fg Konturen","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":0,"k":44,"ix":3},"y":{"a":0,"k":41.5,"ix":4}},"a":{"a":0,"k":[44,41.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,56.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,36],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":18.000000733155,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"app_start_animation_bg Konturen","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[108,108,0],"ix":2},"a":{"a":0,"k":[108,108,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,187.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129,187.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[87,187.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,187.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 4","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 5","np":2,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.628,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.628,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129.333,167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 6","np":2,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[86.666,167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 7","np":2,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 8","np":2,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,143.833],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 9","np":2,"cix":2,"bm":0,"ix":9,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129,143.833],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 10","np":2,"cix":2,"bm":0,"ix":10,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[87,143.833],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 11","np":2,"cix":2,"bm":0,"ix":11,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,143.833],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 12","np":2,"cix":2,"bm":0,"ix":12,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.628],[-6.627,0]],"o":[[0,6.628],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,123.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 13","np":2,"cix":2,"bm":0,"ix":13,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.628,0],[0,6.628],[-6.627,0]],"o":[[0,6.628],[-6.627,0],[0,-6.627],[6.628,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129.333,123.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 14","np":2,"cix":2,"bm":0,"ix":14,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.628],[-6.627,0]],"o":[[0,6.628],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[86.666,123.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 15","np":2,"cix":2,"bm":0,"ix":15,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.628],[-6.627,0]],"o":[[0,6.628],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,123.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 16","np":2,"cix":2,"bm":0,"ix":16,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,100.167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 17","np":2,"cix":2,"bm":0,"ix":17,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129,100.167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 18","np":2,"cix":2,"bm":0,"ix":18,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[87,100.167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 19","np":2,"cix":2,"bm":0,"ix":19,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,100.167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 20","np":2,"cix":2,"bm":0,"ix":20,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,79.667],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 21","np":2,"cix":2,"bm":0,"ix":21,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.628,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.628,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129.333,79.667],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 22","np":2,"cix":2,"bm":0,"ix":22,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[86.666,79.667],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 23","np":2,"cix":2,"bm":0,"ix":23,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,79.667],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 24","np":2,"cix":2,"bm":0,"ix":24,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,56.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 25","np":2,"cix":2,"bm":0,"ix":25,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129,56.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 26","np":2,"cix":2,"bm":0,"ix":26,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[87,56.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 27","np":2,"cix":2,"bm":0,"ix":27,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,36],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 28","np":2,"cix":2,"bm":0,"ix":28,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.628,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.628,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129.333,36],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 29","np":2,"cix":2,"bm":0,"ix":29,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[86.666,36],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 30","np":2,"cix":2,"bm":0,"ix":30,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[4.42,0],[0,0],[0,4.42],[0,0],[-4.421,0],[0,0],[0,-4.421],[0,0]],"o":[[0,0],[-4.421,0],[0,0],[0,-4.421],[0,0],[4.42,0],[0,0],[0,4.42]],"v":[[83.996,92],[-83.996,92],[-92,83.996],[-92,-83.996],[-83.996,-92],[83.996,-92],[92,-83.996],[92,83.996]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[108,108],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 31","np":2,"cix":2,"bm":0,"ix":31,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":18.000000733155,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/src/main/res/raw/app_start_anim_m.json b/app/src/main/res/raw/app_start_anim_m.json new file mode 100644 index 00000000..6de84a97 --- /dev/null +++ b/app/src/main/res/raw/app_start_anim_m.json @@ -0,0 +1 @@ +{"v":"5.5.10","fr":29.9700012207031,"ip":0,"op":18.000000733155,"w":216,"h":216,"nm":"app_start_animation_fg","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Formebene 1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":9,"s":[0]},{"t":10.0000004073083,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[108,108,0],"to":[10.333,12,0],"ti":[-108.333,-121.667,0]},{"t":17.0000006924242,"s":[758,838,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":10,"s":[100,100,100]},{"t":17.0000006924242,"s":[1000,1000,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[23.99,23.99],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rechteckpfad: 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-64.055,-71.999],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Rechteck 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":18.000000733155,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"app_start_animation_fg Konturen","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":0,"k":44,"ix":3},"y":{"a":0,"k":41.5,"ix":4}},"a":{"a":0,"k":[44,41.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,56.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,36],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":18.000000733155,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"app_start_animation_bg Konturen","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[108,108,0],"ix":2},"a":{"a":0,"k":[108,108,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,187.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129,187.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[87,187.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,187.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 4","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 5","np":2,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.628,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.628,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129.333,167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 6","np":2,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[86.666,167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 7","np":2,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 8","np":2,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,143.833],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 9","np":2,"cix":2,"bm":0,"ix":9,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129,143.833],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 10","np":2,"cix":2,"bm":0,"ix":10,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[87,143.833],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 11","np":2,"cix":2,"bm":0,"ix":11,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,143.833],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 12","np":2,"cix":2,"bm":0,"ix":12,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.628],[-6.627,0]],"o":[[0,6.628],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,123.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 13","np":2,"cix":2,"bm":0,"ix":13,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.628,0],[0,6.628],[-6.627,0]],"o":[[0,6.628],[-6.627,0],[0,-6.627],[6.628,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129.333,123.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 14","np":2,"cix":2,"bm":0,"ix":14,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.628],[-6.627,0]],"o":[[0,6.628],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[86.666,123.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 15","np":2,"cix":2,"bm":0,"ix":15,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.628],[-6.627,0]],"o":[[0,6.628],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,123.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 16","np":2,"cix":2,"bm":0,"ix":16,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,100.167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 17","np":2,"cix":2,"bm":0,"ix":17,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129,100.167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 18","np":2,"cix":2,"bm":0,"ix":18,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[87,100.167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 19","np":2,"cix":2,"bm":0,"ix":19,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,100.167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 20","np":2,"cix":2,"bm":0,"ix":20,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,79.667],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 21","np":2,"cix":2,"bm":0,"ix":21,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.628,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.628,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129.333,79.667],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 22","np":2,"cix":2,"bm":0,"ix":22,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[86.666,79.667],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 23","np":2,"cix":2,"bm":0,"ix":23,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,79.667],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 24","np":2,"cix":2,"bm":0,"ix":24,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,56.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 25","np":2,"cix":2,"bm":0,"ix":25,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129,56.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 26","np":2,"cix":2,"bm":0,"ix":26,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[87,56.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 27","np":2,"cix":2,"bm":0,"ix":27,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,36],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 28","np":2,"cix":2,"bm":0,"ix":28,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.628,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.628,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129.333,36],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 29","np":2,"cix":2,"bm":0,"ix":29,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[86.666,36],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 30","np":2,"cix":2,"bm":0,"ix":30,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[4.42,0],[0,0],[0,4.42],[0,0],[-4.421,0],[0,0],[0,-4.421],[0,0]],"o":[[0,0],[-4.421,0],[0,0],[0,-4.421],[0,0],[4.42,0],[0,0],[0,4.42]],"v":[[83.996,92],[-83.996,92],[-92,83.996],[-92,-83.996],[-83.996,-92],[83.996,-92],[92,-83.996],[92,83.996]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[108,108],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 31","np":2,"cix":2,"bm":0,"ix":31,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":18.000000733155,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/src/main/res/raw/app_start_anim_slide_bottom.json b/app/src/main/res/raw/app_start_anim_slide_bottom.json new file mode 100644 index 00000000..3c7aa1d6 --- /dev/null +++ b/app/src/main/res/raw/app_start_anim_slide_bottom.json @@ -0,0 +1 @@ +{"v":"5.5.10","fr":29.9700012207031,"ip":0,"op":18.000000733155,"w":216,"h":216,"nm":"app_start_animation_fg","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Formebene 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[66.653,315.216,0],"to":[0,-36.5,0],"ti":[0,36.5,0]},{"t":17.0000006924242,"s":[66.653,96.216,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[216,216],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rechteckpfad: 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[41.347,11.784],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Rechteck 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":18.000000733155,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"app_start_animation Konturen","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[100]},{"t":17.0000006924242,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[108,108,0],"to":[0,-3.167,0],"ti":[0,3.167,0]},{"t":17.0000006924242,"s":[108,89,0]}],"ix":2},"a":{"a":0,"k":[108,108,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,187.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129,187.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[87,187.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,187.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 4","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 5","np":2,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.628,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.628,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129.333,167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 6","np":2,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[86.666,167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 7","np":2,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 8","np":2,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,143.833],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 9","np":2,"cix":2,"bm":0,"ix":9,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129,143.833],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 10","np":2,"cix":2,"bm":0,"ix":10,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[87,143.833],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 11","np":2,"cix":2,"bm":0,"ix":11,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,143.833],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 12","np":2,"cix":2,"bm":0,"ix":12,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.628],[-6.627,0]],"o":[[0,6.628],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,123.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 13","np":2,"cix":2,"bm":0,"ix":13,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.628,0],[0,6.628],[-6.627,0]],"o":[[0,6.628],[-6.627,0],[0,-6.627],[6.628,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129.333,123.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 14","np":2,"cix":2,"bm":0,"ix":14,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.628],[-6.627,0]],"o":[[0,6.628],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[86.666,123.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 15","np":2,"cix":2,"bm":0,"ix":15,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.628],[-6.627,0]],"o":[[0,6.628],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,123.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 16","np":2,"cix":2,"bm":0,"ix":16,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,100.167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 17","np":2,"cix":2,"bm":0,"ix":17,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129,100.167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 18","np":2,"cix":2,"bm":0,"ix":18,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[87,100.167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 19","np":2,"cix":2,"bm":0,"ix":19,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,100.167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 20","np":2,"cix":2,"bm":0,"ix":20,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,79.667],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 21","np":2,"cix":2,"bm":0,"ix":21,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.628,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.628,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129.333,79.667],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 22","np":2,"cix":2,"bm":0,"ix":22,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[86.666,79.667],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 23","np":2,"cix":2,"bm":0,"ix":23,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,79.667],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 24","np":2,"cix":2,"bm":0,"ix":24,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,56.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 25","np":2,"cix":2,"bm":0,"ix":25,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129,56.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 26","np":2,"cix":2,"bm":0,"ix":26,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[87,56.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 27","np":2,"cix":2,"bm":0,"ix":27,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,36],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 28","np":2,"cix":2,"bm":0,"ix":28,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.628,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.628,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129.333,36],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 29","np":2,"cix":2,"bm":0,"ix":29,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[86.666,36],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 30","np":2,"cix":2,"bm":0,"ix":30,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,56.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 31","np":2,"cix":2,"bm":0,"ix":31,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,36],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 32","np":2,"cix":2,"bm":0,"ix":32,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[4.42,0],[0,0],[0,4.42],[0,0],[-4.421,0],[0,0],[0,-4.421],[0,0]],"o":[[0,0],[-4.421,0],[0,0],[0,-4.421],[0,0],[4.42,0],[0,0],[0,4.42]],"v":[[83.996,92],[-83.996,92],[-92,83.996],[-92,-83.996],[-83.996,-92],[83.996,-92],[92,-83.996],[92,83.996]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[108,108],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 33","np":2,"cix":2,"bm":0,"ix":33,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":18.000000733155,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/src/main/res/raw/app_start_anim_splash1.json b/app/src/main/res/raw/app_start_anim_splash1.json new file mode 100644 index 00000000..7e462696 --- /dev/null +++ b/app/src/main/res/raw/app_start_anim_splash1.json @@ -0,0 +1 @@ +{"v":"5.5.10","fr":29.9700012207031,"ip":0,"op":18.000000733155,"w":216,"h":216,"nm":"app_start_animation_fg","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"app_start_animation_fg Konturen","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[100]},{"t":17.0000006924242,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.829],"y":[0.456]},"o":{"x":[0.194],"y":[0.028]},"t":0,"s":[44]},{"t":7.00000028511585,"s":[108]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.03],"y":[0.233]},"t":0,"s":[41.5]},{"t":7.00000028511585,"s":[108]}],"ix":4}},"a":{"a":0,"k":[44,41.5,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":7,"s":[200,200,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":10,"s":[200,200,100]},{"t":17.0000006924242,"s":[2000,2000,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,56.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,36],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":18.000000733155,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Formebene 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.93],"y":[0.475]},"o":{"x":[0.111],"y":[0.03]},"t":0,"s":[42.158]},{"t":7.00000028511585,"s":[89.576]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.684],"y":[0.983]},"o":{"x":[0.043],"y":[0.297]},"t":0,"s":[34.158]},{"t":7.00000028511585,"s":[89.576]}],"ix":4}},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[0,0,100]},{"t":7.00000028511585,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[254.848,254.848],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Elliptischer Pfad 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[18.424,18.424],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[123.368,123.368],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":18.000000733155,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"app_start_animation_bg Konturen","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":7.00000028511585,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[108,108,0],"ix":2},"a":{"a":0,"k":[108,108,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[100,100,100]},{"t":7.00000028511585,"s":[80,80,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,187.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129,187.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[87,187.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,187.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 4","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 5","np":2,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.628,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.628,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129.333,167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 6","np":2,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[86.666,167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 7","np":2,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 8","np":2,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,143.833],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 9","np":2,"cix":2,"bm":0,"ix":9,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129,143.833],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 10","np":2,"cix":2,"bm":0,"ix":10,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[87,143.833],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 11","np":2,"cix":2,"bm":0,"ix":11,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,143.833],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 12","np":2,"cix":2,"bm":0,"ix":12,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.628],[-6.627,0]],"o":[[0,6.628],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,123.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 13","np":2,"cix":2,"bm":0,"ix":13,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.628,0],[0,6.628],[-6.627,0]],"o":[[0,6.628],[-6.627,0],[0,-6.627],[6.628,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129.333,123.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 14","np":2,"cix":2,"bm":0,"ix":14,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.628],[-6.627,0]],"o":[[0,6.628],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[86.666,123.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 15","np":2,"cix":2,"bm":0,"ix":15,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.628],[-6.627,0]],"o":[[0,6.628],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,123.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 16","np":2,"cix":2,"bm":0,"ix":16,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,100.167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 17","np":2,"cix":2,"bm":0,"ix":17,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129,100.167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 18","np":2,"cix":2,"bm":0,"ix":18,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[87,100.167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 19","np":2,"cix":2,"bm":0,"ix":19,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,100.167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 20","np":2,"cix":2,"bm":0,"ix":20,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,79.667],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 21","np":2,"cix":2,"bm":0,"ix":21,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.628,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.628,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129.333,79.667],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 22","np":2,"cix":2,"bm":0,"ix":22,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[86.666,79.667],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 23","np":2,"cix":2,"bm":0,"ix":23,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,79.667],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 24","np":2,"cix":2,"bm":0,"ix":24,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,56.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 25","np":2,"cix":2,"bm":0,"ix":25,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129,56.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 26","np":2,"cix":2,"bm":0,"ix":26,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[87,56.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 27","np":2,"cix":2,"bm":0,"ix":27,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,36],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 28","np":2,"cix":2,"bm":0,"ix":28,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.628,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.628,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129.333,36],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 29","np":2,"cix":2,"bm":0,"ix":29,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[86.666,36],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 30","np":2,"cix":2,"bm":0,"ix":30,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[4.42,0],[0,0],[0,4.42],[0,0],[-4.421,0],[0,0],[0,-4.421],[0,0]],"o":[[0,0],[-4.421,0],[0,0],[0,-4.421],[0,0],[4.42,0],[0,0],[0,4.42]],"v":[[83.996,92],[-83.996,92],[-92,83.996],[-92,-83.996],[-83.996,-92],[83.996,-92],[92,-83.996],[92,83.996]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[108,108],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 31","np":2,"cix":2,"bm":0,"ix":31,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":18.000000733155,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/src/main/res/raw/app_start_anim_splash2.json b/app/src/main/res/raw/app_start_anim_splash2.json new file mode 100644 index 00000000..d9a86c57 --- /dev/null +++ b/app/src/main/res/raw/app_start_anim_splash2.json @@ -0,0 +1 @@ +{"v":"5.5.10","fr":29.9700012207031,"ip":0,"op":18.000000733155,"w":216,"h":216,"nm":"app_start_animation_fg","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"app_start_animation_fg Konturen","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[100]},{"t":17.0000006924242,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.829],"y":[0.456]},"o":{"x":[0.194],"y":[0.028]},"t":0,"s":[44]},{"t":7.00000028511585,"s":[108]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.03],"y":[0.233]},"t":0,"s":[41.5]},{"t":7.00000028511585,"s":[108]}],"ix":4}},"a":{"a":0,"k":[44,41.5,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":7,"s":[200,200,100]},{"t":10.0000004073083,"s":[200,200,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,56.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,36],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":18.000000733155,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Formebene 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.93],"y":[0.475]},"o":{"x":[0.111],"y":[0.03]},"t":0,"s":[42.158]},{"t":7.00000028511585,"s":[89.576]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.684],"y":[0.983]},"o":{"x":[0.043],"y":[0.297]},"t":0,"s":[34.158]},{"t":7.00000028511585,"s":[89.576]}],"ix":4}},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[0,0,100]},{"t":7.00000028511585,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[254.848,254.848],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Elliptischer Pfad 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[18.424,18.424],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[123.368,123.368],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":18.000000733155,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"app_start_animation_bg Konturen","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":7.00000028511585,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[108,108,0],"ix":2},"a":{"a":0,"k":[108,108,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[100,100,100]},{"t":7.00000028511585,"s":[80,80,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,187.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129,187.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[87,187.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,187.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 4","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 5","np":2,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.628,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.628,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129.333,167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 6","np":2,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[86.666,167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 7","np":2,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 8","np":2,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,143.833],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 9","np":2,"cix":2,"bm":0,"ix":9,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129,143.833],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 10","np":2,"cix":2,"bm":0,"ix":10,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[87,143.833],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 11","np":2,"cix":2,"bm":0,"ix":11,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,143.833],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 12","np":2,"cix":2,"bm":0,"ix":12,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.628],[-6.627,0]],"o":[[0,6.628],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,123.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 13","np":2,"cix":2,"bm":0,"ix":13,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.628,0],[0,6.628],[-6.627,0]],"o":[[0,6.628],[-6.627,0],[0,-6.627],[6.628,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129.333,123.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 14","np":2,"cix":2,"bm":0,"ix":14,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.628],[-6.627,0]],"o":[[0,6.628],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[86.666,123.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 15","np":2,"cix":2,"bm":0,"ix":15,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.628],[-6.627,0]],"o":[[0,6.628],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,123.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 16","np":2,"cix":2,"bm":0,"ix":16,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,100.167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 17","np":2,"cix":2,"bm":0,"ix":17,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129,100.167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 18","np":2,"cix":2,"bm":0,"ix":18,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[87,100.167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 19","np":2,"cix":2,"bm":0,"ix":19,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,100.167],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 20","np":2,"cix":2,"bm":0,"ix":20,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,79.667],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 21","np":2,"cix":2,"bm":0,"ix":21,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.628,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.628,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129.333,79.667],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 22","np":2,"cix":2,"bm":0,"ix":22,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[86.666,79.667],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 23","np":2,"cix":2,"bm":0,"ix":23,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[44,79.667],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 24","np":2,"cix":2,"bm":0,"ix":24,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,56.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 25","np":2,"cix":2,"bm":0,"ix":25,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129,56.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 26","np":2,"cix":2,"bm":0,"ix":26,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[16,2.5],[-16,2.5],[-16,-2.5],[16,-2.5]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2,0.2,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[87,56.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 27","np":2,"cix":2,"bm":0,"ix":27,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[172,36],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 28","np":2,"cix":2,"bm":0,"ix":28,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.628,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.628,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[129.333,36],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 29","np":2,"cix":2,"bm":0,"ix":29,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-6.627],[6.627,0],[0,6.627],[-6.627,0]],"o":[[0,6.627],[-6.627,0],[0,-6.627],[6.627,0]],"v":[[12,0],[0,12],[-12,0],[0,-12]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.118000000598,0.532999973671,0.898000021542,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[86.666,36],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 30","np":2,"cix":2,"bm":0,"ix":30,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[4.42,0],[0,0],[0,4.42],[0,0],[-4.421,0],[0,0],[0,-4.421],[0,0]],"o":[[0,0],[-4.421,0],[0,0],[0,-4.421],[0,0],[4.42,0],[0,0],[0,4.42]],"v":[[83.996,92],[-83.996,92],[-92,83.996],[-92,-83.996],[-83.996,-92],[83.996,-92],[92,-83.996],[92,83.996]],"c":true},"ix":2},"nm":"Pfad 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fläche 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[108,108],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformieren"}],"nm":"Gruppe 31","np":2,"cix":2,"bm":0,"ix":31,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":18.000000733155,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/src/main/res/raw/license_apache_2.txt b/app/src/main/res/raw/license_apache_2.txt new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/app/src/main/res/raw/license_apache_2.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/app/src/main/res/raw/license_bsd_2clause.txt b/app/src/main/res/raw/license_bsd_2clause.txt new file mode 100644 index 00000000..a5f54eb3 --- /dev/null +++ b/app/src/main/res/raw/license_bsd_2clause.txt @@ -0,0 +1,7 @@ +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/app/src/main/res/raw/license_bsd_3clause.txt b/app/src/main/res/raw/license_bsd_3clause.txt new file mode 100644 index 00000000..57326dd0 --- /dev/null +++ b/app/src/main/res/raw/license_bsd_3clause.txt @@ -0,0 +1,9 @@ +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/app/src/main/res/raw/license_glide.txt b/app/src/main/res/raw/license_glide.txt new file mode 100644 index 00000000..441c3743 --- /dev/null +++ b/app/src/main/res/raw/license_glide.txt @@ -0,0 +1,94 @@ +License for everything not in third_party and not otherwise marked: + +Copyright 2014 Google, Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are +permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of + conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list + of conditions and the following disclaimer in the documentation and/or other materials + provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY GOOGLE, INC. ``AS IS'' AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GOOGLE, INC. OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are those of the +authors and should not be interpreted as representing official policies, either expressed +or implied, of Google, Inc. +--------------------------------------------------------------------------------------------- +License for third_party/disklrucache: + +Copyright 2012 Jake Wharton +Copyright 2011 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. +--------------------------------------------------------------------------------------------- +License for third_party/gif_decoder: + +Copyright (c) 2013 Xcellent Creations, Inc. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +--------------------------------------------------------------------------------------------- +License for third_party/gif_encoder/AnimatedGifEncoder.java and +third_party/gif_encoder/LZWEncoder.java: + +No copyright asserted on the source code of this class. May be used for any +purpose, however, refer to the Unisys LZW patent for restrictions on use of +the associated LZWEncoder class. Please forward any corrections to +kweiner@fmsware.com. + +----------------------------------------------------------------------------- +License for third_party/gif_encoder/NeuQuant.java + +Copyright (c) 1994 Anthony Dekker + +NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994. See +"Kohonen neural networks for optimal colour quantization" in "Network: +Computation in Neural Systems" Vol. 5 (1994) pp 351-367. for a discussion of +the algorithm. + +Any party obtaining a copy of these files from the author, directly or +indirectly, is granted, free of charge, a full and unrestricted irrevocable, +world-wide, paid up, royalty-free, nonexclusive right and license to deal in +this software and documentation files (the "Software"), including without +limitation the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to permit persons who +receive copies from any such party to do so, with the only requirement being +that this copyright notice remain intact. \ No newline at end of file diff --git a/app/src/main/res/raw/license_gpl_3.txt b/app/src/main/res/raw/license_gpl_3.txt new file mode 100644 index 00000000..f288702d --- /dev/null +++ b/app/src/main/res/raw/license_gpl_3.txt @@ -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/src/main/res/raw/license_mit.txt b/app/src/main/res/raw/license_mit.txt new file mode 100644 index 00000000..7215f695 --- /dev/null +++ b/app/src/main/res/raw/license_mit.txt @@ -0,0 +1,17 @@ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml new file mode 100644 index 00000000..892bd7f6 --- /dev/null +++ b/app/src/main/res/values/arrays.xml @@ -0,0 +1,107 @@ + + + + 2 + 0 + 1 + 3 + + + @string/preference_theme_system + @string/preference_theme_light + @string/preference_theme_dark + @string/preference_theme_auto + + + @string/preference_icon_shape_circle + @string/preference_icon_shape_squircle + @string/preference_icon_shape_rounded_square + @string/preference_icon_shape_square + @string/preference_icon_shape_hexagon + @string/preference_icon_shape_triangle + + + 0 + 3 + 1 + 2 + 4 + 5 + + + + @string/preference_searchbar_behavior_fade_alpha + @string/preference_searchbar_behavior_always_visible + @string/preference_searchbar_behavior_hide + + + 0 + 1 + 2 + + + @string/websites_protocol_http + @string/websites_protocol_https + + + 0 + 1 + + + + @string/preference_legacy_icon_bg_none + @string/preference_legacy_icon_bg_color + @string/preference_legacy_icon_bg_white + + + 0 + 1 + 2 + + + + @string/provider_metno + @string/provider_openweathermap + @string/provider_here + + + 2 + 0 + 3 + + + 3 + 5 + 8 + 10 + + + + @string/preference_card_background_default + @string/preference_card_background_black + + + 0 + 2 + + + + @color/red + @color/pink + @color/purple + @color/deeppurple + @color/indigo + @color/blue + @color/lightblue + @color/cyan + @color/teal + @color/green + @color/lightgreen + @color/yellow + @color/amber + @color/orange + @color/deeporange + @color/brown + @color/bluegrey + + \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml new file mode 100644 index 00000000..c8690148 --- /dev/null +++ b/app/src/main/res/values/attrs.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/src/main/res/values/bools.xml b/app/src/main/res/values/bools.xml new file mode 100644 index 00000000..7d18846e --- /dev/null +++ b/app/src/main/res/values/bools.xml @@ -0,0 +1,5 @@ + + + false + false + \ No newline at end of file diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml new file mode 100644 index 00000000..55344e51 --- /dev/null +++ b/app/src/main/res/values/donottranslate.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/values/integers.xml b/app/src/main/res/values/integers.xml new file mode 100644 index 00000000..a9108c88 --- /dev/null +++ b/app/src/main/res/values/integers.xml @@ -0,0 +1,5 @@ + + + 4 + 1 + \ No newline at end of file diff --git a/app/src/main/res/values/licenses.xml b/app/src/main/res/values/licenses.xml new file mode 100644 index 00000000..2ef2eb4c --- /dev/null +++ b/app/src/main/res/values/licenses.xml @@ -0,0 +1,313 @@ + + + + @string/app_name + @string/preference_about_license + @null + @string/gpl3_name + Copyright (c) 2021 MM20 + @raw/license_gpl_3 + https://github.com/MM2-0/Kvaesitso + + + + Kotlin Standard Library + Kotlin Standard Library + + https://kotlinlang.org/assets/images/apple-touch-icon-144x144.png + + @string/apache_license_name + @null + @raw/license_apache_2 + https://kotlinlang.org/ + + + + Android Jetpack + A collection of Android software components to make it easier to develop great Android + apps. + + + https://2.bp.blogspot.com/-2ZMkSo7CnUs/WvMvSK0u9RI/AAAAAAAAFZA/zJOCZ8LUM8ol3hcHYHwVyOpc3iiYaxquACLcBGAs/s1600/Jetpack_logo.png + + @string/apache_license_name + @null + @raw/license_apache_2 + https://developer.android.com/topic/libraries/support-library/index.html + + + + Accompanist + Accompanist is a group of libraries that aim to supplement Jetpack Compose with + features that are commonly required by developers but not yet available. + + https://raw.githubusercontent.com/google/accompanist/main/docs/header.png + @string/apache_license_name + Copyright 2020 The Android Open Source Project + @raw/license_apache_2 + https://google.github.io/accompanist/ + + + + Material Components for Android + + Material Components for Android (MDC-Android) help developers execute Material Design. + + @null + @string/apache_license_name + @null + @raw/license_apache_2 + https://material.io/develop/android/ + + + + Lottie + Lottie is a library for Android, iOS, Web, and Windows that parses Adobe After Effects + animations exported as json with Bodymovin and renders them natively on mobile and on + the web. + + https://airbnb.io/lottie/images/logo.webp + @string/apache_license_name + @null + @raw/license_apache_2 + https://airbnb.io/lottie/#/ + + + + OkHttp + + @null + @string/apache_license_name + Copyright 2019 Square, Inc. + @raw/license_apache_2 + https://square.github.io/okhttp/ + + + + Retrofit + A type-safe HTTP client for Android and Java + @null + @string/apache_license_name + Copyright 2013 Square, Inc. + @raw/license_apache_2 + https://square.github.io/retrofit/ + + + + Gson + Gson is a Java library that can be used to convert Java Objects into their JSON representation. + @null + @string/apache_license_name + Copyright 2008 Google Inc. + @raw/license_apache_2 + https://github.com/google/gson + + + + commons-suncalc + A Java library for calculation of sun and moon positions and phases. + @null + @string/apache_license_name + @null + @raw/license_apache_2 + https://github.com/shred/commons-suncalc + + + + Jsoup + A Java library for working with real-world HTML + @null + @string/mit_license_name + ]]> + @raw/license_mit + https://jsoup.org/ + + + + TextDrawable + A light-weight library providing images with letter/text like the Gmail app + https://github.com/amulyakhare/TextDrawable/raw/master/screens/screen6.png + @string/mit_license_name + Copyright (c) 2014 Amulya Khare + @raw/license_mit + https://github.com/amulyakhare/TextDrawable + + + + Glide + A fast and efficient open source media management and image loading framework for + Android + + https://raw.githubusercontent.com/bumptech/glide/master/static/glide_logo.png + @string/glide_license_name + @null + @raw/license_glide + https://bumptech.github.io/glide/ + + + + Glide Transformations + An Android transformation library providing a variety of image transformations for + Glide + + @null + @string/apache_license_name + Copyright (C) 2020 Wasabeef + @raw/license_apache_2 + https://github.com/wasabeef/glide-transformations + + + + Material Dialogs + Easy to use material design dialogs + + https://raw.githubusercontent.com/afollestad/material-dialogs/master/art/showcase20.jpg + + @string/apache_license_name + @null + @raw/license_apache_2 + https://github.com/afollestad/material-dialogs + + + + Groupie + Groupie is a simple, flexible library for complex RecyclerView layouts. + @null + @string/mit_license_name + @null + @raw/license_mit + https://github.com/lisawray/groupie + + + + DragLinearLayout + An Android LinearLayout that supports draggable and swappable child Views + @null + @string/mit_license_name + Copyright (c) 2014 Justas Medeisis + @raw/license_mit + https://github.com/justasm/DragLinearLayout + + + + ViewPropertyValueAnimator + Wrapper of the ObjectAnimator that can be used similarly to ViewPropertyAnimator. + + @null + @string/apache_license_name + Copyright 2015 Bartosz Lipiński + @raw/license_apache_2 + https://github.com/blipinsk/ViewPropertyObjectAnimator + + + + Nextcloud SingleSignOn + This library allows you to use accounts as well as the network stack provided by the + nextcloud files app. + + @null + @string/gpl3_name + @null + @raw/license_gpl_3 + https://github.com/nextcloud/Android-SingleSignOn + + + + mXparser + A super easy, rich, fast and highly flexible math expression parser library + http://mathparser.org/wp-content/uploads/2017/07/mxparser-logo.png + @string/bsd_2clause_name + Copyright 2010 - 2020 Mariusz Gromada. All rights reserved. + @raw/license_bsd_2clause + https://mathparser.org/ + + + + Google Auth Library + Open source authentication client library for Java. + @null + @string/bsd_3clause_name + + Copyright 2014, Google Inc. All rights reserved. + @raw/license_bsd_3clause + https://github.com/googleapis/google-auth-library-java + + + + Google APIs Client Library for Android + The Google APIs Client Library for Java is a flexible, efficient, and powerful Java client library for accessing any HTTP-based API on the web, not just Google APIs. + @null + @string/apache_license_name + @null + @raw/license_apache_2 + https://github.com/googleapis/google-api-java-client + + + + Microsoft Graph Android SDK + Deprecated API client for Microsoft Graph APIs + @null + @string/mit_license_name + Copyright (c) 2015 Microsoft Corporation + @raw/license_mit + https://github.com/microsoftgraph/msgraph-sdk-android + + + + Microsoft Authentication Library (MSAL) for Android + The MSAL library for Android gives your app the ability to use the Microsoft Cloud by + supporting Microsoft Azure Active Directory and Microsoft accounts in a converged + experience using industry standard OAuth2 and OpenID Connect. The library also supports + Azure AD B2C. + + @null + @string/mit_license_name + Copyright (c) Microsoft Corporation + @raw/license_mit + https://github.com/AzureAD/microsoft-authentication-library-for-android + + + + CrashReporter + CrashReporter is a handy tool to capture app crashes and save them in a file. + + https://github.com/balsikandar/CrashReporter/raw/master/assets/crash_reporter_banner.png + + @string/apache_license_name + " + Copyright (C) 2016 Bal Sikandar + Copyright (C) 2011 Android Open Source Project + " + + @raw/license_apache_2 + https://github.com/MindorksOpenSource/CrashReporter + + + + Material Design Icons + Beautifully crafted, delightful, and easy to use in your web, Android, and iOS + projects + + https://material.io/tools/icons/static/ic_icons_192px_light.svg + @string/apache_license_name + @null + @raw/license_apache_2 + https://material.io/icons/ + + + MIT license + Simplified BSD license + Modified BSD license + Apache license 2.0 + Glide license + GNU General Public License Version 3 + + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..30f3f933 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/xml/motion_calendar_event_view.xml b/app/src/main/res/xml/motion_calendar_event_view.xml new file mode 100644 index 00000000..d7d33d58 --- /dev/null +++ b/app/src/main/res/xml/motion_calendar_event_view.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/preferences_about.xml b/app/src/main/res/xml/preferences_about.xml new file mode 100644 index 00000000..96dace9f --- /dev/null +++ b/app/src/main/res/xml/preferences_about.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/preferences_appearance.xml b/app/src/main/res/xml/preferences_appearance.xml new file mode 100644 index 00000000..74c114fd --- /dev/null +++ b/app/src/main/res/xml/preferences_appearance.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/preferences_badges.xml b/app/src/main/res/xml/preferences_badges.xml new file mode 100644 index 00000000..8ad0dd71 --- /dev/null +++ b/app/src/main/res/xml/preferences_badges.xml @@ -0,0 +1,23 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/preferences_calendar.xml b/app/src/main/res/xml/preferences_calendar.xml new file mode 100644 index 00000000..5e2fd87b --- /dev/null +++ b/app/src/main/res/xml/preferences_calendar.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/preferences_cards.xml b/app/src/main/res/xml/preferences_cards.xml new file mode 100644 index 00000000..10d29aeb --- /dev/null +++ b/app/src/main/res/xml/preferences_cards.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/preferences_main.xml b/app/src/main/res/xml/preferences_main.xml new file mode 100644 index 00000000..7b1c88a2 --- /dev/null +++ b/app/src/main/res/xml/preferences_main.xml @@ -0,0 +1,38 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/preferences_search.xml b/app/src/main/res/xml/preferences_search.xml new file mode 100644 index 00000000..98bc15d2 --- /dev/null +++ b/app/src/main/res/xml/preferences_search.xml @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/preferences_services.xml b/app/src/main/res/xml/preferences_services.xml new file mode 100644 index 00000000..b05f95cf --- /dev/null +++ b/app/src/main/res/xml/preferences_services.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/preferences_weather.xml b/app/src/main/res/xml/preferences_weather.xml new file mode 100644 index 00000000..f9853bea --- /dev/null +++ b/app/src/main/res/xml/preferences_weather.xml @@ -0,0 +1,28 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml new file mode 100644 index 00000000..54f8c691 --- /dev/null +++ b/app/src/main/res/xml/provider_paths.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/app/src/release/ic_launcher-playstore.png b/app/src/release/ic_launcher-playstore.png new file mode 100644 index 00000000..d826b6dc Binary files /dev/null and b/app/src/release/ic_launcher-playstore.png differ diff --git a/app/src/release/java/de/mm20/launcher2/debug/Debug.kt b/app/src/release/java/de/mm20/launcher2/debug/Debug.kt new file mode 100644 index 00000000..018f9d54 --- /dev/null +++ b/app/src/release/java/de/mm20/launcher2/debug/Debug.kt @@ -0,0 +1,14 @@ +package de.mm20.launcher2.debug + +import android.content.Context +import android.content.res.Resources +import android.os.StrictMode +import android.util.Log +import de.mm20.launcher2.R + +// This class does nothing in release builds +class Debug() { + companion object { + const val DEBUG_MODE = false + } +} diff --git a/app/src/release/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/release/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..4ae7d123 --- /dev/null +++ b/app/src/release/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/release/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/release/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..4ae7d123 --- /dev/null +++ b/app/src/release/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/release/res/mipmap-hdpi/ic_launcher.png b/app/src/release/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..d174951b Binary files /dev/null and b/app/src/release/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/release/res/mipmap-hdpi/ic_launcher_background.png b/app/src/release/res/mipmap-hdpi/ic_launcher_background.png new file mode 100644 index 00000000..e6897753 Binary files /dev/null and b/app/src/release/res/mipmap-hdpi/ic_launcher_background.png differ diff --git a/app/src/release/res/mipmap-hdpi/ic_launcher_foreground.png b/app/src/release/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..39690600 Binary files /dev/null and b/app/src/release/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/app/src/release/res/mipmap-hdpi/ic_launcher_round.png b/app/src/release/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 00000000..b0a40711 Binary files /dev/null and b/app/src/release/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/release/res/mipmap-mdpi/ic_launcher.png b/app/src/release/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..073f4ff4 Binary files /dev/null and b/app/src/release/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/release/res/mipmap-mdpi/ic_launcher_background.png b/app/src/release/res/mipmap-mdpi/ic_launcher_background.png new file mode 100644 index 00000000..72a5d9c0 Binary files /dev/null and b/app/src/release/res/mipmap-mdpi/ic_launcher_background.png differ diff --git a/app/src/release/res/mipmap-mdpi/ic_launcher_foreground.png b/app/src/release/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..2e834b54 Binary files /dev/null and b/app/src/release/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/app/src/release/res/mipmap-mdpi/ic_launcher_round.png b/app/src/release/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 00000000..5f95819c Binary files /dev/null and b/app/src/release/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/release/res/mipmap-xhdpi/ic_launcher.png b/app/src/release/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..1ef27480 Binary files /dev/null and b/app/src/release/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/release/res/mipmap-xhdpi/ic_launcher_background.png b/app/src/release/res/mipmap-xhdpi/ic_launcher_background.png new file mode 100644 index 00000000..7a56ec15 Binary files /dev/null and b/app/src/release/res/mipmap-xhdpi/ic_launcher_background.png differ diff --git a/app/src/release/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/release/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..62d4ec60 Binary files /dev/null and b/app/src/release/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/app/src/release/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/release/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 00000000..4723ce57 Binary files /dev/null and b/app/src/release/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/release/res/mipmap-xxhdpi/ic_launcher.png b/app/src/release/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..5a3488d4 Binary files /dev/null and b/app/src/release/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/release/res/mipmap-xxhdpi/ic_launcher_background.png b/app/src/release/res/mipmap-xxhdpi/ic_launcher_background.png new file mode 100644 index 00000000..2fe8f6c3 Binary files /dev/null and b/app/src/release/res/mipmap-xxhdpi/ic_launcher_background.png differ diff --git a/app/src/release/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/release/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..bb9e57b0 Binary files /dev/null and b/app/src/release/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/release/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/release/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..a45b286c Binary files /dev/null and b/app/src/release/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/release/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/release/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..e823bb41 Binary files /dev/null and b/app/src/release/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/release/res/mipmap-xxxhdpi/ic_launcher_background.png b/app/src/release/res/mipmap-xxxhdpi/ic_launcher_background.png new file mode 100644 index 00000000..b597dbda Binary files /dev/null and b/app/src/release/res/mipmap-xxxhdpi/ic_launcher_background.png differ diff --git a/app/src/release/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/release/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..a528c16d Binary files /dev/null and b/app/src/release/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/release/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/release/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..5797243d Binary files /dev/null and b/app/src/release/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/applications/.gitignore b/applications/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/applications/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/applications/build.gradle.kts b/applications/build.gradle.kts new file mode 100644 index 00000000..bda45315 --- /dev/null +++ b/applications/build.gradle.kts @@ -0,0 +1,54 @@ +plugins { + id("com.android.library") + id("kotlin-android") + id("kotlin-android-extensions") +} + +android { + compileSdk = sdk.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = sdk.versions.minSdk.get().toInt() + targetSdk = sdk.versions.targetSdk.get().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } +} + +dependencies { + implementation(libs.bundles.kotlin) + implementation(libs.androidx.core) + implementation(libs.androidx.appcompat) + + implementation(libs.bundles.androidx.lifecycle) + + implementation(project(":search")) + implementation(project(":base")) + implementation(project(":icons")) + implementation(project(":preferences")) + implementation(project(":ktx")) + implementation(project(":badges")) + implementation(project(":hiddenitems")) + implementation(project(":compat")) + +} \ No newline at end of file diff --git a/applications/consumer-rules.pro b/applications/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/applications/proguard-rules.pro b/applications/proguard-rules.pro new file mode 100644 index 00000000..47e28e5f --- /dev/null +++ b/applications/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.kts.kts.kts.kts.kts.kts.kts.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/applications/src/main/AndroidManifest.xml b/applications/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a1e7322f --- /dev/null +++ b/applications/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + / + \ No newline at end of file diff --git a/applications/src/main/java/de/mm20/launcher2/applications/AppRepository.kt b/applications/src/main/java/de/mm20/launcher2/applications/AppRepository.kt new file mode 100644 index 00000000..de9fffb0 --- /dev/null +++ b/applications/src/main/java/de/mm20/launcher2/applications/AppRepository.kt @@ -0,0 +1,227 @@ +package de.mm20.launcher2.applications + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.LauncherActivityInfo +import android.content.pm.LauncherApps +import android.content.pm.PackageInstaller +import android.content.pm.ShortcutInfo +import android.os.Process +import android.os.UserHandle +import android.util.Log +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import de.mm20.launcher2.badges.Badge +import de.mm20.launcher2.badges.BadgeProvider +import de.mm20.launcher2.hiddenitems.HiddenItemsRepository +import de.mm20.launcher2.icons.IconRepository +import de.mm20.launcher2.preferences.LauncherPreferences +import de.mm20.launcher2.search.BaseSearchableRepository +import de.mm20.launcher2.search.SearchRepository +import de.mm20.launcher2.search.data.AppInstallation +import de.mm20.launcher2.search.data.Application +import de.mm20.launcher2.search.data.LauncherApp +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class AppRepository private constructor(val context: Context) : BaseSearchableRepository() { + + private val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps + + val applications = MediatorLiveData>() + + + private val installedApps = MutableLiveData>(emptyList()) + private val installations = MutableLiveData>(mutableListOf()) + private val hiddenItemKeys = HiddenItemsRepository.getInstance(context).hiddenItemsKeys + + private val installingPackages = mutableMapOf() + + private val profiles: List = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + launcherApps.profiles.takeIf { it.isNotEmpty() } ?: listOf(Process.myUserHandle()) + } else { + listOf(Process.myUserHandle()) + } + + + init { + + applications.addSource(installedApps) { + launch { updateAppsForDisplay() } + } + applications.addSource(installations) { + launch { updateAppsForDisplay() } + } + + applications.addSource(hiddenItemKeys) { + launch { updateAppsForDisplay() } + } + + launcherApps.registerCallback(object : LauncherApps.Callback() { + override fun onPackagesUnavailable(packageNames: Array, user: UserHandle, replacing: Boolean) { + installedApps.value = installedApps.value?.filter { !packageNames.contains(it.`package`) } + } + + override fun onPackageChanged(packageName: String, user: UserHandle) { + val apps = installedApps.value?.toMutableList() ?: return + apps.removeAll { packageName == it.`package` } + apps.addAll(getApplications(packageName)) + installedApps.value = apps + } + + override fun onPackagesAvailable(packageNames: Array, user: UserHandle, replacing: Boolean) { + val apps = installedApps.value?.toMutableList() ?: return + for (packageName in packageNames) { + apps.addAll(getApplications(packageName)) + } + installedApps.value = apps + } + + override fun onPackageAdded(packageName: String, user: UserHandle) { + Log.d("MM20", "App installed: $packageName") + val apps = installedApps.value?.toMutableList() ?: return + apps.addAll(getApplications(packageName)) + installedApps.value = apps + } + + override fun onPackageRemoved(packageName: String, user: UserHandle) { + installedApps.value = installedApps.value?.filter { packageName != (it.`package`) } + } + + override fun onShortcutsChanged(packageName: String, shortcuts: MutableList, user: UserHandle) { + super.onShortcutsChanged(packageName, shortcuts, user) + onPackageChanged(packageName, user) + } + + override fun onPackagesSuspended(packageNames: Array?, user: UserHandle?) { + super.onPackagesSuspended(packageNames, user) + packageNames?.forEach { + BadgeProvider.getInstance(context).setBadge("app://$it", Badge(iconRes = R.drawable.ic_badge_suspended)) + } + } + + override fun onPackagesUnsuspended(packageNames: Array?, user: UserHandle?) { + super.onPackagesUnsuspended(packageNames, user) + packageNames?.forEach { + BadgeProvider.getInstance(context).removeBadge("app://$it") + } + } + + }) + + + val packageInstaller = context.packageManager.packageInstaller + + packageInstaller.registerSessionCallback(object : PackageInstaller.SessionCallback() { + override fun onProgressChanged(sessionId: Int, progress: Float) { + val session = packageInstaller.getSessionInfo(sessionId) ?: return + val pkg = session.appPackageName ?: return + BadgeProvider.getInstance(context).updateBadge("app://$pkg", Badge(progress = progress)) + } + + override fun onActiveChanged(sessionId: Int, active: Boolean) { + if (active) onCreated(sessionId) + else onFinished(sessionId, false) + } + + override fun onFinished(sessionId: Int, success: Boolean) { + val pkg = installingPackages[sessionId] + installingPackages.remove(sessionId) + val key = "app://$pkg" + val badge = BadgeProvider.getInstance(context).getBadge(key)?.apply { progress = null } + ?: Badge() + BadgeProvider.getInstance(context).setBadge(key, badge) + val inst = installations.value ?: return + inst.removeAll { + it.session.sessionId == sessionId + } + installations.postValue(inst) + + } + + override fun onBadgingChanged(sessionId: Int) { + val inst = installations.value ?: mutableListOf() + inst.removeAll { + if (it.session.sessionId == sessionId) { + IconRepository.getInstance(context).removeIconFromCache(it) + true + } else false + } + onCreated(sessionId) + } + + override fun onCreated(sessionId: Int) { + val session = packageInstaller.getSessionInfo(sessionId) ?: return + installingPackages[sessionId] = session.appPackageName ?: return + if (installedApps.value?.any { it.`package` == session.appPackageName } == true) return + if (session.appLabel.isNullOrBlank() || !session.isActive) return + val appInstallation = AppInstallation(session) + val inst = installations.value ?: mutableListOf() + inst.add(appInstallation) + installations.postValue(inst) + } + }) + + + val apps = profiles.map { p -> launcherApps.getActivityList(null, p).mapNotNull { getApplication(it, p) } }.flatten() + installedApps.value = apps + } + + override suspend fun search(query: String) { + updateAppsForDisplay() + } + + private suspend fun updateAppsForDisplay() { + val query = SearchRepository.getInstance().currentQuery.value ?: "" + + val componentName = ComponentName.unflattenFromString(query) + + val apps = withContext(Dispatchers.Default) { + val hiddenItems = hiddenItemKeys.value ?: emptyList() + val installed = installedApps.value ?: emptyList() + val installing = installations.value ?: emptyList() + val results = mutableListOf() + results.addAll(installed) + results.addAll(installing) + if (query.isNotEmpty()) { + results.removeAll { !it.label.contains(query, ignoreCase = true) } + getActivityByComponentName(componentName)?.let { results.add(it) } + } + results.removeAll { hiddenItems.contains(it.key) } + results.sort() + results + } + + applications.value = apps + } + + private fun getActivityByComponentName(componentName: ComponentName?): Application? { + if (!LauncherPreferences.instance.searchActivities) return null + componentName ?: return null + val intent = Intent().setComponent(componentName) + val lai = launcherApps.resolveActivity(intent, Process.myUserHandle()) + return lai?.let { + LauncherApp(context, lai) + } + } + + private fun getApplication(launcherActivityInfo: LauncherActivityInfo, profile: UserHandle): Application? { + if (launcherActivityInfo.applicationInfo.packageName == context.packageName && !context.packageName.endsWith(".debug")) return null + return LauncherApp(context, launcherActivityInfo) + } + + private fun getApplications(packageName: String): List { + if (packageName == context.packageName) return emptyList() + + return profiles.map { p -> launcherApps.getActivityList(packageName, p).mapNotNull { getApplication(it, p) } }.flatten() + } + + companion object { + private lateinit var instance: AppRepository + fun getInstance(context: Context): AppRepository { + if (!::instance.isInitialized) instance = AppRepository(context.applicationContext) + return instance + } + } +} \ No newline at end of file diff --git a/applications/src/main/java/de/mm20/launcher2/applications/AppViewModel.kt b/applications/src/main/java/de/mm20/launcher2/applications/AppViewModel.kt new file mode 100644 index 00000000..2a32b84b --- /dev/null +++ b/applications/src/main/java/de/mm20/launcher2/applications/AppViewModel.kt @@ -0,0 +1,13 @@ +package de.mm20.launcher2.applications + +import android.app.Application as AndroidApp +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import de.mm20.launcher2.applications.AppRepository +import de.mm20.launcher2.search.data.Application + +class AppViewModel(app: AndroidApp): AndroidViewModel(app) { + private val repository = AppRepository.getInstance(app) + val applications: LiveData> = repository.applications +} + diff --git a/applications/src/main/java/de/mm20/launcher2/search/data/AppInstallation.kt b/applications/src/main/java/de/mm20/launcher2/search/data/AppInstallation.kt new file mode 100644 index 00000000..ace6257f --- /dev/null +++ b/applications/src/main/java/de/mm20/launcher2/search/data/AppInstallation.kt @@ -0,0 +1,67 @@ +package de.mm20.launcher2.search.data + +import android.content.Context +import android.content.Intent +import android.content.pm.PackageInstaller +import android.graphics.ColorMatrix +import android.graphics.ColorMatrixColorFilter +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.ColorDrawable +import androidx.core.content.ContextCompat +import de.mm20.launcher2.applications.R +import de.mm20.launcher2.icons.LauncherIcon + +class AppInstallation( + val session: PackageInstaller.SessionInfo +) : Application( + label = session.appLabel?.toString() ?: "", + `package` = session.appPackageName ?: "", + activity = "", + flags = 0, + version = null +) { + + override val key: String + get() = "installer://${session.installerPackageName}:${session.appPackageName}" + + override val badgeKey: String + get() = "app://${session.appPackageName}" + + override fun getLaunchIntent(context: Context): Intent? { + return session.createDetailsIntent() + } + + override fun getPlaceholderIcon(context: Context): LauncherIcon { + return LauncherIcon( + foreground = ContextCompat.getDrawable(context, R.drawable.ic_app_placeholder)!!, + background = ColorDrawable(ContextCompat.getColor(context, R.color.grey)), + foregroundScale = 0.5f) + } + + override suspend fun loadIconAsync(context: Context, size: Int): LauncherIcon? { + val icon = session.appIcon ?: return getPlaceholderIcon(context) + val foreground = BitmapDrawable(context.resources, icon) + foreground.colorFilter = ColorMatrixColorFilter(ColorMatrix().apply { + setSaturation(0f) + }) + return LauncherIcon( + foreground = foreground, + background = ColorDrawable(ContextCompat.getColor(context, R.color.grey)) + ) + } + + override fun getStoreDetails(context: Context): StoreLink? { + return getStoreLinkForInstaller(session.installerPackageName, `package`) + } + + companion object { + fun search(context: Context): List { + val installer = context.packageManager.packageInstaller + val sessions = installer.allSessions + val results = sessions.mapNotNull { + if (it.appLabel != null && it.isActive) AppInstallation(it) else null + } + return results + } + } +} \ No newline at end of file diff --git a/applications/src/main/java/de/mm20/launcher2/search/data/AppShortcut.kt b/applications/src/main/java/de/mm20/launcher2/search/data/AppShortcut.kt new file mode 100644 index 00000000..13b1e40a --- /dev/null +++ b/applications/src/main/java/de/mm20/launcher2/search/data/AppShortcut.kt @@ -0,0 +1,161 @@ +package de.mm20.launcher2.search.data + +import android.content.Context +import android.content.Intent +import android.content.pm.LauncherApps +import android.content.pm.PackageManager +import android.content.pm.ShortcutInfo +import android.graphics.drawable.AdaptiveIconDrawable +import android.graphics.drawable.ColorDrawable +import android.os.* +import androidx.annotation.RequiresApi +import androidx.core.content.ContextCompat +import androidx.core.content.getSystemService +import de.mm20.launcher2.applications.R +import de.mm20.launcher2.badges.Badge +import de.mm20.launcher2.badges.BadgeProvider +import de.mm20.launcher2.graphics.BadgeDrawable +import de.mm20.launcher2.icons.LauncherIcon +import de.mm20.launcher2.ktx.getSerialNumber +import de.mm20.launcher2.ktx.isAtLeastApiLevel +import de.mm20.launcher2.ktx.jsonObjectOf +import de.mm20.launcher2.preferences.LauncherPreferences +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.json.JSONObject +import java.lang.IllegalStateException + +@RequiresApi(Build.VERSION_CODES.N_MR1) +class AppShortcut( + context: Context, + val launcherShortcut: ShortcutInfo, + val appName: String +) : Searchable() { + + override val label: String + get() = launcherShortcut.shortLabel?.toString() ?: "" + + + + private val userSerialNumber: Long = launcherShortcut.userHandle.getSerialNumber(context) + private val isMainProfile = launcherShortcut.userHandle == Process.myUserHandle() + + override val key: String + get() = if (isMainProfile) { + "shortcut://${launcherShortcut.`package`}/${launcherShortcut.id}" + } else { + "shortcut://${launcherShortcut.`package`}/${launcherShortcut.id}:${userSerialNumber}" + } + + override val badgeKey: String + get() { + return if (LauncherPreferences.instance.shortcutBadges) { + if (isMainProfile) "shortcut://${launcherShortcut.activity?.flattenToShortString()}" else "profile://$userSerialNumber" + } else { + "null" + } + } + + override fun serialize(): String { + return jsonObjectOf( + "packagename" to launcherShortcut.`package`, + "id" to launcherShortcut.id, + "user" to userSerialNumber, + ).toString() + } + + + override fun getLaunchIntent(context: Context): Intent? { + return launcherShortcut.intent + } + + override fun launch(context: Context, options: Bundle?): Boolean { + val launcherApps = context.getSystemService()!! + try { + launcherApps.startShortcut(launcherShortcut, null, options) + } catch (e: IllegalStateException) { + return false + } + return true + } + + override fun getPlaceholderIcon(context: Context): LauncherIcon { + return LauncherIcon( + foreground = ContextCompat.getDrawable(context, R.drawable.ic_app_placeholder)!!, + background = ColorDrawable(ContextCompat.getColor(context, R.color.green)), + foregroundScale = 0.5f) + } + + override suspend fun loadIconAsync(context: Context, size: Int): LauncherIcon? { + val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps + val icon = launcherApps.getShortcutIconDrawable(launcherShortcut, context.resources.displayMetrics.densityDpi) + icon ?: return null + if (isAtLeastApiLevel(Build.VERSION_CODES.O) && icon is AdaptiveIconDrawable) { + return LauncherIcon( + foreground = icon.foreground, + background = icon.background, + foregroundScale = 1.5f, + backgroundScale = 1.5f + ) + } + return LauncherIcon( + foreground = icon, + foregroundScale = 1f, + autoGenerateBackgroundMode = LauncherPreferences.instance.legacyIconBg.toInt() + ) + } + + companion object { + fun deserialize(context: Context, serialized: String): AppShortcut? { + val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps + if (!launcherApps.hasShortcutHostPermission()) return null + else { + val json = JSONObject(serialized) + val packageName = json.getString("packagename") + val id = json.getString("id") + val userSerial = json.optLong("user") + val query = LauncherApps.ShortcutQuery() + query.setPackage(packageName) + query.setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC or + LauncherApps.ShortcutQuery.FLAG_MATCH_MANIFEST or + LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED) + query.setShortcutIds(mutableListOf(id)) + val userManager = context.getSystemService()!! + val user = userManager.getUserForSerialNumber(userSerial) ?: Process.myUserHandle() + val shortcuts = try { + launcherApps.getShortcuts(query, user) + } catch (e: IllegalStateException) { + return null + } + val pm = context.packageManager + val appName = try { + pm.getApplicationInfo(packageName, 0).loadLabel(pm).toString() + } catch (e: PackageManager.NameNotFoundException) { + return null + } + if (shortcuts == null || shortcuts.isEmpty()) return null else { + GlobalScope.launch { + val activity = shortcuts[0].activity + withContext(Dispatchers.IO) { + val icon = try { + context.packageManager.getActivityIcon(activity + ?: return@withContext) + } catch (e: PackageManager.NameNotFoundException) { + return@withContext + } + val badge = Badge(icon = BadgeDrawable(context, icon)) + BadgeProvider.getInstance(context).setBadge("shortcut://${activity.flattenToShortString()}", badge) + } + } + return AppShortcut( + context = context, + launcherShortcut = shortcuts[0], + appName = appName + ) + } + } + } + } +} \ No newline at end of file diff --git a/applications/src/main/java/de/mm20/launcher2/search/data/Application.kt b/applications/src/main/java/de/mm20/launcher2/search/data/Application.kt new file mode 100644 index 00000000..8cddd3fe --- /dev/null +++ b/applications/src/main/java/de/mm20/launcher2/search/data/Application.kt @@ -0,0 +1,88 @@ +package de.mm20.launcher2.search.data + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.graphics.drawable.ColorDrawable +import android.util.Log +import androidx.core.content.ContextCompat +import de.mm20.launcher2.applications.R +import de.mm20.launcher2.compat.PackageManagerCompat +import de.mm20.launcher2.icons.LauncherIcon +import org.json.JSONObject + +abstract class Application( + override val label: String, + val `package`: String, + val activity: String, + val flags: Int, + val version: String?, + val shortcuts: List = emptyList() +) : Searchable() { + + override val badgeKey: String + get() = "app://${`package`}" + + override fun serialize(): String { + val json = JSONObject() + json.put("package", `package`) + json.put("activity", activity) + return json.toString() + } + + override fun getLaunchIntent(context: Context): Intent? { + val intent = Intent() + intent.component = ComponentName(`package`, activity) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + return intent + } + + override fun getPlaceholderIcon(context: Context): LauncherIcon { + return LauncherIcon( + foreground = ContextCompat.getDrawable(context, R.drawable.ic_app_placeholder)!!, + background = ColorDrawable(ContextCompat.getColor(context, R.color.lightgreen)), + foregroundScale = 0.5f + ) + } + + open fun getStoreDetails(context: Context): StoreLink? { + val pm = context.packageManager + val installSourceInfo = PackageManagerCompat.getInstallSource(pm, `package`) + return getStoreLinkForInstaller(installSourceInfo.initiatingPackageName, `package`) + } + + override val key: String + get() = "app://$`package`:$activity" + + companion object { + internal fun getStoreLinkForInstaller(installerPackage: String?, packageName: String?): StoreLink? { + if (packageName == null) return null + return when (installerPackage) { + "de.amazon.mShop.android", "com.amazon.venezia" -> { + StoreLink( + "Amazon App Shop", + "http://www.amazon.com/gp/mas/dl/android?p=${packageName}" + ) + } + "com.android.vending" -> { + StoreLink( + "Google Play Store", + "https://play.google.com/store/apps/details?id=${packageName}" + ) + } + "org.fdroid.fdroid", "com.aurora.adroid" -> { + StoreLink( + "F-Droid", + "https://f-droid.org/packages/${packageName}" + ) + } + else -> null + } + } + } +} + +data class StoreLink( + val label: String, + val url: String +) \ No newline at end of file diff --git a/applications/src/main/java/de/mm20/launcher2/search/data/LauncherApp.kt b/applications/src/main/java/de/mm20/launcher2/search/data/LauncherApp.kt new file mode 100644 index 00000000..8087ad39 --- /dev/null +++ b/applications/src/main/java/de/mm20/launcher2/search/data/LauncherApp.kt @@ -0,0 +1,123 @@ +package de.mm20.launcher2.search.data + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.LauncherActivityInfo +import android.content.pm.LauncherApps +import android.content.pm.PackageManager +import android.content.pm.ShortcutInfo +import android.os.* +import androidx.core.content.getSystemService +import de.mm20.launcher2.icons.IconPackManager +import de.mm20.launcher2.icons.LauncherIcon +import de.mm20.launcher2.ktx.getSerialNumber +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.json.JSONObject + +/** + * An [Application] based on an [android.content.pm.LauncherActivityInfo] + */ +class LauncherApp( + context: Context, + private val launcherActivityInfo: LauncherActivityInfo +) : Application( + label = launcherActivityInfo.label.toString(), + `package` = launcherActivityInfo.applicationInfo.packageName, + activity = launcherActivityInfo.name, + flags = launcherActivityInfo.applicationInfo.flags, + version = getPackageVersionName(context, launcherActivityInfo.applicationInfo.packageName), + shortcuts = run { + val appShortcuts = mutableListOf() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + val launcherApps = context.getSystemService()!! + if (!launcherApps.hasShortcutHostPermission()) return@run appShortcuts + val query = LauncherApps.ShortcutQuery() + .setPackage(launcherActivityInfo.applicationInfo.packageName) + .setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC or LauncherApps.ShortcutQuery.FLAG_MATCH_MANIFEST) + val shortcuts = try { + launcherApps.getShortcuts(query, launcherActivityInfo.user) + } catch (e: IllegalStateException) { + emptyList() + } + appShortcuts.addAll(shortcuts?.map { AppShortcut(context, it, launcherActivityInfo.label.toString()) } + ?: emptyList()) + } + appShortcuts + } +) { + + private val userSerialNumber: Long = launcherActivityInfo.user.getSerialNumber(context) + private val isMainProfile = launcherActivityInfo.user == Process.myUserHandle() + + override val badgeKey: String = if (isMainProfile) "app://${`package`}" else "profile://$userSerialNumber" + + override val key: String + get() = if (isMainProfile) "app://$`package`:$activity" else "app://$`package`:$activity:${userSerialNumber}" + + override fun serialize(): String { + val json = JSONObject() + json.put("package", `package`) + json.put("activity", activity) + json.put("user", userSerialNumber) + return json.toString() + } + + fun getUser(): UserHandle? { + return launcherActivityInfo.user + } + + override suspend fun loadIconAsync(context: Context, size: Int): LauncherIcon? { + return withContext(Dispatchers.IO) { + IconPackManager.getInstance(context).getIcon(context, launcherActivityInfo, size) + } + } + + override fun launch(context: Context, options: Bundle?): Boolean { + val launcherApps = context.getSystemService()!! + if (isMainProfile) { + val intent = Intent() + intent.component = ComponentName(`package`, activity) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + context.startActivity(intent, options) + } else { + try { + launcherApps.startMainActivity( + ComponentName(`package`, activity), + launcherActivityInfo.user, + null, + options + ) + } catch (e: SecurityException) { + return false + } + } + return true + } + + companion object { + + fun deserialize(context: Context, serialized: String): LauncherApp? { + val json = JSONObject(serialized) + val launcherApps = context.getSystemService()!! + val userManager = context.getSystemService()!! + val userSerial = json.optLong("user") + val user = userManager.getUserForSerialNumber(userSerial) ?: Process.myUserHandle() + val pkg = json.getString("package") + val intent = Intent().also { + it.component = ComponentName(pkg, json.getString("activity")) + } + val launcherActivityInfo = launcherApps.resolveActivity(intent, user) ?: return null + return LauncherApp(context, launcherActivityInfo) + } + + fun getPackageVersionName(context: Context, packageName: String): String? { + return try { + context.packageManager.getPackageInfo(packageName, 0).versionName + } catch (e: PackageManager.NameNotFoundException) { + null + } + } + } +} \ No newline at end of file diff --git a/applications/src/main/res/drawable/ic_app_placeholder.xml b/applications/src/main/res/drawable/ic_app_placeholder.xml new file mode 100644 index 00000000..8d3fd387 --- /dev/null +++ b/applications/src/main/res/drawable/ic_app_placeholder.xml @@ -0,0 +1,5 @@ + + + diff --git a/appsearch/.gitignore b/appsearch/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/appsearch/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/appsearch/build.gradle.kts b/appsearch/build.gradle.kts new file mode 100644 index 00000000..f211a865 --- /dev/null +++ b/appsearch/build.gradle.kts @@ -0,0 +1,58 @@ +plugins { + id("com.android.library") + id("kotlin-android") + id("kotlin-android-extensions") + id("kotlin-kapt") +} + +android { + compileSdk = sdk.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = sdk.versions.minSdk.get().toInt() + targetSdk = sdk.versions.targetSdk.get().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } +} + +dependencies { + implementation(libs.bundles.kotlin) + implementation(libs.androidx.core) + implementation(libs.androidx.appcompat) + + implementation(libs.bundles.androidx.lifecycle) + + implementation(libs.bundles.androidx.appsearch) + kapt(libs.androidx.appsearchcompiler) + + implementation(libs.guava) + + implementation(project(":search")) + implementation(project(":base")) + implementation(project(":icons")) + implementation(project(":preferences")) + implementation(project(":ktx")) + implementation(project(":badges")) + implementation(project(":hiddenitems")) +} \ No newline at end of file diff --git a/appsearch/consumer-rules.pro b/appsearch/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/appsearch/proguard-rules.pro b/appsearch/proguard-rules.pro new file mode 100644 index 00000000..ff59496d --- /dev/null +++ b/appsearch/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/appsearch/src/main/AndroidManifest.xml b/appsearch/src/main/AndroidManifest.xml new file mode 100644 index 00000000..fcb7f5f2 --- /dev/null +++ b/appsearch/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/appsearch/src/main/java/de/mm20/launcher2/appsearch/AppSearchRepository.kt b/appsearch/src/main/java/de/mm20/launcher2/appsearch/AppSearchRepository.kt new file mode 100644 index 00000000..287aa1b0 --- /dev/null +++ b/appsearch/src/main/java/de/mm20/launcher2/appsearch/AppSearchRepository.kt @@ -0,0 +1,69 @@ +package de.mm20.launcher2.appsearch + +import android.app.appsearch.AppSearchManager +import android.app.appsearch.GlobalSearchSession +import android.app.appsearch.SearchResult +import android.app.appsearch.SearchSpec +import android.content.Context +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import de.mm20.launcher2.hiddenitems.HiddenItemsRepository +import de.mm20.launcher2.ktx.isAtLeastApiLevel +import de.mm20.launcher2.search.BaseSearchableRepository +import de.mm20.launcher2.search.data.AppSearchResult +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.util.concurrent.Executors +import kotlin.coroutines.suspendCoroutine + +class AppSearchRepository private constructor(val context: Context) : BaseSearchableRepository() { + + private var session: GlobalSearchSession? = null + + val appSearchResults = MediatorLiveData?>() + + private val allAppSearchResults = MutableLiveData?>(emptyList()) + private val hiddenItemKeys = HiddenItemsRepository.getInstance(context).hiddenItemsKeys + + init { + appSearchResults.addSource(hiddenItemKeys) { keys -> + appSearchResults.value = allAppSearchResults.value?.filter { !keys.contains(it.key) } + } + appSearchResults.addSource(allAppSearchResults) { c -> + appSearchResults.value = c?.filter { hiddenItemKeys.value?.contains(it.key) != true } + } + } + + override suspend fun search(query: String) { + val results = emptyList() + if (!isAtLeastApiLevel(31)) return + val executor = Executors.newSingleThreadExecutor() + if (session == null) { + val appSearchManager = context.getSystemService(AppSearchManager::class.java) + session = suspendCoroutine { cont -> + appSearchManager.createGlobalSearchSession(executor) { + cont.resumeWith(Result.success(it.resultValue)) + } + } + } + + withContext(Dispatchers.IO) { + val searchResults = session?.search(query, SearchSpec.Builder().build()) + val page = suspendCoroutine?> { cont -> + searchResults?.getNextPage(executor) { + cont.resumeWith(Result.success(it.resultValue)) + } + } + } + allAppSearchResults.value = results + } + + companion object { + private lateinit var instance: AppSearchRepository + fun getInstance(context: Context): AppSearchRepository { + if (!::instance.isInitialized) instance = AppSearchRepository(context.applicationContext) + return instance + } + } + +} \ No newline at end of file diff --git a/appsearch/src/main/java/de/mm20/launcher2/appsearch/AppSearchViewModel.kt b/appsearch/src/main/java/de/mm20/launcher2/appsearch/AppSearchViewModel.kt new file mode 100644 index 00000000..ddecaf0e --- /dev/null +++ b/appsearch/src/main/java/de/mm20/launcher2/appsearch/AppSearchViewModel.kt @@ -0,0 +1,11 @@ +package de.mm20.launcher2.appsearch + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import de.mm20.launcher2.search.data.AppSearchResult + +class AppSearchViewModel(app: Application) : AndroidViewModel(app) { + private val repository = AppSearchRepository.getInstance(app) + private val appSearch: LiveData?> = repository.appSearchResults +} \ No newline at end of file diff --git a/appsearch/src/main/java/de/mm20/launcher2/search/data/AppSearchResult.kt b/appsearch/src/main/java/de/mm20/launcher2/search/data/AppSearchResult.kt new file mode 100644 index 00000000..28a6fe07 --- /dev/null +++ b/appsearch/src/main/java/de/mm20/launcher2/search/data/AppSearchResult.kt @@ -0,0 +1,15 @@ +package de.mm20.launcher2.search.data + +import android.content.Context +import de.mm20.launcher2.icons.LauncherIcon + +class AppSearchResult: Searchable() { + override val key: String + get() = TODO("Not yet implemented") + override val label: String + get() = TODO("Not yet implemented") + + override fun getPlaceholderIcon(context: Context): LauncherIcon { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/assets/icons/MM20Launcher2-alpha.svg b/assets/icons/MM20Launcher2-alpha.svg new file mode 100644 index 00000000..85c03a6e --- /dev/null +++ b/assets/icons/MM20Launcher2-alpha.svg @@ -0,0 +1,437 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/MM20Launcher2-beta.svg b/assets/icons/MM20Launcher2-beta.svg new file mode 100644 index 00000000..e161133e --- /dev/null +++ b/assets/icons/MM20Launcher2-beta.svg @@ -0,0 +1,437 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/MM20Launcher2-debug.svg b/assets/icons/MM20Launcher2-debug.svg new file mode 100644 index 00000000..3c10e681 --- /dev/null +++ b/assets/icons/MM20Launcher2-debug.svg @@ -0,0 +1,437 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/MM20Launcher2-release.svg b/assets/icons/MM20Launcher2-release.svg new file mode 100644 index 00000000..1b124053 --- /dev/null +++ b/assets/icons/MM20Launcher2-release.svg @@ -0,0 +1,433 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/MM20Launcher2.svg b/assets/icons/MM20Launcher2.svg new file mode 100644 index 00000000..54559163 --- /dev/null +++ b/assets/icons/MM20Launcher2.svg @@ -0,0 +1,414 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/badges/.gitignore b/badges/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/badges/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/badges/build.gradle.kts b/badges/build.gradle.kts new file mode 100644 index 00000000..415569ba --- /dev/null +++ b/badges/build.gradle.kts @@ -0,0 +1,48 @@ +plugins { + id("com.android.library") + id("kotlin-android") + id("kotlin-android-extensions") +} + +android { + compileSdk = sdk.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = sdk.versions.minSdk.get().toInt() + targetSdk = sdk.versions.targetSdk.get().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation(libs.bundles.kotlin) + implementation(libs.androidx.core) + implementation(libs.androidx.appcompat) + + implementation(libs.bundles.androidx.lifecycle) + + implementation(project(":ktx")) + implementation(project(":preferences")) + +} \ No newline at end of file diff --git a/badges/consumer-rules.pro b/badges/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/badges/proguard-rules.pro b/badges/proguard-rules.pro new file mode 100644 index 00000000..01639a19 --- /dev/null +++ b/badges/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.kts.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/badges/src/main/AndroidManifest.xml b/badges/src/main/AndroidManifest.xml new file mode 100644 index 00000000..839d84a6 --- /dev/null +++ b/badges/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + / + \ No newline at end of file diff --git a/badges/src/main/java/de/mm20/launcher2/badges/Badge.kt b/badges/src/main/java/de/mm20/launcher2/badges/Badge.kt new file mode 100644 index 00000000..c2cfb6db --- /dev/null +++ b/badges/src/main/java/de/mm20/launcher2/badges/Badge.kt @@ -0,0 +1,10 @@ +package de.mm20.launcher2.badges + +import android.graphics.drawable.Drawable + +data class Badge( + var number: Int? = null, + var progress: Float? = null, + var iconRes: Int? = null, + var icon: Drawable? = null +) \ No newline at end of file diff --git a/badges/src/main/java/de/mm20/launcher2/badges/BadgeProvider.kt b/badges/src/main/java/de/mm20/launcher2/badges/BadgeProvider.kt new file mode 100644 index 00000000..36d2565c --- /dev/null +++ b/badges/src/main/java/de/mm20/launcher2/badges/BadgeProvider.kt @@ -0,0 +1,150 @@ +package de.mm20.launcher2.badges + +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.LauncherApps +import android.os.Build +import android.os.Process +import androidx.annotation.RequiresApi +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import de.mm20.launcher2.ktx.getSerialNumber +import de.mm20.launcher2.ktx.isAtLeastApiLevel +import de.mm20.launcher2.preferences.LauncherPreferences +import kotlinx.coroutines.* + +class BadgeProvider private constructor(val context: Context) { + + private val scope = CoroutineScope(Job() + Dispatchers.Main) + + private val badges = mutableMapOf>() + + init { + if (LauncherPreferences.instance.cloudBadges) { + addCloudBadges() + } + if (LauncherPreferences.instance.suspendBadges) { + addSuspendBadges() + } + if (LauncherPreferences.instance.profileBadges && isAtLeastApiLevel(Build.VERSION_CODES.O)) { + addProfileBadges() + } + } + + + fun setBadge(key: String, badge: Badge) { + if (badges.containsKey(key)) { + badges[key]?.postValue(badge) + return + } + badges[key] = MutableLiveData(badge) + } + + /** + * Updates a badge with all set values of another badge but keeps all other values + */ + fun updateBadge(key: String, + badge: Badge) { + if (badges.containsKey(key)) { + badges[key]?.run { + val updatedBadge = value?.also { + if (badge.number != null) it.number = badge.number + if (badge.progress != null) it.progress = badge.progress + if (badge.iconRes != null) it.iconRes = badge.iconRes + if (badge.icon != null) it.icon = badge.icon + } + postValue(updatedBadge) + } + } else { + badges[key] = MutableLiveData(badge) + } + } + + fun removeBadge(key: String) { + badges[key]?.postValue(Badge()) + } + + fun getBadge(key: String): Badge? { + return badges[key]?.value + } + + fun getLiveBadge(key: String): LiveData { + return badges[key] ?: MutableLiveData(Badge()).also { badges[key] = it } + } + + fun removeNotificationBadges() { + for (k in badges.keys) { + if (k.startsWith("app://")) { + val badge = getBadge(k) ?: continue + badge.number = null + badge.progress = null + updateBadge(k, badge) + } + } + } + + fun removeSuspendBadges() { + for ((k, v) in badges) { + if (k.startsWith("app://") && v.value?.iconRes == R.drawable.ic_badge_suspended) { + val badge = getBadge(k) ?: continue + badge.iconRes = null + updateBadge(k, badge) + } + } + } + + fun addSuspendBadges() { + scope.launch { + withContext(Dispatchers.IO) { + val apps = context.packageManager.getInstalledApplications(0) + for (app in apps) { + if (app.flags and ApplicationInfo.FLAG_SUSPENDED != 0) { + setBadge("app://${app.packageName}", Badge(iconRes = R.drawable.ic_badge_suspended)) + } + } + } + } + } + + fun addCloudBadges() { + setBadge("gdrive://", Badge(iconRes = R.drawable.ic_badge_gdrive)) + setBadge("onedrive://", Badge(iconRes = R.drawable.ic_badge_onedrive)) + setBadge("nextcloud://", Badge(iconRes = R.drawable.ic_badge_nextcloud)) + setBadge("owncloud://", Badge(iconRes = R.drawable.ic_badge_owncloud)) + } + + @RequiresApi(Build.VERSION_CODES.O) + fun addProfileBadges() { + val profiles = (context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps).profiles + for (p in profiles) { + if (p == Process.myUserHandle()) continue + setBadge("profile://${p.getSerialNumber(context)}", Badge( + iconRes = R.drawable.ic_badge_workprofile + )) + } + } + + fun removeCloudBadges() { + removeBadge("gdrive://") + removeBadge("onedrive://") + removeBadge("nextcloud://") + removeBadge("owncloud://") + } + + fun removeShortcutBadges() { + for (k in badges.keys) { + if (k.startsWith("shortcut://")) { + removeBadge(k) + } + } + } + + companion object { + private lateinit var instance: BadgeProvider + + fun getInstance(context: Context): BadgeProvider { + if (!::instance.isInitialized) instance = BadgeProvider(context.applicationContext) + return instance + } + } +} \ No newline at end of file diff --git a/badges/src/main/res/drawable-hdpi/ic_badge_gdrive.webp b/badges/src/main/res/drawable-hdpi/ic_badge_gdrive.webp new file mode 100644 index 00000000..a67a50f9 Binary files /dev/null and b/badges/src/main/res/drawable-hdpi/ic_badge_gdrive.webp differ diff --git a/badges/src/main/res/drawable-mdpi/ic_badge_gdrive.webp b/badges/src/main/res/drawable-mdpi/ic_badge_gdrive.webp new file mode 100644 index 00000000..5b5e2821 Binary files /dev/null and b/badges/src/main/res/drawable-mdpi/ic_badge_gdrive.webp differ diff --git a/badges/src/main/res/drawable-xhdpi/ic_badge_gdrive.webp b/badges/src/main/res/drawable-xhdpi/ic_badge_gdrive.webp new file mode 100644 index 00000000..747abbaa Binary files /dev/null and b/badges/src/main/res/drawable-xhdpi/ic_badge_gdrive.webp differ diff --git a/badges/src/main/res/drawable-xxhdpi/ic_badge_gdrive.webp b/badges/src/main/res/drawable-xxhdpi/ic_badge_gdrive.webp new file mode 100644 index 00000000..71f5bba6 Binary files /dev/null and b/badges/src/main/res/drawable-xxhdpi/ic_badge_gdrive.webp differ diff --git a/badges/src/main/res/drawable-xxxhdpi/ic_badge_gdrive.webp b/badges/src/main/res/drawable-xxxhdpi/ic_badge_gdrive.webp new file mode 100644 index 00000000..1823acc9 Binary files /dev/null and b/badges/src/main/res/drawable-xxxhdpi/ic_badge_gdrive.webp differ diff --git a/badges/src/main/res/drawable/ic_badge_nextcloud.xml b/badges/src/main/res/drawable/ic_badge_nextcloud.xml new file mode 100644 index 00000000..8124620b --- /dev/null +++ b/badges/src/main/res/drawable/ic_badge_nextcloud.xml @@ -0,0 +1,10 @@ + + + diff --git a/badges/src/main/res/drawable/ic_badge_onedrive.xml b/badges/src/main/res/drawable/ic_badge_onedrive.xml new file mode 100644 index 00000000..1dd014aa --- /dev/null +++ b/badges/src/main/res/drawable/ic_badge_onedrive.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/badges/src/main/res/drawable/ic_badge_owncloud.xml b/badges/src/main/res/drawable/ic_badge_owncloud.xml new file mode 100644 index 00000000..2be8c5f7 --- /dev/null +++ b/badges/src/main/res/drawable/ic_badge_owncloud.xml @@ -0,0 +1,10 @@ + + + diff --git a/badges/src/main/res/drawable/ic_badge_suspended.xml b/badges/src/main/res/drawable/ic_badge_suspended.xml new file mode 100644 index 00000000..d4caaef0 --- /dev/null +++ b/badges/src/main/res/drawable/ic_badge_suspended.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/badges/src/main/res/drawable/ic_badge_workprofile.xml b/badges/src/main/res/drawable/ic_badge_workprofile.xml new file mode 100644 index 00000000..9e6af7f7 --- /dev/null +++ b/badges/src/main/res/drawable/ic_badge_workprofile.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/base/.gitignore b/base/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/base/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/base/build.gradle.kts b/base/build.gradle.kts new file mode 100644 index 00000000..276669c7 --- /dev/null +++ b/base/build.gradle.kts @@ -0,0 +1,51 @@ +plugins { + id("com.android.library") + id("kotlin-android") + id("kotlin-android-extensions") +} + +android { + compileSdk = sdk.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = sdk.versions.minSdk.get().toInt() + targetSdk = sdk.versions.targetSdk.get().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation(libs.kotlin.stdlib) + implementation(libs.androidx.core) + implementation(libs.androidx.appcompat) + implementation(libs.materialcomponents) + + implementation(libs.androidx.palette) + + implementation(libs.lottie.core) + implementation(libs.bundles.materialdialogs) + + implementation(project(":ktx")) + +} \ No newline at end of file diff --git a/base/consumer-rules.pro b/base/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/base/proguard-rules.pro b/base/proguard-rules.pro new file mode 100644 index 00000000..d99b33c9 --- /dev/null +++ b/base/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.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/base/src/main/AndroidManifest.xml b/base/src/main/AndroidManifest.xml new file mode 100644 index 00000000..e1598c62 --- /dev/null +++ b/base/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/base/src/main/java/de/mm20/launcher2/graphics/BadgeDrawable.kt b/base/src/main/java/de/mm20/launcher2/graphics/BadgeDrawable.kt new file mode 100644 index 00000000..369b2c65 --- /dev/null +++ b/base/src/main/java/de/mm20/launcher2/graphics/BadgeDrawable.kt @@ -0,0 +1,58 @@ +package de.mm20.launcher2.graphics + +import android.content.Context +import android.graphics.* +import android.graphics.drawable.AdaptiveIconDrawable +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.graphics.drawable.LayerDrawable +import android.os.Build +import androidx.core.graphics.drawable.toBitmap +import de.mm20.launcher2.ktx.dp +import kotlin.math.roundToInt + +class BadgeDrawable(context: Context, drawable: Drawable) : Drawable() { + + private val drawable: BitmapDrawable + + init { + val size = (28.8 * context.dp).roundToInt() + val drw: Drawable = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && drawable is AdaptiveIconDrawable) { + LayerDrawable(arrayOf( + drawable.background, + drawable.foreground + )).apply { + val inset = (-size * 0.25).roundToInt() + setLayerInset(1, inset, inset, inset, inset) + setLayerInset(0, inset, inset, inset, inset) + } + } else { + drawable + } + val bitmap = drw.toBitmap(size, size).run { + if(isMutable) this + else this.copy(config, true) + } + this.drawable = BitmapDrawable(context.resources, bitmap) + } + + override fun setAlpha(alpha: Int) { + } + + override fun getOpacity(): Int { + return PixelFormat.TRANSLUCENT + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + } + + override fun draw(canvas: Canvas) { + canvas.clipPath(Path().apply { addOval( + bounds.left.toFloat(), + bounds.top.toFloat(), + bounds.right.toFloat(), + bounds.bottom.toFloat(), Path.Direction.CW) }) + drawable.setBounds(bounds.left, bounds.top, bounds.right, bounds.bottom) + drawable.draw(canvas) + } +} \ No newline at end of file diff --git a/base/src/main/java/de/mm20/launcher2/helper/NetworkUtils.kt b/base/src/main/java/de/mm20/launcher2/helper/NetworkUtils.kt new file mode 100644 index 00000000..fe32552b --- /dev/null +++ b/base/src/main/java/de/mm20/launcher2/helper/NetworkUtils.kt @@ -0,0 +1,17 @@ +package de.mm20.launcher2.helper + +import android.content.Context +import android.net.ConnectivityManager + +object NetworkUtils { + + fun isOffline(context: Context, allowMobile: Boolean = true): Boolean { + val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val info = connectivityManager.activeNetworkInfo ?: return true + return when { + info.type == ConnectivityManager.TYPE_WIFI -> false + info.type == ConnectivityManager.TYPE_MOBILE && allowMobile -> false + else -> true + } + } +} \ No newline at end of file diff --git a/base/src/main/java/de/mm20/launcher2/icons/LauncherIcon.kt b/base/src/main/java/de/mm20/launcher2/icons/LauncherIcon.kt new file mode 100644 index 00000000..43e6b344 --- /dev/null +++ b/base/src/main/java/de/mm20/launcher2/icons/LauncherIcon.kt @@ -0,0 +1,84 @@ +package de.mm20.launcher2.icons + +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import androidx.core.graphics.drawable.toBitmap +import androidx.palette.graphics.Palette +import java.lang.ref.WeakReference + +open class LauncherIcon( + foreground: Drawable, + background: Drawable? = null, + foregroundScale: Float = 1f, + backgroundScale: Float = 1f, + var autoGenerateBackgroundMode: Int = BACKGROUND_WHITE +) { + + var foreground = foreground + set(value) { + field = value + updateBackgroundColor() + notifyCallbacks() + } + + private fun updateBackgroundColor() { + if (background == null) { + when (autoGenerateBackgroundMode) { + BACKGROUND_COLOR -> { + val palette = Palette + .from(foreground.toBitmap()) + .generate() + this.background = ColorDrawable(palette.getDominantColor(Color.WHITE)) + badgeColor = palette.getLightVibrantColor(0xFFF0F0F0.toInt()) + } + BACKGROUND_WHITE -> this.background = ColorDrawable(Color.WHITE) + else -> this.foregroundScale = 1f + } + } + } + + var background = background + set(value) { + field = value + notifyCallbacks() + } + + var foregroundScale = foregroundScale + set(value) { + field = value + notifyCallbacks() + } + + var backgroundScale = backgroundScale + set(value) { + field = value + notifyCallbacks() + } + + private val callbacks = mutableListOf Unit>>() + + fun registerCallback(callback: (LauncherIcon) -> Unit) { + callbacks.add(WeakReference(callback)) + } + + protected fun notifyCallbacks() { + val iterator = callbacks.iterator() + while(iterator.hasNext()) { + val callback = iterator.next() + callback.get()?.invoke(this) ?: iterator.remove() + } + } + + var badgeColor: Int = 0xFFF0F0F0.toInt() + + init { + updateBackgroundColor() + } + + companion object { + const val BACKGROUND_NONE = 0 + const val BACKGROUND_COLOR = 1 + const val BACKGROUND_WHITE = 2 + } +} \ No newline at end of file diff --git a/base/src/main/java/de/mm20/launcher2/view/ElevationImageView.kt b/base/src/main/java/de/mm20/launcher2/view/ElevationImageView.kt new file mode 100644 index 00000000..79bec864 --- /dev/null +++ b/base/src/main/java/de/mm20/launcher2/view/ElevationImageView.kt @@ -0,0 +1,135 @@ +package de.mm20.launcher2.view + +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.* +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.graphics.drawable.Icon +import android.renderscript.Allocation +import android.renderscript.Element +import android.renderscript.RenderScript +import android.renderscript.ScriptIntrinsicBlur +import android.util.AttributeSet +import android.view.View +import com.airbnb.lottie.LottieDrawable +import com.airbnb.lottie.LottieProperty +import com.airbnb.lottie.model.KeyPath +import kotlin.math.max +import kotlin.math.min + +class ElevationImageView : androidx.appcompat.widget.AppCompatImageView { + + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + constructor(context: Context, attrs: AttributeSet?, defStyleRes: Int) : super(context, attrs, defStyleRes) + + + private val secondCanvas = Canvas() + private var shadowBitmap: Bitmap? = null + private var inAllocation: Allocation? = null + private lateinit var renderScript : RenderScript + private lateinit var blur : ScriptIntrinsicBlur + + val shadowPaint = Paint().also { + it.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OVER) + it.colorFilter = PorterDuffColorFilter(Color.BLACK, PorterDuff.Mode.SRC_ATOP) + it.alpha = 66 + setLayerType(View.LAYER_TYPE_SOFTWARE, null) + } + + val clearPaint = Paint().also { + it.color = Color.TRANSPARENT + it.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + blur.destroy() + renderScript.destroy() + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + renderScript = RenderScript.create(context) + blur = ScriptIntrinsicBlur.create(renderScript, Element.U8_4(renderScript)) + } + + override fun onDraw(canvas: Canvas) { + secondCanvas.drawRect(0f, 0f, secondCanvas.width.toFloat(), secondCanvas.height.toFloat(), clearPaint) + + if (drawable == null) { + return // couldn't resolve the URI + } + + val drawableWidth = drawable.intrinsicWidth + val drawableHeight = drawable.intrinsicHeight + + if (drawableWidth == 0 || drawableHeight == 0) { + return // nothing to draw (empty bounds) + } + + if (shadowBitmap?.width != width || shadowBitmap?.height != height) { + shadowBitmap?.recycle() + inAllocation?.destroy() + if (z > 0f && width > 0 && height > 0) { + shadowBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).also { + secondCanvas.setBitmap(it) + } + inAllocation = shadowBitmap?.let { Allocation.createFromBitmap(renderScript, it) } + } + } + + val saveCount = canvas.saveCount + val saveCount2 = secondCanvas.saveCount + canvas.save() + secondCanvas.save() + + + canvas.translate(paddingLeft.toFloat(), paddingTop.toFloat()) + secondCanvas.translate(paddingLeft.toFloat(), paddingTop.toFloat() + getShadowTranslation()) + + if (!imageMatrix.isIdentity) { + canvas.concat(imageMatrix) + secondCanvas.concat(imageMatrix) + } + + + drawable.draw(canvas) + drawable.draw(secondCanvas) + shadowBitmap = shadowBitmap?.let { blurBitmap(it) } + canvas.restoreToCount(saveCount) + secondCanvas.restoreToCount(saveCount2) + shadowBitmap?.let { canvas.drawBitmap(it, 0f, 0f, shadowPaint) } + } + + private fun blurBitmap(bitmap: Bitmap): Bitmap { + val alloc = inAllocation ?: return bitmap + if (z == 0f) return bitmap + alloc.copyFrom(bitmap) + blur.setRadius(max(0f, min(getShadowBlurRadius(), 25f))) + blur.setInput(alloc) + blur.forEach(alloc) + alloc.copyTo(bitmap) + return bitmap + } + + override fun setColorFilter(cf: ColorFilter?) { + super.setColorFilter(cf) + val drawable = drawable + if (drawable is LottieDrawable) { + drawable.addValueCallback(KeyPath("**"), LottieProperty.COLOR_FILTER) { + cf + } + } + } + + + private fun getShadowTranslation(): Float { + return z * 0.5f + } + + private fun getShadowBlurRadius(): Float { + return z * 0.5f + } +} \ No newline at end of file diff --git a/base/src/main/res/color/chip_background.xml b/base/src/main/res/color/chip_background.xml new file mode 100644 index 00000000..94fbf354 --- /dev/null +++ b/base/src/main/res/color/chip_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/base/src/main/res/color/chip_stroke.xml b/base/src/main/res/color/chip_stroke.xml new file mode 100644 index 00000000..63383a75 --- /dev/null +++ b/base/src/main/res/color/chip_stroke.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/base/src/main/res/color/chip_textcolor.xml b/base/src/main/res/color/chip_textcolor.xml new file mode 100644 index 00000000..486c4c2b --- /dev/null +++ b/base/src/main/res/color/chip_textcolor.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/base/src/main/res/color/text_color_primary.xml b/base/src/main/res/color/text_color_primary.xml new file mode 100644 index 00000000..0c9304d6 --- /dev/null +++ b/base/src/main/res/color/text_color_primary.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/base/src/main/res/color/text_color_secondary.xml b/base/src/main/res/color/text_color_secondary.xml new file mode 100644 index 00000000..594414d4 --- /dev/null +++ b/base/src/main/res/color/text_color_secondary.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/base/src/main/res/drawable/ic_permission.xml b/base/src/main/res/drawable/ic_permission.xml new file mode 100644 index 00000000..37b2c790 --- /dev/null +++ b/base/src/main/res/drawable/ic_permission.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/base/src/main/res/drawable/ic_weather_broken_clouds.xml b/base/src/main/res/drawable/ic_weather_broken_clouds.xml new file mode 100644 index 00000000..21ca06e7 --- /dev/null +++ b/base/src/main/res/drawable/ic_weather_broken_clouds.xml @@ -0,0 +1,54 @@ + + + + + + + + diff --git a/base/src/main/res/drawable/ic_weather_broken_clouds_night.xml b/base/src/main/res/drawable/ic_weather_broken_clouds_night.xml new file mode 100644 index 00000000..b6cfbc4a --- /dev/null +++ b/base/src/main/res/drawable/ic_weather_broken_clouds_night.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + diff --git a/base/src/main/res/drawable/ic_weather_clear.xml b/base/src/main/res/drawable/ic_weather_clear.xml new file mode 100644 index 00000000..85150fc5 --- /dev/null +++ b/base/src/main/res/drawable/ic_weather_clear.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/base/src/main/res/drawable/ic_weather_clear_night.xml b/base/src/main/res/drawable/ic_weather_clear_night.xml new file mode 100644 index 00000000..71d0d8cf --- /dev/null +++ b/base/src/main/res/drawable/ic_weather_clear_night.xml @@ -0,0 +1,55 @@ + + + + + + + + + diff --git a/base/src/main/res/drawable/ic_weather_cloudy.xml b/base/src/main/res/drawable/ic_weather_cloudy.xml new file mode 100644 index 00000000..e2c2780f --- /dev/null +++ b/base/src/main/res/drawable/ic_weather_cloudy.xml @@ -0,0 +1,26 @@ + + + + diff --git a/base/src/main/res/drawable/ic_weather_cold.xml b/base/src/main/res/drawable/ic_weather_cold.xml new file mode 100644 index 00000000..04ecb5d7 --- /dev/null +++ b/base/src/main/res/drawable/ic_weather_cold.xml @@ -0,0 +1,37 @@ + + + + + + diff --git a/base/src/main/res/drawable/ic_weather_drizzle.xml b/base/src/main/res/drawable/ic_weather_drizzle.xml new file mode 100644 index 00000000..d0708689 --- /dev/null +++ b/base/src/main/res/drawable/ic_weather_drizzle.xml @@ -0,0 +1,56 @@ + + + + + + + diff --git a/base/src/main/res/drawable/ic_weather_fog.xml b/base/src/main/res/drawable/ic_weather_fog.xml new file mode 100644 index 00000000..bde36fcc --- /dev/null +++ b/base/src/main/res/drawable/ic_weather_fog.xml @@ -0,0 +1,65 @@ + + + + + + + + + + diff --git a/base/src/main/res/drawable/ic_weather_hail.xml b/base/src/main/res/drawable/ic_weather_hail.xml new file mode 100644 index 00000000..4bef7193 --- /dev/null +++ b/base/src/main/res/drawable/ic_weather_hail.xml @@ -0,0 +1,47 @@ + + + + + + + diff --git a/base/src/main/res/drawable/ic_weather_haze.xml b/base/src/main/res/drawable/ic_weather_haze.xml new file mode 100644 index 00000000..b262217a --- /dev/null +++ b/base/src/main/res/drawable/ic_weather_haze.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + diff --git a/base/src/main/res/drawable/ic_weather_haze_night.xml b/base/src/main/res/drawable/ic_weather_haze_night.xml new file mode 100644 index 00000000..675f8513 --- /dev/null +++ b/base/src/main/res/drawable/ic_weather_haze_night.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + diff --git a/base/src/main/res/drawable/ic_weather_hot.xml b/base/src/main/res/drawable/ic_weather_hot.xml new file mode 100644 index 00000000..82a7a06f --- /dev/null +++ b/base/src/main/res/drawable/ic_weather_hot.xml @@ -0,0 +1,37 @@ + + + + + + diff --git a/base/src/main/res/drawable/ic_weather_mostly_cloudy.xml b/base/src/main/res/drawable/ic_weather_mostly_cloudy.xml new file mode 100644 index 00000000..14dff0bf --- /dev/null +++ b/base/src/main/res/drawable/ic_weather_mostly_cloudy.xml @@ -0,0 +1,37 @@ + + + + + + diff --git a/base/src/main/res/drawable/ic_weather_mostly_cloudy_night.xml b/base/src/main/res/drawable/ic_weather_mostly_cloudy_night.xml new file mode 100644 index 00000000..b1556640 --- /dev/null +++ b/base/src/main/res/drawable/ic_weather_mostly_cloudy_night.xml @@ -0,0 +1,44 @@ + + + + + + + diff --git a/base/src/main/res/drawable/ic_weather_partly_cloudy.xml b/base/src/main/res/drawable/ic_weather_partly_cloudy.xml new file mode 100644 index 00000000..906ad6ff --- /dev/null +++ b/base/src/main/res/drawable/ic_weather_partly_cloudy.xml @@ -0,0 +1,44 @@ + + + + + + + diff --git a/base/src/main/res/drawable/ic_weather_partly_cloudy_night.xml b/base/src/main/res/drawable/ic_weather_partly_cloudy_night.xml new file mode 100644 index 00000000..c625e856 --- /dev/null +++ b/base/src/main/res/drawable/ic_weather_partly_cloudy_night.xml @@ -0,0 +1,65 @@ + + + + + + + + + + diff --git a/base/src/main/res/drawable/ic_weather_showers.xml b/base/src/main/res/drawable/ic_weather_showers.xml new file mode 100644 index 00000000..7b41f324 --- /dev/null +++ b/base/src/main/res/drawable/ic_weather_showers.xml @@ -0,0 +1,47 @@ + + + + + + + diff --git a/base/src/main/res/drawable/ic_weather_sleet.xml b/base/src/main/res/drawable/ic_weather_sleet.xml new file mode 100644 index 00000000..3eff5116 --- /dev/null +++ b/base/src/main/res/drawable/ic_weather_sleet.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + diff --git a/base/src/main/res/drawable/ic_weather_snow.xml b/base/src/main/res/drawable/ic_weather_snow.xml new file mode 100644 index 00000000..f2304db1 --- /dev/null +++ b/base/src/main/res/drawable/ic_weather_snow.xml @@ -0,0 +1,47 @@ + + + + + + + diff --git a/base/src/main/res/drawable/ic_weather_thunderstorm.xml b/base/src/main/res/drawable/ic_weather_thunderstorm.xml new file mode 100644 index 00000000..ea8a9c8e --- /dev/null +++ b/base/src/main/res/drawable/ic_weather_thunderstorm.xml @@ -0,0 +1,33 @@ + + + + + diff --git a/base/src/main/res/drawable/ic_weather_thunderstorm_with_rain.xml b/base/src/main/res/drawable/ic_weather_thunderstorm_with_rain.xml new file mode 100644 index 00000000..0c140809 --- /dev/null +++ b/base/src/main/res/drawable/ic_weather_thunderstorm_with_rain.xml @@ -0,0 +1,47 @@ + + + + + + + diff --git a/base/src/main/res/drawable/ic_weather_wind.xml b/base/src/main/res/drawable/ic_weather_wind.xml new file mode 100644 index 00000000..5aaa0792 --- /dev/null +++ b/base/src/main/res/drawable/ic_weather_wind.xml @@ -0,0 +1,13 @@ + + + diff --git a/base/src/main/res/values-night/bools.xml b/base/src/main/res/values-night/bools.xml new file mode 100644 index 00000000..9d20ac83 --- /dev/null +++ b/base/src/main/res/values-night/bools.xml @@ -0,0 +1,4 @@ + + + true + \ No newline at end of file diff --git a/base/src/main/res/values-night/colors.xml b/base/src/main/res/values-night/colors.xml new file mode 100644 index 00000000..367f4d29 --- /dev/null +++ b/base/src/main/res/values-night/colors.xml @@ -0,0 +1,49 @@ + + + #E57373 + #F06292 + #BA68C8 + #9575CD + #7986CB + #64B5F6 + #4FC3F7 + #4DD0E1 + #4DB6AC + #81C784 + #AED581 + #DCE775 + #FFF176 + #FFD54F + #FFB74D + #FF8A65 + #A1887F + #E0E0E0 + #90A4AE + + @color/design_dark_default_color_surface + @android:color/black + #CC424242 + #393939 + + #FFFFFFFF + #B3FFFFFF + #80FFFFFF + #1FFFFFFF + #FFFFFF + #424242 + + #FFFFFF + #DF000000 + + @color/blue + #333 + #222 + @color/cardview_light_background + + #222222 + #282828 + + #DE000000 + #7000 + @color/design_dark_default_color_background + \ No newline at end of file diff --git a/base/src/main/res/values-night/styles.xml b/base/src/main/res/values-night/styles.xml new file mode 100644 index 00000000..e6ff1207 --- /dev/null +++ b/base/src/main/res/values-night/styles.xml @@ -0,0 +1,20 @@ + + + + + + + + + + \ No newline at end of file diff --git a/base/src/main/res/values-notnight-v23/colors.xml b/base/src/main/res/values-notnight-v23/colors.xml new file mode 100644 index 00000000..0d2c94c9 --- /dev/null +++ b/base/src/main/res/values-notnight-v23/colors.xml @@ -0,0 +1,4 @@ + + + #ddd + \ No newline at end of file diff --git a/base/src/main/res/values/attrs.xml b/base/src/main/res/values/attrs.xml new file mode 100644 index 00000000..b17ea6c8 --- /dev/null +++ b/base/src/main/res/values/attrs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/base/src/main/res/values/colors.xml b/base/src/main/res/values/colors.xml new file mode 100644 index 00000000..3f06735d --- /dev/null +++ b/base/src/main/res/values/colors.xml @@ -0,0 +1,53 @@ + + + #8A000000 + + #E53935 + #D81B60 + #9C27B0 + #5E35B1 + #3949AB + #1E88E5 + #039BE5 + #00ACC1 + #00897B + #43A047 + #7CB342 + #C0CA33 + #FDD835 + #FFB300 + #FB8C00 + #F4511E + #6D4C41 + #757575 + #546E7A + + @color/design_default_color_surface + @android:color/white + #CCFFFFFF + #E0E0E0 + + #DE000000 + #8A000000 + #64000000 + #1F000000 + #000000 + #FFFFFF + + #DF000000 + #FFFFFF + + @color/indigo + #fff + #ccc + @color/cardview_dark_background + + #ffffff + #ffffff + + #ffffff + @android:color/transparent + #f5f5f5 + @color/design_default_color_background + + diff --git a/base/src/main/res/values/styles.xml b/base/src/main/res/values/styles.xml new file mode 100644 index 00000000..7f2c22ce --- /dev/null +++ b/base/src/main/res/values/styles.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/base/src/main/res/values/typography.xml b/base/src/main/res/values/typography.xml new file mode 100644 index 00000000..332f6efb --- /dev/null +++ b/base/src/main/res/values/typography.xml @@ -0,0 +1,28 @@ + + + + + + + + + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 00000000..513577fd --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,36 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath("com.android.tools.build:gradle:7.1.0-alpha03") + classpath(libs.kotlin.gradle) + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.30") + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + google() + mavenCentral() + maven(url = "https://jitpack.io") + maven(url = "https://oss.sonatype.org/content/repositories/snapshots") + maven(url = "https://androidx.dev/snapshots/builds/7559387/artifacts/repository/") + maven(url = "https://dl.bintray.com/amulyakhare/maven") { + content { + includeGroup("com.amulyakhare") + } + } + maven(url = "https://pkgs.dev.azure.com/MicrosoftDeviceSDK/DuoSDK-Public/_packaging/Duo-SDK-Feed/maven/v1") + jcenter() // For MS SDK + } +} + +tasks.create("clean") { + delete(rootProject.buildDir) +} \ No newline at end of file diff --git a/calculator/.gitignore b/calculator/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/calculator/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/calculator/build.gradle.kts b/calculator/build.gradle.kts new file mode 100644 index 00000000..d8f01b4f --- /dev/null +++ b/calculator/build.gradle.kts @@ -0,0 +1,50 @@ +plugins { + id("com.android.library") + id("kotlin-android") + id("kotlin-android-extensions") +} + +android { + compileSdk = sdk.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = sdk.versions.minSdk.get().toInt() + targetSdk = sdk.versions.targetSdk.get().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation(libs.bundles.kotlin) + implementation(libs.androidx.core) + implementation(libs.androidx.appcompat) + + implementation(libs.bundles.androidx.lifecycle) + + implementation(libs.mathparser) + + implementation(project(":preferences")) + implementation(project(":search")) + +} \ No newline at end of file diff --git a/calculator/consumer-rules.pro b/calculator/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/calculator/proguard-rules.pro b/calculator/proguard-rules.pro new file mode 100644 index 00000000..01639a19 --- /dev/null +++ b/calculator/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.kts.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/calculator/src/main/AndroidManifest.xml b/calculator/src/main/AndroidManifest.xml new file mode 100644 index 00000000..be86c1bd --- /dev/null +++ b/calculator/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + / + \ No newline at end of file diff --git a/calculator/src/main/java/de/mm20/launcher2/calculator/CalculatorRepository.kt b/calculator/src/main/java/de/mm20/launcher2/calculator/CalculatorRepository.kt new file mode 100644 index 00000000..5df784ac --- /dev/null +++ b/calculator/src/main/java/de/mm20/launcher2/calculator/CalculatorRepository.kt @@ -0,0 +1,64 @@ +package de.mm20.launcher2.calculator + +import androidx.lifecycle.MutableLiveData +import de.mm20.launcher2.preferences.LauncherPreferences +import de.mm20.launcher2.search.BaseSearchableRepository +import de.mm20.launcher2.search.data.Calculator +import org.mariuszgromada.math.mxparser.Expression + +class CalculatorRepository private constructor() : BaseSearchableRepository() { + + val calculator = MutableLiveData() + + override suspend fun search(query: String) { + if (query.isBlank()) { + calculator.value = null + return + } + if (!LauncherPreferences.instance.searchCalculator) return + val calc = when { + query.matches(Regex("0x[0-9a-fA-F]+")) -> { + val solution = query.substring(2).toIntOrNull(16) ?: run { + calculator.value = null + return + } + Calculator(term = query, solution = solution.toDouble()) + } + query.matches(Regex("0b[01]+")) -> { + val solution = query.substring(2).toIntOrNull(2) ?: run { + calculator.value = null + return + } + Calculator(term = query, solution = solution.toDouble()) + } + query.matches(Regex("0[0-7]+")) -> { + val solution = query.substring(1).toIntOrNull(8) ?: run { + calculator.value = null + return + } + Calculator(term = query, solution = solution.toDouble()) + } + else -> { + val exp = Expression(query) + if (exp.checkSyntax()) { + Calculator(term = query, solution = exp.calculate()) + } else { + val exp2 = Expression(query.replace(',', '.').replace(';', ',')) + if (exp2.checkSyntax()) { + Calculator(term = query, solution = exp2.calculate()) + } else null + } + } + } + calculator.value = calc + } + + companion object { + private lateinit var instance: CalculatorRepository + + fun getInstance(): CalculatorRepository { + if (!::instance.isInitialized) instance = CalculatorRepository() + return instance + } + } +} \ No newline at end of file diff --git a/calculator/src/main/java/de/mm20/launcher2/calculator/CalculatorViewModel.kt b/calculator/src/main/java/de/mm20/launcher2/calculator/CalculatorViewModel.kt new file mode 100644 index 00000000..7dae9c4a --- /dev/null +++ b/calculator/src/main/java/de/mm20/launcher2/calculator/CalculatorViewModel.kt @@ -0,0 +1,7 @@ +package de.mm20.launcher2.calculator + +import androidx.lifecycle.ViewModel + +class CalculatorViewModel: ViewModel() { + val calculator = CalculatorRepository.getInstance().calculator +} \ No newline at end of file diff --git a/calculator/src/main/java/de/mm20/launcher2/search/data/Calculator.kt b/calculator/src/main/java/de/mm20/launcher2/search/data/Calculator.kt new file mode 100644 index 00000000..ba1570bd --- /dev/null +++ b/calculator/src/main/java/de/mm20/launcher2/search/data/Calculator.kt @@ -0,0 +1,89 @@ +package de.mm20.launcher2.search.data + +import java.text.DecimalFormat +import java.util.* +import kotlin.math.abs +import kotlin.math.roundToInt + +class Calculator( + val term: String, + val solution: Double +) { + + val formattedString: String + val formattedBinaryString: String + val formattedHexString: String + val formattedOctString: String + + init { + if (solution.isNaN()) { + formattedString = "NaN" + formattedOctString = "NaN" + formattedBinaryString = "NaN" + formattedHexString = "NaN" + } else { + val nf = + if ((abs(solution) > 1e12 || abs(solution) < 1e-5) && solution != 0.0) DecimalFormat( + "#.######E0" + ) + else DecimalFormat("#,###.######") + formattedString = nf.format(solution) + var s = StringBuffer(solution.roundToInt().toString(2)) + while (s.length % 4 != 0) { + s = s.insert(0, '0') + } + + for (i in s.length - 4 downTo 4 step 4) { + s.insert(i, ' ') + } + formattedBinaryString = s.toString() + + s = StringBuffer(solution.roundToInt().toString(8)) + while (s.length % 3 != 0) { + s = s.insert(0, '0') + } + + for (i in s.length - 3 downTo 3 step 3) { + s.insert(i, ' ') + } + formattedOctString = s.toString() + + s = StringBuffer(solution.roundToInt().toString(16).toUpperCase()) + while (s.length % 2 != 0) { + s = s.insert(0, '0') + } + + for (i in s.length - 2 downTo 2 step 2) { + s.insert(i, ' ') + } + formattedHexString = s.toString() + } + } + + fun getBeatifiedTerm(): String { + if(term.matches(Regex("0x[0-9a-fA-F]+"))) { + return term.substring(2).toUpperCase(Locale.ROOT) + "₁₆" + } + if(term.matches(Regex("0b[01]+"))) { + return term.substring(2) + "₂" + } + if(term.matches(Regex("0[0-7]+"))) { + return term.substring(1) + "₈" + } + return term.replace(Regex("\\s+"), "") + .replace("pi", " \u03C0 ", ignoreCase = true) + .replace("*", " \u00D7 ") + .replace("-", " \u2212 ") + .replace("/", " \u2215 ") + .replace("+", " + ") + .replace(Regex("&{1,2}"), " \u2227 ") + .replace(Regex("\\|{1,2}"), " \u2228 ") + .replace("!=", " \u2260 ") + .replace("<>", " \u2260 ") + .replace(">=", " \u2265 ") + .replace("<=", " \u2264 ") + .replace("=", " = ") + .replace("<", " < ") + .replace(">", " > ") + } +} \ No newline at end of file diff --git a/calendar/.gitignore b/calendar/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/calendar/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/calendar/build.gradle.kts b/calendar/build.gradle.kts new file mode 100644 index 00000000..f3634891 --- /dev/null +++ b/calendar/build.gradle.kts @@ -0,0 +1,52 @@ +plugins { + id("com.android.library") + id("kotlin-android") + id("kotlin-android-extensions") +} + +android { + compileSdk = sdk.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = sdk.versions.minSdk.get().toInt() + targetSdk = sdk.versions.targetSdk.get().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation(libs.bundles.kotlin) + implementation(libs.androidx.core) + implementation(libs.androidx.appcompat) + + implementation(libs.textdrawable) + + api(project(":search")) + implementation(project(":preferences")) + implementation(project(":ktx")) + implementation(project(":base")) + implementation(project(":hiddenitems")) + implementation(project(":permissions")) + +} \ No newline at end of file diff --git a/calendar/consumer-rules.pro b/calendar/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/calendar/proguard-rules.pro b/calendar/proguard-rules.pro new file mode 100644 index 00000000..52039aba --- /dev/null +++ b/calendar/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.kts.kts.kts.kts.kts.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/calendar/src/main/AndroidManifest.xml b/calendar/src/main/AndroidManifest.xml new file mode 100644 index 00000000..40b502ee --- /dev/null +++ b/calendar/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/calendar/src/main/java/de/mm20/launcher2/calendar/CalendarRepository.kt b/calendar/src/main/java/de/mm20/launcher2/calendar/CalendarRepository.kt new file mode 100644 index 00000000..70233402 --- /dev/null +++ b/calendar/src/main/java/de/mm20/launcher2/calendar/CalendarRepository.kt @@ -0,0 +1,81 @@ +package de.mm20.launcher2.calendar + +import android.content.Context +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import de.mm20.launcher2.hiddenitems.HiddenItemsRepository +import de.mm20.launcher2.preferences.LauncherPreferences +import de.mm20.launcher2.search.BaseSearchableRepository +import de.mm20.launcher2.search.data.CalendarEvent +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class CalendarRepository private constructor(val context: Context) : BaseSearchableRepository() { + + val calendarEvents = MediatorLiveData?>() + val upcomingCalendarEvents = MutableLiveData>(emptyList()) + + private val allEvents = MutableLiveData?>(emptyList()) + private val hiddenItemKeys = HiddenItemsRepository.getInstance(context).hiddenItemsKeys + + init { + calendarEvents.addSource(hiddenItemKeys) { keys -> + calendarEvents.value = allEvents.value?.filter { !keys.contains(it.key) } + } + calendarEvents.addSource(allEvents) { e -> + calendarEvents.value = e?.filter { hiddenItemKeys.value?.contains(it.key) != true } + } + hiddenItemKeys.observeForever { + requestCalendarUpdate() + } + + } + + fun requestCalendarUpdate() { + launch { + val unselectedCalendars = LauncherPreferences.instance.unselectedCalendars + val hideAlldayEvents = LauncherPreferences.instance.calendarHideAllday + + val now = System.currentTimeMillis() + val end = now + 14 * 24 * 60 * 60 * 1000L + val events = withContext(Dispatchers.IO) { + CalendarEvent.search( + context = context, + query = "", + intervalStart = now, + intervalEnd = end, + limit = 700, + hideAllDayEvents = hideAlldayEvents, + unselectedCalendars = unselectedCalendars, + hiddenEvents = hiddenItemKeys.value?.mapNotNull { + if (it.startsWith("calendar")) it.substringAfterLast("/").toLong() + else null + } ?: emptyList() + ) + } + upcomingCalendarEvents.value = events + } + } + + override suspend fun search(query: String) { + if (query.isBlank()) { + allEvents.value = null + return + } + val startTime = System.currentTimeMillis() + val endTime = System.currentTimeMillis() + 365L * 24 * 60 * 60 * 1000 + val events = withContext(Dispatchers.IO) { + CalendarEvent.search(context, query, startTime, endTime) + } + allEvents.value = events + } + + + companion object { + private lateinit var instance: CalendarRepository + fun getInstance(context: Context): CalendarRepository { + if (!::instance.isInitialized) instance = CalendarRepository(context.applicationContext) + return instance + } + } +} \ No newline at end of file diff --git a/calendar/src/main/java/de/mm20/launcher2/calendar/CalendarViewModel.kt b/calendar/src/main/java/de/mm20/launcher2/calendar/CalendarViewModel.kt new file mode 100644 index 00000000..f137f669 --- /dev/null +++ b/calendar/src/main/java/de/mm20/launcher2/calendar/CalendarViewModel.kt @@ -0,0 +1,11 @@ +package de.mm20.launcher2.calendar + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import de.mm20.launcher2.search.data.CalendarEvent + +class CalendarViewModel(app:Application): AndroidViewModel(app) { + val calendarEvents: LiveData?> = CalendarRepository.getInstance(app).calendarEvents + val upcomingCalendarEvents: LiveData> = CalendarRepository.getInstance(app).upcomingCalendarEvents +} \ No newline at end of file diff --git a/calendar/src/main/java/de/mm20/launcher2/search/data/CalendarEvent.kt b/calendar/src/main/java/de/mm20/launcher2/search/data/CalendarEvent.kt new file mode 100644 index 00000000..c0ef3681 --- /dev/null +++ b/calendar/src/main/java/de/mm20/launcher2/search/data/CalendarEvent.kt @@ -0,0 +1,321 @@ +package de.mm20.launcher2.search.data + +import android.Manifest +import android.content.ContentUris +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.Color +import android.graphics.Typeface +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.LayerDrawable +import android.provider.CalendarContract +import androidx.core.content.ContextCompat +import androidx.core.database.getStringOrNull +import androidx.core.graphics.ColorUtils +import androidx.core.graphics.blue +import androidx.core.graphics.green +import androidx.core.graphics.red +import com.amulyakhare.textdrawable.TextDrawable +import de.mm20.launcher2.calendar.R +import de.mm20.launcher2.permissions.PermissionsManager +import de.mm20.launcher2.icons.LauncherIcon +import de.mm20.launcher2.ktx.checkPermission +import de.mm20.launcher2.ktx.dp +import de.mm20.launcher2.preferences.LauncherPreferences +import org.json.JSONObject +import java.lang.NullPointerException +import java.text.SimpleDateFormat +import java.util.* + +class CalendarEvent( + override val label: String, + val id: Long, + val color: Int, + val startTime: Long, + val endTime: Long, + val allDay: Boolean, + val location: String, + val attendees: List, + val description: String, + val calendar: Long +) : Searchable() { + + override fun serialize(): String { + val json = JSONObject() + json.put("id", id) + return json.toString() + } + + override val key: String + get() = "calendar://$id" + + + override fun getPlaceholderIcon(context: Context): LauncherIcon { + val df = SimpleDateFormat("d") + val day = df.format(startTime) + df.applyPattern("MMM") + val month = df.format(startTime) + val fgLayers = arrayOf( + TextDrawable + .builder() + .beginConfig() + .textColor(Color.WHITE) + .useFont(Typeface.DEFAULT_BOLD) + .fontSize((36 * context.dp).toInt()) + .endConfig() + .buildRect(day, 0), + TextDrawable + .builder() + .beginConfig() + .textColor(Color.WHITE) + .bold() + .fontSize((26 * context.dp).toInt()) + .endConfig() + .buildRect(month, 0) + ) + val foreground = LayerDrawable(fgLayers) + foreground.setLayerInset(0, 0, 0, 0, (26 * context.dp).toInt()) + foreground.setLayerInset(1, 0, (36 * context.dp).toInt(), 0, 0) + val background = ColorDrawable(getDisplayColor(context, color)) + return LauncherIcon( + foreground = foreground, + background = background, + foregroundScale = 0.74f + ) + } + + override fun getLaunchIntent(context: Context): Intent? { + return null + } + + companion object { + fun search(context: Context, + query: String, + intervalStart: Long, + intervalEnd: Long, + limit: Int = 10, + hideAllDayEvents: Boolean = false, + unselectedCalendars: List = emptyList(), + hiddenEvents: List = emptyList() + ): List { + val results = mutableListOf() + if (!query.isEmpty() && query.length < 3) return results + if (!LauncherPreferences.instance.searchCalendars) return listOf() + if (!PermissionsManager.checkPermission(context, PermissionsManager.CALENDAR)) { + return emptyList() + } + val builder = CalendarContract.Instances.CONTENT_URI.buildUpon() + ContentUris.appendId(builder, intervalStart) + ContentUris.appendId(builder, intervalEnd) + val uri = builder.build() + val projection = arrayOf( + CalendarContract.Instances.EVENT_ID, + CalendarContract.Instances.TITLE, + CalendarContract.Instances.BEGIN, + CalendarContract.Instances.END, + CalendarContract.Instances.ALL_DAY, + CalendarContract.Instances.DISPLAY_COLOR, + CalendarContract.Instances.EVENT_LOCATION, + CalendarContract.Instances.CALENDAR_ID, + CalendarContract.Instances.DESCRIPTION + ) + val selection = mutableListOf() + if (query.isNotEmpty()) selection.add("${CalendarContract.Instances.TITLE} LIKE ?") + if (hiddenEvents.isNotEmpty()) selection.add("${CalendarContract.Instances.EVENT_ID} NOT IN (${hiddenEvents.joinToString()})") + if (unselectedCalendars.isNotEmpty()) selection.add("${CalendarContract.Instances.CALENDAR_ID} NOT IN (${unselectedCalendars.joinToString()})") + if (hideAllDayEvents) selection.add("${CalendarContract.Instances.ALL_DAY} = 0") + val selArgs = if (query.isBlank()) null else arrayOf("%$query%") + val sort = "${CalendarContract.Instances.BEGIN} ASC" + if (limit > -1) " LIMIT $limit" else "" + val cursor = context.contentResolver.query(uri, projection, selection.joinToString(separator = " AND "), selArgs, sort) + ?: return mutableListOf() + val proj = arrayOf( + CalendarContract.Attendees.EVENT_ID, + CalendarContract.Attendees.ATTENDEE_NAME, + CalendarContract.Attendees.ATTENDEE_EMAIL + ) + val s = "${CalendarContract.Attendees.ATTENDEE_NAME} COLLATE NOCASE ASC" + while (cursor.moveToNext()) { + val sel = "${CalendarContract.Attendees.EVENT_ID} = ${cursor.getLong(0)}" + val cur = context.contentResolver.query( + CalendarContract.Attendees.CONTENT_URI, + proj, sel, null, s + ) ?: return mutableListOf() + val attendees = mutableListOf() + while (cur.moveToNext()) { + attendees.add(cur.getString(1).takeUnless { it.isNullOrBlank() } + ?: cur.getString(2)) + } + cur.close() + val allday = cursor.getInt(4) > 0 + val begin = cursor.getLong(2) + + val tzOffset = if (allday) { + Calendar.getInstance().timeZone.getOffset(begin) + } else { + 0 + } + val event = CalendarEvent( + label = cursor.getString(1) ?: "", + id = cursor.getLong(0), + color = cursor.getInt(5), + startTime = begin - tzOffset, + endTime = cursor.getLong(3) - tzOffset - if (allday) 1 else 0, + allDay = allday, + location = cursor.getString(6) ?: "", + attendees = attendees, + description = cursor.getStringOrNull(8) + ?: "", + calendar = cursor.getLong(7) + ) + results.add(event) + } + cursor.close() + + return results + } + + fun deserialize(context: Context, serialized: String): CalendarEvent? { + if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) return null + val json = JSONObject(serialized) + val id = json.getLong("id") + val builder = CalendarContract.Instances.CONTENT_URI.buildUpon() + ContentUris.appendId(builder, System.currentTimeMillis()) + ContentUris.appendId(builder, System.currentTimeMillis() + 63072000000L) + val uri = builder.build() + val projection = arrayOf( + CalendarContract.Instances.EVENT_ID, + CalendarContract.Instances.TITLE, + CalendarContract.Instances.BEGIN, + CalendarContract.Instances.END, + CalendarContract.Instances.ALL_DAY, + CalendarContract.Instances.DISPLAY_COLOR, + CalendarContract.Instances.EVENT_LOCATION, + CalendarContract.Instances.CALENDAR_ID, + CalendarContract.Instances.DESCRIPTION + ) + val selection = CalendarContract.Instances.EVENT_ID + " = ?" + val selArgs = arrayOf(id.toString()) + val cursor = context.contentResolver.query(uri, projection, selection, selArgs, null) + ?: return null + if (cursor.moveToNext()) { + val title = cursor.getString(1) + val begin = cursor.getLong(2) + val end = cursor.getLong(3) + val allday = cursor.getInt(4) != 0 + val color = cursor.getInt(5) + val location = cursor.getString(6) + val calendar = cursor.getLong(7) + val description = cursor.getStringOrNull(8) + ?: "" + cursor.close() + val proj = arrayOf( + CalendarContract.Attendees.EVENT_ID, + CalendarContract.Attendees.ATTENDEE_NAME, + CalendarContract.Attendees.ATTENDEE_EMAIL + ) + val sel = "${CalendarContract.Attendees.EVENT_ID} = $id" + val s = "${CalendarContract.Attendees.ATTENDEE_NAME} COLLATE NOCASE ASC" + val cur = context.contentResolver.query( + CalendarContract.Attendees.CONTENT_URI, + proj, sel, null, s + ) ?: return null + val attendees = mutableListOf() + while (cur.moveToNext()) { + attendees.add(cur.getString(1).takeUnless { it.isNullOrBlank() } + ?: cur.getString(2)) + } + cur.close() + val tzOffset = if (allday) { + Calendar.getInstance().timeZone.getOffset(begin) + } else { + 0 + } + return CalendarEvent( + label = title, + id = id, + color = color, + startTime = begin - tzOffset, + endTime = end - tzOffset - if (allday) 1 else 0, + allDay = allday, + location = location ?: "", + attendees = attendees, + description = description, + calendar = calendar + ) + } + cursor.close() + return null + } + + fun getCalendars(context: Context): List { + val calendars = mutableListOf() + val uri = CalendarContract.Calendars.CONTENT_URI + val proj = arrayOf( + CalendarContract.Calendars._ID, + CalendarContract.Calendars.NAME, + CalendarContract.Calendars.ACCOUNT_NAME, + CalendarContract.Calendars.CALENDAR_COLOR, + CalendarContract.Calendars.VISIBLE, + CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, + ) + if (!context.checkPermission(Manifest.permission.READ_CALENDAR)) return calendars + val cursor = context.contentResolver.query(uri, proj, null, null, null) + ?: return emptyList() + while (cursor.moveToNext()) { + try { + calendars.add(UserCalendar( + id = cursor.getLong(0), + name = cursor.getString(5) ?: cursor.getString(1) ?: "", + owner = cursor.getString(2), + color = cursor.getInt(3) + )) + } catch (e: NullPointerException) { + continue + } + } + cursor.close() + calendars.sortBy { it.owner } + return calendars + } + + fun getDisplayColor(context: Context, color: Int): Int { + val hsl = FloatArray(3).let { + ColorUtils.RGBToHSL(color.red, color.green, color.blue, it) + it + } + return if (context.resources.getBoolean(R.bool.is_dark_theme)) { + if (ColorUtils.calculateContrast(ContextCompat.getColor(context, R.color.calendar_foreground_color), color) < 2.5 || true) { + if (color.red == color.green && color.red == color.blue) { + val level = 0xFF - ((0xFF - color.red) * 0.7f).toInt() + Color.rgb(level, level, level) + } else { + hsl[2] = hsl[2] + (1 - hsl[2]) * 0.2f + hsl[1] = 1 - (1 - hsl[1]) * 0.9f + ColorUtils.HSLToColor(hsl) + } + } else return color + } else { + if (ColorUtils.calculateContrast(ContextCompat.getColor(context, R.color.calendar_foreground_color), color) < 1.8) { + if (color.red == color.green && color.red == color.blue) { + val level = (color.red * 0.7f).toInt() + Color.rgb(level, level, level) + } else { + hsl[2] = (0.5f - hsl[2]) * 0.8f + hsl[2] + hsl[1] = 1 - (1 - hsl[1]) * 0.8f + ColorUtils.HSLToColor(hsl) + } + } else return color + } + } + + + } +} + +data class UserCalendar( + val id: Long, + val name: String, + val owner: String, + val color: Int +) \ No newline at end of file diff --git a/calendar/src/main/res/drawable-hdpi/ic_calendar.webp b/calendar/src/main/res/drawable-hdpi/ic_calendar.webp new file mode 100644 index 00000000..ca9bb8a8 Binary files /dev/null and b/calendar/src/main/res/drawable-hdpi/ic_calendar.webp differ diff --git a/calendar/src/main/res/drawable-mdpi/ic_calendar.webp b/calendar/src/main/res/drawable-mdpi/ic_calendar.webp new file mode 100644 index 00000000..ada1d797 Binary files /dev/null and b/calendar/src/main/res/drawable-mdpi/ic_calendar.webp differ diff --git a/calendar/src/main/res/drawable-xhdpi/ic_calendar.webp b/calendar/src/main/res/drawable-xhdpi/ic_calendar.webp new file mode 100644 index 00000000..ed69d698 Binary files /dev/null and b/calendar/src/main/res/drawable-xhdpi/ic_calendar.webp differ diff --git a/calendar/src/main/res/drawable-xxhdpi/ic_calendar.webp b/calendar/src/main/res/drawable-xxhdpi/ic_calendar.webp new file mode 100644 index 00000000..b3602c94 Binary files /dev/null and b/calendar/src/main/res/drawable-xxhdpi/ic_calendar.webp differ diff --git a/calendar/src/main/res/drawable-xxxhdpi/ic_calendar.webp b/calendar/src/main/res/drawable-xxxhdpi/ic_calendar.webp new file mode 100644 index 00000000..2d0bf6ab Binary files /dev/null and b/calendar/src/main/res/drawable-xxxhdpi/ic_calendar.webp differ diff --git a/compat/.gitignore b/compat/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/compat/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/compat/build.gradle.kts b/compat/build.gradle.kts new file mode 100644 index 00000000..392a13ed --- /dev/null +++ b/compat/build.gradle.kts @@ -0,0 +1,42 @@ +plugins { + id("com.android.library") + id("kotlin-android") +} + +android { + compileSdk = sdk.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = sdk.versions.minSdk.get().toInt() + targetSdk = sdk.versions.targetSdk.get().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + + implementation(libs.androidx.core) + implementation(libs.androidx.appcompat) + implementation(libs.materialcomponents) +} \ No newline at end of file diff --git a/compat/consumer-rules.pro b/compat/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/compat/proguard-rules.pro b/compat/proguard-rules.pro new file mode 100644 index 00000000..ff59496d --- /dev/null +++ b/compat/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/compat/src/main/AndroidManifest.xml b/compat/src/main/AndroidManifest.xml new file mode 100644 index 00000000..4446b93f --- /dev/null +++ b/compat/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/compat/src/main/java/de/mm20/launcher2/compat/PackageManagerCompat.kt b/compat/src/main/java/de/mm20/launcher2/compat/PackageManagerCompat.kt new file mode 100644 index 00000000..3bfc8fa3 --- /dev/null +++ b/compat/src/main/java/de/mm20/launcher2/compat/PackageManagerCompat.kt @@ -0,0 +1,33 @@ +package de.mm20.launcher2.compat + +import android.content.pm.PackageManager +import android.os.Build + +object PackageManagerCompat { + fun getInstallSource( + packageManager: PackageManager, + packageName: String + ): InstallSourceInfoCompat { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val installSourceInfo = packageManager.getInstallSourceInfo(packageName) + return InstallSourceInfoCompat( + originatingPackageName = installSourceInfo.originatingPackageName, + initiatingPackageName = installSourceInfo.initiatingPackageName, + installingPackageName = installSourceInfo.installingPackageName, + ) + } else { + val installerPackageName = packageManager.getInstallerPackageName(packageName) + return InstallSourceInfoCompat( + originatingPackageName = installerPackageName, + initiatingPackageName = installerPackageName, + installingPackageName = installerPackageName + ) + } + } +} + +data class InstallSourceInfoCompat( + val originatingPackageName: String?, + val initiatingPackageName: String?, + val installingPackageName: String? +) \ No newline at end of file diff --git a/contacts/.gitignore b/contacts/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/contacts/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/contacts/build.gradle.kts b/contacts/build.gradle.kts new file mode 100644 index 00000000..19f51ff6 --- /dev/null +++ b/contacts/build.gradle.kts @@ -0,0 +1,52 @@ +plugins { + id("com.android.library") + id("kotlin-android") + id("kotlin-android-extensions") +} + +android { + compileSdk = sdk.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = sdk.versions.minSdk.get().toInt() + targetSdk = sdk.versions.targetSdk.get().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation(libs.bundles.kotlin) + implementation(libs.androidx.core) + implementation(libs.androidx.appcompat) + + implementation(libs.textdrawable) + + implementation(project(":search")) + implementation(project(":preferences")) + implementation(project(":ktx")) + implementation(project(":base")) + implementation(project(":hiddenitems")) + implementation(project(":permissions")) + +} \ No newline at end of file diff --git a/contacts/consumer-rules.pro b/contacts/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/contacts/proguard-rules.pro b/contacts/proguard-rules.pro new file mode 100644 index 00000000..52039aba --- /dev/null +++ b/contacts/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.kts.kts.kts.kts.kts.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/contacts/src/main/AndroidManifest.xml b/contacts/src/main/AndroidManifest.xml new file mode 100644 index 00000000..7842fbc7 --- /dev/null +++ b/contacts/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + / + \ No newline at end of file diff --git a/contacts/src/main/java/de/mm20/launcher2/contacts/ContactRepository.kt b/contacts/src/main/java/de/mm20/launcher2/contacts/ContactRepository.kt new file mode 100644 index 00000000..7ab42335 --- /dev/null +++ b/contacts/src/main/java/de/mm20/launcher2/contacts/ContactRepository.kt @@ -0,0 +1,47 @@ +package de.mm20.launcher2.contacts + +import android.content.Context +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import de.mm20.launcher2.hiddenitems.HiddenItemsRepository +import de.mm20.launcher2.search.BaseSearchableRepository +import de.mm20.launcher2.search.data.Contact +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class ContactRepository private constructor(val context: Context) : BaseSearchableRepository() { + + val contacts = MediatorLiveData?>() + + private val allContacts = MutableLiveData?>(emptyList()) + private val hiddenItemKeys = HiddenItemsRepository.getInstance(context).hiddenItemsKeys + + init { + contacts.addSource(hiddenItemKeys) { keys -> + contacts.value = allContacts.value?.filter { !keys.contains(it.key) } + } + contacts.addSource(allContacts) { c -> + contacts.value = c?.filter { hiddenItemKeys.value?.contains(it.key) != true } + } + } + + override suspend fun search(query: String) { + if (query.isBlank()) { + allContacts.value = null + return + } + val results = withContext(Dispatchers.IO) { + Contact.search(context, query) + } + allContacts.value = results + } + + companion object { + private lateinit var instance: ContactRepository + + fun getInstance(context: Context): ContactRepository { + if (!::instance.isInitialized) instance = ContactRepository(context.applicationContext) + return instance + } + } +} \ No newline at end of file diff --git a/contacts/src/main/java/de/mm20/launcher2/contacts/ContactViewModel.kt b/contacts/src/main/java/de/mm20/launcher2/contacts/ContactViewModel.kt new file mode 100644 index 00000000..72d35fa8 --- /dev/null +++ b/contacts/src/main/java/de/mm20/launcher2/contacts/ContactViewModel.kt @@ -0,0 +1,10 @@ +package de.mm20.launcher2.contacts + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import de.mm20.launcher2.search.data.Contact + +class ContactViewModel(app: Application) : AndroidViewModel(app) { + val contacts: LiveData?> = ContactRepository.getInstance(app).contacts +} \ No newline at end of file diff --git a/contacts/src/main/java/de/mm20/launcher2/search/data/Contact.kt b/contacts/src/main/java/de/mm20/launcher2/search/data/Contact.kt new file mode 100644 index 00000000..d1637fc8 --- /dev/null +++ b/contacts/src/main/java/de/mm20/launcher2/search/data/Contact.kt @@ -0,0 +1,210 @@ +package de.mm20.launcher2.search.data + +import android.Manifest +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.provider.ContactsContract +import androidx.core.content.ContextCompat +import androidx.core.database.getStringOrNull +import androidx.core.graphics.drawable.toDrawable +import com.amulyakhare.textdrawable.TextDrawable +import de.mm20.launcher2.contacts.R +import de.mm20.launcher2.ktx.asBitmap +import de.mm20.launcher2.ktx.jsonObjectOf +import de.mm20.launcher2.icons.LauncherIcon +import de.mm20.launcher2.permissions.PermissionsManager +import de.mm20.launcher2.preferences.LauncherPreferences +import org.json.JSONObject + +class Contact( + val id: Long, + val firstName: String, + val lastName: String, + val displayName: String, + val lookupKey: String, + val phones: Set, + val emails: Set, + val telegram: Set, + val whatsapp: Set, + val postals: Set +) : Searchable() { + override val key: String + get() = "contact://$id" + override val label: String + get() = "$firstName $lastName" + + val summary: String + get() { + return phones.union(emails).joinToString(separator = ", ") + } + + override fun serialize(): String { + return jsonObjectOf( + "id" to id + ).toString() + } + + override fun getPlaceholderIcon(context: Context): LauncherIcon { + val iconText = if (firstName.isNotEmpty()) firstName[0].toString() else "" + if (lastName.isNotEmpty()) lastName[0].toString() else "" + return LauncherIcon( + foreground = TextDrawable.builder().buildRect(iconText, ContextCompat.getColor(context, R.color.blue)) + ) + } + + override suspend fun loadIconAsync(context: Context, size: Int): LauncherIcon? { + val contentResolver = context.contentResolver + val uri = ContactsContract.Contacts.getLookupUri(id, lookupKey) ?: return null + val bmp = ContactsContract.Contacts.openContactPhotoInputStream(contentResolver, uri, false)?.asBitmap() + ?: return null + return LauncherIcon( + foreground = bmp.toDrawable(context.resources) + ) + } + + override fun getLaunchIntent(context: Context): Intent? { + return null + } + + companion object { + fun search(context: Context, query: String): List { + if (query.length < 3) return mutableListOf() + if (!LauncherPreferences.instance.searchContacts) { + return mutableListOf() + } + if (!PermissionsManager.checkPermission(context, PermissionsManager.CONTACTS)) { + return mutableListOf() + } + val proj = arrayOf( + ContactsContract.RawContacts.CONTACT_ID, + ContactsContract.RawContacts._ID + ) + val sel = "${ContactsContract.RawContacts.DISPLAY_NAME_PRIMARY} LIKE ?" + val selArgs = arrayOf("%$query%") + val cursor = context.contentResolver.query( + ContactsContract.RawContacts.CONTENT_URI, proj, sel, selArgs, null) ?: return mutableListOf() + //Maps raw contact ids to contact ids + val contactMap = mutableMapOf>() + while (cursor.moveToNext()) { + contactMap.getOrPut(cursor.getLong(0)) { mutableSetOf() }.add(cursor.getLong(1)) + } + cursor.close() + val results = mutableListOf() + for ((id, rawIds) in contactMap) { + contactById(context, id, rawIds)?.let { results.add(it) } + } + return results.sortedBy { it } + } + + private fun contactById(context: Context, id: Long, rawIds: Set): Contact? { + val s = "(" + rawIds.joinToString(separator = " OR ", + transform = { "${ContactsContract.Data.RAW_CONTACT_ID} = $it" }) + ")" + + " AND (${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE}\"" + + " OR ${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE}\"" + + " OR ${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE}\"" + + " OR ${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE}\"" + + " OR ${ContactsContract.Data.MIMETYPE} = \"vnd.android.cursor.item/vnd.org.telegram.messenger.android.profile\"" + + " OR ${ContactsContract.Data.MIMETYPE} = \"vnd.android.cursor.item/vnd.com.whatsapp.profile\"" + + ")" + val dataCursor = context.contentResolver.query( + ContactsContract.Data.CONTENT_URI, + null, s, null, null + ) ?: return null + val phones = mutableSetOf() + val emails = mutableSetOf() + val telegram = mutableSetOf() + val whatsapp = mutableSetOf() + val postals = mutableSetOf() + var firstName = "" + var lastName = "" + var displayName = "" + val mimeTypeColumn = dataCursor.getColumnIndex(ContactsContract.Data.MIMETYPE) + val emailAddressColumn = dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.ADDRESS) + val numberColumn = dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER) + val addressColumn = dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS) + val displayNameColumn = dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME) + val givenNameColumn = dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME) + val familyNameColumn = dataCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME) + val data1Column = dataCursor.getColumnIndex(ContactsContract.Data.DATA1) + val data3Column = dataCursor.getColumnIndex(ContactsContract.Data.DATA3) + val idColumn = dataCursor.getColumnIndex(ContactsContract.Data._ID) + loop@ while (dataCursor.moveToNext()) { + when (dataCursor.getStringOrNull(mimeTypeColumn)) { + ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE -> + dataCursor.getStringOrNull(emailAddressColumn)?.let { emails.add(it) } + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE -> + dataCursor.getStringOrNull(numberColumn)?.let { + phones.add(it.replace(Regex("[^+0-9]"), "")) + } + ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE -> + dataCursor.getStringOrNull(addressColumn)?.let { postals.add(it) } + ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE -> { + firstName = dataCursor.getStringOrNull(givenNameColumn) ?: "" + lastName = dataCursor.getStringOrNull(familyNameColumn) ?: "" + displayName = dataCursor.getStringOrNull(displayNameColumn) ?: "" + } + "vnd.android.cursor.item/vnd.org.telegram.messenger.android.profile" -> { + val data1 = dataCursor.getStringOrNull(data1Column) + ?: continue@loop + val data3 = dataCursor.getStringOrNull(data3Column) + ?: continue@loop + telegram.add("$data1$$data3") + } + "vnd.android.cursor.item/vnd.com.whatsapp.profile" -> { + val data1 = dataCursor.getStringOrNull(data1Column) + ?: continue@loop + val dataId = dataCursor.getLong(idColumn) + whatsapp.add("$dataId$+${data1.substringBefore('@')}") + } + } + } + dataCursor.close() + + val lookupKeyCursor = context.contentResolver.query( + ContactsContract.Contacts.CONTENT_URI, + arrayOf(ContactsContract.Contacts.LOOKUP_KEY), + "${ContactsContract.Contacts._ID} = ?", + arrayOf(id.toString()), + null + ) ?: return null + var lookUpKey = "" + if (lookupKeyCursor.moveToNext()) { + lookUpKey = lookupKeyCursor.getString(0) + } + lookupKeyCursor.close() + + return Contact( + id = id, + emails = emails, + phones = phones, + firstName = firstName, + lastName = lastName, + displayName = displayName, + postals = postals, + telegram = telegram, + whatsapp = whatsapp, + lookupKey = lookUpKey + ) + } + + fun deserialize(context: Context, serialized: String): Contact? { + if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) return null + val id = JSONObject(serialized).getLong("id") + val rawContactsCursor = context.contentResolver.query( + ContactsContract.RawContacts.CONTENT_URI, + arrayOf(ContactsContract.RawContacts._ID), + "${ContactsContract.RawContacts.CONTACT_ID} = ?", + arrayOf(id.toString()), + null + ) ?: return null + val rawContacts = mutableSetOf() + while (rawContactsCursor.moveToNext()) { + rawContacts.add(rawContactsCursor.getLong(0)) + } + rawContactsCursor.close() + if (rawContacts.isEmpty()) return null + + return contactById(context, id, rawContacts) + } + } +} \ No newline at end of file diff --git a/crashreporter/.gitignore b/crashreporter/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/crashreporter/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/crashreporter/build.gradle.kts b/crashreporter/build.gradle.kts new file mode 100644 index 00000000..74c1ae97 --- /dev/null +++ b/crashreporter/build.gradle.kts @@ -0,0 +1,46 @@ +plugins { + id("com.android.library") + id("kotlin-android") + id("kotlin-android-extensions") +} + +android { + compileSdk = sdk.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = sdk.versions.minSdk.get().toInt() + targetSdk = sdk.versions.targetSdk.get().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation(libs.kotlin.stdlib) + implementation(libs.androidx.appcompat) + implementation(libs.materialcomponents) + implementation(libs.androidx.recyclerview) + + + implementation(project(":base")) +} \ No newline at end of file diff --git a/crashreporter/consumer-rules.pro b/crashreporter/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/crashreporter/proguard-rules.pro b/crashreporter/proguard-rules.pro new file mode 100644 index 00000000..d99b33c9 --- /dev/null +++ b/crashreporter/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.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/crashreporter/readme.md b/crashreporter/readme.md new file mode 100644 index 00000000..c5b4fec5 --- /dev/null +++ b/crashreporter/readme.md @@ -0,0 +1,26 @@ +# :crashreporter + +The crash reporter that can be found under Settings > About > Crash Reporter. + +## License + +This code is based on this library: +[https://github.com/MindorksOpenSource/CrashReporter](https://github.com/MindorksOpenSource/CrashReporter) +, originally licensed under the Apache 2.0 license. + +``` +Copyright (C) 2016 Bal Sikandar +Copyright (C) 2011 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. +``` \ No newline at end of file diff --git a/crashreporter/src/main/AndroidManifest.xml b/crashreporter/src/main/AndroidManifest.xml new file mode 100644 index 00000000..05f763f2 --- /dev/null +++ b/crashreporter/src/main/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + \ No newline at end of file diff --git a/crashreporter/src/main/java/com/balsikandar/crashreporter/CrashReporter.java b/crashreporter/src/main/java/com/balsikandar/crashreporter/CrashReporter.java new file mode 100644 index 00000000..5a1e8fdb --- /dev/null +++ b/crashreporter/src/main/java/com/balsikandar/crashreporter/CrashReporter.java @@ -0,0 +1,72 @@ +package com.balsikandar.crashreporter; + +import android.content.Context; +import android.content.Intent; + +import com.balsikandar.crashreporter.ui.CrashReporterActivity; +import com.balsikandar.crashreporter.utils.CrashReporterNotInitializedException; +import com.balsikandar.crashreporter.utils.CrashReporterExceptionHandler; +import com.balsikandar.crashreporter.utils.CrashUtil; + +public class CrashReporter { + + private static Context applicationContext; + + private static String crashReportPath; + + private static boolean isNotificationEnabled = true; + + private CrashReporter() { + // This class in not publicly instantiable + } + + public static void initialize(Context context) { + applicationContext = context; + setUpExceptionHandler(); + } + + public static void initialize(Context context, String crashReportSavePath) { + applicationContext = context; + crashReportPath = crashReportSavePath; + setUpExceptionHandler(); + } + + private static void setUpExceptionHandler() { + if (!(Thread.getDefaultUncaughtExceptionHandler() instanceof CrashReporterExceptionHandler)) { + Thread.setDefaultUncaughtExceptionHandler(new CrashReporterExceptionHandler()); + } + } + + public static Context getContext() { + if (applicationContext == null) { + try { + throw new CrashReporterNotInitializedException("Initialize CrashReporter : call CrashReporter.initialize(context, crashReportPath)"); + } catch (Exception e) { + e.printStackTrace(); + } + } + return applicationContext; + } + + public static String getCrashReportPath() { + return crashReportPath; + } + + public static boolean isNotificationEnabled() { + return isNotificationEnabled; + } + + //LOG Exception APIs + public static void logException(Exception exception) { + CrashUtil.logException(exception); + } + + public static Intent getLaunchIntent() { + return new Intent(applicationContext, CrashReporterActivity.class).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } + + public static void disableNotification() { + isNotificationEnabled = false; + } + +} diff --git a/crashreporter/src/main/java/com/balsikandar/crashreporter/CrashReporterInitProvider.java b/crashreporter/src/main/java/com/balsikandar/crashreporter/CrashReporterInitProvider.java new file mode 100644 index 00000000..19512fa3 --- /dev/null +++ b/crashreporter/src/main/java/com/balsikandar/crashreporter/CrashReporterInitProvider.java @@ -0,0 +1,68 @@ +package com.balsikandar.crashreporter; + +/** + * Created by bali on 02/08/17. + */ + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.Context; +import android.content.pm.ProviderInfo; +import android.database.Cursor; +import android.net.Uri; + +/** + * Created by amitshekhar on 16/11/16. + */ + +public class CrashReporterInitProvider extends ContentProvider { + + + public CrashReporterInitProvider() { + } + + @Override + public boolean onCreate() { + CrashReporter.initialize(getContext()); + return true; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + return null; + } + + @Override + public String getType(Uri uri) { + return null; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + return null; + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + return 0; + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + return 0; + } + + @Override + public void attachInfo(Context context, ProviderInfo providerInfo) { + if (providerInfo == null) { + throw new NullPointerException("CrashReporterInitProvider ProviderInfo cannot be null."); + } + // So if the authorities equal the library internal ones, the developer forgot to set his applicationId + if ("com.balsikandar.crashreporter.CrashReporterInitProvider".equals(providerInfo.authority)) { + throw new IllegalStateException("Incorrect provider authority in manifest. Most likely due to a " + + "missing applicationId variable in application\'s build.gradle.kts.kts."); + } + super.attachInfo(context, providerInfo); + } + +} \ No newline at end of file diff --git a/crashreporter/src/main/java/com/balsikandar/crashreporter/adapter/CrashLogAdapter.java b/crashreporter/src/main/java/com/balsikandar/crashreporter/adapter/CrashLogAdapter.java new file mode 100644 index 00000000..8bf7834a --- /dev/null +++ b/crashreporter/src/main/java/com/balsikandar/crashreporter/adapter/CrashLogAdapter.java @@ -0,0 +1,81 @@ +package com.balsikandar.crashreporter.adapter; + +import android.content.Context; +import android.content.Intent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.recyclerview.widget.RecyclerView; + +import com.balsikandar.crashreporter.ui.LogMessageActivity; +import com.balsikandar.crashreporter.utils.FileUtils; + +import java.io.File; +import java.util.ArrayList; + +import de.mm20.launcher2.crashreporter.R; + +/** + * Created by bali on 10/08/17. + */ + +public class CrashLogAdapter extends RecyclerView.Adapter { + + private Context context; + private ArrayList crashFileList; + + public CrashLogAdapter(Context context, ArrayList allCrashLogs) { + this.context = context; + crashFileList = allCrashLogs; + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(context).inflate(R.layout.custom_item, null); + return new CrashLogViewHolder(view); + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + ((CrashLogViewHolder) holder).setUpViewHolder(context, crashFileList.get(position)); + } + + @Override + public int getItemCount() { + return crashFileList.size(); + } + + + public void updateList(ArrayList allCrashLogs) { + crashFileList = allCrashLogs; + notifyDataSetChanged(); + } + + + private class CrashLogViewHolder extends RecyclerView.ViewHolder { + private TextView textViewMsg, messageLogTime; + + CrashLogViewHolder(View itemView) { + super(itemView); + messageLogTime = itemView.findViewById(R.id.messageLogTime); + textViewMsg = itemView.findViewById(R.id.textViewMsg); + } + + void setUpViewHolder(final Context context, final File file) { + final String filePath = file.getAbsolutePath(); + messageLogTime.setText(file.getName().replaceAll("[a-zA-Z_.]", "")); + textViewMsg.setText(FileUtils.readFirstLineFromFile(new File(filePath))); + + textViewMsg.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(context, LogMessageActivity.class); + intent.putExtra("LogMessage", filePath); + context.startActivity(intent); + } + }); + } + } +} diff --git a/crashreporter/src/main/java/com/balsikandar/crashreporter/adapter/MainPagerAdapter.java b/crashreporter/src/main/java/com/balsikandar/crashreporter/adapter/MainPagerAdapter.java new file mode 100644 index 00000000..bf572d1c --- /dev/null +++ b/crashreporter/src/main/java/com/balsikandar/crashreporter/adapter/MainPagerAdapter.java @@ -0,0 +1,50 @@ +package com.balsikandar.crashreporter.adapter; + +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentPagerAdapter; + +import com.balsikandar.crashreporter.ui.CrashLogFragment; +import com.balsikandar.crashreporter.ui.ExceptionLogFragment; + +/** + * Created by bali on 11/08/17. + */ + +public class MainPagerAdapter extends FragmentPagerAdapter { + + private CrashLogFragment crashLogFragment; + private ExceptionLogFragment exceptionLogFragment; + private String[] titles; + + public MainPagerAdapter(FragmentManager fm, String[] titles) { + super(fm); + this.titles = titles; + } + + @Override + public Fragment getItem(int position) { + if (position == 0) { + return crashLogFragment = new CrashLogFragment(); + } else if (position == 1) { + return exceptionLogFragment = new ExceptionLogFragment(); + } else { + return new CrashLogFragment(); + } + } + + @Override + public int getCount() { + return 2; + } + + @Override + public CharSequence getPageTitle(int position) { + return titles[position]; + } + + public void clearLogs() { + crashLogFragment.clearLog(); + exceptionLogFragment.clearLog(); + } +} \ No newline at end of file diff --git a/crashreporter/src/main/java/com/balsikandar/crashreporter/ui/CrashLogFragment.java b/crashreporter/src/main/java/com/balsikandar/crashreporter/ui/CrashLogFragment.java new file mode 100644 index 00000000..9df1a709 --- /dev/null +++ b/crashreporter/src/main/java/com/balsikandar/crashreporter/ui/CrashLogFragment.java @@ -0,0 +1,95 @@ +package com.balsikandar.crashreporter.ui; + +import android.content.Context; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.balsikandar.crashreporter.CrashReporter; +import com.balsikandar.crashreporter.adapter.CrashLogAdapter; +import com.balsikandar.crashreporter.utils.Constants; +import com.balsikandar.crashreporter.utils.CrashUtil; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; + +import de.mm20.launcher2.crashreporter.R; + +/** + * Created by bali on 11/08/17. + */ + +public class CrashLogFragment extends Fragment { + + private CrashLogAdapter logAdapter; + + private RecyclerView crashRecyclerView; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.crash_log, container, false); + crashRecyclerView = (RecyclerView) view.findViewById(R.id.crashRecyclerView); + + return view; + } + + @Override + public void onResume() { + super.onResume(); + loadAdapter(getActivity(), crashRecyclerView); + } + + private void loadAdapter(Context context, RecyclerView crashRecyclerView) { + + logAdapter = new CrashLogAdapter(context, getAllCrashes()); + crashRecyclerView.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)); + crashRecyclerView.setAdapter(logAdapter); + } + + public void clearLog() { + if (logAdapter != null) { + logAdapter.updateList(getAllCrashes()); + } + } + + + private ArrayList getAllCrashes() { + String directoryPath; + String crashReportPath = CrashReporter.getCrashReportPath(); + + if (TextUtils.isEmpty(crashReportPath)) { + directoryPath = CrashUtil.getDefaultPath(); + } else { + directoryPath = crashReportPath; + } + File directory = new File(directoryPath); + if (!directory.exists() || !directory.isDirectory()) { + throw new RuntimeException("The path provided doesn't exists : " + directoryPath); + } + ArrayList listOfFiles = new ArrayList<>(Arrays.asList(directory.listFiles())); + for (Iterator iterator = listOfFiles.iterator(); iterator.hasNext(); ) { + if (iterator.next().getName().contains(Constants.EXCEPTION_SUFFIX)) { + iterator.remove(); + } + } + Collections.sort(listOfFiles, Collections.reverseOrder()); + return listOfFiles; + } + +} diff --git a/crashreporter/src/main/java/com/balsikandar/crashreporter/ui/CrashReporterActivity.java b/crashreporter/src/main/java/com/balsikandar/crashreporter/ui/CrashReporterActivity.java new file mode 100644 index 00000000..72658bb7 --- /dev/null +++ b/crashreporter/src/main/java/com/balsikandar/crashreporter/ui/CrashReporterActivity.java @@ -0,0 +1,114 @@ +package com.balsikandar.crashreporter.ui; + +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.Menu; +import android.view.MenuItem; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.viewpager.widget.ViewPager; + +import com.balsikandar.crashreporter.CrashReporter; +import com.balsikandar.crashreporter.adapter.MainPagerAdapter; +import com.balsikandar.crashreporter.utils.Constants; +import com.balsikandar.crashreporter.utils.CrashUtil; +import com.balsikandar.crashreporter.utils.FileUtils; +import com.balsikandar.crashreporter.utils.SimplePageChangeListener; +import com.google.android.material.tabs.TabLayout; + +import java.io.File; + +import de.mm20.launcher2.crashreporter.R; + +public class CrashReporterActivity extends AppCompatActivity { + + private MainPagerAdapter mainPagerAdapter; + private int selectedTabPosition = 0; + + //region activity callbacks + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.log_main_menu, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.delete_crash_logs) { + clearCrashLog(); + return true; + } else { + return super.onOptionsItemSelected(item); + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.crash_reporter_activity); + + Toolbar toolbar = findViewById(R.id.toolbar); + toolbar.setTitle(getString(R.string.crash_reporter)); + toolbar.setSubtitle(getApplicationName()); + setSupportActionBar(toolbar); + + ViewPager viewPager = findViewById(R.id.viewpager); + if (viewPager != null) { + setupViewPager(viewPager); + } + + TabLayout tabLayout = findViewById(R.id.tabs); + tabLayout.setupWithViewPager(viewPager); + } + //endregion + + private void clearCrashLog() { + new Thread(new Runnable() { + @Override + public void run() { + String crashReportPath = TextUtils.isEmpty(CrashReporter.getCrashReportPath()) ? + CrashUtil.getDefaultPath() : CrashReporter.getCrashReportPath(); + + File[] logs = new File(crashReportPath).listFiles(); + for (File file : logs) { + FileUtils.delete(file); + } + runOnUiThread(new Runnable() { + @Override + public void run() { + mainPagerAdapter.clearLogs(); + } + }); + } + }).start(); + } + + private void setupViewPager(ViewPager viewPager) { + String[] titles = {getString(R.string.crashes), getString(R.string.exceptions)}; + mainPagerAdapter = new MainPagerAdapter(getSupportFragmentManager(), titles); + viewPager.setAdapter(mainPagerAdapter); + + viewPager.addOnPageChangeListener(new SimplePageChangeListener() { + @Override + public void onPageSelected(int position) { + selectedTabPosition = position; + } + }); + + Intent intent = getIntent(); + if (intent != null && !intent.getBooleanExtra(Constants.LANDING, false)) { + selectedTabPosition = 1; + } + viewPager.setCurrentItem(selectedTabPosition); + } + + private String getApplicationName() { + ApplicationInfo applicationInfo = getApplicationInfo(); + int stringId = applicationInfo.labelRes; + return stringId == 0 ? applicationInfo.nonLocalizedLabel.toString() : getString(stringId); + } + +} diff --git a/crashreporter/src/main/java/com/balsikandar/crashreporter/ui/ExceptionLogFragment.java b/crashreporter/src/main/java/com/balsikandar/crashreporter/ui/ExceptionLogFragment.java new file mode 100644 index 00000000..6fa4c706 --- /dev/null +++ b/crashreporter/src/main/java/com/balsikandar/crashreporter/ui/ExceptionLogFragment.java @@ -0,0 +1,96 @@ +package com.balsikandar.crashreporter.ui; + +import android.content.Context; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.balsikandar.crashreporter.CrashReporter; +import com.balsikandar.crashreporter.adapter.CrashLogAdapter; +import com.balsikandar.crashreporter.utils.Constants; +import com.balsikandar.crashreporter.utils.CrashUtil; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; + +import de.mm20.launcher2.crashreporter.R; + +/** + * Created by bali on 11/08/17. + */ + +public class ExceptionLogFragment extends Fragment { + + private CrashLogAdapter logAdapter; + + private RecyclerView exceptionRecyclerView; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.exception_log, container, false); + exceptionRecyclerView = (RecyclerView) view.findViewById(R.id.exceptionRecyclerView); + + return view; + } + + @Override + public void onResume() { + super.onResume(); + loadAdapter(getActivity(), exceptionRecyclerView); + } + + private void loadAdapter(Context context, RecyclerView exceptionRecyclerView) { + + logAdapter = new CrashLogAdapter(context, getAllExceptions()); + exceptionRecyclerView.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)); + exceptionRecyclerView.setAdapter(logAdapter); + } + + public void clearLog() { + if (logAdapter != null) { + logAdapter.updateList(getAllExceptions()); + } + } + + public ArrayList getAllExceptions() { + String directoryPath; + String crashReportPath = CrashReporter.getCrashReportPath(); + + if (TextUtils.isEmpty(crashReportPath)){ + directoryPath = CrashUtil.getDefaultPath(); + } else{ + directoryPath = crashReportPath; + } + + File directory = new File(directoryPath); + if (!directory.exists() || !directory.isDirectory()){ + throw new RuntimeException("The path provided doesn't exists : " + directoryPath); + } + + ArrayList listOfFiles = new ArrayList<>(Arrays.asList(directory.listFiles())); + for (Iterator iterator = listOfFiles.iterator(); iterator.hasNext(); ) { + if (iterator.next().getName().contains(Constants.CRASH_SUFFIX)) { + iterator.remove(); + } + } + Collections.sort(listOfFiles, Collections.reverseOrder()); + return listOfFiles; + } + +} diff --git a/crashreporter/src/main/java/com/balsikandar/crashreporter/ui/LogMessageActivity.java b/crashreporter/src/main/java/com/balsikandar/crashreporter/ui/LogMessageActivity.java new file mode 100644 index 00000000..d06fca6b --- /dev/null +++ b/crashreporter/src/main/java/com/balsikandar/crashreporter/ui/LogMessageActivity.java @@ -0,0 +1,90 @@ +package com.balsikandar.crashreporter.ui; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.TextView; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.core.content.FileProvider; + +import com.balsikandar.crashreporter.utils.AppUtils; +import com.balsikandar.crashreporter.utils.FileUtils; + +import java.io.File; + +import de.mm20.launcher2.crashreporter.R; + +public class LogMessageActivity extends AppCompatActivity { + + private TextView appInfo; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_log_message); + appInfo = findViewById(R.id.appInfo); + + Intent intent = getIntent(); + if (intent != null) { + String dirPath = intent.getStringExtra("LogMessage"); + File file = new File(dirPath); + String crashLog = FileUtils.readFromFile(file); + TextView textView = findViewById(R.id.logMessage); + textView.setText(crashLog); + } + + Toolbar myToolbar = findViewById(R.id.toolbar); + myToolbar.setTitle(getString(R.string.crash_reporter)); + setSupportActionBar(myToolbar); + + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + getAppInfo(); + } + + private void getAppInfo() { + appInfo.setText(AppUtils.getDeviceDetails(this)); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.crash_detail_menu, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + Intent intent = getIntent(); + String filePath = null; + if (intent != null) { + filePath = intent.getStringExtra("LogMessage"); + } + + if (item.getItemId() == R.id.delete_log) { + if (FileUtils.delete(filePath)) { + finish(); + } + return true; + } else if (item.getItemId() == R.id.share_crash_log) { + shareCrashReport(filePath); + return true; + } else { + return super.onOptionsItemSelected(item); + } + } + + private void shareCrashReport(String filePath) { + Uri uri = FileProvider.getUriForFile(this, + this.getApplicationContext().getPackageName() + ".fileprovider", + new File(filePath)); + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType("*/*"); + intent.putExtra(Intent.EXTRA_TEXT, appInfo.getText().toString()); + intent.putExtra(Intent.EXTRA_STREAM, uri); + startActivity(Intent.createChooser(intent, "Share via")); + } +} diff --git a/crashreporter/src/main/java/com/balsikandar/crashreporter/utils/AppUtils.java b/crashreporter/src/main/java/com/balsikandar/crashreporter/utils/AppUtils.java new file mode 100644 index 00000000..c304dae1 --- /dev/null +++ b/crashreporter/src/main/java/com/balsikandar/crashreporter/utils/AppUtils.java @@ -0,0 +1,106 @@ +package com.balsikandar.crashreporter.utils; + +import android.Manifest; +import android.accounts.Account; +import android.accounts.AccountManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Build; +import android.util.Log; + +import androidx.core.app.ActivityCompat; + +import java.util.TimeZone; +import java.util.UUID; + +/** + * Created by bali on 12/08/17. + */ + +public class AppUtils { + private static String getCurrentLauncherApp(Context context) { + String str = ""; + PackageManager localPackageManager = context.getPackageManager(); + Intent intent = new Intent("android.intent.action.MAIN"); + intent.addCategory("android.intent.category.HOME"); + try { + ResolveInfo resolveInfo = localPackageManager.resolveActivity(intent, + PackageManager.MATCH_DEFAULT_ONLY); + if (resolveInfo != null && resolveInfo.activityInfo != null) { + str = resolveInfo.activityInfo.packageName; + } + } catch (Exception e) { + Log.e("AppUtils", "Exception : " + e.getMessage()); + } + return str; + } + + public static String getDeviceDetails(Context context) { + + return "Device Information\n" + + "\nDEVICE.ID : " + getDeviceId(context) + + "\nAPP.VERSION : " + getAppVersion(context) + + "\nLAUNCHER.APP : " + getCurrentLauncherApp(context) + + "\nTIMEZONE : " + timeZone() + + "\nVERSION.RELEASE : " + Build.VERSION.RELEASE + + "\nVERSION.INCREMENTAL : " + Build.VERSION.INCREMENTAL + + "\nVERSION.SDK.NUMBER : " + Build.VERSION.SDK_INT + + "\nBOARD : " + Build.BOARD + + "\nBOOTLOADER : " + Build.BOOTLOADER + + "\nBRAND : " + Build.BRAND + + "\nCPU_ABI : " + Build.CPU_ABI + + "\nCPU_ABI2 : " + Build.CPU_ABI2 + + "\nDISPLAY : " + Build.DISPLAY + + "\nFINGERPRINT : " + Build.FINGERPRINT + + "\nHARDWARE : " + Build.HARDWARE + + "\nHOST : " + Build.HOST + + "\nID : " + Build.ID + + "\nMANUFACTURER : " + Build.MANUFACTURER + + "\nMODEL : " + Build.MODEL + + "\nPRODUCT : " + Build.PRODUCT + + "\nSERIAL : " + Build.SERIAL + + "\nTAGS : " + Build.TAGS + + "\nTIME : " + Build.TIME + + "\nTYPE : " + Build.TYPE + + "\nUNKNOWN : " + Build.UNKNOWN + + "\nUSER : " + Build.USER; + } + + private static String timeZone() { + TimeZone tz = TimeZone.getDefault(); + return tz.getID(); + } + + private static String getDeviceId(Context context) { + String androidDeviceId = getAndroidDeviceId(context); + if (androidDeviceId == null) + androidDeviceId = UUID.randomUUID().toString(); + return androidDeviceId; + + } + + private static String getAndroidDeviceId(Context context) { + final String INVALID_ANDROID_ID = "9774d56d682e549c"; + final String androidId = android.provider.Settings.Secure.getString( + context.getContentResolver(), + android.provider.Settings.Secure.ANDROID_ID); + if (androidId == null + || androidId.toLowerCase().equals(INVALID_ANDROID_ID)) { + return null; + } + return androidId; + } + + private static int getAppVersion(Context context) { + try { + PackageInfo packageInfo = context.getPackageManager() + .getPackageInfo(context.getPackageName(), 0); + return packageInfo.versionCode; + } catch (PackageManager.NameNotFoundException e) { + throw new RuntimeException("Could not get package name: " + e); + } + } +} diff --git a/crashreporter/src/main/java/com/balsikandar/crashreporter/utils/Constants.java b/crashreporter/src/main/java/com/balsikandar/crashreporter/utils/Constants.java new file mode 100644 index 00000000..14c68fa3 --- /dev/null +++ b/crashreporter/src/main/java/com/balsikandar/crashreporter/utils/Constants.java @@ -0,0 +1,15 @@ +package com.balsikandar.crashreporter.utils; + +/** + * Created by bali on 15/08/17. + */ + +public class Constants { + public static final String EXCEPTION_SUFFIX = "_exception"; + public static final String CRASH_SUFFIX = "_crash"; + public static final String FILE_EXTENSION = ".txt"; + public static final String CRASH_REPORT_DIR = "crashReports"; + public static final int NOTIFICATION_ID = 1; + public static final String CHANNEL_NOTIFICATION_ID = "crashreporter_channel_id"; + public static final String LANDING = "landing"; +} diff --git a/crashreporter/src/main/java/com/balsikandar/crashreporter/utils/CrashReporterException.java b/crashreporter/src/main/java/com/balsikandar/crashreporter/utils/CrashReporterException.java new file mode 100644 index 00000000..ede6f5fd --- /dev/null +++ b/crashreporter/src/main/java/com/balsikandar/crashreporter/utils/CrashReporterException.java @@ -0,0 +1,64 @@ +package com.balsikandar.crashreporter.utils; + +/** + * Created by bali on 02/08/17. + */ + +/** + * Represents an error condition specific to the Crash Reporter for Android. + */ +public class CrashReporterException extends RuntimeException { + static final long serialVersionUID = 1; + + /** + * Constructs a new CrashReporterException. + */ + public CrashReporterException() { + super(); + } + + /** + * Constructs a new CrashReporterException. + * + * @param message the detail message of this exception + */ + public CrashReporterException(String message) { + super(message); + } + + /** + * Constructs a new CrashReporterException. + * + * @param format the format string (see {@link java.util.Formatter#format}) + * @param args the list of arguments passed to the formatter. + */ + public CrashReporterException(String format, Object... args) { + this(String.format(format, args)); + } + + /** + * Constructs a new CrashReporterException. + * + * @param message the detail message of this exception + * @param throwable the cause of this exception + */ + public CrashReporterException(String message, Throwable throwable) { + super(message, throwable); + } + + /** + * Constructs a new CrashReporterException. + * + * @param throwable the cause of this exception + */ + public CrashReporterException(Throwable throwable) { + super(throwable); + } + + @Override + public String toString() { + // Throwable.toString() returns "CrashReporterException:{message}". Returning just "{message}" + // should be fine here. + return getMessage(); + } +} diff --git a/crashreporter/src/main/java/com/balsikandar/crashreporter/utils/CrashReporterExceptionHandler.java b/crashreporter/src/main/java/com/balsikandar/crashreporter/utils/CrashReporterExceptionHandler.java new file mode 100644 index 00000000..b2e25391 --- /dev/null +++ b/crashreporter/src/main/java/com/balsikandar/crashreporter/utils/CrashReporterExceptionHandler.java @@ -0,0 +1,18 @@ +package com.balsikandar.crashreporter.utils; + +public class CrashReporterExceptionHandler implements Thread.UncaughtExceptionHandler { + + private Thread.UncaughtExceptionHandler exceptionHandler; + + public CrashReporterExceptionHandler() { + this.exceptionHandler = Thread.getDefaultUncaughtExceptionHandler(); + } + + @Override + public void uncaughtException(Thread thread, Throwable throwable) { + + CrashUtil.saveCrashReport(throwable); + + exceptionHandler.uncaughtException(thread, throwable); + } +} diff --git a/crashreporter/src/main/java/com/balsikandar/crashreporter/utils/CrashReporterNotInitializedException.java b/crashreporter/src/main/java/com/balsikandar/crashreporter/utils/CrashReporterNotInitializedException.java new file mode 100644 index 00000000..5b6d35a5 --- /dev/null +++ b/crashreporter/src/main/java/com/balsikandar/crashreporter/utils/CrashReporterNotInitializedException.java @@ -0,0 +1,47 @@ +package com.balsikandar.crashreporter.utils; + +/** + * Created by bali on 02/08/17. + */ + +/** + * An Exception indicating that the Crash Reporter has not been correctly initialized. + */ +public class CrashReporterNotInitializedException extends CrashReporterException { + static final long serialVersionUID = 1; + + /** + * Constructs a CrashReporterNotInitializedException with no additional information. + */ + public CrashReporterNotInitializedException() { + super(); + } + + /** + * Constructs a CrashReporterNotInitializedException with a message. + * + * @param message A String to be returned from getMessage. + */ + public CrashReporterNotInitializedException(String message) { + super(message); + } + + /** + * Constructs a CrashReporterNotInitializedException with a message and inner error. + * + * @param message A String to be returned from getMessage. + * @param throwable A Throwable to be returned from getCause. + */ + public CrashReporterNotInitializedException(String message, Throwable throwable) { + super(message, throwable); + } + + /** + * Constructs a CrashReporterNotInitializedException with an inner error. + * + * @param throwable A Throwable to be returned from getCause. + */ + public CrashReporterNotInitializedException(Throwable throwable) { + super(throwable); + } +} \ No newline at end of file diff --git a/crashreporter/src/main/java/com/balsikandar/crashreporter/utils/CrashUtil.java b/crashreporter/src/main/java/com/balsikandar/crashreporter/utils/CrashUtil.java new file mode 100644 index 00000000..aa6b601f --- /dev/null +++ b/crashreporter/src/main/java/com/balsikandar/crashreporter/utils/CrashUtil.java @@ -0,0 +1,154 @@ +package com.balsikandar.crashreporter.utils; + +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.text.TextUtils; +import android.util.Log; + +import androidx.core.app.NotificationCompat; +import androidx.core.content.ContextCompat; + +import com.balsikandar.crashreporter.CrashReporter; +import de.mm20.launcher2.crashreporter.R; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +import static android.content.Context.NOTIFICATION_SERVICE; +import static com.balsikandar.crashreporter.utils.Constants.CHANNEL_NOTIFICATION_ID; + +public class CrashUtil { + + private static final String TAG = CrashUtil.class.getSimpleName(); + + private CrashUtil() { + //this class is not publicly instantiable + } + + private static String getCrashLogTime() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); + return dateFormat.format(new Date()); + } + + public static void saveCrashReport(final Throwable throwable) { + + String crashReportPath = CrashReporter.getCrashReportPath(); + String filename = getCrashLogTime() + Constants.CRASH_SUFFIX + Constants.FILE_EXTENSION; + writeToFile(crashReportPath, filename, getStackTrace(throwable)); + + showNotification(throwable.getLocalizedMessage(), true); + } + + public static void logException(final Exception exception) { + + new Thread(new Runnable() { + @Override + public void run() { + + String crashReportPath = CrashReporter.getCrashReportPath(); + final String filename = getCrashLogTime() + Constants.EXCEPTION_SUFFIX + Constants.FILE_EXTENSION; + writeToFile(crashReportPath, filename, getStackTrace(exception)); + + //showNotification(exception.getLocalizedMessage(), false); + } + }).start(); + } + + private static void writeToFile(String crashReportPath, String filename, String crashLog) { + + if (TextUtils.isEmpty(crashReportPath)) { + crashReportPath = getDefaultPath(); + } + + File crashDir = new File(crashReportPath); + if (!crashDir.exists() || !crashDir.isDirectory()) { + crashReportPath = getDefaultPath(); + Log.e(TAG, "Path provided doesn't exists : " + crashDir + "\nSaving crash report at : " + getDefaultPath()); + } + + BufferedWriter bufferedWriter; + try { + bufferedWriter = new BufferedWriter(new FileWriter( + crashReportPath + File.separator + filename)); + + bufferedWriter.write(crashLog); + bufferedWriter.flush(); + bufferedWriter.close(); + Log.d(TAG, "crash report saved in : " + crashReportPath); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private static void showNotification(String localisedMsg, boolean isCrash) { + + if (CrashReporter.isNotificationEnabled()) { + Context context = CrashReporter.getContext(); + NotificationManager notificationManager = (NotificationManager) context. + getSystemService(NOTIFICATION_SERVICE); + createNotificationChannel(notificationManager, context); + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_NOTIFICATION_ID); + builder.setSmallIcon(R.drawable.ic_warning_black_24dp); + + Intent intent = CrashReporter.getLaunchIntent(); + intent.putExtra(Constants.LANDING, isCrash); + intent.setAction(Long.toString(System.currentTimeMillis())); + + PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0); + builder.setContentIntent(pendingIntent); + + builder.setContentTitle(context.getString(R.string.view_crash_report)); + + if (TextUtils.isEmpty(localisedMsg)) { + builder.setContentText(context.getString(R.string.check_your_message_here)); + } else { + builder.setContentText(localisedMsg); + } + + builder.setAutoCancel(true); + builder.setColor(ContextCompat.getColor(context, R.color.colorAccent_CrashReporter)); + + notificationManager.notify(Constants.NOTIFICATION_ID, builder.build()); + } + } + + private static void createNotificationChannel(NotificationManager notificationManager, Context context) { + if (Build.VERSION.SDK_INT >= 26) { + CharSequence name = context.getString(R.string.notification_crash_report_title); + String description = ""; + NotificationChannel channel = new NotificationChannel(CHANNEL_NOTIFICATION_ID, name, NotificationManager.IMPORTANCE_DEFAULT); + channel.setDescription(description); + notificationManager.createNotificationChannel(channel); + } + } + + private static String getStackTrace(Throwable e) { + final Writer result = new StringWriter(); + final PrintWriter printWriter = new PrintWriter(result); + + e.printStackTrace(printWriter); + String crashLog = result.toString(); + printWriter.close(); + return crashLog; + } + + public static String getDefaultPath() { + String defaultPath = CrashReporter.getContext().getExternalFilesDir(null).getAbsolutePath() + + File.separator + Constants.CRASH_REPORT_DIR; + + File file = new File(defaultPath); + file.mkdirs(); + return defaultPath; + } +} diff --git a/crashreporter/src/main/java/com/balsikandar/crashreporter/utils/FileUtils.java b/crashreporter/src/main/java/com/balsikandar/crashreporter/utils/FileUtils.java new file mode 100644 index 00000000..7a91d8d8 --- /dev/null +++ b/crashreporter/src/main/java/com/balsikandar/crashreporter/utils/FileUtils.java @@ -0,0 +1,119 @@ +package com.balsikandar.crashreporter.utils; + +import android.text.TextUtils; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; + +/** + * Created by bali on 10/08/17. + */ + +public class FileUtils { + + public static final String TAG = FileUtils.class.getSimpleName(); + + private FileUtils() { + //this class is not publicly instantiable + } + + public static boolean delete(String absPath) { + if (TextUtils.isEmpty(absPath)) { + return false; + } + + File file = new File(absPath); + return delete(file); + } + + public static boolean delete(File file) { + if (!exists(file)) { + return true; + } + + if (file.isFile()) { + return file.delete(); + } + + boolean result = true; + File files[] = file.listFiles(); + if (files == null) return false; + for (int index = 0; index < files.length; index++) { + result |= delete(files[index]); + } + result |= file.delete(); + + return result; + } + + public static boolean exists(File file) { + return file != null && file.exists(); + } + + public static String cleanPath(String absPath) { + if (TextUtils.isEmpty(absPath)) { + return absPath; + } + try { + File file = new File(absPath); + absPath = file.getCanonicalPath(); + } catch (Exception e) { + + } + return absPath; + } + + public final static String getParent(File file) { + return file == null ? null : file.getParent(); + } + + public final static String getParent(String absPath) { + if (TextUtils.isEmpty(absPath)) { + return null; + } + absPath = cleanPath(absPath); + File file = new File(absPath); + return getParent(file); + } + + public static boolean deleteFiles(String directoryPath) { + String directoryToDelete; + if (!TextUtils.isEmpty(directoryPath)) { + directoryToDelete = directoryPath; + } else { + directoryToDelete = CrashUtil.getDefaultPath(); + } + + return delete(directoryToDelete); + } + + public static String readFirstLineFromFile(File file) { + String line = ""; + try { + BufferedReader reader = new BufferedReader(new FileReader(file)); + line = reader.readLine(); + reader.close(); + } catch (IOException e) { + e.printStackTrace(); + } + return line; + } + + public static String readFromFile(File file) { + StringBuilder crash = new StringBuilder(); + try { + BufferedReader reader = new BufferedReader(new FileReader(file)); + String line; + while ((line = reader.readLine()) != null) { + crash.append(line); + crash.append('\n'); + } + reader.close(); + } catch (IOException e) { + e.printStackTrace(); + } + return crash.toString(); + } +} diff --git a/crashreporter/src/main/java/com/balsikandar/crashreporter/utils/SimplePageChangeListener.java b/crashreporter/src/main/java/com/balsikandar/crashreporter/utils/SimplePageChangeListener.java new file mode 100644 index 00000000..e452d920 --- /dev/null +++ b/crashreporter/src/main/java/com/balsikandar/crashreporter/utils/SimplePageChangeListener.java @@ -0,0 +1,17 @@ +package com.balsikandar.crashreporter.utils; + + +import androidx.viewpager.widget.ViewPager; + +/** + * Created by bali on 11/08/17. + */ + +public abstract class SimplePageChangeListener implements ViewPager.OnPageChangeListener { + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {} + @Override + public abstract void onPageSelected(int position); + @Override + public void onPageScrollStateChanged(int state) {} +} diff --git a/crashreporter/src/main/java/de/mm20/launcher2/crashreporter/CrashReporter.kt b/crashreporter/src/main/java/de/mm20/launcher2/crashreporter/CrashReporter.kt new file mode 100644 index 00000000..9ccaf905 --- /dev/null +++ b/crashreporter/src/main/java/de/mm20/launcher2/crashreporter/CrashReporter.kt @@ -0,0 +1,14 @@ +package de.mm20.launcher2.crashreporter + +import android.content.Intent +import android.util.Log + +object CrashReporter { + fun logException(e: Exception) { + com.balsikandar.crashreporter.CrashReporter.logException(e) + Log.e("MM20", Log.getStackTraceString(e)) + } + fun getLaunchIntent() : Intent { + return com.balsikandar.crashreporter.CrashReporter.getLaunchIntent() + } +} \ No newline at end of file diff --git a/crashreporter/src/main/res/drawable/ic_menu_delete_white_24dp.xml b/crashreporter/src/main/res/drawable/ic_menu_delete_white_24dp.xml new file mode 100644 index 00000000..21af9a57 --- /dev/null +++ b/crashreporter/src/main/res/drawable/ic_menu_delete_white_24dp.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/crashreporter/src/main/res/drawable/ic_menu_share_white_24dp.xml b/crashreporter/src/main/res/drawable/ic_menu_share_white_24dp.xml new file mode 100644 index 00000000..01726c74 --- /dev/null +++ b/crashreporter/src/main/res/drawable/ic_menu_share_white_24dp.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/crashreporter/src/main/res/drawable/ic_search_white_24dp.xml b/crashreporter/src/main/res/drawable/ic_search_white_24dp.xml new file mode 100644 index 00000000..91972807 --- /dev/null +++ b/crashreporter/src/main/res/drawable/ic_search_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/crashreporter/src/main/res/drawable/ic_warning_black_24dp.xml b/crashreporter/src/main/res/drawable/ic_warning_black_24dp.xml new file mode 100644 index 00000000..7c69ee84 --- /dev/null +++ b/crashreporter/src/main/res/drawable/ic_warning_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/crashreporter/src/main/res/layout/activity_log_message.xml b/crashreporter/src/main/res/layout/activity_log_message.xml new file mode 100644 index 00000000..bd16c5e8 --- /dev/null +++ b/crashreporter/src/main/res/layout/activity_log_message.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + diff --git a/crashreporter/src/main/res/layout/crash_log.xml b/crashreporter/src/main/res/layout/crash_log.xml new file mode 100644 index 00000000..21025eda --- /dev/null +++ b/crashreporter/src/main/res/layout/crash_log.xml @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/crashreporter/src/main/res/layout/crash_reporter_activity.xml b/crashreporter/src/main/res/layout/crash_reporter_activity.xml new file mode 100644 index 00000000..59e46bf7 --- /dev/null +++ b/crashreporter/src/main/res/layout/crash_reporter_activity.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + diff --git a/crashreporter/src/main/res/layout/custom_item.xml b/crashreporter/src/main/res/layout/custom_item.xml new file mode 100644 index 00000000..110486c3 --- /dev/null +++ b/crashreporter/src/main/res/layout/custom_item.xml @@ -0,0 +1,41 @@ + + + + + + + + + \ No newline at end of file diff --git a/crashreporter/src/main/res/layout/exception_log.xml b/crashreporter/src/main/res/layout/exception_log.xml new file mode 100644 index 00000000..86d642fc --- /dev/null +++ b/crashreporter/src/main/res/layout/exception_log.xml @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/crashreporter/src/main/res/menu/crash_detail_menu.xml b/crashreporter/src/main/res/menu/crash_detail_menu.xml new file mode 100644 index 00000000..977da47a --- /dev/null +++ b/crashreporter/src/main/res/menu/crash_detail_menu.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/crashreporter/src/main/res/menu/log_main_menu.xml b/crashreporter/src/main/res/menu/log_main_menu.xml new file mode 100644 index 00000000..5a0b306b --- /dev/null +++ b/crashreporter/src/main/res/menu/log_main_menu.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/crashreporter/src/main/res/values/colors.xml b/crashreporter/src/main/res/values/colors.xml new file mode 100644 index 00000000..451218ab --- /dev/null +++ b/crashreporter/src/main/res/values/colors.xml @@ -0,0 +1,7 @@ + + + #ff4081 + #f50057 + #cf1162 + #000000 + \ No newline at end of file diff --git a/crashreporter/src/main/res/values/strings.xml b/crashreporter/src/main/res/values/strings.xml new file mode 100644 index 00000000..e1bb35f0 --- /dev/null +++ b/crashreporter/src/main/res/values/strings.xml @@ -0,0 +1,11 @@ + + CrashReporter + Crashes + Exceptions + View Crash Report + Crash Reporter notifications + Check your crashes and exceptions here. + Are you sure to delete all the crash logs + CANCEL + OK + diff --git a/crashreporter/src/main/res/values/styles.xml b/crashreporter/src/main/res/values/styles.xml new file mode 100644 index 00000000..d08da4cc --- /dev/null +++ b/crashreporter/src/main/res/values/styles.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/currencies/.gitignore b/currencies/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/currencies/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/currencies/build.gradle.kts b/currencies/build.gradle.kts new file mode 100644 index 00000000..220ef135 --- /dev/null +++ b/currencies/build.gradle.kts @@ -0,0 +1,52 @@ +plugins { + id("com.android.library") + id("kotlin-android") + id("kotlin-android-extensions") +} + +android { + compileSdk = sdk.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = sdk.versions.minSdk.get().toInt() + targetSdk = sdk.versions.targetSdk.get().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + + implementation(libs.bundles.kotlin) + implementation(libs.androidx.core) + implementation(libs.androidx.appcompat) + implementation(libs.materialcomponents) + implementation(libs.androidx.work) + + implementation(libs.okhttp) + + implementation(project(":ktx")) + implementation(project(":i18n")) + implementation(project(":database")) + implementation(project(":crashreporter")) +} \ No newline at end of file diff --git a/currencies/consumer-rules.pro b/currencies/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/currencies/proguard-rules.pro b/currencies/proguard-rules.pro new file mode 100644 index 00000000..6cad0a65 --- /dev/null +++ b/currencies/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.kts.kts.kts.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/currencies/src/main/AndroidManifest.xml b/currencies/src/main/AndroidManifest.xml new file mode 100644 index 00000000..78f9c468 --- /dev/null +++ b/currencies/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/currencies/src/main/java/de/mm20/launcher2/currencies/Currency.kt b/currencies/src/main/java/de/mm20/launcher2/currencies/Currency.kt new file mode 100644 index 00000000..c873faee --- /dev/null +++ b/currencies/src/main/java/de/mm20/launcher2/currencies/Currency.kt @@ -0,0 +1,19 @@ +package de.mm20.launcher2.currencies + +import de.mm20.launcher2.database.entities.CurrencyEntity + +data class Currency( + val symbol: String, + val value: Double, + val lastUpdate: Long +) { + constructor(entity: CurrencyEntity) : this( + symbol = entity.symbol, + value = entity.value, + lastUpdate = entity.lastUpdate + ) + + fun toDatabaseEntity(): CurrencyEntity { + return CurrencyEntity(symbol, value, lastUpdate) + } +} \ No newline at end of file diff --git a/currencies/src/main/java/de/mm20/launcher2/currencies/CurrencyRepository.kt b/currencies/src/main/java/de/mm20/launcher2/currencies/CurrencyRepository.kt new file mode 100644 index 00000000..864ac1d3 --- /dev/null +++ b/currencies/src/main/java/de/mm20/launcher2/currencies/CurrencyRepository.kt @@ -0,0 +1,109 @@ +package de.mm20.launcher2.currencies + +import android.content.Context +import android.util.Log +import androidx.work.* +import de.mm20.launcher2.database.AppDatabase +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.util.concurrent.TimeUnit + +class CurrencyRepository(val context: Context) { + + init { + val currencyWorker = PeriodicWorkRequest.Builder(ExchangeRateWorker::class.java, 60, TimeUnit.MINUTES) + .build() + WorkManager.getInstance().enqueueUniquePeriodicWork("ExchangeRates", + ExistingPeriodicWorkPolicy.REPLACE, currencyWorker) + } + + suspend fun convertCurrency(fromCurrency: String, value: Double, toCurrency: String? = null): List> { + + return withContext>>(Dispatchers.IO) { + val dao = AppDatabase.getInstance(context) + .currencyDao() + + val from = Currency(dao.getCurrency(fromCurrency) ?: return@withContext emptyList()) + + return@withContext if (toCurrency == null) { + dao.getAllCurrencies().mapNotNull { + val to = Currency(it) + if (from.lastUpdate != to.lastUpdate) { + Log.w("MM20", "Exchange rate update dates do not match: $fromCurrency, $it") + return@mapNotNull null + } + if (from.symbol == to.symbol) return@mapNotNull null + to.symbol to value * to.value / from.value + } + } else { + val to = Currency(dao.getCurrency(toCurrency) ?: return@withContext emptyList()) + if (from.lastUpdate != to.lastUpdate) { + Log.w("MM20", "Exchange rate update dates do not match: $fromCurrency, $toCurrency") + return@withContext emptyList() + } + listOf(toCurrency to value * to.value / from.value) + } + } + } + + fun getFlag(currencySymbol: String): String { + return when (currencySymbol) { + "EUR" -> "\uD83C\uDDEA\uD83C\uDDFA" // European Union + "USD" -> "\uD83C\uDDFA\uD83C\uDDF8" // United States + "JPY" -> "\uD83C\uDDEF\uD83C\uDDF5" // Japan + "GBP" -> "\uD83C\uDDEC\uD83C\uDDE7" // United Kingdom + "AUD" -> "\uD83C\uDDE6\uD83C\uDDFA" // Australia + "CAD" -> "\uD83C\uDDE8\uD83C\uDDE6" // Canada + "CHF" -> "\uD83C\uDDE8\uD83C\uDDED" // Switzerland + "CNY" -> "\uD83C\uDDE8\uD83C\uDDF3" // China + "SEK" -> "\uD83C\uDDF8\uD83C\uDDEA" // Sweden + "NZD" -> "\uD83C\uDDF3\uD83C\uDDFF" // New Zealand + + "HKD" -> "\uD83C\uDDED\uD83C\uDDF0" // Hong Kong + "IDR" -> "\uD83C\uDDEE\uD83C\uDDE9" // Indonesia + "ILS" -> "\uD83C\uDDEE\uD83C\uDDF1" // Israel + "DKK" -> "\uD83C\uDDE9\uD83C\uDDF0" // Denmark + "INR" -> "\uD83C\uDDEE\uD83C\uDDF3" // India + "MXN" -> "\uD83C\uDDF2\uD83C\uDDFD" // Mexico + "CZK" -> "\uD83C\uDDE8\uD83C\uDDFF" // Czechia + "SGD" -> "\uD83C\uDDF8\uD83C\uDDEC" // Singapore + "THB" -> "\uD83C\uDDF9\uD83C\uDDED" // Thailand + "HRK" -> "\uD83C\uDDED\uD83C\uDDF7" // Croatia + "MYR" -> "\uD83C\uDDF2\uD83C\uDDFE" // Malaysia + "NOK" -> "\uD83C\uDDF3\uD83C\uDDF4" // Norway + "BGN" -> "\uD83C\uDDE7\uD83C\uDDEC" // Bulgaria + "PHP" -> "\uD83C\uDDF5\uD83C\uDDED" // Philippines + "PLN" -> "\uD83C\uDDF5\uD83C\uDDF1" // Poland + "ZAR" -> "\uD83C\uDDFF\uD83C\uDDE6" // South Africa + "ISK" -> "\uD83C\uDDEE\uD83C\uDDF8" // Iceland + "BRL" -> "\uD83C\uDDE7\uD83C\uDDF7" // Brazil + "RON" -> "\uD83C\uDDF7\uD83C\uDDF4" // Romania + "TRY" -> "\uD83C\uDDF9\uD83C\uDDF7" // Turkey + "RUB" -> "\uD83C\uDDF7\uD83C\uDDFA" // Russia + "KRW" -> "\uD83C\uDDF0\uD83C\uDDF7" // South Korea + "HUF" -> "\uD83C\uDDED\uD83C\uDDFA" // Hungary + + else -> "" + } + } + + suspend fun isValidCurrency(symbol: String): Boolean { + return withContext(Dispatchers.IO) { + AppDatabase.getInstance(context).currencyDao().exists(symbol) + } + } + + suspend fun getLastUpdate(symbol: String): Long { + return withContext(Dispatchers.IO) { + AppDatabase.getInstance(context).currencyDao().getLastUpdate(symbol) + } + } + + companion object { + private lateinit var instance: CurrencyRepository + fun getInstance(context: Context): CurrencyRepository { + if (!::instance.isInitialized) instance = CurrencyRepository(context.applicationContext) + return instance + } + } +} \ No newline at end of file diff --git a/currencies/src/main/java/de/mm20/launcher2/currencies/ExchangeRateWorker.kt b/currencies/src/main/java/de/mm20/launcher2/currencies/ExchangeRateWorker.kt new file mode 100644 index 00000000..65b2bbd6 --- /dev/null +++ b/currencies/src/main/java/de/mm20/launcher2/currencies/ExchangeRateWorker.kt @@ -0,0 +1,56 @@ +package de.mm20.launcher2.currencies + +import android.content.Context +import android.util.Log +import androidx.work.Worker +import androidx.work.WorkerParameters +import de.mm20.launcher2.crashreporter.CrashReporter +import de.mm20.launcher2.database.AppDatabase +import okhttp3.OkHttpClient +import okhttp3.Request +import org.w3c.dom.Element +import java.text.SimpleDateFormat +import javax.xml.parsers.DocumentBuilderFactory + +class ExchangeRateWorker(val context: Context, params: WorkerParameters) : Worker(context, params) { + override fun doWork(): Result { + Log.d("MM20", "Updating currency exchange rates") + val httpClient = OkHttpClient() + val request = Request.Builder() + .url("https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml") + .get() + .build() + try { + val response = httpClient.newCall(request).execute() + val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(response.body?.byteStream() + ?: return Result.retry()) + val cubes = document.getElementsByTagName("Cube") + val values = mutableListOf>() + var timestamp = System.currentTimeMillis() + values += "EUR" to 1.0 + for (i in 0 until cubes.length) { + val cube = cubes.item(i) as? Element ?: continue + if (cube.hasAttribute("currency")) { + val symbol = cube.getAttribute("currency") + val value = cube.getAttribute("rate").toDoubleOrNull() ?: continue + values += symbol to value + } else if (cube.hasAttribute("time")) { + val date = cube.getAttribute("time") + timestamp = SimpleDateFormat("yyyy-MM-dd").parse(date).time + } + } + val currencies = values.map { + Currency( + symbol = it.first, + value = it.second, + lastUpdate = timestamp + ).toDatabaseEntity() + } + AppDatabase.getInstance(context).currencyDao().insertAll(currencies) + return Result.success() + } catch (e: Exception) { + CrashReporter.logException(e) + return Result.retry() + } + } +} \ No newline at end of file diff --git a/database/.gitignore b/database/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/database/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/database/build.gradle.kts b/database/build.gradle.kts new file mode 100644 index 00000000..ee81a793 --- /dev/null +++ b/database/build.gradle.kts @@ -0,0 +1,57 @@ +plugins { + id("com.android.library") + id("kotlin-android") + id("kotlin-android-extensions") + id("kotlin-kapt") +} + +android { + compileSdk = sdk.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = sdk.versions.minSdk.get().toInt() + targetSdk = sdk.versions.targetSdk.get().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + + javaCompileOptions { + annotationProcessorOptions { + arguments.put("room.schemaLocation", "$projectDir/schemas") + } + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + + implementation(libs.kotlin.stdlib) + implementation(libs.androidx.core) + implementation(libs.androidx.appcompat) + api(libs.androidx.roomruntime) + kapt(libs.androidx.roomcompiler) + implementation(libs.androidx.room) + + implementation(project(":i18n")) + implementation(project(":ktx")) + +} \ No newline at end of file diff --git a/database/consumer-rules.pro b/database/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/database/proguard-rules.pro b/database/proguard-rules.pro new file mode 100644 index 00000000..01639a19 --- /dev/null +++ b/database/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.kts.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/database/readme.md b/database/readme.md new file mode 100644 index 00000000..8a61e7d4 --- /dev/null +++ b/database/readme.md @@ -0,0 +1,4 @@ +# :database + +The central database module for everything that needs to be stored in an SQLite database. Uses +AndroidX Room. \ No newline at end of file diff --git a/database/schemas/de.mm20.launcher2.database.AppDatabase/0.json b/database/schemas/de.mm20.launcher2.database.AppDatabase/0.json new file mode 100644 index 00000000..6a0172ee --- /dev/null +++ b/database/schemas/de.mm20.launcher2.database.AppDatabase/0.json @@ -0,0 +1,218 @@ +{ + "formatVersion": 1, + "database": { + "version": 0, + "identityHash": "d7a9ad5e5af58e6f63ac1a9a90f2b6b1", + "entities": [ + { + "tableName": "forecasts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `temperature` REAL NOT NULL, `minTemp` REAL NOT NULL, `maxTemp` REAL NOT NULL, `pressure` REAL NOT NULL, `humidity` REAL NOT NULL, `icon` INTEGER NOT NULL, `condition` TEXT NOT NULL, `clouds` INTEGER NOT NULL, `windSpeed` REAL NOT NULL, `windDirection` REAL NOT NULL, `rain` REAL NOT NULL, `snow` REAL NOT NULL, `night` INTEGER NOT NULL, `location` TEXT NOT NULL, `provider` TEXT NOT NULL, `providerUrl` TEXT NOT NULL, PRIMARY KEY(`timestamp`))", + "fields": [ + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "temperature", + "columnName": "temperature", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "minTemp", + "columnName": "minTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "maxTemp", + "columnName": "maxTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "pressure", + "columnName": "pressure", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "humidity", + "columnName": "humidity", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "condition", + "columnName": "condition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clouds", + "columnName": "clouds", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "windSpeed", + "columnName": "windSpeed", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "windDirection", + "columnName": "windDirection", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rain", + "columnName": "rain", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snow", + "columnName": "snow", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "night", + "columnName": "night", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "provider", + "columnName": "provider", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "providerUrl", + "columnName": "providerUrl", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "timestamp" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Searchable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `searchable` TEXT NOT NULL, `launchCount` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `hidden` INTEGER NOT NULL, `inAllApps` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "searchable", + "columnName": "searchable", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "launchCount", + "columnName": "launchCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inAllApps", + "columnName": "inAllApps", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Websearch", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`urlTemplate` TEXT NOT NULL, `label` TEXT NOT NULL, `color` INTEGER NOT NULL, `icon` TEXT, PRIMARY KEY(`urlTemplate`))", + "fields": [ + { + "fieldPath": "urlTemplate", + "columnName": "urlTemplate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "urlTemplate" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"d7a9ad5e5af58e6f63ac1a9a90f2b6b1\")" + ] + } +} \ No newline at end of file diff --git a/database/schemas/de.mm20.launcher2.database.AppDatabase/1.json b/database/schemas/de.mm20.launcher2.database.AppDatabase/1.json new file mode 100644 index 00000000..f0bbc8ab --- /dev/null +++ b/database/schemas/de.mm20.launcher2.database.AppDatabase/1.json @@ -0,0 +1,224 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "a28f601f4a9dee37e9a41f895f3ac512", + "entities": [ + { + "tableName": "forecasts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `temperature` REAL NOT NULL, `minTemp` REAL NOT NULL, `maxTemp` REAL NOT NULL, `pressure` REAL NOT NULL, `humidity` REAL NOT NULL, `icon` INTEGER NOT NULL, `condition` TEXT NOT NULL, `clouds` INTEGER NOT NULL, `windSpeed` REAL NOT NULL, `windDirection` REAL NOT NULL, `rain` REAL NOT NULL, `snow` REAL NOT NULL, `night` INTEGER NOT NULL, `location` TEXT NOT NULL, `provider` TEXT NOT NULL, `providerUrl` TEXT NOT NULL, PRIMARY KEY(`timestamp`))", + "fields": [ + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "temperature", + "columnName": "temperature", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "minTemp", + "columnName": "minTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "maxTemp", + "columnName": "maxTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "pressure", + "columnName": "pressure", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "humidity", + "columnName": "humidity", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "condition", + "columnName": "condition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clouds", + "columnName": "clouds", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "windSpeed", + "columnName": "windSpeed", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "windDirection", + "columnName": "windDirection", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rain", + "columnName": "rain", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snow", + "columnName": "snow", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "night", + "columnName": "night", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "provider", + "columnName": "provider", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "providerUrl", + "columnName": "providerUrl", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "timestamp" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Searchable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `searchable` TEXT NOT NULL, `launchCount` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `hidden` INTEGER NOT NULL, `inAllApps` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "searchable", + "columnName": "searchable", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "launchCount", + "columnName": "launchCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inAllApps", + "columnName": "inAllApps", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Websearch", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`urlTemplate` TEXT NOT NULL, `label` TEXT NOT NULL, `color` INTEGER NOT NULL, `icon` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "urlTemplate", + "columnName": "urlTemplate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"a28f601f4a9dee37e9a41f895f3ac512\")" + ] + } +} \ No newline at end of file diff --git a/database/schemas/de.mm20.launcher2.database.AppDatabase/10.json b/database/schemas/de.mm20.launcher2.database.AppDatabase/10.json new file mode 100644 index 00000000..33fda801 --- /dev/null +++ b/database/schemas/de.mm20.launcher2.database.AppDatabase/10.json @@ -0,0 +1,380 @@ +{ + "formatVersion": 1, + "database": { + "version": 10, + "identityHash": "05dab1b38f9c52d852630743e48eb4c9", + "entities": [ + { + "tableName": "forecasts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `temperature` REAL NOT NULL, `minTemp` REAL NOT NULL, `maxTemp` REAL NOT NULL, `pressure` REAL NOT NULL, `humidity` REAL NOT NULL, `icon` INTEGER NOT NULL, `condition` TEXT NOT NULL, `clouds` INTEGER NOT NULL, `windSpeed` REAL NOT NULL, `windDirection` REAL NOT NULL, `rain` REAL NOT NULL, `snow` REAL NOT NULL, `night` INTEGER NOT NULL, `location` TEXT NOT NULL, `provider` TEXT NOT NULL, `providerUrl` TEXT NOT NULL, `rainProbability` INTEGER NOT NULL, `snowProbability` INTEGER NOT NULL, `updateTime` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))", + "fields": [ + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "temperature", + "columnName": "temperature", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "minTemp", + "columnName": "minTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "maxTemp", + "columnName": "maxTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "pressure", + "columnName": "pressure", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "humidity", + "columnName": "humidity", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "condition", + "columnName": "condition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clouds", + "columnName": "clouds", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "windSpeed", + "columnName": "windSpeed", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "windDirection", + "columnName": "windDirection", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rain", + "columnName": "rain", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snow", + "columnName": "snow", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "night", + "columnName": "night", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "provider", + "columnName": "provider", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "providerUrl", + "columnName": "providerUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rainProbability", + "columnName": "rainProbability", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "snowProbability", + "columnName": "snowProbability", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updateTime", + "columnName": "updateTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "timestamp" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Searchable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `searchable` TEXT NOT NULL, `launchCount` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `hidden` INTEGER NOT NULL, `inAllApps` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serializedSearchable", + "columnName": "searchable", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "launchCount", + "columnName": "launchCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinPosition", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inAllApps", + "columnName": "inAllApps", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Websearch", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`urlTemplate` TEXT NOT NULL, `label` TEXT NOT NULL, `color` INTEGER NOT NULL, `icon` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "urlTemplate", + "columnName": "urlTemplate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Icons", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `componentName` TEXT, `drawable` TEXT, `iconPack` TEXT NOT NULL, `scale` REAL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "componentName", + "columnName": "componentName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "drawable", + "columnName": "drawable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "iconPack", + "columnName": "iconPack", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scale", + "columnName": "scale", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "IconPack", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `packageName` TEXT NOT NULL, `version` TEXT NOT NULL, `scale` REAL NOT NULL, PRIMARY KEY(`packageName`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scale", + "columnName": "scale", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "packageName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Widget", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `data` TEXT NOT NULL, `height` INTEGER NOT NULL, `position` INTEGER NOT NULL, `label` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "height", + "columnName": "height", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '05dab1b38f9c52d852630743e48eb4c9')" + ] + } +} \ No newline at end of file diff --git a/database/schemas/de.mm20.launcher2.database.AppDatabase/11.json b/database/schemas/de.mm20.launcher2.database.AppDatabase/11.json new file mode 100644 index 00000000..7c353bfe --- /dev/null +++ b/database/schemas/de.mm20.launcher2.database.AppDatabase/11.json @@ -0,0 +1,374 @@ +{ + "formatVersion": 1, + "database": { + "version": 11, + "identityHash": "c9a2ea19df17751b9f0d986ad51beb2a", + "entities": [ + { + "tableName": "forecasts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `temperature` REAL NOT NULL, `minTemp` REAL NOT NULL, `maxTemp` REAL NOT NULL, `pressure` REAL NOT NULL, `humidity` REAL NOT NULL, `icon` INTEGER NOT NULL, `condition` TEXT NOT NULL, `clouds` INTEGER NOT NULL, `windSpeed` REAL NOT NULL, `windDirection` REAL NOT NULL, `rain` REAL NOT NULL, `snow` REAL NOT NULL, `night` INTEGER NOT NULL, `location` TEXT NOT NULL, `provider` TEXT NOT NULL, `providerUrl` TEXT NOT NULL, `rainProbability` INTEGER NOT NULL, `snowProbability` INTEGER NOT NULL, `updateTime` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))", + "fields": [ + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "temperature", + "columnName": "temperature", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "minTemp", + "columnName": "minTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "maxTemp", + "columnName": "maxTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "pressure", + "columnName": "pressure", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "humidity", + "columnName": "humidity", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "condition", + "columnName": "condition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clouds", + "columnName": "clouds", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "windSpeed", + "columnName": "windSpeed", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "windDirection", + "columnName": "windDirection", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rain", + "columnName": "rain", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snow", + "columnName": "snow", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "night", + "columnName": "night", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "provider", + "columnName": "provider", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "providerUrl", + "columnName": "providerUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rainProbability", + "columnName": "rainProbability", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "snowProbability", + "columnName": "snowProbability", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updateTime", + "columnName": "updateTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "timestamp" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Searchable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `searchable` TEXT NOT NULL, `launchCount` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `hidden` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serializedSearchable", + "columnName": "searchable", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "launchCount", + "columnName": "launchCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinPosition", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Websearch", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`urlTemplate` TEXT NOT NULL, `label` TEXT NOT NULL, `color` INTEGER NOT NULL, `icon` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "urlTemplate", + "columnName": "urlTemplate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Icons", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `componentName` TEXT, `drawable` TEXT, `iconPack` TEXT NOT NULL, `scale` REAL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "componentName", + "columnName": "componentName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "drawable", + "columnName": "drawable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "iconPack", + "columnName": "iconPack", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scale", + "columnName": "scale", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "IconPack", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `packageName` TEXT NOT NULL, `version` TEXT NOT NULL, `scale` REAL NOT NULL, PRIMARY KEY(`packageName`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scale", + "columnName": "scale", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "packageName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Widget", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `data` TEXT NOT NULL, `height` INTEGER NOT NULL, `position` INTEGER NOT NULL, `label` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "height", + "columnName": "height", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c9a2ea19df17751b9f0d986ad51beb2a')" + ] + } +} \ No newline at end of file diff --git a/database/schemas/de.mm20.launcher2.database.AppDatabase/12.json b/database/schemas/de.mm20.launcher2.database.AppDatabase/12.json new file mode 100644 index 00000000..a2a7c5e8 --- /dev/null +++ b/database/schemas/de.mm20.launcher2.database.AppDatabase/12.json @@ -0,0 +1,406 @@ +{ + "formatVersion": 1, + "database": { + "version": 12, + "identityHash": "7e492b4e6f9b150fd7a36e3e028650e8", + "entities": [ + { + "tableName": "forecasts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `temperature` REAL NOT NULL, `minTemp` REAL NOT NULL, `maxTemp` REAL NOT NULL, `pressure` REAL NOT NULL, `humidity` REAL NOT NULL, `icon` INTEGER NOT NULL, `condition` TEXT NOT NULL, `clouds` INTEGER NOT NULL, `windSpeed` REAL NOT NULL, `windDirection` REAL NOT NULL, `rain` REAL NOT NULL, `snow` REAL NOT NULL, `night` INTEGER NOT NULL, `location` TEXT NOT NULL, `provider` TEXT NOT NULL, `providerUrl` TEXT NOT NULL, `rainProbability` INTEGER NOT NULL, `snowProbability` INTEGER NOT NULL, `updateTime` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))", + "fields": [ + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "temperature", + "columnName": "temperature", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "minTemp", + "columnName": "minTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "maxTemp", + "columnName": "maxTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "pressure", + "columnName": "pressure", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "humidity", + "columnName": "humidity", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "condition", + "columnName": "condition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clouds", + "columnName": "clouds", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "windSpeed", + "columnName": "windSpeed", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "windDirection", + "columnName": "windDirection", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rain", + "columnName": "rain", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snow", + "columnName": "snow", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "night", + "columnName": "night", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "provider", + "columnName": "provider", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "providerUrl", + "columnName": "providerUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rainProbability", + "columnName": "rainProbability", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "snowProbability", + "columnName": "snowProbability", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updateTime", + "columnName": "updateTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "timestamp" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Searchable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `searchable` TEXT NOT NULL, `launchCount` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `hidden` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serializedSearchable", + "columnName": "searchable", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "launchCount", + "columnName": "launchCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinPosition", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Websearch", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`urlTemplate` TEXT NOT NULL, `label` TEXT NOT NULL, `color` INTEGER NOT NULL, `icon` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "urlTemplate", + "columnName": "urlTemplate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Currency", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`symbol` TEXT NOT NULL, `value` REAL NOT NULL, `lastUpdate` INTEGER NOT NULL, PRIMARY KEY(`symbol`))", + "fields": [ + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "lastUpdate", + "columnName": "lastUpdate", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "symbol" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Icons", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `componentName` TEXT, `drawable` TEXT, `iconPack` TEXT NOT NULL, `scale` REAL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "componentName", + "columnName": "componentName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "drawable", + "columnName": "drawable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "iconPack", + "columnName": "iconPack", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scale", + "columnName": "scale", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "IconPack", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `packageName` TEXT NOT NULL, `version` TEXT NOT NULL, `scale` REAL NOT NULL, PRIMARY KEY(`packageName`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scale", + "columnName": "scale", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "packageName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Widget", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `data` TEXT NOT NULL, `height` INTEGER NOT NULL, `position` INTEGER NOT NULL, `label` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "height", + "columnName": "height", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7e492b4e6f9b150fd7a36e3e028650e8')" + ] + } +} \ No newline at end of file diff --git a/database/schemas/de.mm20.launcher2.database.AppDatabase/13.json b/database/schemas/de.mm20.launcher2.database.AppDatabase/13.json new file mode 100644 index 00000000..531d76df --- /dev/null +++ b/database/schemas/de.mm20.launcher2.database.AppDatabase/13.json @@ -0,0 +1,439 @@ +{ + "formatVersion": 1, + "database": { + "version": 13, + "identityHash": "89c214d548f80f5c25612f5e6ebf9725", + "entities": [ + { + "tableName": "forecasts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `temperature` REAL NOT NULL, `minTemp` REAL NOT NULL, `maxTemp` REAL NOT NULL, `pressure` REAL NOT NULL, `humidity` REAL NOT NULL, `icon` INTEGER NOT NULL, `condition` TEXT NOT NULL, `clouds` INTEGER NOT NULL, `windSpeed` REAL NOT NULL, `windDirection` REAL NOT NULL, `rain` REAL NOT NULL, `snow` REAL NOT NULL, `night` INTEGER NOT NULL, `location` TEXT NOT NULL, `provider` TEXT NOT NULL, `providerUrl` TEXT NOT NULL, `rainProbability` INTEGER NOT NULL, `snowProbability` INTEGER NOT NULL, `updateTime` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))", + "fields": [ + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "temperature", + "columnName": "temperature", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "minTemp", + "columnName": "minTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "maxTemp", + "columnName": "maxTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "pressure", + "columnName": "pressure", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "humidity", + "columnName": "humidity", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "condition", + "columnName": "condition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clouds", + "columnName": "clouds", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "windSpeed", + "columnName": "windSpeed", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "windDirection", + "columnName": "windDirection", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "precipitation", + "columnName": "rain", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snow", + "columnName": "snow", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "night", + "columnName": "night", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "provider", + "columnName": "provider", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "providerUrl", + "columnName": "providerUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "precipProbability", + "columnName": "rainProbability", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "snowProbability", + "columnName": "snowProbability", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updateTime", + "columnName": "updateTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "timestamp" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Searchable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `searchable` TEXT NOT NULL, `launchCount` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `hidden` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serializedSearchable", + "columnName": "searchable", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "launchCount", + "columnName": "launchCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinPosition", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Websearch", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`urlTemplate` TEXT NOT NULL, `label` TEXT NOT NULL, `color` INTEGER NOT NULL, `icon` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "urlTemplate", + "columnName": "urlTemplate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Currency", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`symbol` TEXT NOT NULL, `value` REAL NOT NULL, `lastUpdate` INTEGER NOT NULL, PRIMARY KEY(`symbol`))", + "fields": [ + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "lastUpdate", + "columnName": "lastUpdate", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "symbol" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Icons", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `componentName` TEXT, `drawable` TEXT, `iconPack` TEXT NOT NULL, `scale` REAL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "componentName", + "columnName": "componentName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "drawable", + "columnName": "drawable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "iconPack", + "columnName": "iconPack", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scale", + "columnName": "scale", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "IconPack", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `packageName` TEXT NOT NULL, `version` TEXT NOT NULL, `scale` REAL NOT NULL, PRIMARY KEY(`packageName`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scale", + "columnName": "scale", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "packageName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Plugin", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `data` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`packageName`, `data`))", + "fields": [ + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "packageName", + "data" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Widget", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `data` TEXT NOT NULL, `height` INTEGER NOT NULL, `position` INTEGER NOT NULL, `label` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "height", + "columnName": "height", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '89c214d548f80f5c25612f5e6ebf9725')" + ] + } +} \ No newline at end of file diff --git a/database/schemas/de.mm20.launcher2.database.AppDatabase/14.json b/database/schemas/de.mm20.launcher2.database.AppDatabase/14.json new file mode 100644 index 00000000..111bb63d --- /dev/null +++ b/database/schemas/de.mm20.launcher2.database.AppDatabase/14.json @@ -0,0 +1,439 @@ +{ + "formatVersion": 1, + "database": { + "version": 14, + "identityHash": "89c214d548f80f5c25612f5e6ebf9725", + "entities": [ + { + "tableName": "forecasts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `temperature` REAL NOT NULL, `minTemp` REAL NOT NULL, `maxTemp` REAL NOT NULL, `pressure` REAL NOT NULL, `humidity` REAL NOT NULL, `icon` INTEGER NOT NULL, `condition` TEXT NOT NULL, `clouds` INTEGER NOT NULL, `windSpeed` REAL NOT NULL, `windDirection` REAL NOT NULL, `rain` REAL NOT NULL, `snow` REAL NOT NULL, `night` INTEGER NOT NULL, `location` TEXT NOT NULL, `provider` TEXT NOT NULL, `providerUrl` TEXT NOT NULL, `rainProbability` INTEGER NOT NULL, `snowProbability` INTEGER NOT NULL, `updateTime` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))", + "fields": [ + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "temperature", + "columnName": "temperature", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "minTemp", + "columnName": "minTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "maxTemp", + "columnName": "maxTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "pressure", + "columnName": "pressure", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "humidity", + "columnName": "humidity", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "condition", + "columnName": "condition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clouds", + "columnName": "clouds", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "windSpeed", + "columnName": "windSpeed", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "windDirection", + "columnName": "windDirection", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "precipitation", + "columnName": "rain", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snow", + "columnName": "snow", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "night", + "columnName": "night", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "provider", + "columnName": "provider", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "providerUrl", + "columnName": "providerUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "precipProbability", + "columnName": "rainProbability", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "snowProbability", + "columnName": "snowProbability", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updateTime", + "columnName": "updateTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "timestamp" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Searchable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `searchable` TEXT NOT NULL, `launchCount` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `hidden` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serializedSearchable", + "columnName": "searchable", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "launchCount", + "columnName": "launchCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinPosition", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Websearch", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`urlTemplate` TEXT NOT NULL, `label` TEXT NOT NULL, `color` INTEGER NOT NULL, `icon` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "urlTemplate", + "columnName": "urlTemplate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Currency", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`symbol` TEXT NOT NULL, `value` REAL NOT NULL, `lastUpdate` INTEGER NOT NULL, PRIMARY KEY(`symbol`))", + "fields": [ + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "lastUpdate", + "columnName": "lastUpdate", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "symbol" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Icons", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `componentName` TEXT, `drawable` TEXT, `iconPack` TEXT NOT NULL, `scale` REAL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "componentName", + "columnName": "componentName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "drawable", + "columnName": "drawable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "iconPack", + "columnName": "iconPack", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scale", + "columnName": "scale", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "IconPack", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `packageName` TEXT NOT NULL, `version` TEXT NOT NULL, `scale` REAL NOT NULL, PRIMARY KEY(`packageName`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scale", + "columnName": "scale", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "packageName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Plugin", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `data` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`packageName`, `data`))", + "fields": [ + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "packageName", + "data" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Widget", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `data` TEXT NOT NULL, `height` INTEGER NOT NULL, `position` INTEGER NOT NULL, `label` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "height", + "columnName": "height", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '89c214d548f80f5c25612f5e6ebf9725')" + ] + } +} \ No newline at end of file diff --git a/database/schemas/de.mm20.launcher2.database.AppDatabase/2.json b/database/schemas/de.mm20.launcher2.database.AppDatabase/2.json new file mode 100644 index 00000000..c136d87a --- /dev/null +++ b/database/schemas/de.mm20.launcher2.database.AppDatabase/2.json @@ -0,0 +1,224 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "a28f601f4a9dee37e9a41f895f3ac512", + "entities": [ + { + "tableName": "forecasts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `temperature` REAL NOT NULL, `minTemp` REAL NOT NULL, `maxTemp` REAL NOT NULL, `pressure` REAL NOT NULL, `humidity` REAL NOT NULL, `icon` INTEGER NOT NULL, `condition` TEXT NOT NULL, `clouds` INTEGER NOT NULL, `windSpeed` REAL NOT NULL, `windDirection` REAL NOT NULL, `rain` REAL NOT NULL, `snow` REAL NOT NULL, `night` INTEGER NOT NULL, `location` TEXT NOT NULL, `provider` TEXT NOT NULL, `providerUrl` TEXT NOT NULL, PRIMARY KEY(`timestamp`))", + "fields": [ + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "temperature", + "columnName": "temperature", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "minTemp", + "columnName": "minTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "maxTemp", + "columnName": "maxTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "pressure", + "columnName": "pressure", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "humidity", + "columnName": "humidity", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "condition", + "columnName": "condition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clouds", + "columnName": "clouds", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "windSpeed", + "columnName": "windSpeed", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "windDirection", + "columnName": "windDirection", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rain", + "columnName": "rain", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snow", + "columnName": "snow", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "night", + "columnName": "night", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "provider", + "columnName": "provider", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "providerUrl", + "columnName": "providerUrl", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "timestamp" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Searchable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `searchable` TEXT NOT NULL, `launchCount` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `hidden` INTEGER NOT NULL, `inAllApps` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "searchable", + "columnName": "searchable", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "launchCount", + "columnName": "launchCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inAllApps", + "columnName": "inAllApps", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Websearch", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`urlTemplate` TEXT NOT NULL, `label` TEXT NOT NULL, `color` INTEGER NOT NULL, `icon` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "urlTemplate", + "columnName": "urlTemplate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"a28f601f4a9dee37e9a41f895f3ac512\")" + ] + } +} \ No newline at end of file diff --git a/database/schemas/de.mm20.launcher2.database.AppDatabase/3.json b/database/schemas/de.mm20.launcher2.database.AppDatabase/3.json new file mode 100644 index 00000000..38c81314 --- /dev/null +++ b/database/schemas/de.mm20.launcher2.database.AppDatabase/3.json @@ -0,0 +1,224 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "a28f601f4a9dee37e9a41f895f3ac512", + "entities": [ + { + "tableName": "forecasts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `temperature` REAL NOT NULL, `minTemp` REAL NOT NULL, `maxTemp` REAL NOT NULL, `pressure` REAL NOT NULL, `humidity` REAL NOT NULL, `icon` INTEGER NOT NULL, `condition` TEXT NOT NULL, `clouds` INTEGER NOT NULL, `windSpeed` REAL NOT NULL, `windDirection` REAL NOT NULL, `rain` REAL NOT NULL, `snow` REAL NOT NULL, `night` INTEGER NOT NULL, `location` TEXT NOT NULL, `provider` TEXT NOT NULL, `providerUrl` TEXT NOT NULL, PRIMARY KEY(`timestamp`))", + "fields": [ + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "temperature", + "columnName": "temperature", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "minTemp", + "columnName": "minTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "maxTemp", + "columnName": "maxTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "pressure", + "columnName": "pressure", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "humidity", + "columnName": "humidity", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "condition", + "columnName": "condition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clouds", + "columnName": "clouds", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "windSpeed", + "columnName": "windSpeed", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "windDirection", + "columnName": "windDirection", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rain", + "columnName": "rain", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snow", + "columnName": "snow", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "night", + "columnName": "night", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "provider", + "columnName": "provider", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "providerUrl", + "columnName": "providerUrl", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "timestamp" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Searchable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `searchable` TEXT NOT NULL, `launchCount` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `hidden` INTEGER NOT NULL, `inAllApps` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "searchable", + "columnName": "searchable", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "launchCount", + "columnName": "launchCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inAllApps", + "columnName": "inAllApps", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Websearch", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`urlTemplate` TEXT NOT NULL, `label` TEXT NOT NULL, `color` INTEGER NOT NULL, `icon` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "urlTemplate", + "columnName": "urlTemplate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"a28f601f4a9dee37e9a41f895f3ac512\")" + ] + } +} \ No newline at end of file diff --git a/database/schemas/de.mm20.launcher2.database.AppDatabase/4.json b/database/schemas/de.mm20.launcher2.database.AppDatabase/4.json new file mode 100644 index 00000000..727168ed --- /dev/null +++ b/database/schemas/de.mm20.launcher2.database.AppDatabase/4.json @@ -0,0 +1,312 @@ +{ + "formatVersion": 1, + "database": { + "version": 4, + "identityHash": "d557636d2e0f727ae8a980c1eb0395a5", + "entities": [ + { + "tableName": "forecasts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `temperature` REAL NOT NULL, `minTemp` REAL NOT NULL, `maxTemp` REAL NOT NULL, `pressure` REAL NOT NULL, `humidity` REAL NOT NULL, `icon` INTEGER NOT NULL, `condition` TEXT NOT NULL, `clouds` INTEGER NOT NULL, `windSpeed` REAL NOT NULL, `windDirection` REAL NOT NULL, `rain` REAL NOT NULL, `snow` REAL NOT NULL, `night` INTEGER NOT NULL, `location` TEXT NOT NULL, `provider` TEXT NOT NULL, `providerUrl` TEXT NOT NULL, PRIMARY KEY(`timestamp`))", + "fields": [ + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "temperature", + "columnName": "temperature", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "minTemp", + "columnName": "minTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "maxTemp", + "columnName": "maxTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "pressure", + "columnName": "pressure", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "humidity", + "columnName": "humidity", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "condition", + "columnName": "condition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clouds", + "columnName": "clouds", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "windSpeed", + "columnName": "windSpeed", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "windDirection", + "columnName": "windDirection", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rain", + "columnName": "rain", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snow", + "columnName": "snow", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "night", + "columnName": "night", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "provider", + "columnName": "provider", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "providerUrl", + "columnName": "providerUrl", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "timestamp" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Searchable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `searchable` TEXT NOT NULL, `launchCount` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `hidden` INTEGER NOT NULL, `inAllApps` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "searchable", + "columnName": "searchable", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "launchCount", + "columnName": "launchCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inAllApps", + "columnName": "inAllApps", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Websearch", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`urlTemplate` TEXT NOT NULL, `label` TEXT NOT NULL, `color` INTEGER NOT NULL, `icon` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "urlTemplate", + "columnName": "urlTemplate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Icons", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `componentName` TEXT, `drawable` TEXT, `iconPack` TEXT NOT NULL, `scale` REAL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "componentName", + "columnName": "componentName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "drawable", + "columnName": "drawable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "iconPack", + "columnName": "iconPack", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scale", + "columnName": "scale", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "IconPack", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `packageName` TEXT NOT NULL, `version` TEXT NOT NULL, `scale` REAL NOT NULL, PRIMARY KEY(`packageName`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scale", + "columnName": "scale", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "packageName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"d557636d2e0f727ae8a980c1eb0395a5\")" + ] + } +} \ No newline at end of file diff --git a/database/schemas/de.mm20.launcher2.database.AppDatabase/5.json b/database/schemas/de.mm20.launcher2.database.AppDatabase/5.json new file mode 100644 index 00000000..3bd89cf2 --- /dev/null +++ b/database/schemas/de.mm20.launcher2.database.AppDatabase/5.json @@ -0,0 +1,312 @@ +{ + "formatVersion": 1, + "database": { + "version": 5, + "identityHash": "d557636d2e0f727ae8a980c1eb0395a5", + "entities": [ + { + "tableName": "forecasts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `temperature` REAL NOT NULL, `minTemp` REAL NOT NULL, `maxTemp` REAL NOT NULL, `pressure` REAL NOT NULL, `humidity` REAL NOT NULL, `icon` INTEGER NOT NULL, `condition` TEXT NOT NULL, `clouds` INTEGER NOT NULL, `windSpeed` REAL NOT NULL, `windDirection` REAL NOT NULL, `rain` REAL NOT NULL, `snow` REAL NOT NULL, `night` INTEGER NOT NULL, `location` TEXT NOT NULL, `provider` TEXT NOT NULL, `providerUrl` TEXT NOT NULL, PRIMARY KEY(`timestamp`))", + "fields": [ + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "temperature", + "columnName": "temperature", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "minTemp", + "columnName": "minTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "maxTemp", + "columnName": "maxTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "pressure", + "columnName": "pressure", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "humidity", + "columnName": "humidity", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "condition", + "columnName": "condition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clouds", + "columnName": "clouds", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "windSpeed", + "columnName": "windSpeed", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "windDirection", + "columnName": "windDirection", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rain", + "columnName": "rain", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snow", + "columnName": "snow", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "night", + "columnName": "night", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "provider", + "columnName": "provider", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "providerUrl", + "columnName": "providerUrl", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "timestamp" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Searchable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `searchable` TEXT NOT NULL, `launchCount` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `hidden` INTEGER NOT NULL, `inAllApps` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "searchable", + "columnName": "searchable", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "launchCount", + "columnName": "launchCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inAllApps", + "columnName": "inAllApps", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Websearch", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`urlTemplate` TEXT NOT NULL, `label` TEXT NOT NULL, `color` INTEGER NOT NULL, `icon` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "urlTemplate", + "columnName": "urlTemplate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Icons", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `componentName` TEXT, `drawable` TEXT, `iconPack` TEXT NOT NULL, `scale` REAL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "componentName", + "columnName": "componentName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "drawable", + "columnName": "drawable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "iconPack", + "columnName": "iconPack", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scale", + "columnName": "scale", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "IconPack", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `packageName` TEXT NOT NULL, `version` TEXT NOT NULL, `scale` REAL NOT NULL, PRIMARY KEY(`packageName`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scale", + "columnName": "scale", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "packageName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"d557636d2e0f727ae8a980c1eb0395a5\")" + ] + } +} \ No newline at end of file diff --git a/database/schemas/de.mm20.launcher2.database.AppDatabase/6.json b/database/schemas/de.mm20.launcher2.database.AppDatabase/6.json new file mode 100644 index 00000000..192ed783 --- /dev/null +++ b/database/schemas/de.mm20.launcher2.database.AppDatabase/6.json @@ -0,0 +1,362 @@ +{ + "formatVersion": 1, + "database": { + "version": 6, + "identityHash": "b70a2f4ee8900ba1492884e061351de0", + "entities": [ + { + "tableName": "forecasts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `temperature` REAL NOT NULL, `minTemp` REAL NOT NULL, `maxTemp` REAL NOT NULL, `pressure` REAL NOT NULL, `humidity` REAL NOT NULL, `icon` INTEGER NOT NULL, `condition` TEXT NOT NULL, `clouds` INTEGER NOT NULL, `windSpeed` REAL NOT NULL, `windDirection` REAL NOT NULL, `rain` REAL NOT NULL, `snow` REAL NOT NULL, `night` INTEGER NOT NULL, `location` TEXT NOT NULL, `provider` TEXT NOT NULL, `providerUrl` TEXT NOT NULL, PRIMARY KEY(`timestamp`))", + "fields": [ + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "temperature", + "columnName": "temperature", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "minTemp", + "columnName": "minTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "maxTemp", + "columnName": "maxTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "pressure", + "columnName": "pressure", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "humidity", + "columnName": "humidity", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "condition", + "columnName": "condition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clouds", + "columnName": "clouds", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "windSpeed", + "columnName": "windSpeed", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "windDirection", + "columnName": "windDirection", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rain", + "columnName": "rain", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snow", + "columnName": "snow", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "night", + "columnName": "night", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "provider", + "columnName": "provider", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "providerUrl", + "columnName": "providerUrl", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "timestamp" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Searchable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `searchable` TEXT NOT NULL, `launchCount` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `hidden` INTEGER NOT NULL, `inAllApps` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "searchable", + "columnName": "searchable", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "launchCount", + "columnName": "launchCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inAllApps", + "columnName": "inAllApps", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Websearch", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`urlTemplate` TEXT NOT NULL, `label` TEXT NOT NULL, `color` INTEGER NOT NULL, `icon` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "urlTemplate", + "columnName": "urlTemplate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Icons", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `componentName` TEXT, `drawable` TEXT, `iconPack` TEXT NOT NULL, `scale` REAL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "componentName", + "columnName": "componentName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "drawable", + "columnName": "drawable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "iconPack", + "columnName": "iconPack", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scale", + "columnName": "scale", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "IconPack", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `packageName` TEXT NOT NULL, `version` TEXT NOT NULL, `scale` REAL NOT NULL, PRIMARY KEY(`packageName`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scale", + "columnName": "scale", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "packageName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Widget", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `data` TEXT NOT NULL, `height` INTEGER NOT NULL, `position` INTEGER NOT NULL, `label` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "height", + "columnName": "height", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"b70a2f4ee8900ba1492884e061351de0\")" + ] + } +} \ No newline at end of file diff --git a/database/schemas/de.mm20.launcher2.database.AppDatabase/7.json b/database/schemas/de.mm20.launcher2.database.AppDatabase/7.json new file mode 100644 index 00000000..b4047e9b --- /dev/null +++ b/database/schemas/de.mm20.launcher2.database.AppDatabase/7.json @@ -0,0 +1,362 @@ +{ + "formatVersion": 1, + "database": { + "version": 7, + "identityHash": "e5bc7c00f579b2da9ad18dec65a0aae7", + "entities": [ + { + "tableName": "forecasts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `temperature` REAL NOT NULL, `minTemp` REAL NOT NULL, `maxTemp` REAL NOT NULL, `pressure` REAL NOT NULL, `humidity` REAL NOT NULL, `icon` INTEGER NOT NULL, `condition` TEXT NOT NULL, `clouds` INTEGER NOT NULL, `windSpeed` REAL NOT NULL, `windDirection` REAL NOT NULL, `rain` REAL NOT NULL, `snow` REAL NOT NULL, `night` INTEGER NOT NULL, `location` TEXT NOT NULL, `provider` TEXT NOT NULL, `providerUrl` TEXT NOT NULL, PRIMARY KEY(`timestamp`))", + "fields": [ + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "temperature", + "columnName": "temperature", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "minTemp", + "columnName": "minTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "maxTemp", + "columnName": "maxTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "pressure", + "columnName": "pressure", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "humidity", + "columnName": "humidity", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "condition", + "columnName": "condition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clouds", + "columnName": "clouds", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "windSpeed", + "columnName": "windSpeed", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "windDirection", + "columnName": "windDirection", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rain", + "columnName": "rain", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snow", + "columnName": "snow", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "night", + "columnName": "night", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "provider", + "columnName": "provider", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "providerUrl", + "columnName": "providerUrl", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "timestamp" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Searchable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `searchable` TEXT, `launchCount` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `hidden` INTEGER NOT NULL, `inAllApps` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "searchable", + "columnName": "searchable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "launchCount", + "columnName": "launchCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inAllApps", + "columnName": "inAllApps", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Websearch", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`urlTemplate` TEXT NOT NULL, `label` TEXT NOT NULL, `color` INTEGER NOT NULL, `icon` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "urlTemplate", + "columnName": "urlTemplate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Icons", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `componentName` TEXT, `drawable` TEXT, `iconPack` TEXT NOT NULL, `scale` REAL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "componentName", + "columnName": "componentName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "drawable", + "columnName": "drawable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "iconPack", + "columnName": "iconPack", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scale", + "columnName": "scale", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "IconPack", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `packageName` TEXT NOT NULL, `version` TEXT NOT NULL, `scale` REAL NOT NULL, PRIMARY KEY(`packageName`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scale", + "columnName": "scale", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "packageName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Widget", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `data` TEXT NOT NULL, `height` INTEGER NOT NULL, `position` INTEGER NOT NULL, `label` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "height", + "columnName": "height", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"e5bc7c00f579b2da9ad18dec65a0aae7\")" + ] + } +} \ No newline at end of file diff --git a/database/schemas/de.mm20.launcher2.database.AppDatabase/8.json b/database/schemas/de.mm20.launcher2.database.AppDatabase/8.json new file mode 100644 index 00000000..beff3eac --- /dev/null +++ b/database/schemas/de.mm20.launcher2.database.AppDatabase/8.json @@ -0,0 +1,374 @@ +{ + "formatVersion": 1, + "database": { + "version": 8, + "identityHash": "b6cb2687ae974dfa5ab2e437d5d58184", + "entities": [ + { + "tableName": "forecasts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `temperature` REAL NOT NULL, `minTemp` REAL NOT NULL, `maxTemp` REAL NOT NULL, `pressure` REAL NOT NULL, `humidity` REAL NOT NULL, `icon` INTEGER NOT NULL, `condition` TEXT NOT NULL, `clouds` INTEGER NOT NULL, `windSpeed` REAL NOT NULL, `windDirection` REAL NOT NULL, `rain` REAL NOT NULL, `snow` REAL NOT NULL, `night` INTEGER NOT NULL, `location` TEXT NOT NULL, `provider` TEXT NOT NULL, `providerUrl` TEXT NOT NULL, `rainPropability` INTEGER NOT NULL, `snowProbability` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))", + "fields": [ + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "temperature", + "columnName": "temperature", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "minTemp", + "columnName": "minTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "maxTemp", + "columnName": "maxTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "pressure", + "columnName": "pressure", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "humidity", + "columnName": "humidity", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "condition", + "columnName": "condition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clouds", + "columnName": "clouds", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "windSpeed", + "columnName": "windSpeed", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "windDirection", + "columnName": "windDirection", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rain", + "columnName": "rain", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snow", + "columnName": "snow", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "night", + "columnName": "night", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "provider", + "columnName": "provider", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "providerUrl", + "columnName": "providerUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rainPropability", + "columnName": "rainPropability", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "snowProbability", + "columnName": "snowProbability", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "timestamp" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Searchable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `searchable` TEXT, `launchCount` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `hidden` INTEGER NOT NULL, `inAllApps` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "searchable", + "columnName": "searchable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "launchCount", + "columnName": "launchCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inAllApps", + "columnName": "inAllApps", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Websearch", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`urlTemplate` TEXT NOT NULL, `label` TEXT NOT NULL, `color` INTEGER NOT NULL, `icon` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "urlTemplate", + "columnName": "urlTemplate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Icons", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `componentName` TEXT, `drawable` TEXT, `iconPack` TEXT NOT NULL, `scale` REAL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "componentName", + "columnName": "componentName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "drawable", + "columnName": "drawable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "iconPack", + "columnName": "iconPack", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scale", + "columnName": "scale", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "IconPack", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `packageName` TEXT NOT NULL, `version` TEXT NOT NULL, `scale` REAL NOT NULL, PRIMARY KEY(`packageName`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scale", + "columnName": "scale", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "packageName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Widget", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `data` TEXT NOT NULL, `height` INTEGER NOT NULL, `position` INTEGER NOT NULL, `label` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "height", + "columnName": "height", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b6cb2687ae974dfa5ab2e437d5d58184')" + ] + } +} \ No newline at end of file diff --git a/database/schemas/de.mm20.launcher2.database.AppDatabase/9.json b/database/schemas/de.mm20.launcher2.database.AppDatabase/9.json new file mode 100644 index 00000000..37ba3b61 --- /dev/null +++ b/database/schemas/de.mm20.launcher2.database.AppDatabase/9.json @@ -0,0 +1,380 @@ +{ + "formatVersion": 1, + "database": { + "version": 9, + "identityHash": "e6c6e581b9cfb88daecbe977dfdb0d05", + "entities": [ + { + "tableName": "forecasts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `temperature` REAL NOT NULL, `minTemp` REAL NOT NULL, `maxTemp` REAL NOT NULL, `pressure` REAL NOT NULL, `humidity` REAL NOT NULL, `icon` INTEGER NOT NULL, `condition` TEXT NOT NULL, `clouds` INTEGER NOT NULL, `windSpeed` REAL NOT NULL, `windDirection` REAL NOT NULL, `rain` REAL NOT NULL, `snow` REAL NOT NULL, `night` INTEGER NOT NULL, `location` TEXT NOT NULL, `provider` TEXT NOT NULL, `providerUrl` TEXT NOT NULL, `rainProbability` INTEGER NOT NULL, `snowProbability` INTEGER NOT NULL, `updateTime` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))", + "fields": [ + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "temperature", + "columnName": "temperature", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "minTemp", + "columnName": "minTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "maxTemp", + "columnName": "maxTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "pressure", + "columnName": "pressure", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "humidity", + "columnName": "humidity", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "condition", + "columnName": "condition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clouds", + "columnName": "clouds", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "windSpeed", + "columnName": "windSpeed", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "windDirection", + "columnName": "windDirection", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "rain", + "columnName": "rain", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snow", + "columnName": "snow", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "night", + "columnName": "night", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "provider", + "columnName": "provider", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "providerUrl", + "columnName": "providerUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rainProbability", + "columnName": "rainProbability", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "snowProbability", + "columnName": "snowProbability", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updateTime", + "columnName": "updateTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "timestamp" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Searchable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `searchable` TEXT, `launchCount` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `hidden` INTEGER NOT NULL, `inAllApps` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "searchable", + "columnName": "searchable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "launchCount", + "columnName": "launchCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inAllApps", + "columnName": "inAllApps", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Websearch", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`urlTemplate` TEXT NOT NULL, `label` TEXT NOT NULL, `color` INTEGER NOT NULL, `icon` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "urlTemplate", + "columnName": "urlTemplate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Icons", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `componentName` TEXT, `drawable` TEXT, `iconPack` TEXT NOT NULL, `scale` REAL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "componentName", + "columnName": "componentName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "drawable", + "columnName": "drawable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "iconPack", + "columnName": "iconPack", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scale", + "columnName": "scale", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "IconPack", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `packageName` TEXT NOT NULL, `version` TEXT NOT NULL, `scale` REAL NOT NULL, PRIMARY KEY(`packageName`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scale", + "columnName": "scale", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "packageName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Widget", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `data` TEXT NOT NULL, `height` INTEGER NOT NULL, `position` INTEGER NOT NULL, `label` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "height", + "columnName": "height", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e6c6e581b9cfb88daecbe977dfdb0d05')" + ] + } +} \ No newline at end of file diff --git a/database/src/main/AndroidManifest.xml b/database/src/main/AndroidManifest.xml new file mode 100644 index 00000000..d8973773 --- /dev/null +++ b/database/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + / + \ No newline at end of file diff --git a/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt b/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt new file mode 100644 index 00000000..67cc6f7e --- /dev/null +++ b/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt @@ -0,0 +1,147 @@ +package de.mm20.launcher2.database + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import de.mm20.launcher2.database.entities.* + +@Database(entities = [ForecastEntity::class, + FavoritesItemEntity::class, + WebsearchEntity::class, + CurrencyEntity::class, + IconEntity::class, + IconPackEntity::class, + PluginEntity::class, + WidgetEntity::class], version = 14, exportSchema = true) +@TypeConverters(ComponentNameConverter::class, StringListConverter::class) +abstract class AppDatabase : RoomDatabase() { + + abstract fun weatherDao(): WeatherDao + abstract fun searchDao(): SearchDao + abstract fun iconDao(): IconDao + abstract fun widgetDao(): WidgetDao + abstract fun currencyDao(): CurrencyDao + + companion object { + private var _instance: AppDatabase? = null + fun getInstance(context: Context): AppDatabase { + val instance = _instance + ?: Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "room") + //.fallbackToDestructiveMigration() + .addCallback(object : Callback() { + override fun onCreate(db: SupportSQLiteDatabase) { + super.onCreate(db) + db.execSQL("INSERT INTO Websearch (urlTemplate, label, color, icon) VALUES " + + "('${context.getString(R.string.websearch_google_url)}', '${context.getString(R.string.websearch_google)}', 0xFF4285F4, NULL )," + + "('${context.getString(R.string.websearch_youtube_url)}', '${context.getString(R.string.websearch_youtube)}', 0xFFFF0000, NULL )," + + "('${context.getString(R.string.websearch_playstore_url)}', '${context.getString(R.string.websearch_playstore)}', 0xFF00D3FF, NULL );") + + db.execSQL("INSERT INTO Widget (type, data, height, position, label) VALUES " + + "('internal', 'weather', -1, 0, '${context.getString(R.string.widget_name_weather)}')," + + "('internal', 'music', -1, 1, '${context.getString(R.string.widget_name_music)}')," + + "('internal', 'calendar', -1, 2, '${context.getString(R.string.widget_name_calendar)}');") + } + }) + .addMigrations( + Migration_6_7(), + Migration_7_8(), + Migration_8_9(), + Migration_9_10(), + Migration_10_11(), + Migration_11_12(), + Migration_12_13(), + Migration_13_14() + ).build() + if (_instance == null) _instance = instance + return instance + } + } +} + +class Migration_6_7 : Migration(6, 7) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("CREATE TABLE Searchable2 (`key` TEXT NOT NULL, `searchable` TEXT, `launchCount` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `hidden` INTEGER NOT NULL, `inAllApps` INTEGER NOT NULL, PRIMARY KEY(`key`))") + database.execSQL("INSERT INTO Searchable2 SELECT * FROM Searchable") + database.execSQL("DROP TABLE Searchable") + database.execSQL("ALTER TABLE Searchable2 RENAME TO Searchable") + } + +} + +class Migration_7_8 : Migration(7, 8) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("CREATE TABLE IF NOT EXISTS `${ForecastEntity.TABLE_NAME}2` (`timestamp` INTEGER NOT NULL, `temperature` REAL NOT NULL, `minTemp` REAL NOT NULL, `maxTemp` REAL NOT NULL, `pressure` REAL NOT NULL, `humidity` REAL NOT NULL, `icon` INTEGER NOT NULL, `condition` TEXT NOT NULL, `clouds` INTEGER NOT NULL, `windSpeed` REAL NOT NULL, `windDirection` REAL NOT NULL, `rain` REAL NOT NULL, `snow` REAL NOT NULL, `night` INTEGER NOT NULL, `location` TEXT NOT NULL, `provider` TEXT NOT NULL, `providerUrl` TEXT NOT NULL, `rainPropability` INTEGER NOT NULL, `snowProbability` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))") + database.execSQL("INSERT INTO ${ForecastEntity.TABLE_NAME}2 SELECT *, -1 as rainPropability, -1 as snowPropability FROM ${ForecastEntity.TABLE_NAME}") + database.execSQL("DROP TABLE ${ForecastEntity.TABLE_NAME}") + database.execSQL("ALTER TABLE ${ForecastEntity.TABLE_NAME}2 RENAME TO ${ForecastEntity.TABLE_NAME}") + } +} + +class Migration_8_9 : Migration(8, 9) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("CREATE TABLE IF NOT EXISTS `${ForecastEntity.TABLE_NAME}2` (" + + "`timestamp` INTEGER NOT NULL, " + + "`temperature` REAL NOT NULL, " + + "`minTemp` REAL NOT NULL, " + + "`maxTemp` REAL NOT NULL, " + + "`pressure` REAL NOT NULL, " + + "`humidity` REAL NOT NULL, " + + "`icon` INTEGER NOT NULL, " + + "`condition` TEXT NOT NULL, " + + "`clouds` INTEGER NOT NULL, " + + "`windSpeed` REAL NOT NULL, " + + "`windDirection` REAL NOT NULL, " + + "`rain` REAL NOT NULL, " + + "`snow` REAL NOT NULL, " + + "`night` INTEGER NOT NULL, " + + "`location` TEXT NOT NULL, " + + "`provider` TEXT NOT NULL, " + + "`providerUrl` TEXT NOT NULL, " + + "`rainProbability` INTEGER NOT NULL, " + + "`snowProbability` INTEGER NOT NULL, " + + "`updateTime` INTEGER NOT NULL, " + + "PRIMARY KEY(`timestamp`))") + database.execSQL("INSERT INTO ${ForecastEntity.TABLE_NAME}2 SELECT timestamp, temperature, minTemp, maxTemp, pressure, humidity, icon, condition, clouds, windSpeed, windDirection, rain, snow, night, location, provider, providerUrl, rainPropability as rainProbability, snowProbability, 0 as updateTime FROM ${ForecastEntity.TABLE_NAME}") + database.execSQL("DROP TABLE ${ForecastEntity.TABLE_NAME}") + database.execSQL("ALTER TABLE ${ForecastEntity.TABLE_NAME}2 RENAME TO ${ForecastEntity.TABLE_NAME}") + } + +} + +class Migration_9_10 : Migration(9, 10) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("CREATE TABLE IF NOT EXISTS `Plugins` (`packageName` TEXT NOT NULL, `label` TEXT NOT NULL, `description` TEXT NOT NULL, `pluginClassName` TEXT NOT NULL, `enabled` INTEGER NOT NULL, PRIMARY KEY(`packageName`) );") + } + +} + +class Migration_10_11 : Migration(10, 11) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("CREATE TABLE IF NOT EXISTS `temp` (`key` TEXT NOT NULL, `searchable` TEXT NOT NULL, `launchCount` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `hidden` INTEGER NOT NULL, PRIMARY KEY(`key`))") + database.execSQL("INSERT INTO `temp` SELECT `key`, `searchable`, `launchCount`, `pinned`, `hidden` FROM `Searchable`") + database.execSQL("DROP TABLE `Searchable`") + database.execSQL("ALTER TABLE `temp` RENAME TO `Searchable`") + } +} + +class Migration_11_12 : Migration(11, 12) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("CREATE TABLE IF NOT EXISTS `Currency` (`symbol` TEXT NOT NULL, `value` REAL NOT NULL, `lastUpdate` INTEGER NOT NULL, PRIMARY KEY(`symbol`))") + } +} + +class Migration_12_13 : Migration(12, 13) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("CREATE TABLE IF NOT EXISTS `Plugin` (`packageName` TEXT NOT NULL, `data` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`packageName`, `data`))") + } +} +class Migration_13_14 : Migration(13, 14) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("DROP TABLE IF EXISTS `Plugins`;") + } + +} \ No newline at end of file diff --git a/database/src/main/java/de/mm20/launcher2/database/Converters.kt b/database/src/main/java/de/mm20/launcher2/database/Converters.kt new file mode 100644 index 00000000..4df57adb --- /dev/null +++ b/database/src/main/java/de/mm20/launcher2/database/Converters.kt @@ -0,0 +1,34 @@ +package de.mm20.launcher2.database + +import android.content.ComponentName +import androidx.room.TypeConverter +import org.json.JSONArray + +class ComponentNameConverter { + @TypeConverter + fun toString(componentName: ComponentName?): String? { + return componentName?.flattenToString() + } + + @TypeConverter + fun toComponentName(string: String?) : ComponentName? { + string ?: return null + return ComponentName.unflattenFromString(string) + } + +} + +class StringListConverter { + @TypeConverter + fun toString(list: List): String { + val json = JSONArray() + list.forEach { json.put(it) } + return json.toString() + } + + @TypeConverter + fun toStringList(string: String): List { + val json = JSONArray(string) + return (0..json.length()).map { json.getString(it) } + } +} diff --git a/database/src/main/java/de/mm20/launcher2/database/CurrencyDao.kt b/database/src/main/java/de/mm20/launcher2/database/CurrencyDao.kt new file mode 100644 index 00000000..c5e6e8c0 --- /dev/null +++ b/database/src/main/java/de/mm20/launcher2/database/CurrencyDao.kt @@ -0,0 +1,34 @@ +package de.mm20.launcher2.database + +import androidx.room.* +import de.mm20.launcher2.database.entities.CurrencyEntity + +@Dao +interface CurrencyDao { + + @Query("SELECT value FROM Currency WHERE symbol = :symbol") + fun getExchangeRate(symbol: String) : Double? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(currency: CurrencyEntity) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertAll(currencies: List) + + @Query("SELECT * FROM Currency WHERE symbol = :symbol") + fun getCurrency(symbol: String) : CurrencyEntity? + + @Query("SELECT * FROM Currency WHERE symbol IN (:symbols)") + fun getCurrencies(symbols: List) : List + + @Query("SELECT * FROM Currency") + fun getAllCurrencies() : List + + @Transaction + fun exists(symbol: String): Boolean { + return getCurrency(symbol) != null + } + + @Query("SELECT lastUpdate FROM Currency WHERE symbol = :symbol") + fun getLastUpdate(symbol: String) : Long +} \ No newline at end of file diff --git a/database/src/main/java/de/mm20/launcher2/database/IconDao.kt b/database/src/main/java/de/mm20/launcher2/database/IconDao.kt new file mode 100644 index 00000000..444c8ca3 --- /dev/null +++ b/database/src/main/java/de/mm20/launcher2/database/IconDao.kt @@ -0,0 +1,73 @@ +package de.mm20.launcher2.database + +import androidx.lifecycle.LiveData +import androidx.room.* +import de.mm20.launcher2.database.entities.IconEntity +import de.mm20.launcher2.database.entities.IconPackEntity + +@Dao +interface IconDao { + @Insert + fun insertAll(icons: List) + + @Query("SELECT drawable FROM Icons WHERE componentName = :componentName AND iconPack = :iconPack") + fun getIconName(componentName: String, iconPack: String): String? + + @Query("SELECT * FROM Icons WHERE componentName = :componentName AND iconPack = :iconPack") + fun getIcon(componentName: String, iconPack: String): IconEntity? + + @Query("DELETE FROM Icons WHERE iconPack = :iconPack") + fun deleteIcons(iconPack: String) + + @Transaction + fun installIconPack(iconPack: IconPackEntity, icons: List) { + deleteIconPack(iconPack) + deleteIcons(iconPack.packageName) + insertAll(icons) + installIconPack(iconPack) + } + + @Insert + fun installIconPack(iconPack: IconPackEntity) + + @Query("SELECT * FROM IconPack") + fun getInstalledIconPacks(): List + + @Query("SELECT * FROM IconPack") + fun getInstalledIconPacksLiveData(): LiveData> + + @Delete + fun deleteIconPack(iconPack: IconPackEntity) + + @Query("SELECT * FROM IconPack WHERE packageName = :packageName AND version = :version") + fun getPacks(packageName: String, version: String): List + + @Transaction + fun isInstalled(iconPack: IconPackEntity): Boolean { + return getPacks(iconPack.packageName, iconPack.version).isNotEmpty() + } + + @Query("DELETE FROM Icons WHERE iconPack NOT IN (:packs)") + fun deleteAllIconsExcept(packs: List) + + @Query("DELETE FROM IconPack WHERE packageName NOT IN (:packs)") + fun deleteAllPacksExcept(packs: List) + + @Transaction + fun uninstallIconPacksExcept(packs: List) { + deleteAllIconsExcept(packs) + deleteAllPacksExcept(packs) + } + + @Query("SELECT drawable FROM Icons WHERE iconPack = :pack AND type = 'iconback'") + fun getIconBacks(pack: String): List + + @Query("SELECT drawable FROM Icons WHERE iconPack = :pack AND type = 'iconupon'") + fun getIconUpons(pack: String): List + + @Query("SELECT drawable FROM Icons WHERE iconPack = :pack AND type = 'iconmask'") + fun getIconMasks(pack: String): List + + @Query("SELECT scale FROM IconPack WHERE packageName = :pack") + fun getScale(pack: String): Float? +} \ No newline at end of file diff --git a/database/src/main/java/de/mm20/launcher2/database/SearchDao.kt b/database/src/main/java/de/mm20/launcher2/database/SearchDao.kt new file mode 100644 index 00000000..d66fa64f --- /dev/null +++ b/database/src/main/java/de/mm20/launcher2/database/SearchDao.kt @@ -0,0 +1,120 @@ +package de.mm20.launcher2.database + +import androidx.lifecycle.LiveData +import androidx.room.* +import de.mm20.launcher2.database.entities.FavoritesItemEntity +import de.mm20.launcher2.database.entities.WebsearchEntity + +@Dao +interface SearchDao { + + @Insert() + fun insertAll(items: List) + + @Insert(onConflict = OnConflictStrategy.IGNORE) + fun insertAllSkipExisting(items: List) + + @Insert(onConflict = OnConflictStrategy.IGNORE) + fun insertSkipExisting(items: FavoritesItemEntity) + + @Query("SELECT * FROM Searchable WHERE pinned > 0 ORDER BY pinned DESC, launchCount DESC") + fun getFavorites() : LiveData> + + + @Query("SELECT COUNT(key) as count FROM Searchable WHERE pinned = 1;") + fun getPinCount(): Int + + @Query("SELECT * FROM Searchable WHERE pinned = 0 AND launchCount > 0 AND hidden = 0 ORDER BY launchCount DESC LIMIT :count") + fun getAutoFavorites(count: Int): List + + @Query("DELETE FROM Searchable WHERE `key` IN (:keys)") + fun deleteAll(keys: List) + + + @Query("UPDATE Searchable SET pinned = 1, hidden = 0 WHERE `key` = :key") + fun pinExistingItem(key: String) + + @Transaction + fun pinToFavorites(item: FavoritesItemEntity) { + pinExistingItem(item.key) + insertSkipExisting(item) + } + + @Query("UPDATE Searchable SET pinned = 0 WHERE `key` = :key") + fun unpinFavorite(key: String) + + @Query("DELETE FROM Searchable WHERE `key` = :key") + fun deleteByKey(key: String) + + @Query("UPDATE Searchable SET pinned = 0 WHERE `key` = :key") + fun unpinApp(key: String) + + + @Query("SELECT pinned FROM Searchable WHERE `key` = :key UNION SELECT 0 as pinned ORDER BY pinned DESC LIMIT 1") + fun isPinned(key: String): LiveData + + + @Query("UPDATE Searchable SET hidden = 1, pinned = 0 WHERE `key` = :key") + fun hideExistingItem(key: String) + + @Transaction + fun hideItem(item: FavoritesItemEntity) { + hideExistingItem(item.key) + insertSkipExisting(item) + } + + @Query("UPDATE Searchable SET hidden = 0 WHERE `key` = :key") + fun unhideItem(key: String) + + @Query("SELECT hidden FROM Searchable WHERE `key` = :key UNION SELECT 0 as hidden ORDER BY hidden DESC LIMIT 1") + fun isHidden(key: String): LiveData + + @Query("SELECT `key` FROM SEARCHABLE WHERE hidden = 1") + fun getHiddenItemKeys(): LiveData> + + @Query("SELECT * FROM SEARCHABLE WHERE hidden = 1") + fun getHiddenItems(): LiveData> + + @Query("SELECT * FROM Websearch ORDER BY label ASC") + fun getWebSearches(): List + + @Query("SELECT * FROM Websearch ORDER BY label ASC") + fun getWebSearchesLiveData(): LiveData> + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertWebsearch(websearch: WebsearchEntity) + + @Delete + fun deleteWebsearch(websearch: WebsearchEntity) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertAllWebsearches(websearches: List) + + @Query("UPDATE Searchable SET launchCount = launchCount + 1 WHERE `key` = :key") + fun incrementExistingLaunchCount(key: String) + + @Transaction + fun incrementLaunchCount(item: FavoritesItemEntity) { + incrementExistingLaunchCount(item.key) + insertSkipExisting(item) + } + + @Query("SELECT * FROM Searchable WHERE `key` = :key") + fun getFavorite(key: String): FavoritesItemEntity? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertReplaceExisting(toDatabaseEntity: FavoritesItemEntity) + + @Query("SELECT * FROM Searchable WHERE (pinned > 0 OR launchCount > 0) AND hidden = 0 ORDER BY pinned DESC, launchCount DESC") + fun getAllFavoriteItems(): List + + @Transaction + fun saveFavorites(favorites: List) { + deleteAllFavorites() + insertAll(favorites) + } + + @Query("DELETE FROM Searchable WHERE hidden = 0") + fun deleteAllFavorites() + +} diff --git a/database/src/main/java/de/mm20/launcher2/database/WeatherDao.kt b/database/src/main/java/de/mm20/launcher2/database/WeatherDao.kt new file mode 100644 index 00000000..9bd7bfc7 --- /dev/null +++ b/database/src/main/java/de/mm20/launcher2/database/WeatherDao.kt @@ -0,0 +1,27 @@ +package de.mm20.launcher2.database + +import androidx.lifecycle.LiveData +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy.REPLACE +import androidx.room.Query +import androidx.room.Transaction +import de.mm20.launcher2.database.entities.ForecastEntity + +@Dao +interface WeatherDao { + @Query("SELECT * FROM ${ForecastEntity.TABLE_NAME} ORDER BY timestamp ASC") + fun getForecasts(): LiveData> + + @Insert(onConflict = REPLACE) + fun insertAll(forecasts: List) + + @Query("DELETE FROM ${ForecastEntity.TABLE_NAME}") + fun deleteAll() + + @Transaction + fun replaceAll(forecasts: List) { + deleteAll() + insertAll(forecasts) + } +} \ No newline at end of file diff --git a/database/src/main/java/de/mm20/launcher2/database/WidgetDao.kt b/database/src/main/java/de/mm20/launcher2/database/WidgetDao.kt new file mode 100644 index 00000000..ef317713 --- /dev/null +++ b/database/src/main/java/de/mm20/launcher2/database/WidgetDao.kt @@ -0,0 +1,23 @@ +package de.mm20.launcher2.database + +import androidx.lifecycle.LiveData +import androidx.room.* +import de.mm20.launcher2.database.entities.WidgetEntity + +@Dao +interface WidgetDao { + @Query("SELECT * FROM Widget ORDER BY position ASC") + fun getWidgets(): List + + @Transaction + fun updateWidgets(widgets: List) { + deleteAll() + insertAll(widgets) + } + + @Insert + fun insertAll(widgets: List) + + @Query("DELETE FROM Widget") + fun deleteAll() +} \ No newline at end of file diff --git a/database/src/main/java/de/mm20/launcher2/database/entities/CurrencyEntity.kt b/database/src/main/java/de/mm20/launcher2/database/entities/CurrencyEntity.kt new file mode 100644 index 00000000..be95bb83 --- /dev/null +++ b/database/src/main/java/de/mm20/launcher2/database/entities/CurrencyEntity.kt @@ -0,0 +1,11 @@ +package de.mm20.launcher2.database.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "Currency") +data class CurrencyEntity( + @PrimaryKey val symbol: String, + val value: Double, + val lastUpdate: Long +) \ No newline at end of file diff --git a/database/src/main/java/de/mm20/launcher2/database/entities/FavoritesItemEntity.kt b/database/src/main/java/de/mm20/launcher2/database/entities/FavoritesItemEntity.kt new file mode 100644 index 00000000..03a54ca7 --- /dev/null +++ b/database/src/main/java/de/mm20/launcher2/database/entities/FavoritesItemEntity.kt @@ -0,0 +1,14 @@ +package de.mm20.launcher2.database.entities + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "Searchable") +data class FavoritesItemEntity( + @PrimaryKey val key: String, + @ColumnInfo(name = "searchable") val serializedSearchable: String, + var launchCount: Int, + @ColumnInfo(name = "pinned") var pinPosition: Int, + var hidden: Boolean +) diff --git a/database/src/main/java/de/mm20/launcher2/database/entities/ForecastEntity.kt b/database/src/main/java/de/mm20/launcher2/database/entities/ForecastEntity.kt new file mode 100644 index 00000000..7304ec83 --- /dev/null +++ b/database/src/main/java/de/mm20/launcher2/database/entities/ForecastEntity.kt @@ -0,0 +1,33 @@ +package de.mm20.launcher2.database.entities + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = ForecastEntity.TABLE_NAME) +data class ForecastEntity( + @PrimaryKey val timestamp: Long, + val temperature: Double, + val minTemp: Double = -1.0, + val maxTemp: Double = -1.0, + val pressure: Double = -1.0, + val humidity: Double = -1.0, + val icon: Int, + val condition: String, + val clouds: Int = -1, + val windSpeed: Double = -1.0, + val windDirection: Double = -1.0, + @ColumnInfo(name = "rain") val precipitation: Double = -1.0, + val snow: Double = -1.0, + val night: Boolean = false, + val location: String, + val provider: String, + val providerUrl: String = "", + @ColumnInfo(name = "rainProbability") val precipProbability: Int = -1, + val snowProbability: Int = -1, + val updateTime: Long +) { + companion object { + const val TABLE_NAME = "forecasts" + } +} \ No newline at end of file diff --git a/database/src/main/java/de/mm20/launcher2/database/entities/IconEntity.kt b/database/src/main/java/de/mm20/launcher2/database/entities/IconEntity.kt new file mode 100644 index 00000000..f5564e88 --- /dev/null +++ b/database/src/main/java/de/mm20/launcher2/database/entities/IconEntity.kt @@ -0,0 +1,15 @@ +package de.mm20.launcher2.database.entities + +import android.content.ComponentName +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "Icons") +data class IconEntity( + val type: String, + val componentName: ComponentName?, + val drawable: String?, + val iconPack: String, + val scale : Float? = null, + @PrimaryKey(autoGenerate = true) val id : Long? = null +) \ No newline at end of file diff --git a/database/src/main/java/de/mm20/launcher2/database/entities/IconPackEntity.kt b/database/src/main/java/de/mm20/launcher2/database/entities/IconPackEntity.kt new file mode 100644 index 00000000..0c54c17a --- /dev/null +++ b/database/src/main/java/de/mm20/launcher2/database/entities/IconPackEntity.kt @@ -0,0 +1,12 @@ +package de.mm20.launcher2.database.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "IconPack") +data class IconPackEntity( + val name: String, + @PrimaryKey val packageName: String, + val version: String, + var scale: Float = 1f +) \ No newline at end of file diff --git a/database/src/main/java/de/mm20/launcher2/database/entities/PluginEntity.kt b/database/src/main/java/de/mm20/launcher2/database/entities/PluginEntity.kt new file mode 100644 index 00000000..f43e7188 --- /dev/null +++ b/database/src/main/java/de/mm20/launcher2/database/entities/PluginEntity.kt @@ -0,0 +1,10 @@ +package de.mm20.launcher2.database.entities + +import androidx.room.Entity + +@Entity(tableName = "Plugin", primaryKeys = ["packageName", "data"]) +data class PluginEntity( + val packageName: String, + val data: String, + val type: String +) \ No newline at end of file diff --git a/database/src/main/java/de/mm20/launcher2/database/entities/WebsearchEntity.kt b/database/src/main/java/de/mm20/launcher2/database/entities/WebsearchEntity.kt new file mode 100644 index 00000000..5a14a97a --- /dev/null +++ b/database/src/main/java/de/mm20/launcher2/database/entities/WebsearchEntity.kt @@ -0,0 +1,13 @@ +package de.mm20.launcher2.database.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "Websearch") +data class WebsearchEntity( + var urlTemplate: String, + var label: String, + var color: Int, + var icon: String?, + @PrimaryKey(autoGenerate = true) val id: Long? +) \ No newline at end of file diff --git a/database/src/main/java/de/mm20/launcher2/database/entities/WidgetEntity.kt b/database/src/main/java/de/mm20/launcher2/database/entities/WidgetEntity.kt new file mode 100644 index 00000000..97bbe8cf --- /dev/null +++ b/database/src/main/java/de/mm20/launcher2/database/entities/WidgetEntity.kt @@ -0,0 +1,15 @@ +package de.mm20.launcher2.database.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey + + +@Entity(tableName = "Widget") +data class WidgetEntity( + val type: String, + var data: String, + var height: Int, + var position: Int, + val label: String = "", + @PrimaryKey(autoGenerate = true) val id: Int? = null +) \ No newline at end of file diff --git a/favorites/.gitignore b/favorites/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/favorites/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/favorites/build.gradle.kts b/favorites/build.gradle.kts new file mode 100644 index 00000000..88c08a8c --- /dev/null +++ b/favorites/build.gradle.kts @@ -0,0 +1,54 @@ +plugins { + id("com.android.library") + id("kotlin-android") + id("kotlin-android-extensions") +} + +android { + compileSdk = sdk.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = sdk.versions.minSdk.get().toInt() + targetSdk = sdk.versions.targetSdk.get().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation(libs.bundles.kotlin) + implementation(libs.androidx.core) + implementation(libs.androidx.appcompat) + + implementation(project(":search")) + implementation(project(":calendar")) + implementation(project(":database")) + implementation(project(":preferences")) + implementation(project(":applications")) + implementation(project(":contacts")) + implementation(project(":ktx")) + implementation(project(":files")) + implementation(project(":websites")) + implementation(project(":wikipedia")) + +} \ No newline at end of file diff --git a/favorites/consumer-rules.pro b/favorites/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/favorites/proguard-rules.pro b/favorites/proguard-rules.pro new file mode 100644 index 00000000..6f610ec0 --- /dev/null +++ b/favorites/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.kts.kts.kts.kts.kts.kts.kts.kts.kts.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/favorites/src/main/AndroidManifest.xml b/favorites/src/main/AndroidManifest.xml new file mode 100644 index 00000000..ec0d4793 --- /dev/null +++ b/favorites/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/favorites/src/main/java/de/mm20/launcher2/favorites/FavoritesItem.kt b/favorites/src/main/java/de/mm20/launcher2/favorites/FavoritesItem.kt new file mode 100644 index 00000000..fa5914b4 --- /dev/null +++ b/favorites/src/main/java/de/mm20/launcher2/favorites/FavoritesItem.kt @@ -0,0 +1,35 @@ +package de.mm20.launcher2.favorites + +import android.content.Context +import de.mm20.launcher2.database.entities.FavoritesItemEntity +import de.mm20.launcher2.search.data.Searchable + +data class FavoritesItem( + val key: String, + /** + * null if searchable could not be deserialized (i.e. the app has been uninstalled) + */ + val searchable: Searchable?, + var launchCount: Int, + var pinPosition: Int, + var hidden: Boolean +){ + constructor(context: Context, entity: FavoritesItemEntity) : this( + key = entity.key, + searchable = SearchableDeserializer(context).deserialize(entity.serializedSearchable), + launchCount = entity.launchCount, + pinPosition = entity.pinPosition, + hidden = entity.hidden + ) + + + fun toDatabaseEntity(): FavoritesItemEntity { + return FavoritesItemEntity( + key = key, + serializedSearchable = searchable?.let { "${SearchableDeserializer.getTypePrefix(it)}#${it.serialize()}" } ?: "", + hidden = hidden, + pinPosition = pinPosition, + launchCount = launchCount + ) + } +} \ No newline at end of file diff --git a/favorites/src/main/java/de/mm20/launcher2/favorites/FavoritesRepository.kt b/favorites/src/main/java/de/mm20/launcher2/favorites/FavoritesRepository.kt new file mode 100644 index 00000000..0bd8861c --- /dev/null +++ b/favorites/src/main/java/de/mm20/launcher2/favorites/FavoritesRepository.kt @@ -0,0 +1,218 @@ +package de.mm20.launcher2.favorites + +import android.content.Context +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import de.mm20.launcher2.database.AppDatabase +import de.mm20.launcher2.database.entities.FavoritesItemEntity +import de.mm20.launcher2.ktx.ceilToInt +import de.mm20.launcher2.preferences.LauncherPreferences +import de.mm20.launcher2.search.BaseSearchableRepository +import de.mm20.launcher2.search.data.CalendarEvent +import de.mm20.launcher2.search.data.Searchable +import kotlinx.coroutines.* +import kotlin.math.max +import kotlin.math.min + +class FavoritesRepository private constructor(private val context: Context) : BaseSearchableRepository() { + + private val scope = CoroutineScope(Job() + Dispatchers.Main) + + private val favorites = MediatorLiveData>() + private val favoriteItems: LiveData> = MutableLiveData() + + val hiddenItems = MediatorLiveData>() + + private val pinnedFavorites = AppDatabase.getInstance(context).searchDao().getFavorites() + + + val pinnedCalendarEvents = MediatorLiveData>() + + private val reloadFavorites: (String) -> Unit = { + scope.launch { + if(!LauncherPreferences.instance.searchShowFavorites) { + favorites.value = emptyList() + return@launch + } + val favs = mutableListOf() + withContext(Dispatchers.IO) { + val dao = AppDatabase.getInstance(context).searchDao() + val favItems = pinnedFavorites.value ?: emptyList() + favs.addAll(favItems.mapNotNull { + val item = FavoritesItem(context, it) + if (item.searchable == null) { + dao.deleteByKey(item.key) + } + if (item.searchable is CalendarEvent) return@mapNotNull null + item.searchable + }) + var favCount = (favs.size.toDouble() / columns).ceilToInt() * columns + if(favItems.size < columns) favCount += columns + val autoFavs = dao.getAutoFavorites(favCount - favs.size) + favs.addAll(autoFavs.mapNotNull { + val item = FavoritesItem(context, it) + if (item.searchable == null) { + dao.deleteByKey(item.key) + } + item.searchable + }) + } + favorites.value = favs + } + } + + private var columns = 1 + + init { + val hidden = AppDatabase.getInstance(context).searchDao().getHiddenItems() + hiddenItems.addSource(hidden) { h -> + hiddenItems.value = h.mapNotNull { FavoritesItem(context, it).searchable } + } + favorites.addSource(pinnedFavorites) { + reloadFavorites("") + } + pinnedCalendarEvents.addSource(pinnedFavorites) { + scope.launch { + val dao = AppDatabase.getInstance(context).searchDao() + pinnedCalendarEvents.value = it.filter { it.key.startsWith("calendar://") }.mapNotNull { + val item = FavoritesItem(context, it) + if (item.searchable == null) { + withContext(Dispatchers.IO) { dao.deleteByKey(item.key) } + } + item.searchable as? CalendarEvent + } + } + } + LauncherPreferences.instance.doOnPreferenceChange( + "search_show_favorites", + "search_auto_add_favorites", + action = reloadFavorites + ) + } + + + fun isHidden(searchable: Searchable): LiveData { + return AppDatabase.getInstance(context).searchDao().isHidden(searchable.key) + } + + fun getFavorites(columns: Int): LiveData> { + if (columns != this.columns) { + this.columns = columns + reloadFavorites("") + } + return favorites + } + + override suspend fun search(query: String) { + if (query.isEmpty()) { + reloadFavorites("") + } else { + favorites.value = emptyList() + } + } + + fun isPinned(searchable: Searchable): LiveData { + return AppDatabase.getInstance(context).searchDao().isPinned(searchable.key) + } + + fun pinItem(searchable: Searchable) { + scope.launch { + withContext(Dispatchers.IO) { + val dao = AppDatabase.getInstance(context).searchDao() + val databaseItem = dao.getFavorite(searchable.key) + val favoritesItem = FavoritesItem( + key = searchable.key, + searchable = searchable, + launchCount = databaseItem?.launchCount ?: 0, + pinPosition = 1, + hidden = false + ) + dao.insertReplaceExisting(favoritesItem.toDatabaseEntity()) + } + } + } + + fun unpinItem(searchable: Searchable) { + scope.launch { + withContext(Dispatchers.IO) { + AppDatabase.getInstance(context).searchDao().unpinFavorite(searchable.key) + } + } + } + + fun hideItem(searchable: Searchable) { + scope.launch { + withContext(Dispatchers.IO) { + val dao = AppDatabase.getInstance(context).searchDao() + val databaseItem = dao.getFavorite(searchable.key) + val favoritesItem = FavoritesItem( + key = searchable.key, + searchable = searchable, + launchCount = databaseItem?.launchCount ?: 0, + pinPosition = 0, + hidden = true + ) + dao.insertReplaceExisting(favoritesItem.toDatabaseEntity()) + } + } + } + + fun unhideItem(searchable: Searchable) { + scope.launch { + withContext(Dispatchers.IO) { + AppDatabase.getInstance(context).searchDao().unhideItem(searchable.key) + } + } + } + + fun deleteItem(key: String) { + scope.launch { + withContext(Dispatchers.IO) { + AppDatabase.getInstance(context).searchDao().deleteByKey(key) + } + } + } + + fun incrementLaunchCount(searchable: Searchable) { + scope.launch { + withContext(Dispatchers.IO) { + val item = FavoritesItem(searchable.key, searchable, 0, 0, false) + AppDatabase.getInstance(context).searchDao().incrementLaunchCount(item.toDatabaseEntity()) + } + } + } + + suspend fun getAllFavoriteItems(): List { + return withContext(Dispatchers.IO) { + AppDatabase.getInstance(context).searchDao().getAllFavoriteItems().mapNotNull { + FavoritesItem(context, it).takeIf { it.searchable != null } + } + } + } + + fun saveFavorites(favorites: MutableList) { + scope.launch { + withContext(Dispatchers.IO) { + AppDatabase.getInstance(context).searchDao().saveFavorites(favorites.map { it.toDatabaseEntity() }) + } + } + } + + fun getTopFavorites(count: Int): LiveData> { + val favs = MediatorLiveData>() + favs.addSource(favorites) { + favs.value = it.subList(0, min(count, it.size)) + } + return favs + } + + companion object { + private lateinit var instance: FavoritesRepository + fun getInstance(context: Context): FavoritesRepository { + if (!::instance.isInitialized) instance = FavoritesRepository(context.applicationContext) + return instance + } + } +} \ No newline at end of file diff --git a/favorites/src/main/java/de/mm20/launcher2/favorites/FavoritesViewModel.kt b/favorites/src/main/java/de/mm20/launcher2/favorites/FavoritesViewModel.kt new file mode 100644 index 00000000..18adeb51 --- /dev/null +++ b/favorites/src/main/java/de/mm20/launcher2/favorites/FavoritesViewModel.kt @@ -0,0 +1,58 @@ +package de.mm20.launcher2.favorites + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import de.mm20.launcher2.search.data.CalendarEvent +import de.mm20.launcher2.search.data.Searchable +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class FavoritesViewModel(app: Application) : AndroidViewModel(app) { + + val repository = FavoritesRepository.getInstance(app) + + fun getTopFavorites(count: Int): LiveData> { + return repository.getTopFavorites(count) + } + + fun getFavorites(columns: Int): LiveData> { + return repository.getFavorites(columns) + } + + fun pinItem(searchable: Searchable) { + repository.pinItem(searchable) + } + + fun unpinItem(searchable: Searchable) { + repository.unpinItem(searchable) + } + + fun isPinned(searchable: Searchable): LiveData { + return repository.isPinned(searchable) + } + + fun isHidden(searchable: Searchable): LiveData { + return repository.isHidden(searchable) + } + + fun hideItem(searchable: Searchable) { + repository.hideItem(searchable) + } + + fun unhideItem(searchable: Searchable) { + repository.unhideItem(searchable) + } + + suspend fun getAllFavoriteItems(): List { + return repository.getAllFavoriteItems() + } + + fun saveFavorites(favorites: MutableList) { + repository.saveFavorites(favorites) + } + + val hiddenItems: LiveData> = repository.hiddenItems + val pinnedCalendarEvents: LiveData> = repository.pinnedCalendarEvents +} \ No newline at end of file diff --git a/favorites/src/main/java/de/mm20/launcher2/favorites/SearchableDeserializer.kt b/favorites/src/main/java/de/mm20/launcher2/favorites/SearchableDeserializer.kt new file mode 100644 index 00000000..1363003c --- /dev/null +++ b/favorites/src/main/java/de/mm20/launcher2/favorites/SearchableDeserializer.kt @@ -0,0 +1,47 @@ +package de.mm20.launcher2.favorites + +import android.content.Context +import android.util.Log +import de.mm20.launcher2.search.data.* + +class SearchableDeserializer(val context: Context) { + fun deserialize(serialized: String?): Searchable? { + val type = serialized?.substringBefore("#") ?: return null + val data = serialized.substringAfter("#") + return when (type) { + "app" -> LauncherApp.deserialize(context, data) + "shortcut" -> AppShortcut.deserialize(context, data) + "calculator" -> null + "calendar" -> CalendarEvent.deserialize(context, data) + "contact" -> Contact.deserialize(context, data) + "gdrive" -> GDriveFile.deserialize(data) + "owncloud" -> OwncloudFile.deserialize(data) + "nextcloud" -> NextcloudFile.deserialize(data) + "file" -> File.deserialize(context, data) + "onedrive" -> OneDriveFile.deserialize(data) + "websearch" -> null + "website" -> Website.deserialize(data) + "wikipedia" -> Wikipedia.deserialize(data) + else -> null + } + } + + companion object { + fun getTypePrefix(searchable: Searchable): String { + return when(searchable) { + is Application -> "app" + is AppShortcut -> "shortcut" + is CalendarEvent -> "calendar" + is Contact -> "contact" + is GDriveFile -> "gdrive" + is OneDriveFile -> "onedrive" + is NextcloudFile -> "nextcloud" + is OwncloudFile -> "owncloud" + is File -> "file" + is Website -> "website" + is Wikipedia -> "wikipedia" + else -> "" + } + } + } +} diff --git a/files/.gitignore b/files/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/files/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/files/build.gradle.kts b/files/build.gradle.kts new file mode 100644 index 00000000..f0e66d51 --- /dev/null +++ b/files/build.gradle.kts @@ -0,0 +1,57 @@ +plugins { + id("com.android.library") + id("kotlin-android") + id("kotlin-android-extensions") +} + +android { + compileSdk = sdk.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = sdk.versions.minSdk.get().toInt() + targetSdk = sdk.versions.targetSdk.get().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation(libs.bundles.kotlin) + implementation(libs.androidx.core) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.exifinterface) + + implementation(libs.bundles.androidx.lifecycle) + + implementation(project(":search")) + implementation(project(":hiddenitems")) + implementation(project(":preferences")) + implementation(project(":base")) + implementation(project(":ktx")) + implementation(project(":ms-services")) + implementation(project(":g-services")) + implementation(project(":nextcloud")) + implementation(project(":owncloud")) + implementation(project(":i18n")) + implementation(project(":permissions")) +} \ No newline at end of file diff --git a/files/consumer-rules.pro b/files/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/files/proguard-rules.pro b/files/proguard-rules.pro new file mode 100644 index 00000000..35280e32 --- /dev/null +++ b/files/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.kts.kts.kts.kts.kts.kts.kts.kts.kts.kts.kts.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/files/src/main/AndroidManifest.xml b/files/src/main/AndroidManifest.xml new file mode 100644 index 00000000..b5b954c3 --- /dev/null +++ b/files/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + / + \ No newline at end of file diff --git a/files/src/main/java/de/mm20/launcher2/files/FilesRepository.kt b/files/src/main/java/de/mm20/launcher2/files/FilesRepository.kt new file mode 100644 index 00000000..56bc12c8 --- /dev/null +++ b/files/src/main/java/de/mm20/launcher2/files/FilesRepository.kt @@ -0,0 +1,70 @@ +package de.mm20.launcher2.files + +import android.content.Context +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import de.mm20.launcher2.hiddenitems.HiddenItemsRepository +import de.mm20.launcher2.nextcloud.NextcloudApiHelper +import de.mm20.launcher2.owncloud.OwncloudClient +import de.mm20.launcher2.search.BaseSearchableRepository +import de.mm20.launcher2.search.data.* +import kotlinx.coroutines.* + +class FilesRepository private constructor(val context: Context) : BaseSearchableRepository() { + + val files = MediatorLiveData?>() + + private val allFiles = MutableLiveData?>(emptyList()) + private val hiddenItemKeys = HiddenItemsRepository.getInstance(context).hiddenItemsKeys + + private val nextcloudClient by lazy { + NextcloudApiHelper(context) + } + private val owncloudClient by lazy { + OwncloudClient(context) + } + + init { + files.addSource(hiddenItemKeys) { keys -> + files.value = allFiles.value?.filter { !keys.contains(it.key) } + } + files.addSource(allFiles) { f -> + files.value = f?.filter { hiddenItemKeys.value?.contains(it.key) != true } + } + } + + override suspend fun search(query: String) { + if (query.isBlank()) { + allFiles.value = null + return + } + val localFiles = withContext(Dispatchers.IO) { + File.search(context, query).sorted().toMutableList() + } + allFiles.value = localFiles + + val cloudFiles = withContext(Dispatchers.IO) { + delay(300) + listOf( + async { OneDriveFile.search(context, query) }, + async { GDriveFile.search(context, query) }, + async { NextcloudFile.search(context, query, nextcloudClient) }, + async { OwncloudFile.search(context, query, owncloudClient) } + ).awaitAll().flatten() + } + yield() + allFiles.value = localFiles + cloudFiles + } + + fun removeFile(file: File) { + allFiles.value = allFiles.value?.filter { it != file } + } + + companion object { + private lateinit var instance: FilesRepository + fun getInstance(context: Context): FilesRepository { + if (!::instance.isInitialized) instance = FilesRepository(context.applicationContext) + return instance + } + } +} \ No newline at end of file diff --git a/files/src/main/java/de/mm20/launcher2/files/FilesViewModel.kt b/files/src/main/java/de/mm20/launcher2/files/FilesViewModel.kt new file mode 100644 index 00000000..ef3e91b9 --- /dev/null +++ b/files/src/main/java/de/mm20/launcher2/files/FilesViewModel.kt @@ -0,0 +1,16 @@ +package de.mm20.launcher2.files + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import de.mm20.launcher2.search.data.File + +class FilesViewModel(app: Application): AndroidViewModel(app) { + + + private val repository = FilesRepository.getInstance(app) + val files = repository.files + + fun removeFile(file: File) { + repository.removeFile(file) + } +} \ No newline at end of file diff --git a/files/src/main/java/de/mm20/launcher2/media/ThumbnailUtilsCompat.kt b/files/src/main/java/de/mm20/launcher2/media/ThumbnailUtilsCompat.kt new file mode 100644 index 00000000..7d955347 --- /dev/null +++ b/files/src/main/java/de/mm20/launcher2/media/ThumbnailUtilsCompat.kt @@ -0,0 +1,26 @@ +package de.mm20.launcher2.media + +import android.graphics.Bitmap +import android.media.ThumbnailUtils +import android.os.Build +import android.os.CancellationSignal +import android.provider.MediaStore +import android.util.Size +import androidx.core.content.ContentResolverCompat +import java.io.File +import java.io.IOException + +object ThumbnailUtilsCompat { + fun createVideoThumbnail(file: File, size: Size, signal: CancellationSignal? = null): Bitmap? { + return try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ThumbnailUtils.createVideoThumbnail(file, size, signal) + } else { + ThumbnailUtils.createVideoThumbnail(file.absolutePath, + MediaStore.Video.Thumbnails.MICRO_KIND) + } + } catch (e: IOException) { + null + } + } +} \ No newline at end of file diff --git a/files/src/main/java/de/mm20/launcher2/search/data/File.kt b/files/src/main/java/de/mm20/launcher2/search/data/File.kt new file mode 100644 index 00000000..bb548a68 --- /dev/null +++ b/files/src/main/java/de/mm20/launcher2/search/data/File.kt @@ -0,0 +1,403 @@ +package de.mm20.launcher2.search.data + +import android.Manifest +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteQueryBuilder +import android.graphics.BitmapFactory +import android.graphics.drawable.AdaptiveIconDrawable +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.ColorDrawable +import android.location.Geocoder +import android.media.MediaMetadataRetriever +import android.media.ThumbnailUtils +import android.net.Uri +import android.os.Build +import android.provider.MediaStore +import android.text.format.DateUtils +import android.util.Size +import androidx.core.content.ContentResolverCompat +import androidx.core.content.ContextCompat +import androidx.core.content.FileProvider +import androidx.core.database.getStringOrNull +import androidx.exifinterface.media.ExifInterface +import de.mm20.launcher2.files.R +import de.mm20.launcher2.ktx.checkPermission +import de.mm20.launcher2.ktx.formatToString +import de.mm20.launcher2.ktx.jsonObjectOf +import de.mm20.launcher2.icons.LauncherIcon +import de.mm20.launcher2.media.ThumbnailUtilsCompat +import de.mm20.launcher2.permissions.PermissionsManager +import de.mm20.launcher2.preferences.LauncherPreferences +import org.json.JSONObject +import java.io.IOException +import java.util.* +import java.io.File as JavaIOFile + +open class File( + val id: Long, + val path: String, + val mimeType: String, + val size: Long, + val isDirectory: Boolean, + val metaData: List> +) : Searchable() { + + override val label = path.substringAfterLast('/') + + override val key = "file://$path" + + open val isStoredInCloud = false + + override suspend fun loadIconAsync(context: Context, size: Int): LauncherIcon? { + if (!JavaIOFile(path).exists()) return null + when { + mimeType.startsWith("image/") -> { + val thumbnail = ThumbnailUtils.extractThumbnail(BitmapFactory.decodeFile(path), + size, size) ?: return null + return LauncherIcon( + foreground = BitmapDrawable(context.resources, thumbnail), + autoGenerateBackgroundMode = LauncherIcon.BACKGROUND_COLOR + ) + } + mimeType.startsWith("video/") -> { + val thumbnail = ThumbnailUtilsCompat.createVideoThumbnail(JavaIOFile(path), + Size(size, size)) ?: return null + return LauncherIcon( + foreground = BitmapDrawable(context.resources, thumbnail), + autoGenerateBackgroundMode = LauncherIcon.BACKGROUND_COLOR + ) + } + mimeType.startsWith("audio/") -> { + val mediaMetadataRetriever = MediaMetadataRetriever() + try { + mediaMetadataRetriever.setDataSource(path) + val thumbData = mediaMetadataRetriever.embeddedPicture + if (thumbData != null) { + val thumbnail = ThumbnailUtils.extractThumbnail( + BitmapFactory.decodeByteArray(thumbData, 0, thumbData.size), size, size) + mediaMetadataRetriever.release() + thumbnail ?: return null + return LauncherIcon( + foreground = BitmapDrawable(context.resources, thumbnail), + autoGenerateBackgroundMode = LauncherIcon.BACKGROUND_COLOR + ) + } + } catch (e: RuntimeException) { + mediaMetadataRetriever.release() + return null + } + } + mimeType == "application/vnd.android.package-archive" -> { + val pkgInfo = context.packageManager.getPackageArchiveInfo(path, 0) + val icon = pkgInfo?.applicationInfo?.loadIcon(context.packageManager) ?: return null + when { + Build.VERSION.SDK_INT > Build.VERSION_CODES.O && icon is AdaptiveIconDrawable -> { + return LauncherIcon( + foreground = icon.foreground, + background = icon.background, + foregroundScale = 1.5f, + backgroundScale = 1.5f + ) + } + else -> { + return LauncherIcon( + foreground = icon, + foregroundScale = 0.7f, + autoGenerateBackgroundMode = LauncherIcon.BACKGROUND_COLOR + ) + } + } + } + } + return null + } + + override fun getPlaceholderIcon(context: Context): LauncherIcon { + val (resId, bgColor) = when { + isDirectory -> R.drawable.ic_file_folder to R.color.lightblue + mimeType.startsWith("image/") -> R.drawable.ic_file_picture to R.color.teal + mimeType.startsWith("audio/") -> R.drawable.ic_file_music to R.color.orange + mimeType.startsWith("video/") -> R.drawable.ic_file_video to R.color.purple + else -> when (mimeType) { + "application/zip", "application/x-gtar", "application/x-tar", + "application/java-archive", "application/x-7z-compressed", + "application/x-compressed-tar", "application/x-gzip", "application/x-bzip2" -> R.drawable.ic_file_archive to R.color.brown + "application/pdf" -> R.drawable.ic_file_pdf to R.color.red + "application/vnd.oasis.opendocument.text", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/msword", "text/plain", "application/vnd.google-apps.document" -> R.drawable.ic_file_document to R.color.blue + "application/vnd.oasis.opendocument.spreadsheet", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.ms-excel", "application/vnd.google-apps.spreadsheet" -> R.drawable.ic_file_spreadsheet to R.color.green + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "application/vnd.ms-powerpoint", "application/vnd.google-apps.presentation" -> R.drawable.ic_file_presentation to R.color.amber + "text/x-asm", "text/x-c", "text/x-java-source", "text/x-script.phyton", "text/x-pascal", + "text/x-script.perl", "text/javascript", "application/json" -> R.drawable.ic_file_code to R.color.pink + "text/xml", "text/html" -> R.drawable.ic_file_markup to R.color.deeporange + "application/vnd.android.package-archive" -> R.drawable.ic_file_android to R.color.lightgreen + "application/vnd.google-apps.form" -> R.drawable.ic_file_form to R.color.deeppurple + "application/vnd.google-apps.drawing" -> R.drawable.ic_file_picture to R.color.teal + else -> R.drawable.ic_file_generic to R.color.bluegrey + } + } + return LauncherIcon( + foreground = context.getDrawable(resId)!!, + background = ColorDrawable(ContextCompat.getColor(context, bgColor)), + foregroundScale = 0.5f + ) + } + + override fun getLaunchIntent(context: Context): Intent? { + val uri = FileProvider.getUriForFile(context, + context.applicationContext.packageName + ".fileprovider", JavaIOFile(path)) + return Intent(Intent.ACTION_VIEW) + .setDataAndType(uri, mimeType) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + + override fun serialize(): String { + return jsonObjectOf( + "id" to id + ).toString() + } + + fun getFileType(context: Context): String { + if (isDirectory) return context.getString(R.string.file_type_directory) + val resource = when (mimeType) { + "application/zip", + "application/x-zip-compressed", + "application/x-gtar", + "application/x-tar", + "application/java-archive", + "application/x-7z-compressed" -> R.string.file_type_archive + "application/x-gzip", + "application/x-bzip2" -> R.string.file_type_compressed + "application/vnd.android.package-archive" -> R.string.file_type_android + "text/x-asm", + "text/x-c", + "text/x-java-source", + "text/x-script.phyton", + "text/x-pascal", + "text/x-script.perl", + "text/javascript", + "application/json" -> R.string.file_type_source_code + "application/vnd.oasis.opendocument.text", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/msword", + "application/x-iwork-pages-sffpages", + "application/vnd.apple.pages", + "application/vnd.google-apps.document" -> R.string.file_type_document + "application/vnd.oasis.opendocument.spreadsheet", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.ms-excel", + "application/x-iwork-numbers-sffnumbers", + "application/vnd.apple.numbers", + "application/vnd.google-apps.spreadsheet" -> R.string.file_type_spreadsheet + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "application/vnd.ms-powerpoint", + "application/x-iwork-keynote-sffkey", + "application/vnd.apple.keynote", + "application/vnd.google-apps.presentation" -> R.string.file_type_presentation + "text/plain" -> R.string.file_type_text + "application/vnd.google-apps.drawing" -> R.string.file_type_drawing + "application/vnd.google-apps.form" -> R.string.file_type_form + "application/epub+zip" -> R.string.file_type_ebook + else -> when { + mimeType.startsWith("image/") -> R.string.file_type_image + mimeType.startsWith("video/") -> R.string.file_type_video + mimeType.startsWith("audio/") -> R.string.file_type_music + else -> R.string.file_type_none + } + } + if (resource == R.string.file_type_none && label.matches(Regex(".+\\..+"))) { + val extension = label.substringAfterLast(".").toUpperCase(Locale.getDefault()) + return context.getString(R.string.file_type_generic, extension) + } + return context.getString(resource) + } + + companion object { + fun search(context: Context, query: String): List { + val results = mutableListOf() + if (!LauncherPreferences.instance.searchFiles) return results + if (query.isBlank()) return results + if (!PermissionsManager.checkPermission(context, PermissionsManager.EXTERNAL_STORAGE)) return results + val uri = MediaStore.Files.getContentUri("external").buildUpon().appendQueryParameter("limit", "10").build() + val projection = arrayOf( + MediaStore.Files.FileColumns.DISPLAY_NAME, + MediaStore.Files.FileColumns._ID, + MediaStore.Files.FileColumns.SIZE, + MediaStore.Files.FileColumns.DATA, + MediaStore.Files.FileColumns.MIME_TYPE) + val selection = if (query.length > 3) "${MediaStore.Files.FileColumns.TITLE} LIKE ?" else "${MediaStore.Files.FileColumns.TITLE} = ?" + val selArgs = if (query.length > 3) arrayOf("%$query%") else arrayOf(query) + val sort = "${MediaStore.Files.FileColumns.DISPLAY_NAME} COLLATE NOCASE ASC" + + + val cursor = context.contentResolver.query(uri, projection, selection, selArgs, sort) + ?: return results + while (cursor.moveToNext()) { + if (results.size >= 10) { + break + } + val path = cursor.getString(3) + if (!JavaIOFile(path).exists()) continue + val directory = JavaIOFile(path).isDirectory + val mimeType = (cursor.getStringOrNull(4) + ?: if (directory) "inode/directory" else getMimetypeByFileExtension(path.substringAfterLast('.'))) + val file = File( + path = path, + mimeType = mimeType, + size = cursor.getLong(2), + isDirectory = directory, + id = cursor.getLong(1), + metaData = getMetaData(context, mimeType, path)) + results.add(file) + } + cursor.close() + return results.sortedBy { it } + } + + private fun getMimetypeByFileExtension(extension: String): String { + return when (extension) { + "apk" -> "application/vnd.android.package-archive" + "zip" -> "application/zip" + "jar" -> "application/java-archive" + "txt" -> "text/plain" + "js" -> "text/javascript" + "html", "htm" -> "text/html" + "css" -> "text/css" + "gif" -> "image/gif" + "png" -> "image/png" + "jpg", "jpeg" -> "image/jpeg" + "bmp" -> "image/bmp" + "webp" -> "image/webp" + "ico" -> "image/x-icon" + "midi" -> "audio/midi" + "mp3" -> "audio/mpeg3" + "webm" -> "audio/webm" + "ogg" -> "audio/ogg" + "wav" -> "audio/wav" + "mp4" -> "video/mp4" + else -> "application/octet-stream" + } + } + + + private fun getMetaData(context: Context, mimeType: String, path: String): List> { + val metaData = mutableListOf>() + when { + mimeType.startsWith("audio/") -> { + val retriever = MediaMetadataRetriever() + try { + retriever.setDataSource(path) + arrayOf( + R.string.file_meta_title to MediaMetadataRetriever.METADATA_KEY_TITLE, + R.string.file_meta_artist to MediaMetadataRetriever.METADATA_KEY_ARTIST, + R.string.file_meta_album to MediaMetadataRetriever.METADATA_KEY_ALBUM, + R.string.file_meta_year to MediaMetadataRetriever.METADATA_KEY_YEAR + ).forEach { + retriever.extractMetadata(it.second)?.let { m -> metaData.add(it.first to m) } + } + val duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0 + val d = DateUtils.formatElapsedTime((duration) / 1000) + metaData.add(3, R.string.file_meta_duration to d) + retriever.release() + } catch (e: RuntimeException) { + retriever.release() + } + } + mimeType.startsWith("video/") -> { + val retriever = MediaMetadataRetriever() + try { + retriever.setDataSource(path) + val width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toLong() ?: 0 + val height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toLong() ?: 0 + metaData.add(R.string.file_meta_dimensions to "${width}x$height") + val duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0 + val d = DateUtils.formatElapsedTime(duration / 1000) + metaData.add(R.string.file_meta_duration to d) + val loc = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION) + if (Geocoder.isPresent() && loc != null) { + val lon = loc.substring(0, loc.lastIndexOfAny(charArrayOf('+', '-'))).toDouble() + val lat = loc.substring(loc.lastIndexOfAny(charArrayOf('+', '-')), loc.indexOf('/')).toDouble() + val list = Geocoder(context).getFromLocation(lon, lat, 1) + if (list.size > 0) { + metaData.add(R.string.file_meta_location to list[0].formatToString()) + } + } + retriever.release() + } catch (e: RuntimeException) { + retriever.release() + } + } + mimeType.startsWith("image/") -> { + val options = BitmapFactory.Options() + options.inJustDecodeBounds = true + BitmapFactory.decodeFile(path, options) + val width = options.outWidth + val height = options.outHeight + metaData.add(R.string.file_meta_dimensions to "${width}x$height") + try { + val exif = ExifInterface(path) + val loc = exif.latLong + if (loc != null && Geocoder.isPresent()) { + val list = Geocoder(context).getFromLocation(loc[0], loc[1], 1) + if (list.size > 0) { + metaData.add(R.string.file_meta_location to list[0].formatToString()) + } + } + } catch (_: IOException) { + + } + } + mimeType == "application/vnd.android.package-archive" -> { + val pkgInfo = context.packageManager.getPackageArchiveInfo(path, 0) + ?: return metaData + metaData.add(R.string.file_meta_app_name to pkgInfo.applicationInfo.loadLabel(context.packageManager).toString()) + metaData.add(R.string.file_meta_app_pkgname to pkgInfo.packageName) + metaData.add(R.string.file_meta_app_version to pkgInfo.versionName) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + metaData.add(R.string.file_meta_app_min_sdk to pkgInfo.applicationInfo.minSdkVersion.toString()) + } + } + } + return metaData + } + + fun deserialize(context: Context, serialized: String): File? { + if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) return null + val json = JSONObject(serialized) + val uri = MediaStore.Files.getContentUri("external") + val proj = arrayOf(MediaStore.Files.FileColumns._ID, + MediaStore.Files.FileColumns.SIZE, + MediaStore.Files.FileColumns.DATA, + MediaStore.Files.FileColumns.MIME_TYPE) + val sel = "${MediaStore.Files.FileColumns._ID} = ?" + val selArgs = arrayOf(json.getLong("id").toString()) + val cursor = context.contentResolver.query(uri, proj, sel, selArgs, null) ?: return null + if (cursor.moveToNext()) { + val path = cursor.getString(2) + if (!JavaIOFile(path).exists()) return null + val directory = JavaIOFile(path).isDirectory + val id = cursor.getLong(0) + val mimeType = cursor.getStringOrNull(3) + ?: if (directory) "inode/directory" else getMimetypeByFileExtension(path.substringAfterLast('.')) + val size = cursor.getLong(1) + cursor.close() + return File( + path = path, + mimeType = mimeType, + size = size, + isDirectory = directory, + id = id, + metaData = getMetaData(context, mimeType, path)) + } + cursor.close() + return null + } + } +} \ No newline at end of file diff --git a/files/src/main/java/de/mm20/launcher2/search/data/GDriveFile.kt b/files/src/main/java/de/mm20/launcher2/search/data/GDriveFile.kt new file mode 100644 index 00000000..c3b079af --- /dev/null +++ b/files/src/main/java/de/mm20/launcher2/search/data/GDriveFile.kt @@ -0,0 +1,126 @@ +package de.mm20.launcher2.search.data + +import android.content.Context +import android.content.Intent +import android.net.Uri +import de.mm20.launcher2.files.R +import de.mm20.launcher2.gservices.DriveFileMeta +import de.mm20.launcher2.gservices.GoogleApiHelper +import de.mm20.launcher2.helper.NetworkUtils +import de.mm20.launcher2.icons.LauncherIcon +import de.mm20.launcher2.ktx.jsonObjectOf +import de.mm20.launcher2.preferences.LauncherPreferences +import org.json.JSONObject + +class GDriveFile( + val fileId: String, + override val label: String, + path: String, + mimeType: String, + size: Long, + isDirectory: Boolean, + metaData: List>, + val directoryColor: String?, + val viewUri: String +) : File(0, path, mimeType, size, isDirectory, metaData) { + + override val key: String = "gdrive://$fileId" + + override val badgeKey: String + get() = "gdrive://" + + override fun serialize(): String { + return jsonObjectOf( + "id" to fileId, + "label" to label, + "path" to path, + "mimeType" to mimeType, + "size" to size, + "directory" to isDirectory, + "color" to directoryColor, + "uri" to viewUri + ).apply { + for ((k, v) in metaData) { + put(when (k) { + R.string.file_meta_owner -> "owner" + R.string.file_meta_dimensions -> "dimensions" + else -> "other" + }, v) + } + }.toString() + } + + override val isStoredInCloud = true + + override fun getLaunchIntent(context: Context): Intent? { + return Intent(Intent.ACTION_VIEW).apply { + data = Uri.parse(viewUri) + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + } + + override suspend fun loadIconAsync(context: Context, size: Int): LauncherIcon? { + return null + } + + companion object { + suspend fun search(context: Context, query: String): List { + if (query.length < 4) return emptyList() + val prefs = LauncherPreferences.instance + if (!prefs.searchGDrive) return emptyList() + if (NetworkUtils.isOffline(context, prefs.searchGDriveMobileData)) return emptyList() + val driveFiles = GoogleApiHelper.getInstance(context).queryGDriveFiles(query) + return driveFiles.map { + GDriveFile( + fileId = it.fileId, + label = it.label, + size = it.size, + mimeType = it.mimeType, + isDirectory = it.isDirectory, + path = "", + directoryColor = it.directoryColor, + viewUri = it.viewUri, + metaData = getMetadata(it.metadata) + ) + }.sorted() + } + + private fun getMetadata(file: DriveFileMeta): List> { + val metaData = mutableListOf>() + val owners = file.owners + metaData.add(R.string.file_meta_owner to owners.joinToString(separator = ", ")) + val width = file.width ?: file.width + val height = file.height ?: file.height + if (width != null && height != null) metaData.add(R.string.file_meta_dimensions to "${width}x$height") + return metaData + } + + fun deserialize(serialized: String): GDriveFile? { + val json = JSONObject(serialized) + val id = json.getString("id") + val label = json.getString("label") + val path = json.getString("path") + val mimeType = json.getString("mimeType") + val size = json.getLong("size") + val directory = json.getBoolean("directory") + val color = json.optString("color") + val uri = json.getString("uri") + val owner = json.optString("owner") + val dimensions = json.optString("dimensions") + val metaData = mutableListOf>() + owner.takeIf { it.isNotEmpty() }?.let { metaData.add(R.string.file_meta_owner to it) } + dimensions.takeIf { it.isNotEmpty() }?.let { metaData.add(R.string.file_meta_dimensions to it) } + return GDriveFile( + fileId = id, + label = label, + path = path, + mimeType = mimeType, + size = size, + directoryColor = color, + isDirectory = directory, + viewUri = uri, + metaData = metaData + ) + } + } +} \ No newline at end of file diff --git a/files/src/main/java/de/mm20/launcher2/search/data/NextcloudFile.kt b/files/src/main/java/de/mm20/launcher2/search/data/NextcloudFile.kt new file mode 100644 index 00000000..a42789b8 --- /dev/null +++ b/files/src/main/java/de/mm20/launcher2/search/data/NextcloudFile.kt @@ -0,0 +1,101 @@ +package de.mm20.launcher2.search.data + +import android.content.Context +import android.content.Intent +import android.net.Uri +import de.mm20.launcher2.files.R +import de.mm20.launcher2.helper.NetworkUtils +import de.mm20.launcher2.ktx.jsonObjectOf +import de.mm20.launcher2.nextcloud.NextcloudApiHelper +import de.mm20.launcher2.preferences.LauncherPreferences +import org.json.JSONObject + +class NextcloudFile( + fileId: Long, + override val label: String, + path: String, + mimeType: String, + size: Long, + isDirectory: Boolean, + val server: String, + metaData: List> +) : File(fileId, path, mimeType, size, isDirectory, metaData) { + override val badgeKey: String = "nextcloud://" + + override val key: String = "nextcloud://$server/$fileId" + + override val isStoredInCloud: Boolean + get() = true + + override fun getLaunchIntent(context: Context): Intent? { + return Intent(Intent.ACTION_VIEW).apply { + data = Uri.parse("$server/f/$id") + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + } + + override fun serialize(): String { + return jsonObjectOf( + "id" to id, + "label" to label, + "path" to path, + "mimeType" to mimeType, + "size" to size, + "isDirectory" to isDirectory, + "server" to server + ).apply { + for ((k, v) in metaData) { + put(when (k) { + R.string.file_meta_owner -> "owner" + else -> "other" + }, v) + } + }.toString() + } + + companion object { + suspend fun search(context: Context, query: String, nextcloudClient: NextcloudApiHelper) : List { + if (!LauncherPreferences.instance.searchNextcloud) return emptyList() + if (query.length < 4) return emptyList() + val server = nextcloudClient.getServer() ?: return emptyList() + if (NetworkUtils.isOffline(context, LauncherPreferences.instance.searchGDriveMobileData)) return emptyList() + return nextcloudClient.files.search(query).map { + NextcloudFile( + fileId = it.id, + label = it.name, + path = server + it.url, + mimeType = it.mimeType, + size = it.size, + isDirectory = it.isDirectory, + server = server, + metaData = it.owner?.let { listOf(R.string.file_meta_owner to it) } ?: emptyList() + ) + } + } + + fun deserialize(serialized: String): NextcloudFile? { + val json = JSONObject(serialized) + val id = json.getLong("id") + val label = json.getString("label") + val path = json.getString("path") + val mimeType = json.getString("mimeType") + val size = json.getLong("size") + val isDirectory = json.getBoolean("isDirectory") + val server = json.getString("server") + val owner = json.optString("owner").takeIf { it.isNotEmpty() } + + return NextcloudFile( + fileId = id, + label = label, + path = path, + mimeType = mimeType, + size = size, + isDirectory = isDirectory, + server = server, + metaData = owner?.let { listOf(R.string.file_meta_owner to it) } ?: emptyList() + + ) + } + + } +} \ No newline at end of file diff --git a/files/src/main/java/de/mm20/launcher2/search/data/OneDriveFile.kt b/files/src/main/java/de/mm20/launcher2/search/data/OneDriveFile.kt new file mode 100644 index 00000000..4405c98c --- /dev/null +++ b/files/src/main/java/de/mm20/launcher2/search/data/OneDriveFile.kt @@ -0,0 +1,123 @@ +package de.mm20.launcher2.search.data + +import android.content.Context +import android.content.Intent +import android.net.Uri +import de.mm20.launcher2.msservices.DriveItem +import de.mm20.launcher2.files.R +import de.mm20.launcher2.msservices.MicrosoftGraphApiHelper +import de.mm20.launcher2.ktx.jsonObjectOf +import de.mm20.launcher2.icons.LauncherIcon +import de.mm20.launcher2.preferences.LauncherPreferences +import org.json.JSONObject + +class OneDriveFile( + val fileId: String, + override val label: String, + path: String, + mimeType: String, + size: Long, + isDirectory: Boolean, + metaData: List>, + val webUrl: String +) : File(0, path, mimeType, size, isDirectory, metaData) { + + override val badgeKey: String = "onedrive://" + + override val key: String = "onedrive://$fileId" + + override val isStoredInCloud = true + + override suspend fun loadIconAsync(context: Context, size: Int): LauncherIcon? { + return null + } + + override fun getLaunchIntent(context: Context): Intent? { + return Intent(Intent.ACTION_VIEW).apply { + data = Uri.parse(webUrl) + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + } + + override fun serialize(): String { + return jsonObjectOf( + "id" to fileId, + "label" to label, + "mimeType" to mimeType, + "size" to size, + "directory" to isDirectory, + "webUrl" to webUrl + ).apply { + for ((k, v) in metaData) { + put(when (k) { + R.string.file_meta_owner -> "owner" + R.string.file_meta_dimensions -> "dimensions" + else -> "other" + }, v) + } + }.toString() + } + + companion object { + suspend fun search(context: Context, query: String): List { + if (query.length < 4) return emptyList() + if (!LauncherPreferences.instance.searchOneDrive) return emptyList() + val driveItems = MicrosoftGraphApiHelper.getInstance(context).queryOneDriveFiles(query) ?: return emptyList() + val files = mutableListOf() + for (driveItem in driveItems) { + files += OneDriveFile( + fileId = driveItem.id, + label = driveItem.label, + path = "", + mimeType = driveItem.mimeType, + size = driveItem.size, + isDirectory = driveItem.isDirectory, + metaData = getMetaData(driveItem), + webUrl = driveItem.webUrl + ) + } + return files.sorted() + } + + fun deserialize(serialized: String): OneDriveFile? { + val json = JSONObject(serialized) + val fileId = json.getString("id") + val label = json.getString("label") + val mimeType = json.getString("mimeType") + val size = json.getLong("size") + val isDirectory = json.getBoolean("directory") + val webUrl = json.getString("webUrl") + val owner = json.optString("owner") + val dimensions = json.optString("dimensions") + val metaData = mutableListOf>() + owner.takeIf { it.isNotEmpty() }?.let { metaData.add(R.string.file_meta_owner to it) } + dimensions.takeIf { it.isNotEmpty() }?.let { metaData.add(R.string.file_meta_dimensions to it) } + return OneDriveFile( + fileId = fileId, + label = label, + path = "", + mimeType = mimeType, + size = size, + isDirectory = isDirectory, + metaData = metaData, + webUrl = webUrl + ) + } + + private fun getMetaData(driveItem: DriveItem): List> { + val metaData = mutableListOf>() + driveItem.meta.owner?.let { + metaData.add(R.string.file_meta_owner to it) + } ?: driveItem.meta.createdBy?.let { + metaData.add(R.string.file_meta_owner to it) + } + val width = driveItem.meta.width + val height = driveItem.meta.height + + if (width != null && height != null) { + metaData.add(R.string.file_meta_dimensions to "${width}x${height}") + } + return metaData + } + } +} \ No newline at end of file diff --git a/files/src/main/java/de/mm20/launcher2/search/data/OwncloudFile.kt b/files/src/main/java/de/mm20/launcher2/search/data/OwncloudFile.kt new file mode 100644 index 00000000..a9c96111 --- /dev/null +++ b/files/src/main/java/de/mm20/launcher2/search/data/OwncloudFile.kt @@ -0,0 +1,101 @@ +package de.mm20.launcher2.search.data + +import android.content.Context +import android.content.Intent +import android.net.Uri +import de.mm20.launcher2.files.R +import de.mm20.launcher2.helper.NetworkUtils +import de.mm20.launcher2.ktx.jsonObjectOf +import de.mm20.launcher2.owncloud.OwncloudClient +import de.mm20.launcher2.preferences.LauncherPreferences +import org.json.JSONObject + +class OwncloudFile( + fileId: Long, + override val label: String, + path: String, + mimeType: String, + size: Long, + isDirectory: Boolean, + val server: String, + metaData: List> +) : File(fileId, path, mimeType, size, isDirectory, metaData) { + override val badgeKey: String = "owncloud://" + + override val key: String = "owncloud://$server/$fileId" + + override val isStoredInCloud: Boolean + get() = true + + override fun getLaunchIntent(context: Context): Intent? { + return Intent(Intent.ACTION_VIEW).apply { + data = Uri.parse("$server/f/$id") + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + } + + override fun serialize(): String { + return jsonObjectOf( + "id" to id, + "label" to label, + "path" to path, + "mimeType" to mimeType, + "size" to size, + "isDirectory" to isDirectory, + "server" to server + ).apply { + for ((k, v) in metaData) { + put(when (k) { + R.string.file_meta_owner -> "owner" + else -> "other" + }, v) + } + }.toString() + } + + companion object { + suspend fun search(context: Context, query: String, owncloudClient: OwncloudClient) : List { + if (!LauncherPreferences.instance.searchOwncloud) return emptyList() + if (query.length < 4) return emptyList() + val server = owncloudClient.getServer() ?: return emptyList() + if (NetworkUtils.isOffline(context, LauncherPreferences.instance.searchGDriveMobileData)) return emptyList() + return owncloudClient.files.query(query).map { + OwncloudFile( + fileId = it.id, + label = it.name, + path = server + it.url, + mimeType = it.mimeType, + size = it.size, + isDirectory = it.isDirectory, + server = server, + metaData = it.owner?.let { listOf(R.string.file_meta_owner to it) } ?: emptyList() + ) + } + } + + fun deserialize(serialized: String): OwncloudFile? { + val json = JSONObject(serialized) + val id = json.getLong("id") + val label = json.getString("label") + val path = json.getString("path") + val mimeType = json.getString("mimeType") + val size = json.getLong("size") + val isDirectory = json.getBoolean("isDirectory") + val server = json.getString("server") + val owner = json.optString("owner").takeIf { it.isNotEmpty() } + + return OwncloudFile( + fileId = id, + label = label, + path = path, + mimeType = mimeType, + size = size, + isDirectory = isDirectory, + server = server, + metaData = owner?.let { listOf(R.string.file_meta_owner to it) } ?: emptyList() + + ) + } + + } +} \ No newline at end of file diff --git a/files/src/main/res/drawable/ic_file_android.xml b/files/src/main/res/drawable/ic_file_android.xml new file mode 100644 index 00000000..8d3fd387 --- /dev/null +++ b/files/src/main/res/drawable/ic_file_android.xml @@ -0,0 +1,5 @@ + + + diff --git a/files/src/main/res/drawable/ic_file_archive.xml b/files/src/main/res/drawable/ic_file_archive.xml new file mode 100644 index 00000000..f38c06fe --- /dev/null +++ b/files/src/main/res/drawable/ic_file_archive.xml @@ -0,0 +1,5 @@ + + + diff --git a/files/src/main/res/drawable/ic_file_code.xml b/files/src/main/res/drawable/ic_file_code.xml new file mode 100644 index 00000000..f7cc253e --- /dev/null +++ b/files/src/main/res/drawable/ic_file_code.xml @@ -0,0 +1,5 @@ + + + diff --git a/files/src/main/res/drawable/ic_file_document.xml b/files/src/main/res/drawable/ic_file_document.xml new file mode 100644 index 00000000..f55b73f2 --- /dev/null +++ b/files/src/main/res/drawable/ic_file_document.xml @@ -0,0 +1,5 @@ + + + diff --git a/files/src/main/res/drawable/ic_file_folder.xml b/files/src/main/res/drawable/ic_file_folder.xml new file mode 100644 index 00000000..82347288 --- /dev/null +++ b/files/src/main/res/drawable/ic_file_folder.xml @@ -0,0 +1,5 @@ + + + diff --git a/files/src/main/res/drawable/ic_file_form.xml b/files/src/main/res/drawable/ic_file_form.xml new file mode 100644 index 00000000..84cf107e --- /dev/null +++ b/files/src/main/res/drawable/ic_file_form.xml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/files/src/main/res/drawable/ic_file_generic.xml b/files/src/main/res/drawable/ic_file_generic.xml new file mode 100644 index 00000000..a431a5e9 --- /dev/null +++ b/files/src/main/res/drawable/ic_file_generic.xml @@ -0,0 +1,5 @@ + + + diff --git a/files/src/main/res/drawable/ic_file_markup.xml b/files/src/main/res/drawable/ic_file_markup.xml new file mode 100644 index 00000000..6720df48 --- /dev/null +++ b/files/src/main/res/drawable/ic_file_markup.xml @@ -0,0 +1,9 @@ + + + diff --git a/files/src/main/res/drawable/ic_file_music.xml b/files/src/main/res/drawable/ic_file_music.xml new file mode 100644 index 00000000..69f0a3a4 --- /dev/null +++ b/files/src/main/res/drawable/ic_file_music.xml @@ -0,0 +1,5 @@ + + + diff --git a/files/src/main/res/drawable/ic_file_pdf.xml b/files/src/main/res/drawable/ic_file_pdf.xml new file mode 100644 index 00000000..a73638b0 --- /dev/null +++ b/files/src/main/res/drawable/ic_file_pdf.xml @@ -0,0 +1,5 @@ + + + diff --git a/files/src/main/res/drawable/ic_file_picture.xml b/files/src/main/res/drawable/ic_file_picture.xml new file mode 100644 index 00000000..0d8d5030 --- /dev/null +++ b/files/src/main/res/drawable/ic_file_picture.xml @@ -0,0 +1,5 @@ + + + diff --git a/files/src/main/res/drawable/ic_file_presentation.xml b/files/src/main/res/drawable/ic_file_presentation.xml new file mode 100644 index 00000000..4d799702 --- /dev/null +++ b/files/src/main/res/drawable/ic_file_presentation.xml @@ -0,0 +1,5 @@ + + + diff --git a/files/src/main/res/drawable/ic_file_spreadsheet.xml b/files/src/main/res/drawable/ic_file_spreadsheet.xml new file mode 100644 index 00000000..da1dbcc4 --- /dev/null +++ b/files/src/main/res/drawable/ic_file_spreadsheet.xml @@ -0,0 +1,5 @@ + + + diff --git a/files/src/main/res/drawable/ic_file_video.xml b/files/src/main/res/drawable/ic_file_video.xml new file mode 100644 index 00000000..349fbf31 --- /dev/null +++ b/files/src/main/res/drawable/ic_file_video.xml @@ -0,0 +1,5 @@ + + + diff --git a/g-services/.gitignore b/g-services/.gitignore new file mode 100644 index 00000000..bcb56ceb --- /dev/null +++ b/g-services/.gitignore @@ -0,0 +1,2 @@ +/build +*/**/g_services.json diff --git a/g-services/build.gradle.kts b/g-services/build.gradle.kts new file mode 100644 index 00000000..b5a00fb0 --- /dev/null +++ b/g-services/build.gradle.kts @@ -0,0 +1,55 @@ +plugins { + id("com.android.library") + id("kotlin-android") + id("kotlin-android-extensions") +} + +android { + compileSdk = sdk.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = sdk.versions.minSdk.get().toInt() + targetSdk = sdk.versions.targetSdk.get().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation(libs.bundles.kotlin) + implementation(libs.androidx.core) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.browser) + implementation(libs.bundles.androidx.lifecycle) + + implementation(libs.google.auth) + implementation(libs.google.apiclient) + implementation(libs.google.drive) + implementation(libs.google.oauth2) + + + implementation(libs.bundles.materialdialogs) + + implementation(project(":i18n")) + implementation(project(":crashreporter")) +} \ No newline at end of file diff --git a/g-services/consumer-rules.pro b/g-services/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/g-services/proguard-rules.pro b/g-services/proguard-rules.pro new file mode 100644 index 00000000..ff59496d --- /dev/null +++ b/g-services/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/g-services/readme.md b/g-services/readme.md new file mode 100644 index 00000000..4c1333c9 --- /dev/null +++ b/g-services/readme.md @@ -0,0 +1,30 @@ +# :g-services + +⚠️ Depends on non-free external services. + +This module manages API calls to Google APIs and connected Google accounts. + +## Configuration + +This module requires additional configuration in order to work properly. You can skip this step but +then Google API related features (e.g. Google Drive search) won't be available. + +In order to use Google APIs, you need to setup a new project in the Google Cloud Console first. + +1. Open the [Google Cloud Console](https://console.cloud.google.com) +1. Create a new project. +1. Enable the Drive API: + 1. Go to APIs & Services > Library and search for the Google Drive API. + 1. Enable this API for your project. +1. Create a new Oauth 2.0 client (you need to do this twice, for debug builds and for release builds) + 1. Go to APIs & Services > Credentials + 1. Click on Create Credentials > OAuth client ID + 1. Choose application type Android + 1. Enter the package name (de.mm20.launcher2.debug for debug builds or de.mm20.launcher2.release for release builds) + 1. Enter the SHA-1 certificate fingerprint of your APK signing key + 1. Click create +1. Download the client config file (repeat this step for both the debug and the release client) + 1. On the APIs & Services > Credentials page, find your OAuth client in the list under OAuth 2.0 Client IDs. + 1. Click the download icon to download a client_config.json + 1. Place this file under src/debug/res/raw/g_services.json or src/release/res/raw/g_services.json + diff --git a/g-services/src/debug/res/raw/g_services_example.json b/g-services/src/debug/res/raw/g_services_example.json new file mode 100644 index 00000000..71718eec --- /dev/null +++ b/g-services/src/debug/res/raw/g_services_example.json @@ -0,0 +1 @@ +{"installed":{"client_id":"xxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com","project_id":"xxxxx-xxxxxxxxxxxxx","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}} diff --git a/g-services/src/main/AndroidManifest.xml b/g-services/src/main/AndroidManifest.xml new file mode 100644 index 00000000..7a13544e --- /dev/null +++ b/g-services/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/g-services/src/main/java/de/mm20/launcher2/gservices/DriveFile.kt b/g-services/src/main/java/de/mm20/launcher2/gservices/DriveFile.kt new file mode 100644 index 00000000..6f6bcf9f --- /dev/null +++ b/g-services/src/main/java/de/mm20/launcher2/gservices/DriveFile.kt @@ -0,0 +1,40 @@ +package de.mm20.launcher2.gservices + +import com.google.api.services.drive.model.File +import java.util.* + +data class DriveFile( + val fileId : String, + val label: String, + val size: Long, + val mimeType : String, + val isDirectory : Boolean, + val directoryColor: String?, + val viewUri: String, + val metadata: DriveFileMeta +) { + companion object { + fun fromApiDriveFile(file: File): DriveFile { + return DriveFile( + fileId = file.id, + label = file.name, + size = file.getSize() ?: 0, + isDirectory = file.mimeType == "application/vnd.google-apps.folder", + mimeType = file.mimeType, + metadata = DriveFileMeta( + owners = file.owners?.map { it.displayName ?: it.emailAddress ?: "" } ?: emptyList(), + width = file.imageMediaMetadata?.width ?: file.videoMediaMetadata?.width, + height = file.imageMediaMetadata?.height ?: file.videoMediaMetadata?.height + ), + directoryColor = file.folderColorRgb?.toLowerCase(Locale.ROOT), + viewUri = file.webViewLink ?: "" + ) + } + } +} + +data class DriveFileMeta( + val owners : List, + val width: Int?, + val height: Int? +) \ No newline at end of file diff --git a/g-services/src/main/java/de/mm20/launcher2/gservices/GoogleAccount.kt b/g-services/src/main/java/de/mm20/launcher2/gservices/GoogleAccount.kt new file mode 100644 index 00000000..e6cdc69f --- /dev/null +++ b/g-services/src/main/java/de/mm20/launcher2/gservices/GoogleAccount.kt @@ -0,0 +1,5 @@ +package de.mm20.launcher2.gservices + +data class GoogleAccount( + val name: String +) \ No newline at end of file diff --git a/g-services/src/main/java/de/mm20/launcher2/gservices/GoogleApiHelper.kt b/g-services/src/main/java/de/mm20/launcher2/gservices/GoogleApiHelper.kt new file mode 100644 index 00000000..cec25f6f --- /dev/null +++ b/g-services/src/main/java/de/mm20/launcher2/gservices/GoogleApiHelper.kt @@ -0,0 +1,222 @@ +package de.mm20.launcher2.gservices + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.browser.customtabs.* +import androidx.core.content.edit +import com.google.api.client.auth.oauth2.Credential +import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow +import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets +import com.google.api.client.http.HttpRequestInitializer +import com.google.api.client.http.javanet.NetHttpTransport +import com.google.api.client.json.gson.GsonFactory +import com.google.api.client.util.store.FileDataStoreFactory +import com.google.api.services.drive.Drive +import com.google.api.services.oauth2.Oauth2 +import de.mm20.launcher2.crashreporter.CrashReporter +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.IOException + +class GoogleApiHelper private constructor(private val context: Context) { + + val transport by lazy { + NetHttpTransport() + } + + suspend fun queryGDriveFiles(query: String): List { + val requestInitializer = getRequestInitializer() ?: return emptyList() + val jsonFactory = GsonFactory.getDefaultInstance() + return withContext(Dispatchers.IO) { + try { + val drive = + Drive.Builder(transport, jsonFactory, requestInitializer).build() + val request = drive.files().list() + request.q = "name contains '${query.replace("'", "")}'" + request.pageSize = 10 + request.fields = + "files(id, webViewLink, size, name, mimeType, owners, imageMediaMetadata, videoMediaMetadata, folderColorRgb)" + request.corpora = "user" + val response = request.execute() + val files = response.files ?: return@withContext emptyList() + files.map { DriveFile.fromApiDriveFile(it) } + + } catch (e: IOException) { + emptyList() + } catch (e: Error) { + emptyList() + } + } + } + + private suspend fun getCredential(): Credential? { + val authFlow = getAuthFlow() ?: return null + return withContext(Dispatchers.IO) { + val credential: Credential? = authFlow.loadCredential(USER_ID) + if ((credential?.expiresInSeconds ?: 0) < 5 * 60) { + try { + if (credential?.refreshToken() == false) return@withContext null + } catch (e: IOException) { + CrashReporter.logException(e) + } + } + return@withContext credential + } + } + + private suspend fun getRequestInitializer(): HttpRequestInitializer? { + val credential = getCredential() ?: return null + + return HttpRequestInitializer { request -> + credential.initialize(request) + request?.connectTimeout = 5000 + request?.readTimeout = 10000 + } + } + + suspend fun getAccount(): GoogleAccount? { + + val name = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString( + PREF_ACCOUNT_NAME, + null + ) ?: loadAccountName() + + + return name?.let { + GoogleAccount(name = it) + } + } + + fun isAvailable(): Boolean { + return getConfigResId() != 0 + } + + private fun getConfigResId(): Int { + return context.resources.getIdentifier("g_services", "raw", context.packageName) + } + + + private fun getAuthFlow(): GoogleAuthorizationCodeFlow? { + val configResId = getConfigResId() + if (configResId == 0) return null + val jsonFactory = GsonFactory.getDefaultInstance() + return GoogleAuthorizationCodeFlow.Builder( + NetHttpTransport(), + jsonFactory, + GoogleClientSecrets.load( + jsonFactory, + context.resources.openRawResource(configResId).reader() + ), + SCOPES + ) + .setCredentialDataStore( + FileDataStoreFactory(context.filesDir).getDataStore( + "google_signin" + ) + ) + .build() + } + + fun login(activity: Activity) { + val authFlow = getAuthFlow() ?: return + + val url = authFlow + .newAuthorizationUrl() + .setRedirectUri(getRedirectUri()) + .toString() + val themeColor = 0xFF4285f4.toInt() + + val customTabsIntent = CustomTabsIntent + .Builder() + .setDefaultColorSchemeParams( + CustomTabColorSchemeParams.Builder() + .setToolbarColor(themeColor) + .setNavigationBarColor(themeColor) + .build() + ) + .build() + + callingActivity = activity.javaClass + + customTabsIntent.intent.flags = Intent.FLAG_ACTIVITY_NO_HISTORY + customTabsIntent.launchUrl(activity, Uri.parse(url)) + } + + suspend fun finishAuthFlow(activity: Activity, code: String) { + val authFlow = getAuthFlow() ?: return + withContext(Dispatchers.IO) { + val tokenResponse = try { + authFlow.newTokenRequest(code).setRedirectUri(getRedirectUri()).execute() + } catch (e: IOException) { + CrashReporter.logException(e) + return@withContext + } + authFlow.createAndStoreCredential(tokenResponse, USER_ID) + } + loadAccountName() + returnToPreviousActivity(activity) + } + + fun cancelAuthFlow(activity: Activity) { + returnToPreviousActivity(activity) + } + + private fun returnToPreviousActivity(activity: Activity) { + val intent = Intent(activity, callingActivity) + callingActivity = null + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) + activity.startActivity(intent) + } + + private suspend fun loadAccountName(): String? { + val requestInitializer = getRequestInitializer() ?: return null + val jsonFactory = GsonFactory.getDefaultInstance() + val oauth2 = Oauth2.Builder(transport, jsonFactory, requestInitializer).build() + try { + val meResponse = withContext(Dispatchers.IO) { + oauth2.userinfo().v2().me().get().execute() + } + if (meResponse != null) { + val name = meResponse.name + context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit { + putString(PREF_ACCOUNT_NAME, name) + } + return name + } + } catch (e: IOException) { + CrashReporter.logException(e) + } + return null + } + + + fun logout() { + val authFlow = getAuthFlow() ?: return + authFlow.credentialDataStore.clear() + context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit { + putString(PREF_ACCOUNT_NAME, null) + } + } + + private fun getRedirectUri(): String { + return "${context.packageName}:/google-auth-redirect" + } + + companion object { + private lateinit var instance: GoogleApiHelper + + fun getInstance(context: Context): GoogleApiHelper { + if (!::instance.isInitialized) instance = GoogleApiHelper(context.applicationContext) + return instance + } + + val SCOPES = setOf("https://www.googleapis.com/auth/drive", "profile") + const val USER_ID = "google-user" + const val PREFS = "google-account" + const val PREF_ACCOUNT_NAME = "name" + + private var callingActivity: Class? = null + } +} \ No newline at end of file diff --git a/g-services/src/main/java/de/mm20/launcher2/gservices/GoogleAuthRedirectActivity.kt b/g-services/src/main/java/de/mm20/launcher2/gservices/GoogleAuthRedirectActivity.kt new file mode 100644 index 00000000..0c60de11 --- /dev/null +++ b/g-services/src/main/java/de/mm20/launcher2/gservices/GoogleAuthRedirectActivity.kt @@ -0,0 +1,24 @@ +package de.mm20.launcher2.gservices + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.launch + +class GoogleAuthRedirectActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val gServiceHelper = GoogleApiHelper.getInstance(this) + val code = intent.data?.getQueryParameter("code") + if (code == null) { + gServiceHelper.cancelAuthFlow(this) + finish() + } + else { + lifecycleScope.launch { + gServiceHelper.finishAuthFlow(this@GoogleAuthRedirectActivity, code) + finish() + } + } + } +} \ No newline at end of file diff --git a/g-services/src/main/res/values/styles.xml b/g-services/src/main/res/values/styles.xml new file mode 100644 index 00000000..b8c8d4eb --- /dev/null +++ b/g-services/src/main/res/values/styles.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/g-services/src/release/res/raw/g_services_example.json b/g-services/src/release/res/raw/g_services_example.json new file mode 100644 index 00000000..71718eec --- /dev/null +++ b/g-services/src/release/res/raw/g_services_example.json @@ -0,0 +1 @@ +{"installed":{"client_id":"xxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com","project_id":"xxxxx-xxxxxxxxxxxxx","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..c872b9d0 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,23 @@ +# 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=-Xmx4096m -XX:MaxPermSize=1024m -XX:+HeapDumpOnOutOfMemoryError -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 + +org.gradle.java.home=/usr/lib/jvm/default + +android.useAndroidX=true +android.enableJetifier=true +android.injected.testOnly=false + +org.gradle.daemon=true +org.gradle.parallel=true +org.gradle.caching=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..f6b961fd Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..7531f5ae --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Aug 16 10:19:49 CEST 2021 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-rc-2-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew new file mode 100755 index 00000000..cccdd3d5 --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..e95643d6 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/hiddenitems/.gitignore b/hiddenitems/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/hiddenitems/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/hiddenitems/build.gradle.kts b/hiddenitems/build.gradle.kts new file mode 100644 index 00000000..2d9849de --- /dev/null +++ b/hiddenitems/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + id("com.android.library") + id("kotlin-android") + id("kotlin-android-extensions") +} + +android { + compileSdk = sdk.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = sdk.versions.minSdk.get().toInt() + targetSdk = sdk.versions.targetSdk.get().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation(libs.kotlin.stdlib) + implementation(libs.androidx.core) + implementation(libs.androidx.appcompat) + + implementation(project(":database")) + implementation(project(":search")) +} \ No newline at end of file diff --git a/hiddenitems/consumer-rules.pro b/hiddenitems/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/hiddenitems/proguard-rules.pro b/hiddenitems/proguard-rules.pro new file mode 100644 index 00000000..01639a19 --- /dev/null +++ b/hiddenitems/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.kts.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/hiddenitems/src/main/AndroidManifest.xml b/hiddenitems/src/main/AndroidManifest.xml new file mode 100644 index 00000000..0692c026 --- /dev/null +++ b/hiddenitems/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + / + \ No newline at end of file diff --git a/hiddenitems/src/main/java/de/mm20/launcher2/hiddenitems/HiddenItemsRepository.kt b/hiddenitems/src/main/java/de/mm20/launcher2/hiddenitems/HiddenItemsRepository.kt new file mode 100644 index 00000000..3fe5ef6e --- /dev/null +++ b/hiddenitems/src/main/java/de/mm20/launcher2/hiddenitems/HiddenItemsRepository.kt @@ -0,0 +1,29 @@ +package de.mm20.launcher2.hiddenitems + +import android.content.Context +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import de.mm20.launcher2.database.AppDatabase +import de.mm20.launcher2.search.data.Searchable + +/** + * A low level repository for hidden items. This can only be used to retrieve keys and to check + * whether an item is hidden. To retrieve actual Searchable objects, use FavoritesRepository. + */ +class HiddenItemsRepository private constructor(val context: Context) { + + val hiddenItemsKeys : LiveData> = AppDatabase.getInstance(context).searchDao().getHiddenItemKeys() + + fun isHidden(item: Searchable): LiveData { + return AppDatabase.getInstance(context).searchDao().isHidden(item.key) + } + + companion object { + private lateinit var instance: HiddenItemsRepository + + fun getInstance(context: Context): HiddenItemsRepository { + if(!Companion::instance.isInitialized) instance = HiddenItemsRepository(context.applicationContext) + return instance + } + } +} \ No newline at end of file diff --git a/hiddenitems/src/main/java/de/mm20/launcher2/hiddenitems/HiddenItemsViewModel.kt b/hiddenitems/src/main/java/de/mm20/launcher2/hiddenitems/HiddenItemsViewModel.kt new file mode 100644 index 00000000..e0b4c6cc --- /dev/null +++ b/hiddenitems/src/main/java/de/mm20/launcher2/hiddenitems/HiddenItemsViewModel.kt @@ -0,0 +1,9 @@ +package de.mm20.launcher2.hiddenitems + +import android.app.Application +import androidx.lifecycle.AndroidViewModel + +class HiddenItemsViewModel(app: Application): AndroidViewModel(app) { + val hiddenItemsKeys = HiddenItemsRepository.getInstance(app).hiddenItemsKeys + +} \ No newline at end of file diff --git a/i18n/.gitignore b/i18n/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/i18n/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/i18n/build.gradle.kts b/i18n/build.gradle.kts new file mode 100644 index 00000000..c8a0e186 --- /dev/null +++ b/i18n/build.gradle.kts @@ -0,0 +1,43 @@ +plugins { + id("com.android.library") + id("kotlin-android") + id("kotlin-android-extensions") +} + +android { + compileSdk = sdk.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = sdk.versions.minSdk.get().toInt() + targetSdk = sdk.versions.targetSdk.get().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation(libs.kotlin.stdlib) + implementation(libs.androidx.core) + implementation(libs.androidx.appcompat) + +} \ No newline at end of file diff --git a/i18n/consumer-rules.pro b/i18n/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/i18n/proguard-rules.pro b/i18n/proguard-rules.pro new file mode 100644 index 00000000..ff59496d --- /dev/null +++ b/i18n/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/i18n/readme.md b/i18n/readme.md new file mode 100644 index 00000000..533e5123 --- /dev/null +++ b/i18n/readme.md @@ -0,0 +1,12 @@ +# :i18n + +This module contains all data required for internationalization and localization. This includes +strings, icons (if they require localization), and config and default values. **All resources that +might need localization must go to this module.** + +## Contribute + +Currently looking into web translation services (e.g. Crowdin or something similar). For now just +manually translate the XML files or use Android Studio's Translations Editor and submit a pull +request with your changes. + diff --git a/i18n/src/main/AndroidManifest.xml b/i18n/src/main/AndroidManifest.xml new file mode 100644 index 00000000..fab88aca --- /dev/null +++ b/i18n/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/i18n/src/main/res/values-de/defaults.xml b/i18n/src/main/res/values-de/defaults.xml new file mode 100644 index 00000000..9ad73496 --- /dev/null +++ b/i18n/src/main/res/values-de/defaults.xml @@ -0,0 +1,5 @@ + + + false + EUR + \ No newline at end of file diff --git a/i18n/src/main/res/values-de/strings.xml b/i18n/src/main/res/values-de/strings.xml new file mode 100644 index 00000000..bc1b6759 --- /dev/null +++ b/i18n/src/main/res/values-de/strings.xml @@ -0,0 +1,409 @@ + + + Kvæsitso + Suchen + Wird vorbereitet… + %1$s-Link + Paketdatei + Deinstallieren + Teilen + Version %1$s\n%2$s + Einstellungen + Hintergrundbild + Einstellungen + ganztägig + Nord + Nordost + Nordnordost + Ostnordost + Ost + Ostsüdost + Südost + Südsüdost + Süd + Südsüdwest + Südwest + Westsüdwest + West + Westnordwest + Nordwest + Nordnordwest + Kein Niederschlag + Erscheinungsbild + Design + Hell + Dunkel + Über + Version + Entwickler + Links + Open-Source-Lizenzen + Webseite öffnen + Version, Entwickler, Open-Source-Lizenzen + Design, Symbole, Suchleiste, Systemleisten + Provider, Einheiten, Standort + Wetter + Standort + Automatischer Standort + GPS und Ortungsdienste verwenden, um Standort automatisch zu ermitteln + Nicht gesetzt + Standort + Grad Fahrenheit und Meilen pro Stunde verwenden + Imperiale Einheiten + °F + °C + %1$s mph + %1$s km/h + %1$s mm + Durchscheinende Karten + Debug + Debug-Informationen exportieren + Exportiere nach %1$s + https://de.wikipedia.org + Aus Wikipedia + Symbole + Form + Quadrat + Abgerundetes Quadrat + Squircle + Kreis + = %1$s + Suchleiste + Auf Startbildschirm + Hintergrund ausblenden + Nichts tun + Ausblenden + Verzeichnis + Archiv-Datei + Android-Paket-Datei + Quellcode-Datei + Dokument + Kalkulationstabelle + Video + Musik-Datei + Bild + Datei + %1$s-Datei + Auto (nach Tageszeit) + Systemeinstellung verwenden + Keine passende Anwendung installiert! + Präsentation + Komprimierte Datei + Text-Datei + Das Verzeichnis %1$s wird samt Inhalt unwideruflich gelöscht. Fortfahren? + Die Datei %1$s wird unwideruflich gelöscht. Fortfahren? + Titel + Künstler + Album + Länge + Jahr + %1$s: %2$s + Größe + Pfad + Typ + Abmessungen + App-Name + Version + Paketname + Min-SDK-Version + Dienste + Suche + Suchkategorien + Favoriten + Favoriten anzeigen + Favoritenliste über allen Apps anzeigen + Häufig genutze Elemente + Häufig genutzte Elemente automatisch in den Favoriten anzeigen + Dateien + Dateisuche + Ordner, Dokumente, Fotos und andere Dateitypen auf diesem Gerät durchsuchen + Wikipedia + Wikipedia durchsuchen + Mobile Daten verwenden + Zusätzliche Kosten können anfallen + Webseiten + Nach Webseiten suchen + Probieren Sie „wikipedia.org“ + Standard Web-Protokoll + Standard-Protokoll, um Webseiten zu suchen, wenn nicht angegeben. Stellen Sie https:// oder http:// vor Ihre Suchanfrage, um das Protokoll explizit festzulegen + HTTP (unverschlüsselt) + HTTPS (verschlüsselt) + Taschenrechner + Probieren Sie „4*2+9“ + Taschenrechner aktivieren + Probieren Sie „%1$s“ + Websuche + Shortcuts zu verschiedenen Websuch-Engines anzeigen + Websuchen-Shortcuts + Websuchen bearbeiten + Ort + Unschärfeeffekt + Hintergrund hinter Karten unscharf darstellen + Nicht mit Live-Hintergründen kompatibel + Google + YouTube + Google Play + https://google.de/search?q=${1} + https://www.youtube.com/results?search_query=${1} + https://play.google.com/store/search?q=${1} + Websuche hinzufügen… + Löschen + Name + URL + „${1}“ wird durch den eigentlichen Suchbegriff ersetzt. + In dieser URL fehlt der Platzhalter „${1}“ + Wetterdienst + OpenWeatherMap + Von diesem Wetterdienst nicht unterstützt + EE, d. MMMM + %1$s • %2$s + Keine Symbolpakete installiert + Symbolpaket + Dynamischer Hintergrund + Hintergrundfarbe an Symbol anpassen + Widget + Kalender + Kalender, Widgets + Kalender + Berechtigung fehlt + Ganztägige Termine ausblenden + + %1$d Kalender ausgewählt + %1$d Kalender ausgewählt + + %1$s\n%2$s + Widgets bearbeiten + Wetter + Kalender + Widget hinzufügen + Weitere + Systemleisten + Dunkle Statusleisten-Symbole + Dunkle Navigationsleisten-Symbole + Bitte aktivieren Sie den Benachrichtigungszugriff für diese App (wird benötigt um die Musik-Wiedergabe zu steuern) + Musik + In Kontakte-App anzeigen + An Favoriten anheften + Aus Favoriten entfernen + Zurück + Anruf + SMS + Telegram + WhatsApp + E-Mail + %1$d Telefonnummern + %1$d E-Mail-Adressen + Ort + %1$d Postadressen + Kalender + Kalendersuche aktivieren + Kontakte + Kontakte durchsuchen + App-Info + Verbundene Accounts und Dienste verwalten + Google + Angemeldet als %1$s + Abmelden + Eigentümer + Google Drive + Sie sind im Moment nicht angemeldet + %1$ss Dateien auf Google Drive durchsuchen + Zeichnung + E-Book + Formular + Schwarz + Ausblenden + Ausgeblendete Elemente + Löschen + Nicht ausblenden + In Kalender-App anzeigen + BIN:\nOCT:\nHEX: + Bilder anzeigen + Erhöht den Datenverbrauch signifikant + Sechseck + Reuleaux-Dreieck + Teilnehmer + Beschreibung + Ort + Zeit + Über Google anmelden + Anmelden, um Google Drive durchsuchen zu können + Geben Sie den Auth-Code hier ein: + Google-Login fehlgeschlagen! + Wird installiert… (%1$d%%) + Von %1$s + %1$s konnte nicht geöffnet werden + Anzahl der Termine + MET Norway + + Schneeregenschauer + Starker Schneeregen + Leichte Regenschauer und Gewitter + Starkregen + Leichter Schneefall und Gewitter + Leichter Regen + Leichte Regenschauer + Leichter Schnee + Starke Schneeregenschauer und Gewitter + Leichte Schneeschauer + Leichte Schneeregenschauer und Gewitter + Schneefall und Gewitter + Starke Schneeregenschauer + Starker Schneefall + Bedeckt + Leichter Regen und Gewitter + Schneefall + Starke Schneeschauer + Starke Regenschauer + Regenschauer und Gewitter + Klarer Himmel + Schneeregen + Regen + Schneeregen und Gewitter + Leichte Schneeschauer und Gewitter + Starke Regenschauer und Gewitter + Heiter + Nebel + Schneeregenschauer und Gewitter + Regen und Gewitter + Leichter Schneeregen + Starker Schneeregen und Gewitter + Teilweise bewölkt + Starker Schneefall und Gewitter + Regenschauer + Light sleet and thunder + Starke Schneeschauer und Gewitter + Light sleet showers + Schneeschauer und Gewitter + Schneeschauer + Starkregen und Gewitter + Unbekannt + + Von diesem Wetterdienst nicht unterstützt + Einheitenrechner + Probieren Sie „23 kg“ oder„5 cm >> in“ + Einheitenrechner aktivieren + Symbol-Hintergrund + Keiner + Dynamisch + Weiß + Hier gibt es keine Easter Eggs, es sei denn Ihr hättet sie mitgebracht. + Bitte, hör auf, du verschwendest deine Zeit + Ich werde es nicht noch einmal sagen: hier sind definitiv keine Easter Eggs versteckt. + Tja, da sind Sie. Herzlichen Glückwunsch. War es das wert? + Easter Egg deaktiviert! + Easter Egg aktiviert! + Kalender-Berechtigung gewähren, um Ihre nächsten Termine hier anzuzeigen. + Kalender-Berechtigung gewähren, um Termine zu durchsuchen. + Kontakt-Berechtigung gewähren, um Kontakte zu durchsuchen. + Speicher-Berechtigung gewähren, um Fotos, Medien und Dokumente auf diesem Gerät zu durchsuchen. + Schließen + Fortfahren + Eigenes Symbol + Symbolfarbe wählen + Höhe anpassen + Entfernen + Einstellungen + HERE + Fünfeck + Hintergrund + Hintergrundbild setzen + Hintergrund dimmen + Hintergrundbild bei Verwendung von dunklen Desings abdunkeln + Plaketten + Symbolplaketten konfigurieren + Benachrichtigungsplaketten + Plaketten für Anwendungen mit ungelesenen Benachrichtigungen anzeigen + Pausierte Apps + Plaketten für pausierte Apps anzeigen + Cloud-Plaketten + Shortcut-Plaketten + Eine Plakette für Dateien, die in einer Cloud gespeichert sind anzeigen + Für Shortcuts anzeigen, zu welcher App diese gehören + Systemstandard + Datenbanken exportieren + Die exportierten Datenbanken beinhalten persönliche Daten und sollten nicht Dritten zugänglich gemacht werden. + Animationen + App-Start-Animation + Splashscreen 1 + Splashscreen 2 + Von unten gleiten + Blenden + Aus Symbol erweitern + Standard + Microsoft + Bei Microsoft anmelden + Anmelden, um OneDrive durchsuchen zu können + Bei Nextcloud anmelden + Anmelden, um Ihren Nextcloud-Server durchsuchen zu können + Status wird abgerufen… + OneDrive durchsuchen + %1$ss Dateien auf OneDrive durchsuchen + Nextcloud durchsuchen + %1$ss Dateien durchsuchen + Google Drive + OneDrive + Telegram-Gruppe + F-Droid-Repository + Crash-Reporter + Plug-Ins + Plug-Ins aktivieren oder deaktivieren + Installierte Plug-Ins + Einstellungen für %1$s + Favoriten bearbeiten + Nicht angeheftet – häufig genutzt + Angeheftet – automatisch sortiert + Angeheftet – manuell sortiert + Weiter + Nextcloud-Server-URL + Server-URL darf nicht leer sein. + Nextcloud + Diese URL verweist auf keine gültige Nextcloud-Installation + Angemeldet als %1$s. + Anmelden + Anmeldung fehlgeschlagen: Nutzername oder Passwort ungültig + + Owncloud-Server-URL + Owncloud + Diese URL verweist auf keine gültige Owncloud-Installation + Passwort + Benutzername + Wenn Sie Zwei-Faktor-Authentifizierung aktiviert haben, müssen Sie hier ein App-Passwort verwenden. + Anmeldung fehlgeschlagen: Passwort oder Benutzername ist ungültig. + Bei Owncloud anmelden + Anmelden, um Ihren Owncloud-Server durchsuchen zu können + Nutzername darf nicht leer sein + Passwort darf nicht leer sein + Owncloud durchsuchen + Erscheinungsbild von Karten anpassen + Karten + Eckradius + Rahmenstärke + Deckkraft + Haftungsausschluss + Wechselkurse so wie sie einmal täglich von der Europäischen Zentralbank herausgegeben werden. Alle Angaben sind ohne Gewähr. Es wird keine Haftung für die hier dargestellten Informationen übernommen. + Alle anzeigen + Hintergrund + Standard (weiß/dunkelgrau) + Weiß/schwarz + Farbig (aus Hintergrundbild) + + Heute + Morgen + Demnächst + Heute keine Termine + Kalender-App öffnen + Termin erstellen + Details anzeigen + Details ausblenden + Luftfeuchte: + Niederschlag: + Lizenz + Diese App ist freie Software. + Lizenziert unter der GNU General Public License 3.0 + Diese Funktion ist in dieser Version von %1$s nicht verfügbar. + + +%1$d laufender Termin aus vergangenen Tagen + +%1$d laufende Termine aus vergangenen Tagen + + \ No newline at end of file diff --git a/i18n/src/main/res/values-de/units.xml b/i18n/src/main/res/values-de/units.xml new file mode 100644 index 00000000..6d065fd8 --- /dev/null +++ b/i18n/src/main/res/values-de/units.xml @@ -0,0 +1,263 @@ + + + m + + Meter + Meter + + km + + Kilometer + Kilometer + + cm + + Zentimeter + Zentimeter + + mm + + Millimeter + Millimeter + + in + + Zoll + Zoll + + ft + + Fuß + Fuß + + yd + + Yard + Yard + + mi + + Meile + Meilen + + sm + + Seemeile + Seemeilen + + + + + + Quadratmeter + Quadratmeter + + km² + + Quadratkilometer + Quadratkilometer + + cm² + + Quadratzentimeter + Quadratzentimeter + + mm² + + Quadratmillimeter + Quadratmillimeter + + sqin + + Quadratzoll + Quadratzoll + + sqft + + Quadratfuß + Quadratfuß + + sqyd + + Quadratyard + Quadratyard + + ha + + Hektar + Hektar + + ac + + Acre + Acre + + + + s + + Sekunde + Sekunden + + ms + + Millisekunde + Millisekunden + + min + + Minute + Minuten + + h + + Stunde + Stunden + + d + + Tag + Tage + + a + + Jahr + Jahre + + + + B + + Byte + Byte + + kB + + Kilobyte + Kilobyte + + MB + + Megabyte + Megabyte + + GB + + Gigabyte + Gigabyte + + TB + + Terabyte + Terabyte + + kiB + + Kibibyte + Kibibyte + + MiB + + Mebibyte + Mebibyte + + GiB + + Gibiyte + Gibibyte + + TiB + + Tebiyte + Tebiyte + + bit + + Bit + Bit + + kbit + + Kilobit + Kilobit + + Mbit + + Megabit + Megabit + + Gbit + + Gigabit + Gigabit + + Tbit + + Terabit + Terabit + + + + m/s + + Meter pro Sekunde + Meter pro Sekunde + + km/h + + Kilometer pro Stunde + Kilometer pro Stunde + + mph + + Meile pro Stunde + Meilen pro Stunde + + kn + + Knoten + Knoten + + + + kg + + Kilogramm + Kilogramm + + g + + Gramm + Gramm + + t + + Tonne + Tonnen + + tn.l. + + Long Ton + Long Tons + + st. + + Stone + Stones + + lb. + + Pfund + Pfund + + oz. + + Unze + Unzen + + tn.sh. + + Short Ton + Short Tons + + \ No newline at end of file diff --git a/i18n/src/main/res/values-en-rUS/defaults.xml b/i18n/src/main/res/values-en-rUS/defaults.xml new file mode 100644 index 00000000..f679a1ba --- /dev/null +++ b/i18n/src/main/res/values-en-rUS/defaults.xml @@ -0,0 +1,5 @@ + + + true + USD + \ No newline at end of file diff --git a/i18n/src/main/res/values/defaults.xml b/i18n/src/main/res/values/defaults.xml new file mode 100644 index 00000000..e1d5e5d3 --- /dev/null +++ b/i18n/src/main/res/values/defaults.xml @@ -0,0 +1,5 @@ + + + false + USD + \ No newline at end of file diff --git a/i18n/src/main/res/values/strings.xml b/i18n/src/main/res/values/strings.xml new file mode 100644 index 00000000..5abb34be --- /dev/null +++ b/i18n/src/main/res/values/strings.xml @@ -0,0 +1,443 @@ + + + Kvæsitso + + Search + + %1$s,\n%2$s + + Preparing… + + %1$s link + + Package file + + Uninstall + + Share + + Version %1$s\n%2$s + + Settings + + Wallpaper + + Settings + + all-day + + North + + North east + + North north east + + East north east + + East + + East south east + + South east + + South south east + + South + + South south west + + South west + + West south west + + West + + West north west + + North west + + North north west + + No precipitation + + Appearance + Theme + Light + Dark + Black + About + Version + Developer + Links + Open source licenses + + Open website + Version, Developer, Open source licenses + Theme, Icons, Search bar, System bars + Provider, Units, location + Weather + Location + Automatic location + Use GPS and location services to determine location automatically + Not set + Location + Use degrees Fahrenheit and miles per hour + Imperial units + °F + °C + %1$s mph + %1$s km/h + %1$s mm + Translucent cards + Debug + Export debug information + Exporting to %1$s + + https://en.wikipedia.org + From Wikipedia + Icons + Shape + Square + Rounded square + Squircle + Reuleaux triangle + Circle + = %1$s + Search bar + On home screen + Hide background + Do nothing + Hide + Directory + Archive file + Android package file + Source code file + Document + Spreadsheet + Music file + Video + Picture + File + %1$s file + No suitable app installed. + Presentation + Compressed file + Text file + The directory %1$s and all its content will be deleted permanently. Proceed? + The file %1$s will be deleted permanently. Proceed? + Title + Artist + Album + Duration + Year + %1$s: %2$s + Size + Path + Type + Dimensions + App name + Version + Package name + Min SDK version + Services + Search + Search categories + Auto (by time of day) + Follow system + Favorites + Show favorites + Show favorites above the app list + Frequently used items + Automatically add frequently used items to favorites + Files + File search + Search folders, documents, photos and other kinds of file on this device + Wikipedia + Search Wikipedia + Allow mobile data usage + Additional fees may apply + Websites + Search for websites + Try \'wikipedia.org\' + Default web protocol + Default protocol for websites, if not given. You can explicate protocol by adding https:// or http:// in front of your search term + HTTP (unencrypted) + HTTPS (encrypted) + Enable calculator + Try \'4*2+9\' + Unit converter + Try \'23 kg\' or \'5 cm >> in\' + Enable unit converter + Calculator + Try \'%1$s\' + Web search + Show shortcuts to several web search engines + Web search shortcuts + Edit web searches + Location + Blur effect + Blur wallpaper behind translucent cards + Not compatible with live wallpapers + Google + YouTube + Google Play + https://google.com/search?q=${1} + https://www.youtube.com/results?search_query=${1} + https://play.google.com/store/search?q=${1} + Add web search… + Delete + Name + URL + \'${1}\' will be replaced by the actual search term. + The placeholder \'${1}\' is missing in this URL + Provider + MET Norway + OpenWeatherMap + HERE + Not supported by this provider + EE, MMMM d + %1$s • %2$s + Icon pack + No icon packs installed + Dynamic background + Adjust background color to icon + Widget + Calendar + Calendars, Widget settings + Calendars + Permission denied + Hide all-day events + Edit widgets + Weather + Calendar + Add widget + More + System bars + Dark status bar icons + Dark navigation bar icons + Please grant notification listener permission for this app (required to control music playback) + Music + Open in contacts app + Pin to favorites + Unpin + Back + Call + Message + Telegram + WhatsApp + Email + %1$d phone numbers + %1$d email addresses + Location + %1$d postal addresses + Calendar + Search calendar events + Contacts + Search contacts + App info + Manage connected accounts and services + Google + Signed in as %1$s + Log out + Owner + Google Drive + You are currently not logged in + Search %1$s\'s files on Google Drive + E-book + Drawing + Form + + %1$d calendar selected + %1$d calendars selected + + Hide + Don\'t hide + Hidden items + Delete + Open in calendar app + BIN:\nOCT:\nHEX: + Show pictures + Significantly increases data usage + Sign in with Google + Sign in to search Google Drive + Enter the auth code here: + Google sing-in failed! + Hexagon + Time + Description + Location + Attendees + By %1$s + Installation in progress… (%1$d%%) + Couldn\'t open %1$s + Number of events + Not supported by this provider + Icon background + None + Dynamic + White + + There are no easter eggs here, unless you brought them with you. + Please, stop it, you are wasting your time + I won\'t say it again: there are absolutely no easter eggs hidden here + Well, you found me. Congratulations. Was it worth it? + Easter egg enabled! + Easter egg disabled! + Grant calendar permission to display your upcomping events here. + Grant calendar permission to search your calendar. + Grant storage permission to search photos, media and document on this device. + Grant contact permission to search your contact. + Close + Continue + Custom icon + Choose icon color + Adjust height + Remove + Settings + Pentagon + Wallpaper + Dim wallpaper + In dark themes, darken wallpaper + Choose a wallpaper + Badges + Configure icon badges + Notification badges + Show a badge for applications with unread notifications + Show a badge for suspended applicatoins + Suspended apps + Cloud badges + Show a badge for files that are stored in a cloud + Shortcut badges + Show a badge which indicates to which app a shortcut belongs + System default + Export databases + The exported databases contain personal data and are not intended to be shared. + Animations + App start animation + Splash screen 1 + Splash screen 2 + Slide from bottom + Fade + Expand from icon + Default + Microsoft + Sign in with Microsoft + Sign in to search OneDrive + Sign in to Nextcloud + Sign in to search your Nextcloud server + Checking status… + Search OneDrive + Search %1$s\'s files on OneDrive + Search Nextcloud + Search %1$s\'s files + Google Drive + OneDrive + Telegram group + F-Droid repository + Crash reporter + Plugins + Enable or disable plugins + Installed plugins + Settings for %1$s + Edit favorites + Not pinned – frequently used + Pinned – automatically sorted + Pinned – manually sorted + Next + Nextcloud server URL + Server URL must not be empty + Nextcloud + This URL does not point to a valid Nextcloud installation + Logged in as %1$s. + Login failed: incorrect username or password. + + Owncloud server URL + Owncloud + This URL does not point to a valid Owncloud installation + Password + User name + If you have two factor authentication enabled, you must use an app password here. + Log in + Login failed: incorrect username or password. + Sign in to Owncloud + Sign in to search your Owncloud server + User name must not be empty + Password must not be empty + Search Owncloud + Customize card appearance + Cards + Corner radius + Stroke width + Opacity + Disclaimer + "Exchange rates as published once per day by the European Central Bank. All information is provided \"as is\" without any kind of guarantee. No liability is assumed for these information." + Show all + Background + Default (white/dark gray) + White/black + Colored (from wallpaper) + + Today + Tomorrow + Upcoming + No events today + Open calendar app + New event + + +%1$d running event from past days + +%1$d running events from past days + + + + Sleet showers + Heavy sleet + Light rain showers and thunder + Heavy rain + Light snow and thunder + Light rain + Light rain showers + Light snow + Heavy sleet showers and thunder + Light snow showers + Lights sleet showers and thunder + Snow and thunder + Heavy sleet showers + Heavy snow + Cloudy + Light rain and thunder + Snow + Heavy snow showers + Heavy rain showers + Rain showers and thunder + Clear sky + Sleet + Rain + Sleet and thunder + Lights snow showers and thunder + Heavy rain showers and thunder + Fair + Fog + Sleet showers and thunder + Rain and thunder + Light sleet + Heavy sleet and thunder + Partly cloudy + Heavy snow and thunder + Rain showers + Light sleet and thunder + Heavy snow showers and thunder + Light sleet showers + Snow showers and thunder + Snow showers + Heavy rain and thunder + Unknown + Show details + Hide details + Humidity: + Wind: + Precipitation: + License + This app is free software. + Licensed under the GNU General Public License 3.0 + This feature is not available in this version of %1$s + diff --git a/i18n/src/main/res/values/units.xml b/i18n/src/main/res/values/units.xml new file mode 100644 index 00000000..009b6487 --- /dev/null +++ b/i18n/src/main/res/values/units.xml @@ -0,0 +1,267 @@ + + + + + m + + meter + meters + + km + + kilometer + kilometers + + cm + + centimeter + centimeters + + mm + + millimeter + millimeters + + in + + inch + inches + + ft + + foot + feet + + yd + + yard + yards + + mi + + mile + miles + + NM + + nautic mile + nautic miles + + + + + + square meter + square meters + + km² + + square kilometer + square kilometers + + cm² + + square centimeter + square centimeters + + mm² + + square millimeter + square millimeters + + sqin + + square inch + square inches + + sqft + + square foot + square feet + + sqyd + + square yard + square yards + + ha + + hectare + hectares + + ac + + acre + acres + + + + s + + second + seconds + + ms + + millisecond + milliseconds + + min + + minute + minutes + + h + + hour + hours + + d + + day + days + + a + + year + years + + + + B + + byte + bytes + + kB + + kilobyte + kilobytes + + MB + + megabytes + megabytes + + GB + + gigabyte + gigabytes + + TB + + terabyte + terabytes + + kiB + + kibibyte + kibibytes + + MiB + + mebibytes + mebibytes + + GiB + + gibiyte + gibibytes + + TiB + + tebiyte + tebiytes + + bit + + bit + bits + + kbit + + kilobit + kilobits + + Mbit + + megabit + megabits + + Gbit + + gigabit + gigabits + + Tbit + + terabit + terabits + + + + m/s + + meter per second + meters per second + + km/h + + kilometer per hour + kilometers per hour + + mph + + mile per hour + miles per hour + + kn + + knot + knots + + + + kg + + kilogram + kilograms + + g + + gram + grams + + t + + metric ton + metric tons + + tn.l. + + long ton + long tons + + st. + + stone + stones + + lb. + + pound + pounds + + oz. + + ounce + ounces + + tn.sh. + + short ton + short tons + + \ No newline at end of file diff --git a/icons/.gitignore b/icons/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/icons/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/icons/build.gradle.kts b/icons/build.gradle.kts new file mode 100644 index 00000000..7578f80d --- /dev/null +++ b/icons/build.gradle.kts @@ -0,0 +1,53 @@ +plugins { + id("com.android.library") + id("kotlin-android") + id("kotlin-android-extensions") +} + +android { + compileSdk = sdk.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = sdk.versions.minSdk.get().toInt() + targetSdk = sdk.versions.targetSdk.get().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation(libs.bundles.kotlin) + implementation(libs.androidx.core) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.palette) + + implementation(libs.bundles.androidx.lifecycle) + + implementation(project(":database")) + implementation(project(":preferences")) + implementation(project(":ktx")) + implementation(project(":base")) + implementation(project(":search")) + implementation(project(":crashreporter")) + +} \ No newline at end of file diff --git a/icons/consumer-rules.pro b/icons/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/icons/proguard-rules.pro b/icons/proguard-rules.pro new file mode 100644 index 00000000..52039aba --- /dev/null +++ b/icons/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.kts.kts.kts.kts.kts.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/icons/src/main/AndroidManifest.xml b/icons/src/main/AndroidManifest.xml new file mode 100644 index 00000000..6e78bb99 --- /dev/null +++ b/icons/src/main/AndroidManifest.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/icons/src/main/java/de/mm20/launcher2/icons/CalendarDynamicLauncherIcon.kt b/icons/src/main/java/de/mm20/launcher2/icons/CalendarDynamicLauncherIcon.kt new file mode 100644 index 00000000..b538f392 --- /dev/null +++ b/icons/src/main/java/de/mm20/launcher2/icons/CalendarDynamicLauncherIcon.kt @@ -0,0 +1,65 @@ +package de.mm20.launcher2.icons + +import android.content.Context +import android.content.pm.PackageManager +import android.graphics.drawable.AdaptiveIconDrawable +import android.graphics.drawable.Drawable +import android.os.Build +import de.mm20.launcher2.ktx.getDrawableOrNull +import java.util.* +import java.util.concurrent.Executors + +class CalendarDynamicLauncherIcon( + context: Context, + foreground: Drawable, + background: Drawable?, + foregroundScale: Float, + backgroundScale: Float, + badgeNumber: Float = 0f, + val packageName: String, + val drawableIds: IntArray, + autoGenerateBackgroundMode: Int +) : DynamicLauncherIcon( + foreground, + background, + foregroundScale, + backgroundScale, + /** Not needed, we already have a background **/ + autoGenerateBackgroundMode, + badgeNumber, + null +) { + + init { + DynamicIconController.getInstance(context).registerIcon(this) + update(context) + } + + var currentDay = 0 + override fun update(context: Context) { + val calendar = Calendar.getInstance() + val day = calendar[Calendar.DAY_OF_MONTH] + if (day == currentDay || drawableIds.size < currentDay) return + val resources = try { + context.packageManager.getResourcesForApplication(packageName) + } catch (e: PackageManager.NameNotFoundException) { + return + } + Executors.newSingleThreadExecutor().execute { + val currentDayDrawable = resources.getDrawableOrNull(drawableIds[day - 1]) + ?: return@execute + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && currentDayDrawable is AdaptiveIconDrawable) { + foreground = currentDayDrawable.foreground + background = currentDayDrawable.background + foregroundScale = 1.5f + backgroundScale = 1.5f + } else { + foregroundScale = 1f + backgroundScale = 1f + background = null + foreground = currentDayDrawable + } + } + currentDay = day + } +} \ No newline at end of file diff --git a/icons/src/main/java/de/mm20/launcher2/icons/ClockDynamicLauncherIcon.kt b/icons/src/main/java/de/mm20/launcher2/icons/ClockDynamicLauncherIcon.kt new file mode 100644 index 00000000..9aaec4c5 --- /dev/null +++ b/icons/src/main/java/de/mm20/launcher2/icons/ClockDynamicLauncherIcon.kt @@ -0,0 +1,57 @@ +package de.mm20.launcher2.icons + +import android.content.Context +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.graphics.drawable.LayerDrawable +import android.graphics.drawable.RotateDrawable +import android.os.Build +import androidx.annotation.RequiresApi +import java.util.* +import kotlin.math.roundToInt + +@RequiresApi(Build.VERSION_CODES.O) +class ClockDynamicLauncherIcon( + context: Context, + foreground: LayerDrawable, + background: Drawable?, + foregroundScale: Float, + backgroundScale: Float, + badgeNumber: Float, + val hourLayer: Int, + val minuteLayer: Int, + val secondLayer: Int +) : DynamicLauncherIcon( + foreground, + background, + foregroundScale, + backgroundScale, + /** Not needed, we already have a background **/ + LauncherIcon.BACKGROUND_WHITE, + badgeNumber, + null +) { + + init { + foreground.also { + it.setDrawable(secondLayer, ColorDrawable(0)) + (it.getDrawable(hourLayer) as? RotateDrawable)?.fromDegrees = 0f + (it.getDrawable(hourLayer) as? RotateDrawable)?.toDegrees = 360f + (it.getDrawable(minuteLayer) as? RotateDrawable)?.fromDegrees = 0f + (it.getDrawable(minuteLayer) as? RotateDrawable)?.toDegrees = 360f + } + DynamicIconController.getInstance(context).registerIcon(this) + update(context) + } + + override fun update(context: Context) { + val calendar = Calendar.getInstance() + val hourDegrees = calendar[Calendar.HOUR] / 12f * 10000 + calendar[Calendar.MINUTE] / 60f * 10000f / 12 + val minuteDegrees = calendar[Calendar.MINUTE] / 60f * 10000 + (foreground as LayerDrawable).also { + (it.getDrawable(hourLayer) as? RotateDrawable)?.level = hourDegrees.roundToInt() + (it.getDrawable(minuteLayer) as? RotateDrawable)?.level = minuteDegrees.roundToInt() + } + notifyCallbacks() + } +} \ No newline at end of file diff --git a/icons/src/main/java/de/mm20/launcher2/icons/DynamicIconController.kt b/icons/src/main/java/de/mm20/launcher2/icons/DynamicIconController.kt new file mode 100644 index 00000000..31d23cb1 --- /dev/null +++ b/icons/src/main/java/de/mm20/launcher2/icons/DynamicIconController.kt @@ -0,0 +1,66 @@ +package de.mm20.launcher2.icons + +import android.app.Activity +import android.app.Application +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.util.Log +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.OnLifecycleEvent +import java.lang.ref.WeakReference + +class DynamicIconController(val context: Context): LifecycleObserver { + + private var timeReceiver: BroadcastReceiver? = null + private val registeredIcons = mutableListOf>() + + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + fun resume() { + timeReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent?) { + updateAllIcons(context) + } + } + val filter = IntentFilter(Intent.ACTION_TIME_TICK).also { + it.addAction(Intent.ACTION_TIME_CHANGED) + it.addAction(Intent.ACTION_TIMEZONE_CHANGED) + } + context.registerReceiver(timeReceiver, filter) + updateAllIcons(context) + } + + private fun updateAllIcons(context: Context) { + val iterator = registeredIcons.iterator() + while (iterator.hasNext()) { + val iconRef = iterator.next() + if (iconRef.get() != null) iconRef.get()?.update(context) + else iterator.remove() + } + } + + @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) + fun pause() { + try { + context.unregisterReceiver(timeReceiver) + } catch (e: IllegalArgumentException) { + + } + timeReceiver = null + } + + fun registerIcon(icon: DynamicLauncherIcon) { + registeredIcons.add(WeakReference(icon)) + } + + companion object { + private lateinit var instance: DynamicIconController + + fun getInstance(context: Context): DynamicIconController { + if(!::instance.isInitialized) instance = DynamicIconController(context.applicationContext) + return instance + } + } +} \ No newline at end of file diff --git a/icons/src/main/java/de/mm20/launcher2/icons/DynamicLauncherIcon.kt b/icons/src/main/java/de/mm20/launcher2/icons/DynamicLauncherIcon.kt new file mode 100644 index 00000000..bfb0db60 --- /dev/null +++ b/icons/src/main/java/de/mm20/launcher2/icons/DynamicLauncherIcon.kt @@ -0,0 +1,22 @@ +package de.mm20.launcher2.icons + +import android.content.Context +import android.graphics.drawable.Drawable + +abstract class DynamicLauncherIcon( + foreground: Drawable, + background: Drawable?, + foregroundScale: Float, + backgroundScale: Float, + autoGenerateBackgroundMode: Int, + badgeNumber: Float, + badgeDrawable: Drawable?) + : LauncherIcon( + foreground, + background, + foregroundScale, + backgroundScale, + autoGenerateBackgroundMode +) { + abstract fun update(context: Context) +} \ No newline at end of file diff --git a/icons/src/main/java/de/mm20/launcher2/icons/Icon.kt b/icons/src/main/java/de/mm20/launcher2/icons/Icon.kt new file mode 100644 index 00000000..b0e226c2 --- /dev/null +++ b/icons/src/main/java/de/mm20/launcher2/icons/Icon.kt @@ -0,0 +1,30 @@ +package de.mm20.launcher2.icons + +import android.content.ComponentName +import de.mm20.launcher2.database.entities.IconEntity + +data class Icon( + val type: String, + val componentName: ComponentName?, + val drawable: String?, + val iconPack: String, + val scale: Float? = null +) { + constructor(entity: IconEntity) : this( + type = entity.type, + componentName = entity.componentName, + drawable = entity.drawable, + iconPack = entity.iconPack, + scale = entity.scale + ) + + fun toDatabaseEntity(): IconEntity { + return IconEntity( + type = type, + componentName = componentName, + drawable = drawable, + iconPack = iconPack, + scale = scale + ) + } +} \ No newline at end of file diff --git a/icons/src/main/java/de/mm20/launcher2/icons/IconPack.kt b/icons/src/main/java/de/mm20/launcher2/icons/IconPack.kt new file mode 100644 index 00000000..ed388d4a --- /dev/null +++ b/icons/src/main/java/de/mm20/launcher2/icons/IconPack.kt @@ -0,0 +1,26 @@ +package de.mm20.launcher2.icons + +import de.mm20.launcher2.database.entities.IconPackEntity + +data class IconPack( + val name: String, + val packageName: String, + val version: String, + var scale: Float = 1f +) { + constructor(entity: IconPackEntity) : this( + name = entity.name, + packageName = entity.packageName, + version = entity.packageName, + scale = entity.scale + ) + + fun toDatabaseEntity(): IconPackEntity { + return IconPackEntity( + name = name, + scale = scale, + version = version, + packageName = packageName + ) + } +} \ No newline at end of file diff --git a/icons/src/main/java/de/mm20/launcher2/icons/IconPackManager.kt b/icons/src/main/java/de/mm20/launcher2/icons/IconPackManager.kt new file mode 100644 index 00000000..71eadcb1 --- /dev/null +++ b/icons/src/main/java/de/mm20/launcher2/icons/IconPackManager.kt @@ -0,0 +1,517 @@ +package de.mm20.launcher2.icons + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.LauncherActivityInfo +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo +import android.content.res.Resources +import android.content.res.XmlResourceParser +import android.graphics.* +import android.graphics.drawable.AdaptiveIconDrawable +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.LayerDrawable +import android.os.Build +import android.os.UserHandle +import android.util.DisplayMetrics +import android.util.Log +import androidx.core.content.res.ResourcesCompat +import androidx.core.graphics.drawable.toBitmap +import de.mm20.launcher2.crashreporter.CrashReporter +import de.mm20.launcher2.database.AppDatabase +import de.mm20.launcher2.ktx.dp +import de.mm20.launcher2.ktx.obtainTypedArrayOrNull +import de.mm20.launcher2.ktx.randomElementOrNull +import de.mm20.launcher2.preferences.IconShape +import de.mm20.launcher2.preferences.LauncherPreferences +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlPullParserException +import org.xmlpull.v1.XmlPullParserFactory +import java.io.InputStreamReader +import kotlin.math.roundToInt + + +class IconPackManager private constructor(val context: Context) { + var selectedIconPack: String + get() { + return context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE) + .getString(KEY_ICON_PACK, "")!! + } + set(value) { + Log.d("MM20", "Selected icon pack: $value") + context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE) + .edit() + .putString(KEY_ICON_PACK, value) + .apply() + } + + + fun selectIconPack(iconPack: String) { + selectedIconPack = iconPack + } + + fun getIcon(context: Context, activity: LauncherActivityInfo, size: Int): LauncherIcon? { + if (selectedIconPack.isEmpty()) return getDefaultIcon(context, activity) + return getFromPack(context, activity, size) ?: generateIcon(context, activity, size) + } + + private fun getFromPack(context: Context, activity: LauncherActivityInfo, size: Int): LauncherIcon? { + val res = try { + context.packageManager.getResourcesForApplication(selectedIconPack) + } catch (e: PackageManager.NameNotFoundException) { + Log.e("MM20", "Icon pack package $selectedIconPack not found!") + return getDefaultIcon(context, activity) + } + val iconDao = AppDatabase.getInstance(context).iconDao() + val component = ComponentName(activity.applicationInfo.packageName, activity.name) + val icon = iconDao.getIcon(component.flattenToString(), selectedIconPack) + ?: return generateIcon(context, activity, size) + + if (icon.type == "calendar") { + return getIconPackCalendarIcon(context, icon.iconPack, icon.drawable ?: return null) + } + val drawableName = icon.drawable + val resId = res.getIdentifier(drawableName, "drawable", selectedIconPack).takeIf { it != 0 } + ?: return generateIcon(context, activity, size) + val drawable = ResourcesCompat.getDrawable(res, resId, context.theme) ?: return null + return when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && drawable is AdaptiveIconDrawable -> { + LauncherIcon( + foreground = drawable.foreground, + background = drawable.background, + foregroundScale = 1.5f, + backgroundScale = 1.5f + ) + } + else -> { + LauncherIcon( + foreground = drawable, + foregroundScale = getScale(), + autoGenerateBackgroundMode = LauncherPreferences.instance.legacyIconBg.toInt() + ) + } + } + } + + + private fun generateIcon(context: Context, activity: LauncherActivityInfo, size: Int): LauncherIcon? { + val back = getIconBack() + val upon = getIconUpon() + val mask = getIconMask() + val scale = getPackScale() + + if (back == null && upon == null && mask == null) { + return getDefaultIcon(context, activity) + } + + val drawable = activity.getIcon(context.resources.displayMetrics.densityDpi) + val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888) + + val canvas = Canvas(bitmap) + val paint = Paint() + paint.isAntiAlias = true + paint.isFilterBitmap = true + paint.isDither = true + + + var inBounds: Rect + var outBounds: Rect + + val icon = drawable.toBitmap(width = size, height = size) + + inBounds = Rect(0, 0, icon.width, icon.height) + outBounds = Rect((bitmap.width * (1 - scale) * 0.5).roundToInt(), + (bitmap.height * (1 - scale) * 0.5).roundToInt(), + (bitmap.width - bitmap.width * (1 - scale) * 0.5).roundToInt(), + (bitmap.height - bitmap.height * (1 - scale) * 0.5).roundToInt()) + canvas.drawBitmap(icon, inBounds, outBounds, paint) + + val pack = selectedIconPack + val pm = context.packageManager + val res = try { + pm.getResourcesForApplication(pack) + } catch (e: Resources.NotFoundException) { + return getDefaultIcon(context, activity) + } + + if (mask != null) { + res.getIdentifier(mask, "drawable", pack).takeIf { it != 0 }?.let { + paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT) + val maskDrawable = ResourcesCompat.getDrawable(res, it, null) ?: return null + val maskBmp = maskDrawable.toBitmap(size, size) + inBounds = Rect(0, 0, maskBmp.width, maskBmp.height) + outBounds = Rect(0, 0, bitmap.width, bitmap.height) + canvas.drawBitmap(maskBmp, inBounds, outBounds, paint) + } + } + if (upon != null) { + res.getIdentifier(upon, "drawable", pack).takeIf { it != 0 }?.let { + paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_OVER); + val maskDrawable = ResourcesCompat.getDrawable(res, it, null) ?: return null + val maskBmp = maskDrawable.toBitmap(size, size) + inBounds = Rect(0, 0, maskBmp.width, maskBmp.height) + outBounds = Rect(0, 0, bitmap.width, bitmap.height) + canvas.drawBitmap(maskBmp, inBounds, outBounds, paint) + } + } + if (back != null) { + res.getIdentifier(back, "drawable", pack).takeIf { it != 0 }?.let { + paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OVER); + val maskDrawable = ResourcesCompat.getDrawable(res, it, null) ?: return null + val maskBmp = maskDrawable.toBitmap(size, size) + inBounds = Rect(0, 0, maskBmp.width, maskBmp.height) + outBounds = Rect(0, 0, bitmap.width, bitmap.height) + canvas.drawBitmap(maskBmp, inBounds, outBounds, paint) + } + } + + return LauncherIcon( + foreground = BitmapDrawable(context.resources, bitmap), + foregroundScale = getScale(), + autoGenerateBackgroundMode = LauncherPreferences.instance.legacyIconBg.toInt() + ) + } + + private fun getDefaultIcon(context: Context, activity: LauncherActivityInfo): LauncherIcon? { + if (activity.applicationInfo.packageName == GOOGLE_DESK_CLOCK_PACKAGE_NAME) { + getGoogleDeskClockIcon(context)?.let { return it } + } + getCalendarIcon(context, activity)?.let { return it } + try { + val icon = activity.getIcon(context.resources.displayMetrics.densityDpi) ?: return null + when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && icon is AdaptiveIconDrawable -> { + return LauncherIcon( + foreground = icon.foreground ?: return null, + background = icon.background, + foregroundScale = 1.5f, + backgroundScale = 1.5f + ) + } + else -> { + return LauncherIcon( + foreground = icon, + foregroundScale = getScale(), + autoGenerateBackgroundMode = LauncherPreferences.instance.legacyIconBg.toInt() + ) + } + } + } catch (e: PackageManager.NameNotFoundException) { + return null + } + } + + private fun getIconPackCalendarIcon(context: Context, iconPack: String, baseIconName: String): CalendarDynamicLauncherIcon? { + val resources = try { + context.packageManager.getResourcesForApplication(iconPack) + } catch (e: PackageManager.NameNotFoundException) { + return null + } + val drawableIds = (1..31).map { + val drawableName = baseIconName + it + val id = resources.getIdentifier(drawableName, "drawable", iconPack) + if (id == 0) return null + id + }.toIntArray() + return CalendarDynamicLauncherIcon( + context = context, + background = ColorDrawable(0), + foreground = ColorDrawable(0), + foregroundScale = 1.5f, + backgroundScale = 1.5f, + packageName = iconPack, + drawableIds = drawableIds, + autoGenerateBackgroundMode = LauncherPreferences.instance.legacyIconBg.toInt() + ) + } + + private fun getCalendarIcon(context: Context, activity: LauncherActivityInfo): CalendarDynamicLauncherIcon? { + val component = ComponentName(activity.applicationInfo.packageName, activity.name) + val pm = context.packageManager + val ai = try { + pm.getActivityInfo(component, PackageManager.GET_META_DATA) + } catch (e: PackageManager.NameNotFoundException) { + return null + } + val resources = pm.getResourcesForActivity(component) + var arrayId = ai.metaData?.getInt("com.teslacoilsw.launcher.calendarIconArray") ?: 0 + if (arrayId == 0) arrayId = ai.metaData?.getInt("com.google.android.calendar.dynamic_icons") + ?: return null + if (arrayId == 0) return null + val typedArray = resources.obtainTypedArrayOrNull(arrayId) ?: return null + if (typedArray.length() != 31) { + typedArray.recycle() + return null + } + val drawableIds = IntArray(31) + for (i in 0 until 31) { + drawableIds[i] = typedArray.getResourceId(i, 0) + } + typedArray.recycle() + return CalendarDynamicLauncherIcon( + context = context, + background = ColorDrawable(0), + foreground = ColorDrawable(0), + foregroundScale = 1.5f, + backgroundScale = 1.5f, + packageName = component.packageName, + drawableIds = drawableIds, + autoGenerateBackgroundMode = LauncherPreferences.instance.legacyIconBg.toInt() + ) + } + + private fun getGoogleDeskClockIcon(context: Context): ClockDynamicLauncherIcon? { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return null + val pm = context.packageManager + val appInfo = pm.getApplicationInfo(GOOGLE_DESK_CLOCK_PACKAGE_NAME, PackageManager.GET_META_DATA) + ?: return null + val drawable = appInfo.metaData.getInt("com.google.android.apps.nexuslauncher.LEVEL_PER_TICK_ICON_ROUND") + val resources = pm.getResourcesForApplication(appInfo) + val baseIcon = try { + ResourcesCompat.getDrawable(resources, drawable, null) as? AdaptiveIconDrawable ?: return null + } catch (e: Resources.NotFoundException) { + return null + } + val foreground = baseIcon.foreground as? LayerDrawable ?: return null + val hourLayer = appInfo.metaData.getInt("com.google.android.apps.nexuslauncher.HOUR_LAYER_INDEX") + val minuteLayer = appInfo.metaData.getInt("com.google.android.apps.nexuslauncher.MINUTE_LAYER_INDEX") + val secondLayer = appInfo.metaData.getInt("com.google.android.apps.nexuslauncher.SECOND_LAYER_INDEX") + return ClockDynamicLauncherIcon( + context = context, + background = baseIcon.background, + backgroundScale = 1.5f, + foreground = foreground, + foregroundScale = 1.5f, + badgeNumber = 0f, + hourLayer = hourLayer, + minuteLayer = minuteLayer, + secondLayer = secondLayer + ) + } + + private fun getScale(): Float { + return when (LauncherPreferences.instance.iconShape) { + IconShape.CIRCLE, IconShape.PLATFORM_DEFAULT -> 0.7f + else -> 0.8f + + } + } + + private fun getIconBack(): String? { + val iconDao = AppDatabase.getInstance(context).iconDao() + val iconbacks = iconDao.getIconBacks(selectedIconPack) + return iconbacks.randomElementOrNull() + } + + private fun getIconUpon(): String? { + val iconDao = AppDatabase.getInstance(context).iconDao() + val iconupons = iconDao.getIconUpons(selectedIconPack) + return iconupons.randomElementOrNull() + } + + private fun getIconMask(): String? { + val iconDao = AppDatabase.getInstance(context).iconDao() + val iconmasks = iconDao.getIconMasks(selectedIconPack) + return iconmasks.randomElementOrNull() + } + + private fun getPackScale(): Float { + val iconDao = AppDatabase.getInstance(context).iconDao() + return iconDao.getScale(selectedIconPack) ?: 1f + } + + suspend fun getInstalledIconPacks(): List { + return withContext(Dispatchers.IO) { + AppDatabase.getInstance(context).iconDao().getInstalledIconPacks().map { + IconPack(it) + } + } + } + + companion object { + const val GOOGLE_DESK_CLOCK_PACKAGE_NAME = "com.google.android.deskclock" + const val GOOGLE_CALENDAR_PACKAGE_NAME = "com.google.android.calendar" + + private lateinit var instance: IconPackManager + fun getInstance(context: Context): IconPackManager { + if (!::instance.isInitialized) instance = IconPackManager(context.applicationContext) + return instance + } + } + + @Synchronized + suspend fun updateIconPacks() { + withContext(Dispatchers.IO) { + UpdateIconPacksWorker(context).doWork() + } + } +} + + +class UpdateIconPacksWorker(val context: Context) { + + fun doWork() { + val packs = loadInstalledPacks(context).map { it.activityInfo.packageName } + val iconDao = AppDatabase.getInstance(context).iconDao() + iconDao.uninstallIconPacksExcept(packs) + + for (pack in packs) { + try { + val packInfo = context.packageManager.getPackageInfo(pack, 0) + val iconPack = IconPack( + name = packInfo.applicationInfo.loadLabel(context.packageManager).toString(), + packageName = pack, + version = packInfo.versionName + ) + //if (iconDao.isInstalled(iconPack)) continue + installIconPack(iconPack) + } catch (e: PackageManager.NameNotFoundException) { + continue + } + } + } + + private fun loadInstalledPacks(context: Context): List { + val packs = mutableListOf() + val pm = context.packageManager + var intent = Intent("org.adw.ActivityStarter.THEMES") + val adwPacks = pm.queryIntentActivities(intent, 0) + packs.addAll(adwPacks) + intent = Intent("com.novalauncher.THEME") + val novaPacks = pm.queryIntentActivities(intent, 0) + novaPacks.forEach { + if (packs.none { p -> p.activityInfo.packageName == it.activityInfo.packageName }) packs.add(it) + } + packs.sortWith(ResolveInfo.DisplayNameComparator(pm)) + return packs + } + + private fun installIconPack(iconPack: IconPack) { + val pkgName = iconPack.packageName + try { + val res = context.packageManager.getResourcesForApplication(pkgName) + val parser: XmlPullParser + var inStream: InputStreamReader? = null + val xmlId = res.getIdentifier("appfilter", "xml", pkgName) + if (xmlId != 0) parser = res.getXml(xmlId) + else { + val rawId = res.getIdentifier("appfilter", "raw", pkgName) + if (rawId == 0) { + Log.e("MM20", "Icon pack $pkgName has no appfilter.xml, neither in xml nor in raw") + return + } + parser = XmlPullParserFactory.newInstance().newPullParser() + inStream = res.openRawResource(rawId).reader() + parser.setInput(inStream) + } + + val icons = mutableListOf() + val iconDao = AppDatabase.getInstance(context).iconDao() + + loop@ while (parser.next() != XmlPullParser.END_DOCUMENT) { + if (parser.eventType != XmlPullParser.START_TAG) continue + when (parser.name) { + "item" -> { + val component = parser.getAttributeValue(null, "component") + ?: continue@loop + val drawable = parser.getAttributeValue(null, "drawable") + ?: continue@loop + if (component.length <= 14) continue@loop + val componentName = ComponentName.unflattenFromString(component.substring(14, component.lastIndex)) + ?: continue@loop + val icon = Icon( + componentName = componentName, + drawable = drawable, + iconPack = pkgName, + type = "app" + ) + icons.add(icon) + } + "calendar" -> { + val component = parser.getAttributeValue(null, "component") + ?: continue@loop + val drawable = parser.getAttributeValue(null, "prefix") ?: continue@loop + if (component.length < 14) continue@loop + val componentName = ComponentName.unflattenFromString(component.substring(14, component.lastIndex)) + ?: continue@loop + + val icon = Icon( + componentName = componentName, + drawable = drawable, + iconPack = pkgName, + type = "calendar" + ) + icons.add(icon) + } + "iconback" -> { + for (i in 0 until parser.attributeCount) { + if (parser.getAttributeName(i).startsWith("img")) { + val drawable = parser.getAttributeValue(i) + val icon = Icon( + componentName = null, + drawable = drawable, + iconPack = pkgName, + type = "iconback" + ) + icons.add(icon) + } + } + } + "iconupon" -> { + for (i in 0 until parser.attributeCount) { + if (parser.getAttributeName(i).startsWith("img")) { + val drawable = parser.getAttributeValue(i) + val icon = Icon( + componentName = null, + drawable = drawable, + iconPack = pkgName, + type = "iconupon" + ) + icons.add(icon) + } + } + } + "iconmask" -> { + for (i in 0 until parser.attributeCount) { + if (parser.getAttributeName(i).startsWith("img")) { + val drawable = parser.getAttributeValue(i) + val icon = Icon( + componentName = null, + drawable = drawable, + iconPack = pkgName, + type = "iconmask" + ) + icons.add(icon) + } + } + } + "scale" -> { + val scale = parser.getAttributeValue(null, "factor")?.toFloatOrNull() + ?: continue@loop + iconPack.scale = scale + } + } + } + + iconDao.installIconPack(iconPack.toDatabaseEntity(), icons.map { it.toDatabaseEntity() }) + + (parser as? XmlResourceParser)?.close() + inStream?.close() + + Log.d("MM20", "Icon pack has been installed successfully") + } catch (e: PackageManager.NameNotFoundException) { + Log.e("MM20", "Could not install icon pack $pkgName: package not found.") + } catch (e: XmlPullParserException) { + CrashReporter.logException(e) + } + } +} + +private const val PREFERENCE_NAME = "icon_pack" +private const val KEY_ICON_PACK = "icon_pack" +private const val KEY_VERSION = "version" +private const val KEY_ICONSCALE = "iconscale" \ No newline at end of file diff --git a/icons/src/main/java/de/mm20/launcher2/icons/IconRepository.kt b/icons/src/main/java/de/mm20/launcher2/icons/IconRepository.kt new file mode 100644 index 00000000..90c2fcf1 --- /dev/null +++ b/icons/src/main/java/de/mm20/launcher2/icons/IconRepository.kt @@ -0,0 +1,67 @@ +package de.mm20.launcher2.icons + +import android.content.Context +import android.util.Log +import android.util.LruCache +import de.mm20.launcher2.search.data.Searchable +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +class IconRepository private constructor(val context: Context) { + + private val scope = CoroutineScope(Job() + Dispatchers.Main) + + private val cache = LruCache(200) + + fun getIcon(searchable: Searchable, size: Int): Flow = flow { + var icon = cache.get(searchable.key) + if (icon != null) { + emit(icon) + return@flow + } + val placeholderIcon = withContext(Dispatchers.IO) { + searchable.getPlaceholderIcon(context) + } + emit(placeholderIcon) + icon = withContext(Dispatchers.IO) { + searchable.loadIconAsync(context, size) + } + if (icon != null) { + cache.put(searchable.key, icon) + emit(icon) + } + } + + /** + * Returns the icon for the given Searchable if it was requested earlier and is still in cache. + * Returns `null` otherwise. + */ + fun getIconIfCached(searchable: Searchable): LauncherIcon? { + return cache[searchable.key] + } + + fun requestIconPackListUpdate() { + scope.launch { + IconPackManager.getInstance(context).updateIconPacks() + } + } + + fun removeIconFromCache(searchable: Searchable) { + cache.remove(searchable.key) + } + + fun clearCache() { + cache.evictAll() + } + + + companion object { + private lateinit var instance: IconRepository + + fun getInstance(context: Context): IconRepository { + if (!::instance.isInitialized) instance = IconRepository(context.applicationContext) + return instance + } + } +} \ No newline at end of file diff --git a/ktx/.gitignore b/ktx/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/ktx/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/ktx/build.gradle.kts b/ktx/build.gradle.kts new file mode 100644 index 00000000..f158626b --- /dev/null +++ b/ktx/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + id("com.android.library") + id("kotlin-android") + id("kotlin-android-extensions") +} + +android { + compileSdk = sdk.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = sdk.versions.minSdk.get().toInt() + targetSdk = sdk.versions.targetSdk.get().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation(libs.bundles.kotlin) + + implementation(libs.androidx.core) + implementation(libs.androidx.appcompat) + implementation(libs.bundles.androidx.lifecycle) + +} \ No newline at end of file diff --git a/ktx/consumer-rules.pro b/ktx/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/ktx/proguard-rules.pro b/ktx/proguard-rules.pro new file mode 100644 index 00000000..ff59496d --- /dev/null +++ b/ktx/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/ktx/readme.md b/ktx/readme.md new file mode 100644 index 00000000..89f348a4 --- /dev/null +++ b/ktx/readme.md @@ -0,0 +1,3 @@ +# :ktx + +A collection of useful Kotlin extension functions. \ No newline at end of file diff --git a/ktx/src/main/AndroidManifest.xml b/ktx/src/main/AndroidManifest.xml new file mode 100644 index 00000000..3b1edbcf --- /dev/null +++ b/ktx/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + / + \ No newline at end of file diff --git a/ktx/src/main/java/de/mm20/launcher2/ktx/Address.kt b/ktx/src/main/java/de/mm20/launcher2/ktx/Address.kt new file mode 100644 index 00000000..9d4d6dfc --- /dev/null +++ b/ktx/src/main/java/de/mm20/launcher2/ktx/Address.kt @@ -0,0 +1,11 @@ +package de.mm20.launcher2.ktx + +import android.location.Address + +fun Address.formatToString( +): String { + val sb = StringBuilder() + if (locality != null) sb.append(locality).append(", ") + if (countryCode != null) sb.append(countryCode) + return sb.toString() +} \ No newline at end of file diff --git a/ktx/src/main/java/de/mm20/launcher2/ktx/Context.kt b/ktx/src/main/java/de/mm20/launcher2/ktx/Context.kt new file mode 100644 index 00000000..7f614574 --- /dev/null +++ b/ktx/src/main/java/de/mm20/launcher2/ktx/Context.kt @@ -0,0 +1,29 @@ +package de.mm20.launcher2.ktx + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Bundle +import androidx.core.content.ContextCompat + +val Context.dp: Float + get() = resources.displayMetrics.density + + +val Context.sp: Float + get() = resources.displayMetrics.scaledDensity + +fun Context.checkPermission(permission: String): Boolean { + return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED +} + + +fun Context.tryStartActivity(intent: Intent, bundle: Bundle? = null): Boolean { + return try { + startActivity(intent, bundle) + true + } catch (e: ActivityNotFoundException) { + false + } +} \ No newline at end of file diff --git a/ktx/src/main/java/de/mm20/launcher2/ktx/Double.kt b/ktx/src/main/java/de/mm20/launcher2/ktx/Double.kt new file mode 100644 index 00000000..ed7fec8f --- /dev/null +++ b/ktx/src/main/java/de/mm20/launcher2/ktx/Double.kt @@ -0,0 +1,7 @@ +package de.mm20.launcher2.ktx + +import kotlin.math.ceil + +fun Double.ceilToInt(): Int { + return ceil(this).toInt() +} diff --git a/ktx/src/main/java/de/mm20/launcher2/ktx/Drawable.kt b/ktx/src/main/java/de/mm20/launcher2/ktx/Drawable.kt new file mode 100644 index 00000000..53ab699e --- /dev/null +++ b/ktx/src/main/java/de/mm20/launcher2/ktx/Drawable.kt @@ -0,0 +1,16 @@ +package de.mm20.launcher2.ktx + +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import androidx.annotation.Px +import androidx.core.graphics.drawable.toBitmap + +fun Drawable.toBitmapOrNull( + @Px width: Int = intrinsicWidth, + @Px height: Int = intrinsicHeight, + config: Bitmap.Config? = null +): Bitmap? { + if (this is BitmapDrawable && bitmap == null) return null + return toBitmap(width, height, config) +} \ No newline at end of file diff --git a/ktx/src/main/java/de/mm20/launcher2/ktx/Extensions.kt b/ktx/src/main/java/de/mm20/launcher2/ktx/Extensions.kt new file mode 100644 index 00000000..b63d617f --- /dev/null +++ b/ktx/src/main/java/de/mm20/launcher2/ktx/Extensions.kt @@ -0,0 +1,36 @@ +package de.mm20.launcher2.ktx + +import android.os.Build +import androidx.annotation.ChecksSdkIntAtLeast +import org.json.JSONObject + +fun jsonObjectOf(vararg pairs: Pair): JSONObject { + val json = JSONObject() + for ((k, v) in pairs) { + when (v) { + is Float -> json.put(k, v.toDouble()) + is Double -> json.put(k, v) + is Int -> json.put(k, v) + is Long -> json.put(k, v) + is Boolean -> json.put(k, v) + is String -> json.put(k, v) + else -> json.put(k, v) + } + } + return json +} + +@ChecksSdkIntAtLeast(parameter = 0) +fun isAtLeastApiLevel(apiLevel: Int): Boolean { + return Build.VERSION.SDK_INT >= apiLevel +} + +inline fun Any?.castTo(): T { + @Suppress("UNCHECKED_CAST") + return this as T +} + +inline fun Any?.castToOrNull(): T? { + @Suppress("UNCHECKED_CAST") + return this as? T +} \ No newline at end of file diff --git a/ktx/src/main/java/de/mm20/launcher2/ktx/Float.kt b/ktx/src/main/java/de/mm20/launcher2/ktx/Float.kt new file mode 100644 index 00000000..2149c1a7 --- /dev/null +++ b/ktx/src/main/java/de/mm20/launcher2/ktx/Float.kt @@ -0,0 +1,7 @@ +package de.mm20.launcher2.ktx + +import kotlin.math.ceil + +fun Float.ceilToInt(): Int { + return ceil(this).toInt() +} diff --git a/ktx/src/main/java/de/mm20/launcher2/ktx/Fragment.kt b/ktx/src/main/java/de/mm20/launcher2/ktx/Fragment.kt new file mode 100644 index 00000000..7faa1fbc --- /dev/null +++ b/ktx/src/main/java/de/mm20/launcher2/ktx/Fragment.kt @@ -0,0 +1,11 @@ +package de.mm20.launcher2.ktx + +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment + +val Fragment.dp: Float + get() = context?.dp ?: 0f + +val Fragment.sp: Float + get() = context?.sp ?: 0f diff --git a/ktx/src/main/java/de/mm20/launcher2/ktx/InputStream.kt b/ktx/src/main/java/de/mm20/launcher2/ktx/InputStream.kt new file mode 100644 index 00000000..06d55a4f --- /dev/null +++ b/ktx/src/main/java/de/mm20/launcher2/ktx/InputStream.kt @@ -0,0 +1,9 @@ +package de.mm20.launcher2.ktx + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import java.io.InputStream + +fun InputStream.asBitmap(options : BitmapFactory.Options? = null): Bitmap? { + return BitmapFactory.decodeStream(this, null, options) +} \ No newline at end of file diff --git a/ktx/src/main/java/de/mm20/launcher2/ktx/Int.kt b/ktx/src/main/java/de/mm20/launcher2/ktx/Int.kt new file mode 100644 index 00000000..c985623b --- /dev/null +++ b/ktx/src/main/java/de/mm20/launcher2/ktx/Int.kt @@ -0,0 +1,19 @@ +package de.mm20.launcher2.ktx + +import androidx.core.graphics.ColorUtils +import androidx.core.graphics.blue +import androidx.core.graphics.green +import androidx.core.graphics.red + +fun Int.isBrightColor(): Boolean { + val darkness = 1 - (0.299 * red + 0.587 * green + 0.114 * blue) / 255 + return darkness < 0.5 +} + +val Int.sat : Float + get() { + FloatArray(3).also { + ColorUtils.RGBToHSL(red, green, blue, it) + return it[1] + } + } \ No newline at end of file diff --git a/ktx/src/main/java/de/mm20/launcher2/ktx/List.kt b/ktx/src/main/java/de/mm20/launcher2/ktx/List.kt new file mode 100644 index 00000000..9b8e0080 --- /dev/null +++ b/ktx/src/main/java/de/mm20/launcher2/ktx/List.kt @@ -0,0 +1,13 @@ +package de.mm20.launcher2.ktx + +import java.util.* + +fun List.randomElement(): T { + if (isEmpty()) throw IndexOutOfBoundsException("List is empty") + return get(Random().nextInt(size)) +} + +fun List.randomElementOrNull(): T? { + if (isEmpty()) return null + return get(Random().nextInt(size)) +} \ No newline at end of file diff --git a/ktx/src/main/java/de/mm20/launcher2/ktx/Notification.kt b/ktx/src/main/java/de/mm20/launcher2/ktx/Notification.kt new file mode 100644 index 00000000..b5745dbd --- /dev/null +++ b/ktx/src/main/java/de/mm20/launcher2/ktx/Notification.kt @@ -0,0 +1,9 @@ +package de.mm20.launcher2.ktx + +import android.app.Notification +import android.content.Context +import android.graphics.drawable.Drawable + +fun Notification.getBadgeIcon(context: Context, packageName: String): Drawable? { + return smallIcon.loadDrawable(context) +} \ No newline at end of file diff --git a/ktx/src/main/java/de/mm20/launcher2/ktx/Rect.kt b/ktx/src/main/java/de/mm20/launcher2/ktx/Rect.kt new file mode 100644 index 00000000..2eb1873a --- /dev/null +++ b/ktx/src/main/java/de/mm20/launcher2/ktx/Rect.kt @@ -0,0 +1,19 @@ +package de.mm20.launcher2.ktx + +import android.graphics.Rect +import android.graphics.RectF + +fun Rect.translate(x: Int, y: Int): Rect { + top += y + bottom += y + left += x + right += x + return this +} + +fun Rect.toRectF(other: RectF) { + other.left = left.toFloat() + other.bottom = bottom.toFloat() + other.right = right.toFloat() + other.top = top.toFloat() +} diff --git a/ktx/src/main/java/de/mm20/launcher2/ktx/RectF.kt b/ktx/src/main/java/de/mm20/launcher2/ktx/RectF.kt new file mode 100644 index 00000000..78318b37 --- /dev/null +++ b/ktx/src/main/java/de/mm20/launcher2/ktx/RectF.kt @@ -0,0 +1,25 @@ +package de.mm20.launcher2.ktx + +import android.graphics.RectF + +fun RectF.scale(factor: Float) { + val newWidth = width() * factor + val newHeight = height() * factor + bottom += newHeight - height() + right += newWidth - width() +} + +fun RectF.translate(x: Float, y: Float): RectF { + top += y + bottom += y + left += x + right += x + return this +} + +infix fun RectF.copyTo(other: RectF) { + other.top = top + other.left = left + other.right = right + other.bottom = bottom +} \ No newline at end of file diff --git a/ktx/src/main/java/de/mm20/launcher2/ktx/Resources.kt b/ktx/src/main/java/de/mm20/launcher2/ktx/Resources.kt new file mode 100644 index 00000000..a5425f51 --- /dev/null +++ b/ktx/src/main/java/de/mm20/launcher2/ktx/Resources.kt @@ -0,0 +1,29 @@ +package de.mm20.launcher2.ktx + +import android.content.res.Resources +import android.content.res.TypedArray +import android.graphics.drawable.Drawable + +fun Resources.getIntArrayOrNull(id: Int): IntArray? { + return try { + getIntArray(id) + } catch (e: Resources.NotFoundException) { + null + } +} + +fun Resources.getDrawableOrNull(id: Int, theme: Resources.Theme? = null): Drawable? { + return try { + getDrawable(id, theme) + } catch (e: Resources.NotFoundException) { + null + } +} + +fun Resources.obtainTypedArrayOrNull(id: Int): TypedArray? { + return try { + obtainTypedArray(id) + } catch (e: Resources.NotFoundException) { + null + } +} \ No newline at end of file diff --git a/ktx/src/main/java/de/mm20/launcher2/ktx/SharedPreferences.kt b/ktx/src/main/java/de/mm20/launcher2/ktx/SharedPreferences.kt new file mode 100644 index 00000000..0b4a7acb --- /dev/null +++ b/ktx/src/main/java/de/mm20/launcher2/ktx/SharedPreferences.kt @@ -0,0 +1,17 @@ +package de.mm20.launcher2.ktx + +import android.content.SharedPreferences + +fun SharedPreferences.Editor.putDouble(key: String, value: Double) { + putLong(key, value.toBits()) +} + +fun SharedPreferences.getDouble(key: String): Double? { + if (contains(key)) return Double.fromBits(getLong(key, 0)) + return null +} + +fun SharedPreferences.getDouble(key: String, defValue: Double): Double { + if (contains(key)) return Double.fromBits(getLong(key, 0)) + return defValue +} \ No newline at end of file diff --git a/ktx/src/main/java/de/mm20/launcher2/ktx/String.kt b/ktx/src/main/java/de/mm20/launcher2/ktx/String.kt new file mode 100644 index 00000000..d1fd1d1f --- /dev/null +++ b/ktx/src/main/java/de/mm20/launcher2/ktx/String.kt @@ -0,0 +1,7 @@ +package de.mm20.launcher2.ktx + +import java.net.URLDecoder + +fun String.decodeUrl(charset: String): String? { + return URLDecoder.decode(this, charset) +} \ No newline at end of file diff --git a/ktx/src/main/java/de/mm20/launcher2/ktx/TextView.kt b/ktx/src/main/java/de/mm20/launcher2/ktx/TextView.kt new file mode 100644 index 00000000..97d82795 --- /dev/null +++ b/ktx/src/main/java/de/mm20/launcher2/ktx/TextView.kt @@ -0,0 +1,13 @@ +package de.mm20.launcher2.ktx + +import android.graphics.drawable.Drawable +import android.widget.TextView +import androidx.annotation.DrawableRes + +fun TextView.setStartCompoundDrawable(drawable: Drawable?) { + setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null) +} + +fun TextView.setStartCompoundDrawable(@DrawableRes drawableRes: Int) { + setCompoundDrawablesRelativeWithIntrinsicBounds(drawableRes, 0, 0, 0) +} \ No newline at end of file diff --git a/ktx/src/main/java/de/mm20/launcher2/ktx/UserHandle.kt b/ktx/src/main/java/de/mm20/launcher2/ktx/UserHandle.kt new file mode 100644 index 00000000..f07b70ad --- /dev/null +++ b/ktx/src/main/java/de/mm20/launcher2/ktx/UserHandle.kt @@ -0,0 +1,12 @@ +package de.mm20.launcher2.ktx + +import android.content.Context +import android.os.Process +import android.os.UserHandle +import android.os.UserManager + +fun UserHandle.getSerialNumber(context: Context): Long { + if (this == Process.myUserHandle()) return 0L + val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager + return userManager.getSerialNumberForUser(this) +} \ No newline at end of file diff --git a/ktx/src/main/java/de/mm20/launcher2/ktx/View.kt b/ktx/src/main/java/de/mm20/launcher2/ktx/View.kt new file mode 100644 index 00000000..a1336b0d --- /dev/null +++ b/ktx/src/main/java/de/mm20/launcher2/ktx/View.kt @@ -0,0 +1,24 @@ +package de.mm20.launcher2.ktx + +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import kotlin.coroutines.CoroutineContext + +val View.dp: Float + get() = context.dp + +val View.sp: Float + get() = context.sp + +fun View.asViewGroup(): ViewGroup? { + return this as? ViewGroup +} + +val View.lifecycleScope +get() = (context as LifecycleOwner).lifecycleScope + +fun View.setPadding(vertical: Int, horizontal: Int) { + setPadding(vertical, horizontal, vertical, horizontal) +} \ No newline at end of file diff --git a/ms-services/.gitignore b/ms-services/.gitignore new file mode 100644 index 00000000..e14e350a --- /dev/null +++ b/ms-services/.gitignore @@ -0,0 +1,2 @@ +/build +*/**/msal_auth_config.json \ No newline at end of file diff --git a/ms-services/build.gradle.kts b/ms-services/build.gradle.kts new file mode 100644 index 00000000..e84968e7 --- /dev/null +++ b/ms-services/build.gradle.kts @@ -0,0 +1,49 @@ +plugins { + id("com.android.library") + id("kotlin-android") + id("kotlin-android-extensions") +} + +android { + compileSdk = sdk.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = sdk.versions.minSdk.get().toInt() + targetSdk = sdk.versions.targetSdk.get().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation(libs.bundles.kotlin) + implementation(libs.androidx.core) + implementation(libs.androidx.appcompat) + + implementation(libs.microsoft.identity) + implementation(libs.microsoft.graph) + implementation(libs.guava) + + implementation(project(":crashreporter")) + implementation(project(":preferences")) +} \ No newline at end of file diff --git a/ms-services/consumer-rules.pro b/ms-services/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/ms-services/proguard-rules.pro b/ms-services/proguard-rules.pro new file mode 100644 index 00000000..01639a19 --- /dev/null +++ b/ms-services/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.kts.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/ms-services/readme.md b/ms-services/readme.md new file mode 100644 index 00000000..39a828a2 --- /dev/null +++ b/ms-services/readme.md @@ -0,0 +1,41 @@ +# :ms-services + +⚠️ Depends on non-free external services. + +This module manages API calls to Microsoft APIs and connected Microsoft accounts. + +## Configuration + +This module requires additional configuration in order to work properly. You can skip this step but +then Microsoft API related features (e.g. OneDrive search) won't be available. + +In order to use Microsoft Graph APIs, you need to setup a new project in the Microsoft Azure Portal first. + +1. Open the [Microsoft Azure Portal](https://portal.azure.com) +1. Create a new project. + 1. Search for Azure Active Directory + 1. On the left side, select App registrations + 1. Add a new registration + 1. Supported account types: Personal Microsoft Accounts only +1. Add an authentication platform + 1. Go to Authentication + 1. Add a platform > Android + 1. Enter the debug package name (de.mm20.launcher2.debug) and the signature hash of your debug key + 1. You can use the following command to generate the signature hash: + `keytool -exportcert -alias androiddebugkey -keystore ~/.android/debug.keystore | openssl sha1 -binary | openssl base64` + 1. Click Configure > Done + 1. In the newly created Android section, click on Add URI + 1. Add package name (de.mm20.launcher2.release) and signature hash of your release key +1. Download the client details + 1. In the debug client row, click on View + 1. Copy the JSON below MSAL Configuration to ./src/debug/res/raw/msal_auth_config.json +1. Repeat the previous step for the release config +1. Add the required scopes + 1. Go to API permissions + 1. Add a permission + 1. Select Microsoft Graph > Delegated permissions + 1. Tick the following scopes: + - Files.Read.All + - User.Read + 1. Click Add permissions + diff --git a/ms-services/src/debug/res/raw/msal_auth_config_example.json b/ms-services/src/debug/res/raw/msal_auth_config_example.json new file mode 100644 index 00000000..7305e89e --- /dev/null +++ b/ms-services/src/debug/res/raw/msal_auth_config_example.json @@ -0,0 +1,14 @@ +{ + "client_id" : "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "authorization_user_agent" : "DEFAULT", + "account_mode": "SINGLE", + "redirect_uri" : "msauth://de.mm20.launcher2.debug/xxxxxxxxxxxxxxxxxxxxxxxxxxx", + "authorities" : [ + { + "type": "AAD", + "audience": { + "type": "PersonalMicrosoftAccount" + } + } + ] +} diff --git a/ms-services/src/main/AndroidManifest.xml b/ms-services/src/main/AndroidManifest.xml new file mode 100644 index 00000000..1dbe0cd5 --- /dev/null +++ b/ms-services/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ms-services/src/main/java/de/mm20/launcher2/msservices/DriveItem.kt b/ms-services/src/main/java/de/mm20/launcher2/msservices/DriveItem.kt new file mode 100644 index 00000000..cff2ae1b --- /dev/null +++ b/ms-services/src/main/java/de/mm20/launcher2/msservices/DriveItem.kt @@ -0,0 +1,39 @@ +package de.mm20.launcher2.msservices + +import com.microsoft.graph.extensions.DriveItem as MSDriveItem + +data class DriveItem( + val id : String, + val label : String, + val mimeType : String, + val size: Long, + val isDirectory : Boolean, + val webUrl: String, + val meta: DriveItemMeta +) { + companion object { + fun fromApiDriveItem(driveItem: MSDriveItem) : DriveItem? { + return DriveItem( + id = driveItem.id ?: return null, + label = driveItem.name ?: return null, + mimeType = driveItem.file?.mimeType ?: "inode/directory", + size = driveItem.size ?: 0, + isDirectory = driveItem.file == null, + webUrl = driveItem.webUrl ?: return null, + meta = DriveItemMeta( + owner = driveItem.shared?.owner?.user?.displayName, + createdBy = driveItem.createdBy?.user?.displayName, + width = driveItem.image?.width ?: driveItem.video?.width, + height = driveItem.image?.height ?: driveItem.video?.height + ) + ) + } + } +} + +data class DriveItemMeta( + val owner: String?, + val createdBy: String?, + val width: Int?, + val height: Int? +) \ No newline at end of file diff --git a/ms-services/src/main/java/de/mm20/launcher2/msservices/MicrosoftGraphApiHelper.kt b/ms-services/src/main/java/de/mm20/launcher2/msservices/MicrosoftGraphApiHelper.kt new file mode 100644 index 00000000..d698a503 --- /dev/null +++ b/ms-services/src/main/java/de/mm20/launcher2/msservices/MicrosoftGraphApiHelper.kt @@ -0,0 +1,195 @@ +package de.mm20.launcher2.msservices + +import android.app.Activity +import android.content.Context +import android.util.Log +import androidx.core.content.edit +import com.microsoft.graph.core.ClientException +import com.microsoft.graph.core.DefaultClientConfig +import com.microsoft.graph.extensions.GraphServiceClient +import com.microsoft.graph.extensions.IGraphServiceClient +import com.microsoft.graph.http.GraphServiceException +import com.microsoft.identity.client.AuthenticationCallback +import com.microsoft.identity.client.IAuthenticationResult +import com.microsoft.identity.client.ISingleAccountPublicClientApplication +import com.microsoft.identity.client.PublicClientApplication +import com.microsoft.identity.client.exception.MsalException +import de.mm20.launcher2.crashreporter.CrashReporter +import de.mm20.launcher2.preferences.LauncherPreferences +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.net.URLEncoder +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +class MicrosoftGraphApiHelper(val context: Context) { + + private var accessToken: String? = null + private val client: IGraphServiceClient + private var clientApplication: ISingleAccountPublicClientApplication? = null + + init { + client = GraphServiceClient + .Builder() + .fromConfig(DefaultClientConfig.createWithAuthenticationProvider { + it.addHeader("Authorization", "Bearer $accessToken") + }) + .buildClient() + } + + private suspend fun getClientApplication(): ISingleAccountPublicClientApplication? { + val resId = getConfigResId() + if (resId == 0) return null + if (clientApplication == null) { + clientApplication = withContext(Dispatchers.IO) { + PublicClientApplication.createSingleAccountPublicClientApplication( + context.applicationContext, + resId + ) + } + } + return clientApplication!! + } + + private suspend fun acquireAccessToken(): Boolean { + val result = withContext(Dispatchers.IO) { + try { + val application = getClientApplication() ?: return@withContext null + val authority = application.configuration.defaultAuthority.authorityURL.toString() + application.acquireTokenSilent(SCOPES, authority) + } catch (e: MsalException) { + CrashReporter.logException(e) + logout() + null + } catch (e: ClientException) { + CrashReporter.logException(e) + null + } + } + accessToken = result?.accessToken + return result != null + } + + suspend fun login(context: Activity) { + val clientApplication = getClientApplication() ?: return + suspendCoroutine { + clientApplication.signIn(context, "", SCOPES, object : AuthenticationCallback { + override fun onSuccess(authenticationResult: IAuthenticationResult?) { + accessToken = authenticationResult?.accessToken + LauncherPreferences.instance.searchOneDrive = true + it.resume(authenticationResult) + } + + override fun onCancel() { + it.resume(null) + } + + override fun onError(exception: MsalException?) { + if (exception != null) Log.e("MM20", exception.stackTraceToString()) + it.resume(null) + } + + }) + } + loadAccountName() + } + + suspend fun logout() { + accessToken = null + LauncherPreferences.instance.searchOneDrive = false + context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit { + putString(PREF_ACCOUNT_NAME, null) + } + withContext(Dispatchers.IO) { getClientApplication()?.signOut() } + } + + suspend fun getUser(): MsUser? { + val name = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString( + PREF_ACCOUNT_NAME, + null + ) ?: loadAccountName() + + + return name?.let { + MsUser(name = it) + } + } + + private suspend fun loadAccountName(): String? { + if (!isLoggedIn()) return null + if (!acquireAccessToken()) return null + return withContext(Dispatchers.IO) { + try { + val user = client.me.buildRequest().get() ?: return@withContext null + val name = user.displayName ?: user.mail ?: "Microsoft User" + context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit { + putString(PREF_ACCOUNT_NAME, name) + } + return@withContext name + } catch (e: GraphServiceException) { + CrashReporter.logException(e) + logout() + } catch (e: ClientException) { + CrashReporter.logException(e) + } + null + } + } + + suspend fun isLoggedIn(): Boolean { + return withContext(Dispatchers.IO) { + getClientApplication()?.currentAccount?.currentAccount != null + } + } + + + suspend fun queryOneDriveFiles(query: String): List? { + if (!acquireAccessToken()) return null + return try { + withContext(Dispatchers.IO) { + client.me.drive.getSearch( + URLEncoder.encode(query.replace("'", "''"), "utf8") + ) + .buildRequest() + .select("id,name,file,size,video,image,webUrl,shared,createdBy") + .top(10) + .get() + ?.currentPage + ?.mapNotNull { DriveItem.fromApiDriveItem(it) } + } + } catch (e: GraphServiceException) { + CrashReporter.logException(e) + null + } catch (e: ClientException) { + CrashReporter.logException(e) + null + } + } + + fun isAvailable(): Boolean { + return getConfigResId() != 0 + } + + private fun getConfigResId(): Int { + return context.resources.getIdentifier("msal_auth_config", "raw", context.packageName) + } + + companion object { + private lateinit var instance: MicrosoftGraphApiHelper + + fun getInstance(context: Context): MicrosoftGraphApiHelper { + if (!Companion::instance.isInitialized) instance = + MicrosoftGraphApiHelper(context.applicationContext) + return instance + } + + private val SCOPES = arrayOf( + "User.Read", + "Files.Read.All" + ) + + const val PREFS = "ms-account" + const val PREF_ACCOUNT_NAME = "name" + } + +} \ No newline at end of file diff --git a/ms-services/src/main/java/de/mm20/launcher2/msservices/MsUser.kt b/ms-services/src/main/java/de/mm20/launcher2/msservices/MsUser.kt new file mode 100644 index 00000000..dc17af39 --- /dev/null +++ b/ms-services/src/main/java/de/mm20/launcher2/msservices/MsUser.kt @@ -0,0 +1,5 @@ +package de.mm20.launcher2.msservices + +data class MsUser( + val name: String +) \ No newline at end of file diff --git a/ms-services/src/release/res/raw/msal_auth_config_example.json b/ms-services/src/release/res/raw/msal_auth_config_example.json new file mode 100644 index 00000000..2be2262c --- /dev/null +++ b/ms-services/src/release/res/raw/msal_auth_config_example.json @@ -0,0 +1,14 @@ +{ + "client_id" : "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "authorization_user_agent" : "DEFAULT", + "account_mode": "SINGLE", + "redirect_uri" : "msauth://de.mm20.launcher2.release/xxxxxxxxxxxxxxxxxx", + "authorities" : [ + { + "type": "AAD", + "audience": { + "type": "PersonalMicrosoftAccount" + } + } + ] +} diff --git a/music/.gitignore b/music/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/music/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/music/build.gradle.kts b/music/build.gradle.kts new file mode 100644 index 00000000..4b1e5bc1 --- /dev/null +++ b/music/build.gradle.kts @@ -0,0 +1,46 @@ +plugins { + id("com.android.library") + id("kotlin-android") + id("kotlin-android-extensions") +} + +android { + compileSdk = sdk.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = sdk.versions.minSdk.get().toInt() + targetSdk = sdk.versions.targetSdk.get().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation(libs.bundles.kotlin) + implementation(libs.androidx.core) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.media2) + + implementation(project(":ktx")) + +} \ No newline at end of file diff --git a/music/consumer-rules.pro b/music/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/music/proguard-rules.pro b/music/proguard-rules.pro new file mode 100644 index 00000000..d99b33c9 --- /dev/null +++ b/music/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.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/music/src/main/AndroidManifest.xml b/music/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8766846e --- /dev/null +++ b/music/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + / + \ No newline at end of file diff --git a/music/src/main/java/de/mm20/launcher2/music/MusicRepository.kt b/music/src/main/java/de/mm20/launcher2/music/MusicRepository.kt new file mode 100644 index 00000000..615acd66 --- /dev/null +++ b/music/src/main/java/de/mm20/launcher2/music/MusicRepository.kt @@ -0,0 +1,268 @@ +package de.mm20.launcher2.music + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.media.AudioManager +import android.os.Bundle +import android.support.v4.media.session.MediaSessionCompat +import android.util.Log +import android.view.KeyEvent +import androidx.core.content.edit +import androidx.core.graphics.scale +import androidx.lifecycle.MutableLiveData +import androidx.media2.common.MediaItem +import androidx.media2.common.MediaMetadata +import androidx.media2.common.SessionPlayer +import androidx.media2.session.MediaController +import androidx.media2.session.SessionCommand +import androidx.media2.session.SessionCommandGroup +import androidx.media2.session.SessionResult +import kotlinx.coroutines.* +import java.io.File +import java.util.concurrent.Executors + +class MusicRepository private constructor(val context: Context) { + + private val scope = CoroutineScope(Job() + Dispatchers.Main) + + val playbackState = MutableLiveData() + val title = MutableLiveData() + val artist = MutableLiveData() + val album = MutableLiveData() + val albumArt = MutableLiveData() + + private var lastPlayer: String? = null + set(value) { + context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit { + putString(PREFS_KEY_LAST_PLAYER, value) + } + field = value + } + + private var lastToken: String? = null + + fun setMediaSession(token: MediaSessionCompat.Token) { + if (token.toString() == lastToken.toString()) return + mediaController?.close() + mediaController = MediaController.Builder(context) + .setSessionCompatToken(token) + .setControllerCallback(Executors.newSingleThreadExecutor(), mediaSessionCallback) + .build() + lastToken = token.toString() + } + + private var mediaController: MediaController? = null + set(value) { + if (value == null) { + playbackState.postValue(PlaybackState.Stopped) + } + field = value + } + + private val mediaSessionCallback = object : MediaController.ControllerCallback() { + override fun onConnected(controller: MediaController, allowedCommands: SessionCommandGroup) { + super.onConnected(controller, allowedCommands) + updateMetadata() + updateState() + } + + override fun onCurrentMediaItemChanged(controller: MediaController, item: MediaItem?) { + super.onCurrentMediaItemChanged(controller, item) + updateMetadata() + } + + override fun onPlayerStateChanged(controller: MediaController, state: Int) { + super.onPlayerStateChanged(controller, state) + updateState() + } + + override fun onPlaybackInfoChanged(controller: MediaController, info: MediaController.PlaybackInfo) { + super.onPlaybackInfoChanged(controller, info) + Log.d("MM20", "CurrentPosition" + controller.currentPosition.toString()) + } + + override fun onDisconnected(controller: MediaController) { + super.onDisconnected(controller) + mediaController = null + } + + /*override fun onMetadataChanged(metadata: MediaController?) { + super.onMetadataChanged(metadata) + updateState() + } + + override fun onSessionDestroyed() { + super.onSessionDestroyed() + mediaController = null + hasActiveSession.value = false + playbackState.value = PlaybackState.Stopped + }*/ + } + + private fun updateMetadata() { + val metadata = mediaController?.currentMediaItem?.metadata ?: return + val title = metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE) + ?: metadata.getString(MediaMetadata.METADATA_KEY_TITLE) + val artist = metadata.getString(MediaMetadata.METADATA_KEY_ARTIST) + ?: metadata.getString(MediaMetadata.METADATA_KEY_COMPOSER) + ?: metadata.getString(MediaMetadata.METADATA_KEY_AUTHOR) + ?: metadata.getString(MediaMetadata.METADATA_KEY_WRITER) + val album = metadata.getString(MediaMetadata.METADATA_KEY_ALBUM) + + lastPlayer = mediaController?.connectedToken?.packageName ?: lastPlayer + this@MusicRepository.title.postValue(title) + this@MusicRepository.artist.postValue(artist) + this@MusicRepository.album.postValue(album) + + scope.launch { + withContext(Dispatchers.IO) { + val albumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART) + context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit { + putString(PREFS_KEY_ALBUM_ART, if (albumArt == null) "null" else "notnull") + } + if (albumArt == null) { + this@MusicRepository.albumArt.postValue(null) + return@withContext + } + val size = context.resources.getDimensionPixelSize(R.dimen.album_art_size) + val scaledBitmap = albumArt.scale(size, size) + val file = File(context.cacheDir, "album_art") + val outStream = file.outputStream() + scaledBitmap.compress(Bitmap.CompressFormat.PNG, 100, outStream) + outStream.close() + this@MusicRepository.albumArt.postValue(scaledBitmap) + } + } + + context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit { + putString(PREFS_KEY_TITLE, title) + putString(PREFS_KEY_ARTIST, artist) + putString(PREFS_KEY_ALBUM, album) + } + } + + private fun updateState() { + val playbackState = when (mediaController?.playerState) { + SessionPlayer.PLAYER_STATE_PLAYING -> PlaybackState.Playing + SessionPlayer.PLAYER_STATE_PAUSED -> PlaybackState.Paused + else -> PlaybackState.Stopped + } + this@MusicRepository.playbackState.postValue(playbackState) + } + + val hasActiveSession: Boolean = mediaController?.isConnected != null + + init { + loadLastPlaybackMetadata() + } + + + private fun loadLastPlaybackMetadata() { + val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) + lastPlayer = prefs.getString(PREFS_KEY_LAST_PLAYER, null) + title.value = prefs.getString(PREFS_KEY_TITLE, null) + artist.value = prefs.getString(PREFS_KEY_ARTIST, null) + album.value = prefs.getString(PREFS_KEY_ALBUM, null) + if (prefs.getString(PREFS_KEY_ALBUM_ART, "null") == "null") { + albumArt.value = null + } else scope.launch { + val albumArt = withContext(Dispatchers.IO) { + BitmapFactory.decodeFile(File(context.cacheDir, "album_art").absolutePath) + } + this@MusicRepository.albumArt.value = albumArt + } + playbackState.value = PlaybackState.Stopped + } + + fun previous() { + if (mediaController?.skipToPreviousPlaylistItem()?.get() == null) { + val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + val downEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PREVIOUS) + audioManager.dispatchMediaKeyEvent(downEvent) + val upEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PREVIOUS) + audioManager.dispatchMediaKeyEvent(upEvent) + } + } + + fun next() { + if (mediaController?.skipToNextPlaylistItem()?.get() == null) { + val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + val downEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT) + audioManager.dispatchMediaKeyEvent(downEvent) + val upEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_NEXT) + audioManager.dispatchMediaKeyEvent(upEvent) + } + } + + fun play() { + if (mediaController?.play()?.get() == null) { + val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + val downEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY) + audioManager.dispatchMediaKeyEvent(downEvent) + val upEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY) + audioManager.dispatchMediaKeyEvent(upEvent) + } + } + + fun pause() { + if (mediaController?.pause()?.get() == null) { + val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + val downEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PAUSE) + audioManager.dispatchMediaKeyEvent(downEvent) + val upEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PAUSE) + audioManager.dispatchMediaKeyEvent(upEvent) + } + } + + fun togglePause() { + if (playbackState.value != PlaybackState.Playing) play() else pause() + } + + fun getLaunchIntent(context: Context): PendingIntent { + mediaController?.sessionActivity?.let { + return it + } + val intent = Intent(Intent.ACTION_MAIN) + .setPackage(lastPlayer) + .addCategory(Intent.CATEGORY_APP_MUSIC) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE) + } + + fun openPlayerChooser(context: Context) { + context.startActivity(Intent.createChooser( + Intent(Intent.ACTION_MAIN) + .apply { + addCategory(Intent.CATEGORY_APP_MUSIC) + flags = Intent.FLAG_ACTIVITY_NEW_TASK + }, + null) + ) + } + + companion object { + private lateinit var instance: MusicRepository + + fun getInstance(context: Context): MusicRepository { + if (!::instance.isInitialized) instance = MusicRepository(context.applicationContext) + return instance + } + + private const val PREFS = "music" + private const val PREFS_KEY_TITLE = "title" + private const val PREFS_KEY_ARTIST = "artist" + private const val PREFS_KEY_ALBUM = "album" + private const val PREFS_KEY_ALBUM_ART = "album_art" + private const val PREFS_KEY_LAST_PLAYER = "last_player" + } +} + +enum class PlaybackState { + Paused, + Playing, + Stopped +} \ No newline at end of file diff --git a/music/src/main/java/de/mm20/launcher2/music/MusicViewModel.kt b/music/src/main/java/de/mm20/launcher2/music/MusicViewModel.kt new file mode 100644 index 00000000..cc3be95b --- /dev/null +++ b/music/src/main/java/de/mm20/launcher2/music/MusicViewModel.kt @@ -0,0 +1,50 @@ +package de.mm20.launcher2.music + +import android.app.Application +import android.app.PendingIntent +import android.content.Context +import android.graphics.Bitmap +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData + +class MusicViewModel(app: Application) : AndroidViewModel(app) { + + val musicRepository = MusicRepository.getInstance(app) + + val title: LiveData = musicRepository.title + val artist: LiveData = musicRepository.artist + val album: LiveData = musicRepository.album + val albumArt: LiveData = musicRepository.albumArt + val playbackState: LiveData = musicRepository.playbackState + + val hasActiveSession : Boolean = musicRepository.hasActiveSession + + fun previous() { + musicRepository.previous() + } + + fun next() { + musicRepository.next() + } + + fun play() { + musicRepository.play() + } + + fun pause() { + musicRepository.pause() + } + + fun togglePause() { + musicRepository.togglePause() + } + + fun getLaunchIntent(context: Context): PendingIntent { + return musicRepository.getLaunchIntent(context) + } + + fun openPlayerChooser(context: Context) { + musicRepository.openPlayerChooser(context) + } +} \ No newline at end of file diff --git a/music/src/main/res/values/dimens.xml b/music/src/main/res/values/dimens.xml new file mode 100644 index 00000000..c8637f62 --- /dev/null +++ b/music/src/main/res/values/dimens.xml @@ -0,0 +1,3 @@ + + 144dp + diff --git a/nextcloud/.gitignore b/nextcloud/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/nextcloud/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/nextcloud/build.gradle.kts b/nextcloud/build.gradle.kts new file mode 100644 index 00000000..f33d4826 --- /dev/null +++ b/nextcloud/build.gradle.kts @@ -0,0 +1,55 @@ +plugins { + id("com.android.library") + id("kotlin-android") + id("kotlin-android-extensions") +} + +android { + compileSdk = sdk.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = sdk.versions.minSdk.get().toInt() + targetSdk = sdk.versions.targetSdk.get().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation(libs.bundles.kotlin) + implementation(libs.androidx.core) + implementation(libs.androidx.appcompat) + implementation(libs.materialcomponents) + implementation(libs.androidx.browser) + implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.securitycrypto) + + implementation(libs.bundles.androidx.lifecycle) + + implementation(libs.okhttp) + + api(project(":webdav")) + implementation(project(":base")) + implementation(project(":i18n")) + +} \ No newline at end of file diff --git a/nextcloud/consumer-rules.pro b/nextcloud/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/nextcloud/proguard-rules.pro b/nextcloud/proguard-rules.pro new file mode 100644 index 00000000..975872b4 --- /dev/null +++ b/nextcloud/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.kts.kts.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/nextcloud/src/main/AndroidManifest.xml b/nextcloud/src/main/AndroidManifest.xml new file mode 100644 index 00000000..929b68b3 --- /dev/null +++ b/nextcloud/src/main/AndroidManifest.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/nextcloud/src/main/java/de/mm20/launcher2/nextcloud/LoginActivity.kt b/nextcloud/src/main/java/de/mm20/launcher2/nextcloud/LoginActivity.kt new file mode 100644 index 00000000..87b2f7de --- /dev/null +++ b/nextcloud/src/main/java/de/mm20/launcher2/nextcloud/LoginActivity.kt @@ -0,0 +1,81 @@ +package de.mm20.launcher2.nextcloud + +import android.app.Activity +import android.os.Bundle +import android.webkit.WebResourceRequest +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import kotlinx.android.synthetic.main.activity_nextcloud_login.* +import kotlinx.coroutines.* + +class LoginActivity : AppCompatActivity() { + + private val nextcloudClient = NextcloudApiHelper(this) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_nextcloud_login) + nextButton.setOnClickListener { + serverUrlInputLayout.error = null + lifecycleScope.launch { + var url = serverUrlInput.text.toString() + if (!(url.startsWith("http://") || url.startsWith("https://"))) { + url = "https://$url" + } + if (url.isBlank()) { + serverUrlInputLayout.error = getString(R.string.next_cloud_server_url_empty) + return@launch + } + if (nextcloudClient.checkNextcloudInstallation(url)) { + openLoginPage(url) + } else { + serverUrlInputLayout.error = getString(R.string.next_cloud_server_invalid_url) + } + } + } + } + + private fun openLoginPage(url: String) { + val webView = WebView(this) + webView.settings.userAgentString = getString(R.string.app_name) + webView.webViewClient = object : WebViewClient() { + override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { + if (request?.url?.scheme == "nc") { + val path = request.url?.path?.trim('/') ?: run { + setResult(0) + finish() + return false + } + val segments = path.split('&') + var username: String? = null + var token: String? = null + var server: String? = null + for (segment in segments) { + when { + segment.startsWith("server") -> server = segment.substringAfter(":") + segment.startsWith("user") -> username = segment.substringAfter(":") + segment.startsWith("password") -> token = segment.substringAfter(":") + } + } + if (username != null && server != null && token != null) { + nextcloudClient.setServer(server, username, token) + } + setResult(Activity.RESULT_OK) + finish() + return true + } + webView.loadUrl(request?.url?.toString() ?: "") + return false + } + } + webView.settings.javaScriptEnabled = true + setContentView(webView) + val headers = mapOf( + "OCS-APIREQUEST" to "true" + ) + webView.loadUrl("$url/index.php/login/flow", headers) + + } +} \ No newline at end of file diff --git a/nextcloud/src/main/java/de/mm20/launcher2/nextcloud/NcUser.kt b/nextcloud/src/main/java/de/mm20/launcher2/nextcloud/NcUser.kt new file mode 100644 index 00000000..67ada948 --- /dev/null +++ b/nextcloud/src/main/java/de/mm20/launcher2/nextcloud/NcUser.kt @@ -0,0 +1,6 @@ +package de.mm20.launcher2.nextcloud + +data class NcUser( + val displayName: String, + val username: String +) diff --git a/nextcloud/src/main/java/de/mm20/launcher2/nextcloud/NextcloudApiHelper.kt b/nextcloud/src/main/java/de/mm20/launcher2/nextcloud/NextcloudApiHelper.kt new file mode 100644 index 00000000..ae7e0911 --- /dev/null +++ b/nextcloud/src/main/java/de/mm20/launcher2/nextcloud/NextcloudApiHelper.kt @@ -0,0 +1,217 @@ +package de.mm20.launcher2.nextcloud + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import androidx.core.content.edit +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import de.mm20.launcher2.webdav.WebDavApi +import de.mm20.launcher2.webdav.WebDavFile +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.* +import org.json.JSONObject +import java.io.File +import java.io.IOException + +class NextcloudApiHelper(val context: Context) { + + + private val httpClient by lazy { + OkHttpClient.Builder() + .authenticator(object : Authenticator { + override fun authenticate(route: Route?, response: Response): Request? { + if (response.priorResponse?.priorResponse != null) return null + return response.request + .newBuilder() + .addHeader("Authorization", getAuthorization() ?: return null) + .build() + } + + }) + .build() + } + + private val preferences by lazy { + createPreferences() + } + + private fun createPreferences(catchErrors: Boolean = true): SharedPreferences { + try { + val masterKey = + MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build() + return EncryptedSharedPreferences.create( + context, + "nextcloud", + masterKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ).also { + if (!it.getBoolean("encrypted", false)) { + val legacyPrefs = + context.getSharedPreferences("nextcloud", Context.MODE_PRIVATE) + val keys = arrayOf("server", "username", "token", "displayname") + it.edit { + for (k in keys) { + putString(k, legacyPrefs.getString(k, null)) + } + putBoolean("encrypted", true) + } + legacyPrefs.edit { + for (k in keys) { + putString(k, null) + } + } + } + } + } catch (e: IOException) { + if (!catchErrors) throw e + File(context.filesDir, "../shared_prefs/nextcloud.xml").delete() + return createPreferences(false) + } + } + + fun login(activity: Activity) { + activity.startActivity(Intent(context, LoginActivity::class.java)) + } + + + suspend fun checkNextcloudInstallation(url: String): Boolean { + var url = url + if (!url.startsWith("http://") && !url.startsWith("https://")) { + url = "https://$url" + } + val request = Request.Builder() + .url("$url/remote.php/dav") + .build() + val response = runCatching { + withContext(Dispatchers.IO) { + httpClient.newCall(request).execute() + } + }.getOrNull() ?: return false + return response.code == 200 || response.code == 401 + } + + suspend fun getLoggedInUser(): NcUser? { + val server = getServer() + val username = getUserName() + val token = getToken() + + if (server == null || username == null || token == null) { + return null + } + + val displayName = getDisplayName() ?: return null + + return NcUser( + displayName, + username + ) + } + + /** + * Returns the user's display name or user name if the user is logged in and their token has + * not been revoked, + * returns null if they are not logged in. + */ + private suspend fun getDisplayName(): String? { + + val displayname = preferences.getString("displayname", null) + if (displayname != null) { + return displayname + } + + val server = getServer() ?: return null + + val request = Request.Builder() + .addHeader("OCS-APIRequest", "true") + .url("$server/ocs/v1.php/cloud/user?format=json") + .build() + + val response = runCatching { + withContext(Dispatchers.IO) { + httpClient.newCall(request).execute() + } + }.getOrNull() ?: return getUserName() + + if (response.code != 200) { + logout() + return null + } + val body = response.body ?: return getUserName() + + return withContext(Dispatchers.IO) { + val json = JSONObject(body.string()) + val name = json.optJSONObject("ocs") + ?.optJSONObject("data") + ?.optString("display-name") + + preferences.edit { + putString("displayname", name) + } + + return@withContext name + ?: getUserName() + } + } + + private fun getAuthorization(): String? { + return Credentials.basic(getUserName() ?: return null, getToken() ?: return null) + } + + fun getServer(): String? { + return preferences.getString("server", null) + } + + fun getUserName(): String? { + return preferences.getString("username", null) + } + + private fun getToken(): String? { + return preferences.getString("token", null) + } + + internal fun setServer(server: String, username: String, token: String) { + preferences.edit { + putString("server", server) + putString("username", username) + putString("token", token) + } + } + + suspend fun logout() { + val server = getServer() + val username = getUserName() + val token = getToken() + if (server == null || username == null || token == null) return + val request = Request.Builder() + .addHeader("OCS-APIREQUEST", "true") + .delete() + .url("$server/ocs/v2.php/core/apppassword") + .build() + withContext(Dispatchers.IO) { + val response = httpClient.newCall(request).execute() + response + } + preferences.edit { + putString("server", null) + putString("username", null) + putString("token", null) + putString("displayname", null) + } + } + + val files by lazy { + FilesApi() + } + + inner class FilesApi internal constructor() { + suspend fun search(query: String): List { + val server = getServer() ?: return emptyList() + val username = getUserName() ?: return emptyList() + return WebDavApi.search("$server/remote.php/dav/", username, query, httpClient) + } + } +} \ No newline at end of file diff --git a/nextcloud/src/main/res/drawable/ic_nextcloud_logo.xml b/nextcloud/src/main/res/drawable/ic_nextcloud_logo.xml new file mode 100644 index 00000000..4f2494b6 --- /dev/null +++ b/nextcloud/src/main/res/drawable/ic_nextcloud_logo.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + diff --git a/nextcloud/src/main/res/layout/activity_nextcloud_login.xml b/nextcloud/src/main/res/layout/activity_nextcloud_login.xml new file mode 100644 index 00000000..1842077d --- /dev/null +++ b/nextcloud/src/main/res/layout/activity_nextcloud_login.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + +