diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..6f48231 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: Domi04151309 +custom: ['https://www.paypal.com/donate/?hosted_button_id=487FTCX52P9WA'] diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 0000000..5e21670 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,31 @@ +--- +name: Bug Report +about: Create a report to help improving the app +title: "[Bug Report] Your Title" +labels: bug +assignees: '' + +--- + +**Description** + +_A clear and concise description of what the bug is. +Please add steps to reproduce the behavior._ + +
+ Screenshots + + _Add screenshots here to describe the problem._ +
+ +
+ Logs + + _Add a detailed stack trace / crash log here if applicable_ +
+ +**Additional information** + + - Android version: _System Settings > About > Android version (varies per device)_ + - Home App version: _Settings > About > Version_ + - Installation source: _Google Play / F-Drod / Own Build_ diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md new file mode 100644 index 0000000..26704ff --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,18 @@ +--- +name: Feature Request +about: Suggest an idea for this project +title: "[Feature Request] Your Title" +labels: enhancement +assignees: '' + +--- + +**Describe the solution you'd like** + +_A clear and concise description of what you want to happen._ + +
+ Additional context + + _Add any other context or screenshots about the feature request here._ +
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..bca3767 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,72 @@ +name: Continuous Integration + +on: + push: + branches: + - main + pull_request: + branches: + - main + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + ktlint: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + - name: Install + run: | + curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.0.1/ktlint + chmod a+x ktlint + sudo mv ktlint /usr/local/bin/ + - name: Run + run: ktlint --reporter sarif -l none > ktlint.sarif + - name: Upload SARIF + uses: github/codeql-action/upload-sarif@v3 + if: success() || failure() + with: + sarif_file: ktlint.sarif + detekt: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + - name: Run + run: | + chmod +x gradlew + ./gradlew detekt + - name: Upload SARIF + uses: github/codeql-action/upload-sarif@v3 + if: success() || failure() + with: + sarif_file: app/build/reports/detekt/detekt.sarif + - name: Job Summary + if: success() || failure() + run: cat ./app/build/reports/detekt/detekt.md >> $GITHUB_STEP_SUMMARY + build: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + - name: Run + run: | + chmod +x gradlew + ./gradlew build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5d18272 --- /dev/null +++ b/.gitignore @@ -0,0 +1,89 @@ +# 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 diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..7643783 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,123 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.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 0000000..0c0c338 --- /dev/null +++ b/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..15a15b2 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Strict.xml b/.idea/inspectionProfiles/Strict.xml new file mode 100644 index 0000000..7532a01 --- /dev/null +++ b/.idea/inspectionProfiles/Strict.xml @@ -0,0 +1,1006 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..ce11e3f --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..c22b6fa --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..5cc7043 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..61d1860 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ +GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/README.md b/README.md index 9010d67..758fe4b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,56 @@ -# home-app +![App Icon](https://raw.githubusercontent.com/Domi04151309/HomeApp/main/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp) +# Home App for Android™ +HomeApp is a small and easy to use smart home app with a simple framework. The goal of this application is to make remote execution of predefined features as easy and user-friendly as possible to help you get started with smart home technology. -Home-Lab Integration for Android \ No newline at end of file + + Get it on F-Droid + + +## Donate +Support the development by donating. + + + Donate + + +## Supported devices +Home App natively supports the following devices: + +- [Philips Hue Bridge](https://github.com/Domi04151309/HomeApp/wiki/Hue-API-%28v1%29) +- [Shelly](https://github.com/Domi04151309/HomeApp/wiki/Shelly) Gen 1 devices +- [Shelly](https://github.com/Domi04151309/HomeApp/wiki/Shelly) Gen 2 devices +- Devices using [ESP Easy](https://github.com/Domi04151309/HomeApp/wiki/ESP-Easy) +- Devices using [Tasmota](https://github.com/Domi04151309/HomeApp/wiki/Tasmota) +- Devices using the [Node-RED dashboard](https://github.com/Domi04151309/HomeApp/wiki/Node-RED-Dashboard) +- Devices using the [SimpleHome API](https://github.com/Domi04151309/HomeApp/wiki/SimpleHome-API) +- Devices with a [web interface](https://github.com/Domi04151309/HomeApp/wiki/Websites) + +## How it works +Communication between the devices uses HTTP requests and JSON strings. After the commanding device has send a HTTP request to the smart home device, the smart home device sends back a JSON string containing the information the app needs. + +This app is especially useful if you are using microcontrollers or other small devices such as the Raspberry Pi for smart home automation. + +## Previews + + +Android, Google Play and the Google Play logo are trademarks of Google LLC. + +## Legal Notice +Copyright (C) 2020 Domi04151309 + +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 . + +Impressum diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..e1db910 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,103 @@ +import com.android.build.gradle.internal.tasks.factory.dependsOn + +private val readAndUnderstoodLicense = false + +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("io.gitlab.arturbosch.detekt") +} + +android { + namespace = "io.github.domi04151309.home" + compileSdk = 34 + + defaultConfig { + applicationId = "io.github.domi04151309.home" + minSdk = 23 + //noinspection EditedTargetSdkVersion + targetSdk = 34 + versionCode = 1120 + versionName = "1.12.0" + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + debug { + isMinifyEnabled = true + isShrinkResources = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + release { + isMinifyEnabled = true + isShrinkResources = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } + testOptions { + unitTests.isIncludeAndroidResources = true + } + buildFeatures { + buildConfig = true + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = "17" + } + detekt { + config.setFrom(file("detekt-config.yml")) + buildUponDefaultConfig = true + basePath = rootProject.projectDir.absolutePath + } + lint { + disable += "MissingTranslation" + } + project.tasks.preBuild.dependsOn("license") +} + +tasks.register("license") { + doFirst { + val data = + file("./src/main/res/xml/pref_about.xml") + .readText() + .contains("app:key=\"license\"") + if (!data) { + throw Exception( + "Please note that removing the license from the about page is not allowed if you " + + "plan to publish your modified version of this app. " + + "Please read the project's LICENSE.", + ) + } + if (!( + android.defaultConfig.applicationId?.contains("domi04151309") == true || + readAndUnderstoodLicense + ) + ) { + throw Exception( + "Please make sure you have read and understood the LICENSE!", + ) + } + } +} + +dependencies { + implementation("androidx.appcompat:appcompat:1.7.1") + implementation("com.google.android.material:material:1.12.0") + implementation("androidx.preference:preference-ktx:1.2.1") + implementation("androidx.annotation:annotation:1.9.1") + implementation("com.android.volley:volley:1.2.1") + implementation("androidx.security:security-crypto-ktx:1.1.0-beta01") + implementation("com.github.skydoves:colorpickerview:2.3.0") + + testImplementation("junit:junit:4.13.2") + testImplementation("org.robolectric:robolectric:4.14.1") +} diff --git a/app/detekt-config.yml b/app/detekt-config.yml new file mode 100644 index 0000000..af903e4 --- /dev/null +++ b/app/detekt-config.yml @@ -0,0 +1,784 @@ +build: + maxIssues: 0 + excludeCorrectable: true + weights: + # complexity: 2 + # LongParameterList: 1 + # style: 1 + # comments: 1 + +config: + validation: true + warningsAsErrors: true + checkExhaustiveness: true + # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]' + excludes: '' + +processors: + active: true + exclude: + - 'DetektProgressListener' + # - 'KtFileCountProcessor' + # - 'PackageCountProcessor' + # - 'ClassCountProcessor' + # - 'FunctionCountProcessor' + # - 'PropertyCountProcessor' + # - 'ProjectComplexityProcessor' + # - 'ProjectCognitiveComplexityProcessor' + # - 'ProjectLLOCProcessor' + # - 'ProjectCLOCProcessor' + # - 'ProjectLOCProcessor' + # - 'ProjectSLOCProcessor' + # - 'LicenseHeaderLoaderExtension' + +console-reports: + active: true + exclude: + - 'ProjectStatisticsReport' + - 'ComplexityReport' + - 'NotificationReport' + - 'FindingsReport' + - 'FileBasedFindingsReport' + # - 'LiteFindingsReport' + +output-reports: + active: true + exclude: + # - 'TxtOutputReport' + # - 'XmlOutputReport' + # - 'HtmlOutputReport' + # - 'MdOutputReport' + # - 'SarifOutputReport' + +comments: + active: true + AbsentOrWrongFileLicense: + active: false + licenseTemplateFile: 'license.template' + licenseTemplateIsRegex: true + CommentOverPrivateFunction: + active: true + CommentOverPrivateProperty: + active: true + DeprecatedBlockTag: + active: true + EndOfSentenceFormat: + active: true + endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)' + KDocReferencesNonPublicProperty: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + OutdatedDocumentation: + active: true + matchTypeParameters: true + matchDeclarationsOrder: true + allowParamOnConstructorProperties: true + UndocumentedPublicClass: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchInNestedClass: true + searchInInnerClass: true + searchInInnerObject: true + searchInInnerInterface: true + searchInProtectedClass: true + UndocumentedPublicFunction: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchProtectedFunction: true + UndocumentedPublicProperty: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchProtectedProperty: true + +complexity: + active: true + CognitiveComplexMethod: + active: true + threshold: 15 + ComplexCondition: + active: true + threshold: 4 + ComplexInterface: + active: true + threshold: 10 + includeStaticDeclarations: true + includePrivateDeclarations: true + ignoreOverloaded: true + CyclomaticComplexMethod: + active: true + threshold: 15 + ignoreSingleWhenExpression: true + ignoreSimpleWhenEntries: true + ignoreNestingFunctions: true + nestingFunctions: + - 'also' + - 'apply' + - 'forEach' + - 'isNotNull' + - 'ifNull' + - 'let' + - 'run' + - 'use' + - 'with' + LabeledExpression: + active: true + ignoredLabels: [] + LargeClass: + active: true + threshold: 600 + LongMethod: + active: true + threshold: 60 + LongParameterList: + active: true + functionThreshold: 6 + constructorThreshold: 7 + ignoreDefaultParameters: true + ignoreDataClasses: true + ignoreAnnotatedParameter: [] + MethodOverloading: + active: true + threshold: 6 + NamedArguments: + active: true + threshold: 3 + ignoreArgumentsMatchingNames: true + NestedBlockDepth: + active: true + threshold: 4 + NestedScopeFunctions: + active: true + threshold: 1 + functions: + - 'kotlin.apply' + - 'kotlin.run' + - 'kotlin.with' + - 'kotlin.let' + - 'kotlin.also' + ReplaceSafeCallChainWithRun: + active: true + StringLiteralDuplication: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + threshold: 3 + ignoreAnnotation: true + excludeStringsWithLessThan5Characters: true + ignoreStringsRegex: '$^' + TooManyFunctions: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + thresholdInFiles: 11 + thresholdInClasses: 11 + thresholdInInterfaces: 11 + thresholdInObjects: 11 + thresholdInEnums: 11 + ignoreDeprecated: true + ignorePrivate: true + ignoreOverridden: true + +coroutines: + active: true + GlobalCoroutineUsage: + active: true + InjectDispatcher: + active: true + dispatcherNames: + - 'IO' + - 'Default' + - 'Unconfined' + RedundantSuspendModifier: + active: true + SleepInsteadOfDelay: + active: true + SuspendFunSwallowedCancellation: + active: true + SuspendFunWithCoroutineScopeReceiver: + active: true + SuspendFunWithFlowReturnType: + active: true + +empty-blocks: + active: true + EmptyCatchBlock: + active: true + allowedExceptionNameRegex: '_|(ignore|expected).*' + EmptyClassBlock: + active: true + EmptyDefaultConstructor: + active: true + EmptyDoWhileBlock: + active: true + EmptyElseBlock: + active: true + EmptyFinallyBlock: + active: true + EmptyForBlock: + active: true + EmptyFunctionBlock: + active: true + ignoreOverridden: true + EmptyIfBlock: + active: true + EmptyInitBlock: + active: true + EmptyKtFile: + active: true + EmptySecondaryConstructor: + active: true + EmptyTryBlock: + active: true + EmptyWhenBlock: + active: true + EmptyWhileBlock: + active: true + +exceptions: + active: true + ExceptionRaisedInUnexpectedLocation: + active: true + methodNames: + - 'equals' + - 'finalize' + - 'hashCode' + - 'toString' + InstanceOfCheckForException: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + NotImplementedDeclaration: + active: true + ObjectExtendsThrowable: + active: true + PrintStackTrace: + active: true + RethrowCaughtException: + active: true + ReturnFromFinally: + active: true + ignoreLabeled: true + SwallowedException: + active: true + ignoredExceptionTypes: + - 'InterruptedException' + - 'MalformedURLException' + - 'NumberFormatException' + - 'ParseException' + allowedExceptionNameRegex: '_|(ignore|expected).*' + ThrowingExceptionFromFinally: + active: true + ThrowingExceptionInMain: + active: true + ThrowingExceptionsWithoutMessageOrCause: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + exceptions: + - 'ArrayIndexOutOfBoundsException' + - 'Exception' + - 'IllegalArgumentException' + - 'IllegalMonitorStateException' + - 'IllegalStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + ThrowingNewInstanceOfSameException: + active: true + TooGenericExceptionCaught: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + exceptionNames: + - 'ArrayIndexOutOfBoundsException' + - 'Error' + - 'Exception' + - 'IllegalMonitorStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + allowedExceptionNameRegex: '_|(ignore|expected).*' + TooGenericExceptionThrown: + active: true + exceptionNames: + - 'Error' + - 'Exception' + - 'RuntimeException' + - 'Throwable' + +naming: + active: true + BooleanPropertyNaming: + active: true + allowedPattern: '^(is|has|are)' + ClassNaming: + active: true + classPattern: '[A-Z][a-zA-Z0-9]*' + ConstructorParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + privateParameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + EnumNaming: + active: true + enumEntryPattern: '[A-Z][_a-zA-Z0-9]*' + ForbiddenClassName: + active: true + forbiddenName: [] + FunctionMaxLength: + active: true + maximumFunctionNameLength: 30 + FunctionMinLength: + active: true + minimumFunctionNameLength: 3 + FunctionNaming: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + functionPattern: '[a-z][a-zA-Z0-9]*' + excludeClassPattern: '$^' + FunctionParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + InvalidPackageDeclaration: + active: true + rootPackage: '' + requireRootInDeclaration: false + LambdaParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*|_' + MatchingDeclarationName: + active: true + mustBeFirst: true + MemberNameEqualsClassName: + active: true + ignoreOverridden: true + NoNameShadowing: + active: true + NonBooleanPropertyPrefixedWithIs: + active: true + ObjectPropertyNaming: + active: true + constantPattern: '[A-Za-z][_A-Za-z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' + PackageNaming: + active: true + packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*' + TopLevelPropertyNaming: + active: true + constantPattern: '[A-Z][_A-Z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' + VariableMaxLength: + active: true + maximumVariableNameLength: 64 + VariableMinLength: + active: true + minimumVariableNameLength: 1 + VariableNaming: + active: true + variablePattern: '[a-z][A-Za-z0-9]*' + privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + +performance: + active: true + ArrayPrimitive: + active: true + CouldBeSequence: + active: true + threshold: 3 + ForEachOnRange: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + SpreadOperator: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + UnnecessaryPartOfBinaryExpression: + active: true + UnnecessaryTemporaryInstantiation: + active: true + +potential-bugs: + active: true + AvoidReferentialEquality: + active: true + forbiddenTypePatterns: + - 'kotlin.String' + CastNullableToNonNullableType: + active: true + CastToNullableType: + active: true + Deprecation: + active: true + DontDowncastCollectionTypes: + active: true + DoubleMutabilityForCollection: + active: true + mutableTypes: + - 'kotlin.collections.MutableList' + - 'kotlin.collections.MutableMap' + - 'kotlin.collections.MutableSet' + - 'java.util.ArrayList' + - 'java.util.LinkedHashSet' + - 'java.util.HashSet' + - 'java.util.LinkedHashMap' + - 'java.util.HashMap' + ElseCaseInsteadOfExhaustiveWhen: + active: true + ignoredSubjectTypes: [] + EqualsAlwaysReturnsTrueOrFalse: + active: true + EqualsWithHashCodeExist: + active: true + ExitOutsideMain: + active: true + ExplicitGarbageCollectionCall: + active: true + HasPlatformType: + active: true + IgnoredReturnValue: + active: true + restrictToConfig: true + returnValueAnnotations: + - 'CheckResult' + - '*.CheckResult' + - 'CheckReturnValue' + - '*.CheckReturnValue' + ignoreReturnValueAnnotations: + - 'CanIgnoreReturnValue' + - '*.CanIgnoreReturnValue' + returnValueTypes: + - 'kotlin.sequences.Sequence' + - 'kotlinx.coroutines.flow.*Flow' + - 'java.util.stream.*Stream' + ignoreFunctionCall: [] + ImplicitDefaultLocale: + active: true + ImplicitUnitReturnType: + active: true + allowExplicitReturnType: true + InvalidRange: + active: true + IteratorHasNextCallsNextMethod: + active: true + IteratorNotThrowingNoSuchElementException: + active: true + LateinitUsage: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + ignoreOnClassesPattern: '' + MapGetWithNotNullAssertionOperator: + active: true + MissingPackageDeclaration: + active: true + excludes: ['**/*.kts'] + NullCheckOnMutableProperty: + active: true + NullableToStringCall: + active: true + PropertyUsedBeforeDeclaration: + active: true + UnconditionalJumpStatementInLoop: + active: true + UnnecessaryNotNullCheck: + active: true + UnnecessaryNotNullOperator: + active: true + UnnecessarySafeCall: + active: true + UnreachableCatchBlock: + active: true + UnreachableCode: + active: true + UnsafeCallOnNullableType: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + UnsafeCast: + active: true + UnusedUnaryOperator: + active: true + UselessPostfixExpression: + active: true + WrongEqualsTypeParameter: + active: true + +style: + active: true + AlsoCouldBeApply: + active: true + BracesOnIfStatements: + active: true + singleLine: 'never' + multiLine: 'always' + BracesOnWhenStatements: + active: true + singleLine: 'necessary' + multiLine: 'consistent' + CanBeNonNullable: + active: true + CascadingCallWrapping: + active: true + includeElvis: true + ClassOrdering: + active: true + CollapsibleIfStatements: + active: true + DataClassContainsFunctions: + active: true + conversionFunctionPrefix: + - 'to' + allowOperators: true + DataClassShouldBeImmutable: + active: true + DestructuringDeclarationWithTooManyEntries: + active: true + maxDestructuringEntries: 3 + DoubleNegativeLambda: + active: true + negativeFunctions: + - reason: 'Use `takeIf` instead.' + value: 'takeUnless' + - reason: 'Use `all` instead.' + value: 'none' + negativeFunctionNameParts: + - 'not' + - 'non' + EqualsNullCall: + active: true + EqualsOnSignatureLine: + active: true + ExplicitCollectionElementAccessMethod: + active: true + ExplicitItLambdaParameter: + active: true + ExpressionBodySyntax: + active: true + includeLineWrapping: true + ForbiddenAnnotation: + active: true + annotations: + - reason: 'it is a java annotation. Use `Suppress` instead.' + value: 'java.lang.SuppressWarnings' + - reason: 'it is a java annotation. Use `kotlin.Deprecated` instead.' + value: 'java.lang.Deprecated' + - reason: 'it is a java annotation. Use `kotlin.annotation.MustBeDocumented` instead.' + value: 'java.lang.annotation.Documented' + - reason: 'it is a java annotation. Use `kotlin.annotation.Target` instead.' + value: 'java.lang.annotation.Target' + - reason: 'it is a java annotation. Use `kotlin.annotation.Retention` instead.' + value: 'java.lang.annotation.Retention' + - reason: 'it is a java annotation. Use `kotlin.annotation.Repeatable` instead.' + value: 'java.lang.annotation.Repeatable' + - reason: 'Kotlin does not support @Inherited annotation, see https://youtrack.jetbrains.com/issue/KT-22265' + value: 'java.lang.annotation.Inherited' + ForbiddenComment: + active: true + comments: + - reason: 'Forbidden FIXME todo marker in comment, please fix the problem.' + value: 'FIXME:' + - reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.' + value: 'STOPSHIP:' + - reason: 'Forbidden TODO todo marker in comment, please do the changes.' + value: 'TODO:' + allowedPatterns: '' + ForbiddenImport: + active: true + imports: [] + forbiddenPatterns: '' + ForbiddenMethodCall: + active: true + methods: + - reason: 'print does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.print' + - reason: 'println does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.println' + ForbiddenSuppress: + active: true + rules: [] + ForbiddenVoid: + active: true + ignoreOverridden: true + ignoreUsageInGenerics: true + FunctionOnlyReturningConstant: + active: true + ignoreOverridableFunction: true + ignoreActualFunction: true + excludedFunctions: [] + LoopWithTooManyJumpStatements: + active: true + maxJumpCount: 1 + MagicNumber: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts'] + ignoreNumbers: + - '-1' + - '0' + - '1' + - '2' + ignoreHashCodeFunction: true + ignorePropertyDeclaration: true + ignoreLocalVariableDeclaration: true + ignoreConstantDeclaration: true + ignoreCompanionObjectPropertyDeclaration: true + ignoreAnnotation: true + ignoreNamedArgument: true + ignoreEnums: true + ignoreRanges: true + ignoreExtensionFunctions: true + MandatoryBracesLoops: + active: true + MaxChainedCallsOnSameLine: + active: true + maxChainedCalls: 5 + MaxLineLength: + active: true + maxLineLength: 120 + excludePackageStatements: true + excludeImportStatements: true + excludeCommentStatements: true + excludeRawStrings: true + MayBeConst: + active: true + ModifierOrder: + active: true + MultilineLambdaItParameter: + active: true + MultilineRawStringIndentation: + active: false + indentSize: 4 + trimmingMethods: + - 'trimIndent' + - 'trimMargin' + NestedClassesVisibility: + active: true + NewLineAtEndOfFile: + active: true + NoTabs: + active: true + NullableBooleanCheck: + active: true + ObjectLiteralToLambda: + active: true + OptionalAbstractKeyword: + active: true + OptionalUnit: + active: true + PreferToOverPairSyntax: + active: true + ProtectedMemberInFinalClass: + active: true + RedundantExplicitType: + active: true + RedundantHigherOrderMapUsage: + active: true + RedundantVisibilityModifierRule: + active: true + ReturnCount: + active: true + max: 2 + excludedFunctions: + - 'equals' + excludeLabeled: true + excludeReturnFromLambda: true + excludeGuardClauses: true + SafeCast: + active: true + SerialVersionUIDInSerializableClass: + active: true + SpacingBetweenPackageAndImports: + active: true + StringShouldBeRawString: + active: true + maxEscapedCharacterCount: 2 + ignoredCharacters: [] + ThrowsCount: + active: true + max: 2 + excludeGuardClauses: true + TrailingWhitespace: + active: true + TrimMultilineRawString: + active: false + trimmingMethods: + - 'trimIndent' + - 'trimMargin' + UnderscoresInNumericLiterals: + active: true + acceptableLength: 4 + allowNonStandardGrouping: true + UnnecessaryAbstractClass: + active: true + UnnecessaryAnnotationUseSiteTarget: + active: true + UnnecessaryApply: + active: true + UnnecessaryBackticks: + active: true + UnnecessaryBracesAroundTrailingLambda: + active: true + UnnecessaryFilter: + active: true + UnnecessaryInheritance: + active: true + UnnecessaryInnerClass: + active: true + UnnecessaryLet: + active: true + UnnecessaryParentheses: + active: true + allowForUnclearPrecedence: true + UntilInsteadOfRangeTo: + active: true + UnusedImports: + active: true + UnusedParameter: + active: true + allowedNames: 'ignored|expected' + UnusedPrivateClass: + active: true + UnusedPrivateMember: + active: true + allowedNames: '' + UnusedPrivateProperty: + active: true + allowedNames: '_|ignored|expected|serialVersionUID' + UseAnyOrNoneInsteadOfFind: + active: true + UseArrayLiteralsInAnnotations: + active: true + UseCheckNotNull: + active: true + UseCheckOrError: + active: true + UseDataClass: + active: true + allowVars: true + UseEmptyCounterpart: + active: true + UseIfEmptyOrIfBlank: + active: true + UseIfInsteadOfWhen: + active: true + ignoreWhenContainingVariableDeclaration: true + UseIsNullOrEmpty: + active: true + UseLet: + active: true + UseOrEmpty: + active: true + UseRequire: + active: true + UseRequireNotNull: + active: true + UseSumOfInsteadOfFlatMapSize: + active: true + UselessCallOnNotNull: + active: true + UtilityClassWithPublicConstructor: + active: true + VarCouldBeVal: + active: true + ignoreLateinitVar: true + WildcardImport: + active: true + excludeImports: + - 'java.util.*' diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..2f45576 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,27 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +-keepattributes SourceFile,LineNumberTable +-dontobfuscate + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile + +# This is generated automatically by the Android Gradle plugin. +-dontwarn com.google.errorprone.annotations.Immutable +-dontwarn javax.annotation.concurrent.GuardedBy +-dontwarn javax.annotation.Nullable \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..230adaa --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 0000000..183b8ed Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/com/rine/upnpdiscovery/UPnPDevice.kt b/app/src/main/java/com/rine/upnpdiscovery/UPnPDevice.kt new file mode 100644 index 0000000..2ce6696 --- /dev/null +++ b/app/src/main/java/com/rine/upnpdiscovery/UPnPDevice.kt @@ -0,0 +1,110 @@ +package com.rine.upnpdiscovery + +import android.util.Log +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlPullParserException +import org.xmlpull.v1.XmlPullParserFactory +import java.io.ByteArrayInputStream + +class UPnPDevice internal constructor(val hostAddress: String, header: String) { + internal val location: String + val server: String + + // XML content + private var descriptionXML: String = "" + + // From description XML + var friendlyName: String = "" + private var deviceType: String = "" + private var presentationURL: String = "" + private var serialNumber: String = "" + private var modelName: String = "" + private var modelNumber: String = "" + private var modelURL: String = "" + private var manufacturer: String = "" + private var manufacturerURL: String = "" + private var udn: String = "" + private var urlBase: String = "" + + init { + location = parseHeader(header, "LOCATION: ") + server = parseHeader(header, "SERVER: ") + } + + internal fun update(xml: String) { + descriptionXML = xml + try { + xmlParse(xml) + } catch (e: XmlPullParserException) { + Log.w(UPnPDiscovery.TAG, e.toString()) + } + } + + override fun toString(): String = + "FriendlyName: " + friendlyName + LINE_END + + "ModelName: " + modelName + LINE_END + + "HostAddress: " + hostAddress + LINE_END + + "Location: " + location + LINE_END + + "DeviceType: " + deviceType + LINE_END + + "PresentationURL: " + presentationURL + LINE_END + + "SerialNumber: " + serialNumber + LINE_END + + "ModelURL: " + modelURL + LINE_END + + "ModelNumber: " + modelNumber + LINE_END + + "Manufacturer: " + manufacturer + LINE_END + + "ManufacturerURL: " + manufacturerURL + LINE_END + + "UDN: " + udn + LINE_END + + "URLBase: " + urlBase + + private fun parseHeader( + mSearchAnswer: String, + whatSearch: String, + ): String { + var result = "" + var searchLinePos = mSearchAnswer.indexOf(whatSearch) + if (searchLinePos != -1) { + searchLinePos += whatSearch.length + val locColon = mSearchAnswer.indexOf(LINE_END, searchLinePos) + result = mSearchAnswer.substring(searchLinePos, locColon) + } + return result + } + + private fun readText(parser: XmlPullParser): String { + var result = "" + if (parser.next() == XmlPullParser.TEXT) { + result = parser.text + parser.nextTag() + } + return result + } + + private fun xmlParse(xml: String) { + val xmlFactoryObject = XmlPullParserFactory.newInstance() + val parser = xmlFactoryObject.newPullParser() + parser.setInput(ByteArrayInputStream(xml.toByteArray(Charsets.UTF_8)), null) + var event = parser.eventType + while (event != XmlPullParser.END_DOCUMENT) { + val name = parser.name + if (event == XmlPullParser.START_TAG) { + when (name) { + "friendlyName" -> friendlyName = readText(parser) + "deviceType" -> deviceType = readText(parser) + "presentationURL" -> presentationURL = readText(parser) + "serialNumber" -> serialNumber = readText(parser) + "modelName" -> modelName = readText(parser) + "modelNumber" -> modelNumber = readText(parser) + "modelURL" -> modelURL = readText(parser) + "manufacturer" -> manufacturer = readText(parser) + "manufacturerURL" -> manufacturerURL = readText(parser) + "UDN" -> udn = readText(parser) + "URLBase" -> urlBase = readText(parser) + } + } + event = parser.next() + } + } + + companion object { + private const val LINE_END = "\r\n" + } +} diff --git a/app/src/main/java/com/rine/upnpdiscovery/UPnPDiscovery.kt b/app/src/main/java/com/rine/upnpdiscovery/UPnPDiscovery.kt new file mode 100644 index 0000000..deef452 --- /dev/null +++ b/app/src/main/java/com/rine/upnpdiscovery/UPnPDiscovery.kt @@ -0,0 +1,178 @@ +package com.rine.upnpdiscovery + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.net.wifi.WifiManager +import android.os.AsyncTask +import android.util.Log +import com.android.volley.Request +import com.android.volley.toolbox.StringRequest +import com.android.volley.toolbox.Volley +import java.io.IOException +import java.net.DatagramPacket +import java.net.DatagramSocket +import java.net.InetAddress +import java.net.InetSocketAddress + +@Suppress("MagicNumber") +class UPnPDiscovery : AsyncTask { + private val devices = HashSet() + + @SuppressLint("StaticFieldLeak") + private val mContext: Context + private var mThreadsCount: Int = 0 + private val mCustomQuery: String + private val mInternetAddress: String + private val mPort: Int + + private val mListener: OnDiscoveryListener + + interface OnDiscoveryListener { + fun onStart() + + fun onFoundNewDevice(device: UPnPDevice) + + fun onFinish(devices: HashSet) + + fun onError(e: Exception) + } + + private constructor(activity: Activity, listener: OnDiscoveryListener) { + mContext = activity.applicationContext + mListener = listener + mThreadsCount = 0 + mCustomQuery = DEFAULT_QUERY + mInternetAddress = DEFAULT_ADDRESS + mPort = 1900 + } + + private constructor( + activity: Activity, + listener: OnDiscoveryListener, + customQuery: String, + address: String, + port: Int, + ) { + mContext = activity.applicationContext + mListener = listener + mThreadsCount = 0 + mCustomQuery = customQuery + mInternetAddress = address + mPort = port + } + + @Deprecated("Deprecated in Java") + override fun doInBackground(vararg p0: Activity?): Void? { + mListener.onStart() + val wifi = mContext.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + val lock = wifi.createMulticastLock("The Lock") + lock.acquire() + var socket: DatagramSocket? = null + try { + val group = InetAddress.getByName(mInternetAddress) + val port = mPort + val query = mCustomQuery + socket = DatagramSocket(null) + socket.reuseAddress = true + socket.broadcast = true + socket.bind(InetSocketAddress(port)) + + val datagramPacketRequest = DatagramPacket(query.toByteArray(), query.length, group, port) + socket.send(datagramPacketRequest) + + val time = System.currentTimeMillis() + var curTime = System.currentTimeMillis() + + while (curTime - time < 1000) { + val datagramPacket = DatagramPacket(ByteArray(1024), 1024) + socket.receive(datagramPacket) + val response = String(datagramPacket.data, 0, datagramPacket.length) + if (response.substring(0, 12).uppercase() == "HTTP/1.1 200") { + val device = UPnPDevice(datagramPacket.address.hostAddress ?: continue, response) + mThreadsCount++ + getData(device.location, device) + } + curTime = System.currentTimeMillis() + } + } catch (e: IOException) { + mListener.onError(e) + } finally { + socket?.close() + } + lock.release() + return null + } + + private fun getData( + url: String, + device: UPnPDevice, + ) { + val stringRequest = + StringRequest( + Request.Method.GET, + url, + { response -> + device.update(response) + mListener.onFoundNewDevice(device) + devices.add(device) + mThreadsCount-- + if (mThreadsCount == 0) { + mListener.onFinish(devices) + } + }, + { + mThreadsCount-- + Log.e(TAG, "URL: $url get content error!") + }, + ) + stringRequest.tag = TAG + "SSDP description request" + Volley.newRequestQueue(mContext).add(stringRequest) + } + + companion object { + internal val TAG: String = UPnPDiscovery::class.java.simpleName + + private const val DISCOVER_TIMEOUT = 1500 + private const val LINE_END = "\r\n" + private const val DEFAULT_QUERY = + "M-SEARCH * HTTP/1.1" + LINE_END + + "HOST: 239.255.255.250:1900" + LINE_END + + "MAN: \"ssdp:discover\"" + LINE_END + + "MX: 1" + LINE_END + + "ST: ssdp:all" + LINE_END + + LINE_END + private const val DEFAULT_ADDRESS = "239.255.255.250" + + fun discoveryDevices( + activity: Activity, + listener: OnDiscoveryListener, + ): Boolean { + val discover = UPnPDiscovery(activity, listener) + discover.execute() + return try { + Thread.sleep(DISCOVER_TIMEOUT.toLong()) + true + } catch (e: InterruptedException) { + false + } + } + + fun discoveryDevices( + activity: Activity, + listener: OnDiscoveryListener, + customQuery: String, + address: String, + port: Int, + ): Boolean { + val discover = UPnPDiscovery(activity, listener, customQuery, address, port) + discover.execute() + return try { + Thread.sleep(DISCOVER_TIMEOUT.toLong()) + true + } catch (e: InterruptedException) { + false + } + } + } +} diff --git a/app/src/main/java/io/github/domi04151309/home/Application.kt b/app/src/main/java/io/github/domi04151309/home/Application.kt new file mode 100644 index 0000000..a57db64 --- /dev/null +++ b/app/src/main/java/io/github/domi04151309/home/Application.kt @@ -0,0 +1,10 @@ +package io.github.domi04151309.home + +import com.google.android.material.color.DynamicColors + +class Application : android.app.Application() { + override fun onCreate() { + super.onCreate() + DynamicColors.applyToActivitiesIfAvailable(this) + } +} diff --git a/app/src/main/java/io/github/domi04151309/home/activities/AboutActivity.kt b/app/src/main/java/io/github/domi04151309/home/activities/AboutActivity.kt new file mode 100644 index 0000000..4d91266 --- /dev/null +++ b/app/src/main/java/io/github/domi04151309/home/activities/AboutActivity.kt @@ -0,0 +1,112 @@ +package io.github.domi04151309.home.activities + +import android.content.Intent +import android.os.Bundle +import androidx.core.net.toUri +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import io.github.domi04151309.home.BuildConfig +import io.github.domi04151309.home.R + +class AboutActivity : BaseActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_settings) + supportFragmentManager + .beginTransaction() + .replace(R.id.settings, GeneralPreferenceFragment()) + .commit() + } + + class GeneralPreferenceFragment : PreferenceFragmentCompat() { + @Suppress("SameReturnValue") + private fun onIconsClicked(): Boolean { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.about_icons) + .setItems(resources.getStringArray(R.array.about_icons_array)) { _, which -> + startActivity( + Intent( + Intent.ACTION_VIEW, + when (which) { + 0 -> "https://icons8.com/" + 1 -> "https://fonts.google.com/icons?selected=Material+Icons" + else -> "about:blank" + }.toUri(), + ), + ) + } + .show() + return true + } + + @Suppress("SameReturnValue") + private fun onExternalClicked(link: String): Boolean { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.about_privacy) + .setMessage(R.string.about_privacy_desc) + .setPositiveButton(android.R.string.ok) { _, _ -> + startActivity( + Intent( + Intent.ACTION_VIEW, + link.toUri(), + ), + ) + } + .setNegativeButton(android.R.string.cancel) { _, _ -> } + .setNeutralButton(R.string.about_privacy_policy) { _, _ -> + startActivity( + Intent( + Intent.ACTION_VIEW, + "https://docs.github.com/en/github/site-policy/github-privacy-statement".toUri(), + ), + ) + } + .show() + return true + } + + override fun onCreatePreferences( + savedInstanceState: Bundle?, + rootKey: String?, + ) { + addPreferencesFromResource(R.xml.pref_about) + findPreference("app_version")?.apply { + summary = + requireContext().getString( + R.string.about_app_version_desc, + BuildConfig.VERSION_NAME, + BuildConfig.VERSION_CODE, + ) + setOnPreferenceClickListener { + onExternalClicked("$REPOSITORY_URL/releases") + } + } + findPreference("github")?.apply { + summary = REPOSITORY_URL + setOnPreferenceClickListener { + onExternalClicked(REPOSITORY_URL) + } + } + findPreference("license")?.setOnPreferenceClickListener { + onExternalClicked("$REPOSITORY_URL/blob/$BRANCH/LICENSE") + } + findPreference("icons")?.setOnPreferenceClickListener { + onIconsClicked() + } + findPreference("contributors")?.setOnPreferenceClickListener { + onExternalClicked("$REPOSITORY_URL/graphs/contributors") + } + findPreference("libraries")?.setOnPreferenceClickListener { + startActivity(Intent(requireContext(), LibraryActivity::class.java)) + true + } + } + } + + companion object { + private const val REPOSITORY: String = "Domi04151309/HomeApp" + private const val BRANCH: String = "main" + private const val REPOSITORY_URL: String = "https://github.com/$REPOSITORY" + } +} diff --git a/app/src/main/java/io/github/domi04151309/home/activities/BaseActivity.kt b/app/src/main/java/io/github/domi04151309/home/activities/BaseActivity.kt new file mode 100644 index 0000000..f059af5 --- /dev/null +++ b/app/src/main/java/io/github/domi04151309/home/activities/BaseActivity.kt @@ -0,0 +1,22 @@ +package io.github.domi04151309.home.activities + +import android.content.res.Configuration +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import com.google.android.material.elevation.SurfaceColors +import io.github.domi04151309.home.R + +abstract class BaseActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (resources.configuration.uiMode.and( + Configuration.UI_MODE_NIGHT_MASK, + ) != Configuration.UI_MODE_NIGHT_YES + ) { + setTheme(R.style.LightStatusBarOverlay) + } + val color = SurfaceColors.SURFACE_2.getColor(this) + window.statusBarColor = color + window.navigationBarColor = color + } +} diff --git a/app/src/main/java/io/github/domi04151309/home/activities/ControlInfoActivity.kt b/app/src/main/java/io/github/domi04151309/home/activities/ControlInfoActivity.kt new file mode 100644 index 0000000..b1b390c --- /dev/null +++ b/app/src/main/java/io/github/domi04151309/home/activities/ControlInfoActivity.kt @@ -0,0 +1,70 @@ +package io.github.domi04151309.home.activities + +import android.os.Bundle +import com.google.android.material.elevation.SurfaceColors +import io.github.domi04151309.home.R +import io.github.domi04151309.home.data.DeviceItem +import io.github.domi04151309.home.fragments.ControlInfoFragment +import io.github.domi04151309.home.fragments.HueColorFragment +import io.github.domi04151309.home.helpers.Devices +import io.github.domi04151309.home.helpers.Global +import io.github.domi04151309.home.interfaces.HueRoomInterface + +class ControlInfoActivity : BaseActivity() { + private var hueRoom: ControlInfoActivityHueRoom? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_settings) + + window.statusBarColor = SurfaceColors.SURFACE_0.getColor(this) + + val id = intent.getStringExtra(EXTRA_ID) + if (id === null) { + return + } + + val device = Devices(this).getDeviceById(id.substring(0, id.indexOf('@'))) + + if (device.mode == Global.HUE_API) { + showHueFragment(id, device) + return + } + + supportFragmentManager + .beginTransaction() + .replace(R.id.settings, ControlInfoFragment(device, intent.getStringExtra(EXTRA_TITLE) ?: "")) + .commit() + } + + override fun onStart() { + super.onStart() + hueRoom?.onStart() + } + + override fun onStop() { + super.onStop() + hueRoom?.onStop() + } + + override fun onDestroy() { + super.onDestroy() + hueRoom?.onDestroy() + } + + private fun showHueFragment( + id: String, + device: DeviceItem, + ) { + hueRoom = ControlInfoActivityHueRoom(this, device, id.substring(id.indexOf('@') + 1)) + supportFragmentManager + .beginTransaction() + .replace(R.id.settings, HueColorFragment(hueRoom as HueRoomInterface)) + .commit() + } + + companion object { + const val EXTRA_ID: String = "EXTRA_ID" + const val EXTRA_TITLE: String = "EXTRA_TITLE" + } +} diff --git a/app/src/main/java/io/github/domi04151309/home/activities/ControlInfoActivityHueRoom.kt b/app/src/main/java/io/github/domi04151309/home/activities/ControlInfoActivityHueRoom.kt new file mode 100644 index 0000000..1a5ba4c --- /dev/null +++ b/app/src/main/java/io/github/domi04151309/home/activities/ControlInfoActivityHueRoom.kt @@ -0,0 +1,97 @@ +package io.github.domi04151309.home.activities + +import android.content.Context +import com.android.volley.Request +import com.android.volley.toolbox.JsonObjectRequest +import com.android.volley.toolbox.Volley +import io.github.domi04151309.home.api.HueAPI +import io.github.domi04151309.home.data.DeviceItem +import io.github.domi04151309.home.data.LightStates +import io.github.domi04151309.home.helpers.HueLightListener +import io.github.domi04151309.home.helpers.HueUtils.MIN_COLOR_TEMPERATURE +import io.github.domi04151309.home.helpers.UpdateHandler +import io.github.domi04151309.home.interfaces.HueRoomInterface +import org.json.JSONArray + +class ControlInfoActivityHueRoom : HueRoomInterface { + override var lights: JSONArray? + override var lampData: HueLightListener + override var id: String + override var device: DeviceItem + override var addressPrefix: String + override var canReceiveRequest: Boolean + + private var updateDataRequest: JsonObjectRequest? = null + private var updateHandler: UpdateHandler = UpdateHandler() + + constructor(context: Context, device: DeviceItem, id: String) { + val hueApi = HueAPI(context, device.id) + val queue = Volley.newRequestQueue(context) + + this.lights = null + this.lampData = HueLightListener() + this.id = id + this.device = device + this.addressPrefix = device.address + "api/" + hueApi.getUsername() + this.canReceiveRequest = false + + updateDataRequest = getUpdateRequest() + updateHandler.setUpdateFunction { + if (canReceiveRequest && hueApi.readyForRequest) { + queue.add(updateDataRequest) + } + } + + onStart() + } + + fun onStart() { + canReceiveRequest = true + } + + fun onStop() { + canReceiveRequest = false + } + + fun onDestroy() { + updateHandler.stop() + } + + override fun onColorChanged(color: Int) { + // Do nothing. + } + + private fun getUpdateRequest() = + JsonObjectRequest( + Request.Method.GET, + "$addressPrefix/groups/$id", + null, + { response -> + lights = response.getJSONArray("lights") + val action = response.getJSONObject("action") + val light = LightStates.Light() + + light.ct = + if (action.has("ct")) { + action.getInt("ct") - MIN_COLOR_TEMPERATURE + } else { + -1 + } + + if (action.has("hue") && action.has("sat")) { + light.hue = action.getInt("hue") + light.sat = action.getInt("sat") + } else { + light.hue = -1 + light.sat = -1 + } + + light.on = response.getJSONObject("state").getBoolean("any_on") + + lampData.state = light + }, + { + canReceiveRequest = false + }, + ) +} diff --git a/app/src/main/java/io/github/domi04151309/home/activities/DeviceInfoActivity.kt b/app/src/main/java/io/github/domi04151309/home/activities/DeviceInfoActivity.kt new file mode 100644 index 0000000..3b2f045 --- /dev/null +++ b/app/src/main/java/io/github/domi04151309/home/activities/DeviceInfoActivity.kt @@ -0,0 +1,238 @@ +package io.github.domi04151309.home.activities + +import android.os.Bundle +import android.view.View +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.android.volley.Request +import com.android.volley.RequestQueue +import com.android.volley.toolbox.JsonObjectRequest +import com.android.volley.toolbox.Volley +import io.github.domi04151309.home.R +import io.github.domi04151309.home.adapters.SimpleListAdapter +import io.github.domi04151309.home.api.HueAPI +import io.github.domi04151309.home.api.HueAPIParser +import io.github.domi04151309.home.data.DeviceItem +import io.github.domi04151309.home.data.SimpleListItem +import io.github.domi04151309.home.helpers.Devices +import io.github.domi04151309.home.helpers.Global +import io.github.domi04151309.home.interfaces.RecyclerViewHelperInterface +import org.json.JSONObject +import java.util.Locale +import java.util.concurrent.TimeUnit + +@Suppress("TooManyFunctions") +class DeviceInfoActivity : BaseActivity(), RecyclerViewHelperInterface { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_devices) + + val devices = Devices(this) + val id = intent.getStringExtra(Devices.INTENT_EXTRA_DEVICE) ?: "" + if (!devices.idExists(id)) { + finish() + return + } + + val device = devices.getDeviceById(id) + val queue = Volley.newRequestQueue(this) + val recyclerView = findViewById(R.id.recyclerView) + val items = mutableListOf() + recyclerView.layoutManager = LinearLayoutManager(this) + items.add( + SimpleListItem( + device.name, + device.address, + icon = device.iconId, + ), + ) + + when (device.mode) { + Global.HUE_API -> showHueInfo(device, queue, items, recyclerView) + Global.SHELLY_GEN_2 -> showShelly2Info(device, queue, items, recyclerView) + Global.SHELLY_GEN_3 -> showShelly2Info(device, queue, items, recyclerView) + } + } + + override fun onItemClicked( + view: View, + position: Int, + ) { + // Do nothing. + } + + private fun boolToString(bool: Boolean): String = + resources.getString( + if (bool) R.string.str_on else R.string.str_off, + ) + + @Suppress("MagicNumber") + private fun rssiToPercent(rssi: Int): Int = + if (rssi <= -100) { + 0 + } else if (rssi >= -50) { + 100 + } else { + 2 * (rssi + 100) + } + + private fun formatUptime(uptime: Long) = + String.format( + Locale.getDefault(), + "%02d:%02d:%02d", + TimeUnit.SECONDS.toHours(uptime), + TimeUnit.SECONDS.toMinutes(uptime) - + TimeUnit.HOURS.toMinutes( + TimeUnit.SECONDS.toHours( + uptime, + ), + ), + TimeUnit.SECONDS.toSeconds(uptime) - + TimeUnit.MINUTES.toSeconds( + TimeUnit.SECONDS.toMinutes( + uptime, + ), + ), + ) + + private fun showHueInfo( + device: DeviceItem, + queue: RequestQueue, + items: MutableList, + recyclerView: RecyclerView, + ) { + val hueAPI = HueAPI(this, device.id) + val addressPrefix = device.address + "api/" + hueAPI.getUsername() + + queue.add( + JsonObjectRequest( + Request.Method.GET, + "$addressPrefix/config", + null, + { response -> + items.addAll(HueAPIParser.parseHueConfig(resources, response)) + + queue.add( + JsonObjectRequest( + Request.Method.GET, + "$addressPrefix/sensors", + null, + { innerResponse -> + items.addAll(HueAPIParser.parseHueSensors(resources, innerResponse)) + + queue.add( + JsonObjectRequest( + Request.Method.GET, + "$addressPrefix/lights", + null, + { innerInnerResponse -> + items.addAll(HueAPIParser.parseHueLights(resources, innerInnerResponse)) + recyclerView.adapter = SimpleListAdapter(items, this) + }, + { }, + ), + ) + }, + { }, + ), + ) + }, + { }, + ), + ) + } + + @Suppress("LongMethod") + private fun parseShelly2Info(response: JSONObject) = + listOf( + SimpleListItem(summary = resources.getString(R.string.device_config_info_status)), + SimpleListItem( + (response.optJSONObject("wifi") ?: JSONObject()).run { + optString("ssid") + " (" + rssiToPercent(optInt("rssi")) + " %)" + }, + resources.getString(R.string.shelly_wifi), + icon = R.drawable.ic_about_info, + ), + SimpleListItem( + boolToString( + ( + response.optJSONObject("mqtt") + ?: JSONObject() + ).optBoolean("connected"), + ), + resources.getString(R.string.shelly_mqtt), + icon = R.drawable.ic_about_info, + ), + SimpleListItem( + boolToString( + ( + response.optJSONObject("cloud") + ?: JSONObject() + ).optBoolean("connected"), + ), + resources.getString(R.string.shelly_cloud), + icon = R.drawable.ic_about_info, + ), + SimpleListItem( + formatUptime((response.optJSONObject("sys") ?: JSONObject()).optLong("uptime")), + resources.getString(R.string.shelly_uptime), + icon = R.drawable.ic_about_info, + ), + SimpleListItem( + (response.optJSONObject("sys") ?: JSONObject()).run { + "${(optInt("fs_free") / optInt("fs_size").toFloat() * TO_PERCENT).toInt()} %" + }, + resources.getString(R.string.shelly_storage), + icon = R.drawable.ic_about_info, + ), + SimpleListItem( + (response.optJSONObject("sys") ?: JSONObject()).run { + "${(optInt("ram_free") / optInt("ram_size").toFloat() * TO_PERCENT).toInt()} %" + }, + resources.getString(R.string.shelly_ram), + icon = R.drawable.ic_about_info, + ), + SimpleListItem( + resources.getString( + if (( + ( + response.optJSONObject("sys") + ?: JSONObject() + ).optJSONObject("available_updates") + ?: JSONObject() + ).has("stable") + ) { + R.string.str_yes + } else { + R.string.str_no + }, + ), + resources.getString(R.string.shelly_update), + icon = R.drawable.ic_about_info, + ), + ) + + private fun showShelly2Info( + device: DeviceItem, + queue: RequestQueue, + items: MutableList, + recyclerView: RecyclerView, + ) { + queue.add( + JsonObjectRequest( + Request.Method.GET, + device.address + "rpc/Shelly.GetStatus", + null, + { response -> + items.addAll(parseShelly2Info(response)) + recyclerView.adapter = SimpleListAdapter(items, this) + }, + { }, + ), + ) + } + + companion object { + private const val TO_PERCENT = 100 + } +} diff --git a/app/src/main/java/io/github/domi04151309/home/activities/DevicesActivity.kt b/app/src/main/java/io/github/domi04151309/home/activities/DevicesActivity.kt new file mode 100644 index 0000000..52b3f05 --- /dev/null +++ b/app/src/main/java/io/github/domi04151309/home/activities/DevicesActivity.kt @@ -0,0 +1,152 @@ +package io.github.domi04151309.home.activities + +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.widget.TextView +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import io.github.domi04151309.home.R +import io.github.domi04151309.home.adapters.DeviceListAdapter +import io.github.domi04151309.home.data.DeviceItem +import io.github.domi04151309.home.data.SimpleListItem +import io.github.domi04151309.home.helpers.Devices +import io.github.domi04151309.home.interfaces.RecyclerViewHelperInterfaceAdvanced + +class DevicesActivity : BaseActivity(), RecyclerViewHelperInterfaceAdvanced { + private var reset = true + private lateinit var devices: Devices + private lateinit var recyclerView: RecyclerView + private lateinit var itemTouchHelper: ItemTouchHelper + + private val itemTouchHelperCallback = + object : ItemTouchHelper.Callback() { + override fun getMovementFlags( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + ): Int = + if ( + viewHolder.adapterPosition == (recyclerView.adapter?.itemCount ?: -1) - 1 + ) { + makeMovementFlags(0, 0) + } else { + makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0) + } + + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder, + ): Boolean { + val adapter = recyclerView.adapter ?: return false + return if (target.adapterPosition == adapter.itemCount - 1) { + false + } else { + recyclerView.adapter?.notifyItemMoved( + viewHolder.adapterPosition, + target.adapterPosition, + ) + devices.moveDevice(viewHolder.adapterPosition, target.adapterPosition) + true + } + } + + override fun isLongPressDragEnabled(): Boolean = true + + override fun onSwiped( + viewHolder: RecyclerView.ViewHolder, + direction: Int, + ) { + // Do nothing. + } + + override fun clearView( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + ) { + super.clearView(recyclerView, viewHolder) + devices.saveChanges() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_devices) + + devices = Devices(this) + recyclerView = findViewById(R.id.recyclerView) + itemTouchHelper = ItemTouchHelper(itemTouchHelperCallback) + + itemTouchHelper.attachToRecyclerView(recyclerView) + recyclerView.layoutManager = LinearLayoutManager(this) + } + + private fun loadDevices() { + val listItems: ArrayList = ArrayList(devices.length) + var currentDevice: DeviceItem + for (i in 0 until devices.length) { + currentDevice = devices.getDeviceByIndex(i) + listItems += + SimpleListItem( + title = currentDevice.name, + summary = + if (currentDevice.hide) { + resources.getString(R.string.device_config_hidden) + " · " + currentDevice.address + } else { + currentDevice.address + }, + hidden = "edit#${currentDevice.id}", + icon = currentDevice.iconId, + ) + } + listItems += + SimpleListItem( + title = resources.getString(R.string.pref_add), + summary = resources.getString(R.string.pref_add_summary), + hidden = "add", + icon = R.drawable.ic_add, + ) + + recyclerView.adapter = DeviceListAdapter(listItems, this) + } + + override fun onItemClicked( + view: View, + position: Int, + ) { + val action = view.findViewById(R.id.hidden).text + if (action.contains("edit")) { + reset = true + startActivity( + Intent(this, EditDeviceActivity::class.java) + .putExtra("deviceId", action.substring(action.indexOf('#') + 1)), + ) + } else if (action == "add") { + reset = true + MaterialAlertDialogBuilder(this) + .setTitle(R.string.pref_add_method) + .setItems(resources.getStringArray(R.array.pref_add_method_array)) { _, which -> + if (which == 0) { + startActivity(Intent(this, EditDeviceActivity::class.java)) + } else if (which == 1) { + startActivity(Intent(this, SearchDevicesActivity::class.java)) + } + } + .show() + } + } + + override fun onItemHandleTouched(viewHolder: RecyclerView.ViewHolder) { + itemTouchHelper.startDrag(viewHolder) + } + + override fun onStart() { + super.onStart() + if (reset) { + reset = false + loadDevices() + } + } +} diff --git a/app/src/main/java/io/github/domi04151309/home/activities/EditDeviceActivity.kt b/app/src/main/java/io/github/domi04151309/home/activities/EditDeviceActivity.kt new file mode 100644 index 0000000..9abb9d2 --- /dev/null +++ b/app/src/main/java/io/github/domi04151309/home/activities/EditDeviceActivity.kt @@ -0,0 +1,387 @@ +package io.github.domi04151309.home.activities + +import android.content.ActivityNotFoundException +import android.content.Intent +import android.content.pm.ShortcutInfo +import android.content.pm.ShortcutManager +import android.graphics.drawable.Icon +import android.os.Build +import android.os.Bundle +import android.util.Log +import android.view.View +import android.widget.ArrayAdapter +import android.widget.AutoCompleteTextView +import android.widget.Button +import android.widget.CheckBox +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import android.widget.Toast +import androidx.core.net.toUri +import com.google.android.material.appbar.MaterialToolbar +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.elevation.SurfaceColors +import com.google.android.material.floatingactionbutton.FloatingActionButton +import com.google.android.material.textfield.TextInputLayout +import io.github.domi04151309.home.R +import io.github.domi04151309.home.adapters.IconSpinnerAdapter +import io.github.domi04151309.home.custom.TextWatcher +import io.github.domi04151309.home.data.DeviceItem +import io.github.domi04151309.home.helpers.DeviceSecrets +import io.github.domi04151309.home.helpers.Devices +import io.github.domi04151309.home.helpers.Global + +class EditDeviceActivity : BaseActivity() { + private lateinit var devices: Devices + private lateinit var deviceId: String + private lateinit var deviceSecrets: DeviceSecrets + private lateinit var deviceIcon: ImageView + private lateinit var nameText: TextView + private lateinit var nameBox: TextInputLayout + private lateinit var addressBox: TextInputLayout + private lateinit var iconSpinner: AutoCompleteTextView + private lateinit var modeSpinner: AutoCompleteTextView + private lateinit var specialDivider: View + private lateinit var specialSection: LinearLayout + private lateinit var usernameBox: TextInputLayout + private lateinit var passwordBox: TextInputLayout + private lateinit var configHide: CheckBox + private lateinit var configDirectView: CheckBox + private lateinit var configButton: Button + private lateinit var infoButton: Button + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_edit_device) + + window.statusBarColor = SurfaceColors.SURFACE_0.getColor(this) + + devices = Devices(this) + var deviceId = intent.getStringExtra("deviceId") + val editing = + if (deviceId == null) { + deviceId = devices.generateNewId() + false + } else { + true + } + this.deviceId = deviceId + + deviceSecrets = DeviceSecrets(this, deviceId) + + deviceIcon = findViewById(R.id.deviceIcn) + nameText = findViewById(R.id.nameTxt) + nameBox = findViewById(R.id.nameBox) + addressBox = findViewById(R.id.addressBox) + iconSpinner = findViewById(R.id.iconSpinner).editText as AutoCompleteTextView + modeSpinner = findViewById(R.id.modeSpinner).editText as AutoCompleteTextView + specialDivider = findViewById(R.id.specialDivider) + specialSection = findViewById(R.id.specialSection) + usernameBox = findViewById(R.id.usernameBox) + passwordBox = findViewById(R.id.passwordBox) + configHide = findViewById(R.id.configHide) + configDirectView = findViewById(R.id.configDirectView) + configButton = findViewById(R.id.configBtn) + infoButton = findViewById(R.id.infoBtn) + + findViewById(R.id.idTxt).text = resources.getString(R.string.pref_add_id, deviceId) + + iconSpinner.addTextChangedListener(getIconTextWatcher()) + modeSpinner.addTextChangedListener(getModeTextWatcher(editing)) + nameBox.editText?.addTextChangedListener(getNameTextWatcher()) + + if (editing) { + onEditDevice() + } else { + onCreateDevice() + } + + iconSpinner.setAdapter(IconSpinnerAdapter(resources.getStringArray(R.array.pref_icons))) + modeSpinner.setAdapter( + ArrayAdapter( + this, + R.layout.dropdown_item, + resources.getStringArray(R.array.pref_add_mode_array), + ), + ) + + findViewById(R.id.fab).setOnClickListener { + onFloatingActionButtonClicked() + } + + findViewById(R.id.toolbar).apply { + setNavigationIcon(R.drawable.ic_arrow_back) + setNavigationOnClickListener { + onBackPressedDispatcher.onBackPressed() + } + } + } + + private fun getIconTextWatcher() = + TextWatcher { + deviceIcon.setImageResource(Global.getIcon(it)) + } + + private fun showExternalInfoBasedOnMode(mode: String) { + configButton.visibility = + if (HAS_CONFIG.contains(mode)) { + View.VISIBLE + } else { + View.GONE + } + infoButton.visibility = + if (HAS_INFO.contains(mode)) { + View.VISIBLE + } else { + View.GONE + } + } + + @Suppress("ComplexCondition") + private fun getModeTextWatcher(editing: Boolean) = + TextWatcher { + val specialVisibility = + if ( + it == Global.FRITZ_AUTO_LOGIN || + it == Global.GRAFANA_AUTO_LOGIN || + it == Global.PI_HOLE_AUTO_LOGIN || + it == Global.SHELLY_GEN_1 + ) { + View.VISIBLE + } else { + View.GONE + } + val usernameVisibility = + if ( + it == Global.GRAFANA_AUTO_LOGIN || + it == Global.SHELLY_GEN_1 + ) { + View.VISIBLE + } else { + View.GONE + } + specialDivider.visibility = specialVisibility + specialSection.visibility = specialVisibility + usernameBox.visibility = usernameVisibility + + if (SUPPORTS_DIRECT_VIEW.contains(it)) { + configDirectView.isEnabled = true + } else { + configDirectView.isEnabled = false + configDirectView.isChecked = false + } + + if (editing) { + showExternalInfoBasedOnMode(it) + } + } + + private fun getNameTextWatcher() = + TextWatcher { + if (it == "") { + nameText.text = resources.getString(R.string.pref_add_name_empty) + } else { + nameText.text = it + } + } + + private fun onEditDevice() { + val device = devices.getDeviceById(deviceId) + nameBox.editText?.setText(device.name) + addressBox.editText?.setText(device.address) + iconSpinner.setText(device.iconName) + modeSpinner.setText(device.mode) + usernameBox.editText?.setText(deviceSecrets.username) + passwordBox.editText?.setText(deviceSecrets.password) + configHide.isChecked = device.hide + configDirectView.isChecked = device.directView + + configButton.setOnClickListener { + onConfigButtonClicked() + } + + infoButton.setOnClickListener { + startActivity(Intent(this, DeviceInfoActivity::class.java).putExtra(Devices.INTENT_EXTRA_DEVICE, deviceId)) + } + + findViewById