From a0a4d546e165802c422423f768fae5d56bd88a44 Mon Sep 17 00:00:00 2001 From: mirfatif Date: Fri, 9 Jul 2021 17:44:42 +0500 Subject: [PATCH] Initial commit --- .gitignore | 113 +++ LICENSE | 662 ++++++++++++++++ README.md | 49 ++ app/.gitignore | 1 + app/build.gradle | 48 ++ app/proguard-rules.pro | 5 + app/src/main/AndroidManifest.xml | 63 ++ .../org/microg/nlp/api/LocationBackend.aidl | 14 + .../org/microg/nlp/api/LocationCallback.aidl | 7 + .../mylocation/AboutDialogFragment.java | 57 ++ .../java/com/mirfatif/mylocation/App.java | 51 ++ .../mylocation/DonateDialogFragment.java | 77 ++ .../java/com/mirfatif/mylocation/GpsSvc.java | 251 ++++++ .../mirfatif/mylocation/LicenseChecker.java | 17 + .../com/mirfatif/mylocation/MainActivity.java | 750 ++++++++++++++++++ .../com/mirfatif/mylocation/MySettings.java | 96 +++ .../com/mirfatif/mylocation/NlpAdapter.java | 138 ++++ .../com/mirfatif/mylocation/NlpBackend.java | 183 +++++ .../mirfatif/mylocation/NotifDismissSvc.java | 41 + .../com/mirfatif/mylocation/SatAdapter.java | 80 ++ .../mylocation/SatsDialogFragment.java | 65 ++ .../java/com/mirfatif/mylocation/Utils.java | 480 +++++++++++ app/src/main/res/drawable-hdpi/app_name.webp | Bin 0 -> 1060 bytes app/src/main/res/drawable-hdpi/icon.webp | Bin 0 -> 8548 bytes app/src/main/res/drawable-xhdpi/app_name.webp | Bin 0 -> 1316 bytes app/src/main/res/drawable-xhdpi/icon.webp | Bin 0 -> 9370 bytes .../res/drawable-xxhdpi/action_bar_icon.webp | Bin 0 -> 2652 bytes .../main/res/drawable-xxhdpi/app_name.webp | Bin 0 -> 1810 bytes app/src/main/res/drawable-xxhdpi/icon.webp | Bin 0 -> 14384 bytes .../res/drawable/alert_dialog_bg_bordered.xml | 14 + app/src/main/res/drawable/bitcoin.xml | 9 + .../main/res/drawable/bitcoin_qr_code.webp | Bin 0 -> 6822 bytes app/src/main/res/drawable/copy.xml | 10 + app/src/main/res/drawable/dark_mode.xml | 10 + app/src/main/res/drawable/dollar.xml | 9 + app/src/main/res/drawable/donate.xml | 10 + app/src/main/res/drawable/email.xml | 10 + app/src/main/res/drawable/github_mark.xml | 22 + app/src/main/res/drawable/info.xml | 10 + app/src/main/res/drawable/loc_settings.xml | 13 + app/src/main/res/drawable/lock.xml | 10 + app/src/main/res/drawable/map.xml | 13 + .../main/res/drawable/notification_icon.xml | 13 + app/src/main/res/drawable/play_store.xml | 9 + app/src/main/res/drawable/privacy_policy.xml | 10 + app/src/main/res/drawable/progress.xml | 6 + .../main/res/drawable/rectangular_border.xml | 7 + app/src/main/res/drawable/round_border.xml | 8 + app/src/main/res/drawable/round_button_bg.xml | 9 + app/src/main/res/drawable/round_ripple.xml | 5 + app/src/main/res/drawable/satellite.xml | 22 + app/src/main/res/drawable/settings.xml | 10 + app/src/main/res/drawable/share.xml | 10 + .../main/res/drawable/splash_background.xml | 23 + app/src/main/res/drawable/star.xml | 10 + app/src/main/res/drawable/telegram.xml | 10 + app/src/main/res/drawable/update.xml | 13 + app/src/main/res/layout/about_dialog.xml | 234 ++++++ app/src/main/res/layout/activity_main.xml | 68 ++ app/src/main/res/layout/donate_dialog.xml | 107 +++ app/src/main/res/layout/gps_container.xml | 209 +++++ app/src/main/res/layout/net_container.xml | 128 +++ app/src/main/res/layout/nlp_container.xml | 48 ++ app/src/main/res/layout/nlp_item.xml | 133 ++++ app/src/main/res/layout/rv_sats.xml | 17 + app/src/main/res/layout/sat_item.xml | 58 ++ app/src/main/res/menu/main_overflow.xml | 34 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2541 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 4732 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 4628 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 3754 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 7149 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 6854 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6491 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 13032 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 11741 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9800 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 20155 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 17766 bytes app/src/main/res/values-night/colors.xml | 7 + app/src/main/res/values/colors.xml | 13 + app/src/main/res/values/strings.xml | 103 +++ app/src/main/res/values/styles.xml | 56 ++ app/src/main/res/xml/file_paths.xml | 6 + build.gradle | 36 + .../android/en-US/full_description.txt | 13 + .../android/en-US/images/featureGraphic.png | Bin 0 -> 48845 bytes .../metadata/android/en-US/images/icon.png | Bin 0 -> 50089 bytes .../en-US/images/phoneScreenshots/1.jpg | Bin 0 -> 342498 bytes .../en-US/images/phoneScreenshots/2.jpg | Bin 0 -> 169038 bytes .../en-US/images/phoneScreenshots/3.png | Bin 0 -> 192796 bytes .../android/en-US/short_description.txt | 1 + fastlane/metadata/android/en-US/title.txt | 1 + gradle.properties | 20 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 ++++ gradlew.bat | 84 ++ privacy_policy.html | 58 ++ settings.gradle | 2 + 102 files changed, 5177 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/aidl/org/microg/nlp/api/LocationBackend.aidl create mode 100644 app/src/main/aidl/org/microg/nlp/api/LocationCallback.aidl create mode 100644 app/src/main/java/com/mirfatif/mylocation/AboutDialogFragment.java create mode 100644 app/src/main/java/com/mirfatif/mylocation/App.java create mode 100644 app/src/main/java/com/mirfatif/mylocation/DonateDialogFragment.java create mode 100644 app/src/main/java/com/mirfatif/mylocation/GpsSvc.java create mode 100644 app/src/main/java/com/mirfatif/mylocation/LicenseChecker.java create mode 100644 app/src/main/java/com/mirfatif/mylocation/MainActivity.java create mode 100644 app/src/main/java/com/mirfatif/mylocation/MySettings.java create mode 100644 app/src/main/java/com/mirfatif/mylocation/NlpAdapter.java create mode 100644 app/src/main/java/com/mirfatif/mylocation/NlpBackend.java create mode 100644 app/src/main/java/com/mirfatif/mylocation/NotifDismissSvc.java create mode 100644 app/src/main/java/com/mirfatif/mylocation/SatAdapter.java create mode 100644 app/src/main/java/com/mirfatif/mylocation/SatsDialogFragment.java create mode 100644 app/src/main/java/com/mirfatif/mylocation/Utils.java create mode 100644 app/src/main/res/drawable-hdpi/app_name.webp create mode 100644 app/src/main/res/drawable-hdpi/icon.webp create mode 100644 app/src/main/res/drawable-xhdpi/app_name.webp create mode 100644 app/src/main/res/drawable-xhdpi/icon.webp create mode 100644 app/src/main/res/drawable-xxhdpi/action_bar_icon.webp create mode 100644 app/src/main/res/drawable-xxhdpi/app_name.webp create mode 100644 app/src/main/res/drawable-xxhdpi/icon.webp create mode 100644 app/src/main/res/drawable/alert_dialog_bg_bordered.xml create mode 100644 app/src/main/res/drawable/bitcoin.xml create mode 100644 app/src/main/res/drawable/bitcoin_qr_code.webp create mode 100644 app/src/main/res/drawable/copy.xml create mode 100644 app/src/main/res/drawable/dark_mode.xml create mode 100644 app/src/main/res/drawable/dollar.xml create mode 100644 app/src/main/res/drawable/donate.xml create mode 100644 app/src/main/res/drawable/email.xml create mode 100644 app/src/main/res/drawable/github_mark.xml create mode 100644 app/src/main/res/drawable/info.xml create mode 100644 app/src/main/res/drawable/loc_settings.xml create mode 100644 app/src/main/res/drawable/lock.xml create mode 100644 app/src/main/res/drawable/map.xml create mode 100644 app/src/main/res/drawable/notification_icon.xml create mode 100644 app/src/main/res/drawable/play_store.xml create mode 100644 app/src/main/res/drawable/privacy_policy.xml create mode 100644 app/src/main/res/drawable/progress.xml create mode 100644 app/src/main/res/drawable/rectangular_border.xml create mode 100644 app/src/main/res/drawable/round_border.xml create mode 100644 app/src/main/res/drawable/round_button_bg.xml create mode 100644 app/src/main/res/drawable/round_ripple.xml create mode 100644 app/src/main/res/drawable/satellite.xml create mode 100644 app/src/main/res/drawable/settings.xml create mode 100644 app/src/main/res/drawable/share.xml create mode 100644 app/src/main/res/drawable/splash_background.xml create mode 100644 app/src/main/res/drawable/star.xml create mode 100644 app/src/main/res/drawable/telegram.xml create mode 100644 app/src/main/res/drawable/update.xml create mode 100644 app/src/main/res/layout/about_dialog.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/layout/donate_dialog.xml create mode 100644 app/src/main/res/layout/gps_container.xml create mode 100644 app/src/main/res/layout/net_container.xml create mode 100644 app/src/main/res/layout/nlp_container.xml create mode 100644 app/src/main/res/layout/nlp_item.xml create mode 100644 app/src/main/res/layout/rv_sats.xml create mode 100644 app/src/main/res/layout/sat_item.xml create mode 100644 app/src/main/res/menu/main_overflow.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/values-night/colors.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/main/res/xml/file_paths.xml create mode 100644 build.gradle create mode 100644 fastlane/metadata/android/en-US/full_description.txt create mode 100644 fastlane/metadata/android/en-US/images/featureGraphic.png create mode 100644 fastlane/metadata/android/en-US/images/icon.png create mode 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg create mode 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg create mode 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/3.png create mode 100644 fastlane/metadata/android/en-US/short_description.txt create mode 100644 fastlane/metadata/android/en-US/title.txt create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 privacy_policy.html create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bf73fc6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,113 @@ +*.iml +.gradle +/local.properties +.DS_Store +/build +/captures +.externalNativeBuild +.cxx + +############## +# Custom # +############## +publish-checklist.txt +build_signed_release.sh +build_debug.sh +*icon.svg +banner.svg +*ic_launcher-playstore.png +.idea/ +app/release/ +app/debug/ +app/src/androidTest/ +app/src/test/ + +##################################################################### +# https://github.com/github/gitignore/blob/master/Android.gitignore # +##################################################################### + +# 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 +# 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/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2beb9e1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,662 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 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 Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are 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. + + 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. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + 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 Affero 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. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + 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 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 work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 Affero 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 Affero 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 Affero 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + 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 AGPL, see +. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..df6bc11 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# MyLocation [![Download](https://img.shields.io/github/v/release/mirfatif/MyLocation?label="Download")](https://github.com/mirfatif/MyLocation/releases/latest) +Know your geo coordinates using on-device GPS and Network location providers + + + +## Features + +My Location finds your device's location in the following ways: + +* GPS is usually the most accurate method. But a position fix may take some time or may not work at all due to signal loss. "Lock GPS" feature runs a persistent service to keep connected with the satellites. + + You can also see the list of visible satellites with their PRNs (unique identifiers) and SNR (signal quality). + +* Network Location Provider uses Wi-Fi or Cellular ids to estimate the location. On the devices with Google Play Services installed, NLP usually uses Google Location Service at backend. +* UnifiedNLP is an open source API which has been used to develop multiple NLP backends (https://github.com/microg/UnifiedNlp/wiki/Backends). + +Furthermore: + +* Location coordinates can be copied to clipboard or opened in a maps app, if installed. +* Clearing A-GPS aiding data is also supported. + +## Screenshots + + + +## Third-Party Resources + +* https://github.com/androidx/androidx +* https://github.com/microg/android_external_UnifiedNlpApi +* https://github.com/square/leakcanary +* https://github.com/sherter/google-java-format-gradle-plugin + +## License [![License](https://img.shields.io/github/license/mirfatif/MyLocation?label="License")](https://github.com/mirfatif/MyLocation/blob/master/LICENSE) + +You **CANNOT** use and distribute the app icon in anyway, except for **My Location** (`com.mirfatif.mylocation`) app. + + My Location is free software: you can redistribute it and/or modify + it under the terms of the Affero 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 + Affero GNU General Public License for more details. + + You should have received a copy of the Affero GNU General Public License + along with this program. If not, see . diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..1fb562e --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,48 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion compileSdkVer + buildToolsVersion buildToolsVer + + defaultConfig { + applicationId "com.mirfatif.mylocation" + minSdkVersion minSdkVer + targetSdkVersion targetSdkVer + versionCode 101 + versionName "v1.01" + + buildConfigField "boolean", "IS_PS", "false" + } + + buildTypes { + release { + minifyEnabled true + shrinkResources true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + dependenciesInfo { + includeInApk false + } + + buildFeatures { + viewBinding true + } +} + +dependencies { + implementation 'androidx.appcompat:appcompat:1.3.0' + implementation 'androidx.recyclerview:recyclerview:1.2.1' + implementation 'androidx.browser:browser:1.3.0' + implementation "androidx.security:security-crypto:1.1.0-alpha03" + debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7' +} + +apply plugin: 'com.github.sherter.google-java-format' +preBuild.dependsOn(verifyGoogleJavaFormat) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..3020142 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,5 @@ +# Preserve the line number information for debugging stack traces. +-keepattributes SourceFile,LineNumberTable + +# Hide the original source file name. +-renamesourcefileattribute SourceFile diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..71630de --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/aidl/org/microg/nlp/api/LocationBackend.aidl b/app/src/main/aidl/org/microg/nlp/api/LocationBackend.aidl new file mode 100644 index 0000000..70c9878 --- /dev/null +++ b/app/src/main/aidl/org/microg/nlp/api/LocationBackend.aidl @@ -0,0 +1,14 @@ +package org.microg.nlp.api; + +import org.microg.nlp.api.LocationCallback; +import android.content.Intent; +import android.location.Location; + +interface LocationBackend { + void open(LocationCallback callback); + Location update(); + void close(); + Intent getInitIntent(); + Intent getSettingsIntent(); + Intent getAboutIntent(); +} diff --git a/app/src/main/aidl/org/microg/nlp/api/LocationCallback.aidl b/app/src/main/aidl/org/microg/nlp/api/LocationCallback.aidl new file mode 100644 index 0000000..fa323ad --- /dev/null +++ b/app/src/main/aidl/org/microg/nlp/api/LocationCallback.aidl @@ -0,0 +1,7 @@ +package org.microg.nlp.api; + +import android.location.Location; + +interface LocationCallback { + void report(in Location location); +} diff --git a/app/src/main/java/com/mirfatif/mylocation/AboutDialogFragment.java b/app/src/main/java/com/mirfatif/mylocation/AboutDialogFragment.java new file mode 100644 index 0000000..237d520 --- /dev/null +++ b/app/src/main/java/com/mirfatif/mylocation/AboutDialogFragment.java @@ -0,0 +1,57 @@ +package com.mirfatif.mylocation; + +import android.app.Dialog; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AlertDialog.Builder; +import androidx.appcompat.app.AppCompatDialogFragment; +import com.mirfatif.mylocation.databinding.AboutDialogBinding; + +public class AboutDialogFragment extends AppCompatDialogFragment { + + public AboutDialogFragment() {} + + private MainActivity mA; + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + mA = (MainActivity) getActivity(); + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AboutDialogBinding b = AboutDialogBinding.inflate(mA.getLayoutInflater()); + b.version.setText(BuildConfig.VERSION_NAME); + openWebUrl(b.telegram, R.string.telegram_link); + openWebUrl(b.sourceCode, R.string.source_url); + openWebUrl(b.rating, R.string.play_store_url); + openWebUrl(b.privacyPolicy, R.string.privacy_policy_link); + b.contact.setOnClickListener(v -> Utils.sendMail(mA, null)); + b.shareApp.setOnClickListener(v -> sendShareIntent()); + if (BuildConfig.IS_PS) { + openWebUrl(b.checkUpdate, R.string.play_store_url); + } else { + openWebUrl(b.checkUpdate, R.string.release_url); + } + + AlertDialog dialog = new Builder(mA).setView(b.getRoot()).create(); + return Utils.setDialogBg(dialog); + } + + private void openWebUrl(View view, int linkResId) { + view.setOnClickListener(v -> Utils.openWebUrl(mA, getString(linkResId))); + } + + private void sendShareIntent() { + Intent intent = new Intent(Intent.ACTION_SEND).setType("text/plain"); + intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.app_name)); + String text = getString(R.string.share_text, getString(R.string.play_store_url)); + startActivity(Intent.createChooser(intent.putExtra(Intent.EXTRA_TEXT, text), null)); + } +} diff --git a/app/src/main/java/com/mirfatif/mylocation/App.java b/app/src/main/java/com/mirfatif/mylocation/App.java new file mode 100644 index 0000000..9c9aaeb --- /dev/null +++ b/app/src/main/java/com/mirfatif/mylocation/App.java @@ -0,0 +1,51 @@ +package com.mirfatif.mylocation; + +import android.app.Application; +import android.content.Context; +import android.content.res.Resources; +import android.util.Log; +import java.io.PrintWriter; +import java.io.StringWriter; + +public class App extends Application { + + private static final String TAG = "App"; + + private static Context mAppContext; + private Thread.UncaughtExceptionHandler defaultExceptionHandler; + + @Override + public void onCreate() { + super.onCreate(); + mAppContext = getApplicationContext(); + defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler(); + + Thread.setDefaultUncaughtExceptionHandler( + (t, e) -> { + Log.e(TAG, e.toString()); + + StringWriter stringWriter = new StringWriter(); + PrintWriter writer = new PrintWriter(stringWriter, true); + e.printStackTrace(writer); + writer.close(); + Utils.writeCrashLog(stringWriter.toString()); + + defaultExceptionHandler.uncaughtException(t, e); + }); + + Utils.runBg(this::getEncPrefs); + } + + public static Context getCxt() { + return mAppContext; + } + + public static Resources getRes() { + return mAppContext.getResources(); + } + + // To avoid delays later + private void getEncPrefs() { + Utils.getEncPrefs(); + } +} diff --git a/app/src/main/java/com/mirfatif/mylocation/DonateDialogFragment.java b/app/src/main/java/com/mirfatif/mylocation/DonateDialogFragment.java new file mode 100644 index 0000000..028c759 --- /dev/null +++ b/app/src/main/java/com/mirfatif/mylocation/DonateDialogFragment.java @@ -0,0 +1,77 @@ +package com.mirfatif.mylocation; + +import static com.mirfatif.mylocation.Utils.openWebUrl; + +import android.app.Dialog; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Bundle; +import android.view.View; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AlertDialog.Builder; +import androidx.appcompat.app.AppCompatDialogFragment; +import com.mirfatif.mylocation.databinding.DonateDialogBinding; + +public class DonateDialogFragment extends AppCompatDialogFragment { + + public DonateDialogFragment() {} + + private MainActivity mA; + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + mA = (MainActivity) getActivity(); + } + + private DonateDialogBinding mB; + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + mB = DonateDialogBinding.inflate(mA.getLayoutInflater()); + + setButtonClickListener(mB.bitcoinButton, mB.bitcoinContainer); + setButtonClickListener(mB.bankAccountButton, mB.bankAccountLink); + setButtonClickListener(mB.playStoreButton, mB.playStoreLink); + + mB.bitcoinLink.setOnClickListener(v -> handleBitcoinClick()); + mB.playStoreLink.setOnClickListener(v -> openWebUrl(mA, getString(R.string.play_store_url))); + String msg = Utils.getString(R.string.bank_account_request); + mB.bankAccountLink.setOnClickListener(v -> Utils.sendMail(mA, msg)); + + AlertDialog dialog = + new Builder(mA).setTitle(R.string.donate_menu_item).setView(mB.getRoot()).create(); + return Utils.setDialogBg(dialog); + } + + private void setButtonClickListener(View button, View detailsView) { + button.setOnClickListener( + v -> { + hideAll(); + detailsView.setVisibility(View.VISIBLE); + }); + } + + private void hideAll() { + mB.bitcoinContainer.setVisibility(View.GONE); + mB.bankAccountLink.setVisibility(View.GONE); + mB.playStoreLink.setVisibility(View.GONE); + } + + private void handleBitcoinClick() { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse("bitcoin:" + Utils.getString(R.string.bitcoin_address))); + if (App.getCxt() + .getPackageManager() + .queryIntentActivities(intent, PackageManager.MATCH_ALL) + .isEmpty()) { + Utils.showToast(R.string.no_bitcoin_app_installed); + } else { + mA.startActivity(intent); + } + } +} diff --git a/app/src/main/java/com/mirfatif/mylocation/GpsSvc.java b/app/src/main/java/com/mirfatif/mylocation/GpsSvc.java new file mode 100644 index 0000000..fabef51 --- /dev/null +++ b/app/src/main/java/com/mirfatif/mylocation/GpsSvc.java @@ -0,0 +1,251 @@ +package com.mirfatif.mylocation; + +import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; +import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST; +import static androidx.core.app.NotificationManagerCompat.IMPORTANCE_DEFAULT; +import static com.mirfatif.mylocation.BuildConfig.APPLICATION_ID; +import static com.mirfatif.mylocation.Utils.formatLatLng; +import static com.mirfatif.mylocation.Utils.formatLocAccuracy; +import static com.mirfatif.mylocation.Utils.hasFineLocPerm; + +import android.annotation.SuppressLint; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.location.GpsSatellite; +import android.location.GpsStatus; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.os.IBinder; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationCompat.BigTextStyle; +import androidx.core.app.NotificationCompat.Builder; +import androidx.core.app.NotificationManagerCompat; +import java.util.concurrent.Future; + +public class GpsSvc extends Service implements LocationListener, GpsStatus.Listener { + + public static final String ACTION_STOP_SERVICE = APPLICATION_ID + ".action.STOP_SERVICE"; + + public static boolean mIsRunning = false; + + private final LocationManager mLocManager = + (LocationManager) App.getCxt().getSystemService(Context.LOCATION_SERVICE); + + private final PowerManager mPowerManager = + (PowerManager) App.getCxt().getSystemService(Context.POWER_SERVICE); + + private final NotificationManagerCompat mNotifManager = + NotificationManagerCompat.from(App.getCxt()); + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (hasFineLocPerm() && (intent == null || !ACTION_STOP_SERVICE.equals(intent.getAction()))) { + showNotif(); + startGpsLocListener(); + mIsRunning = true; + return START_STICKY; + } else { + stop(); + return START_NOT_STICKY; + } + } + + @Override + public void onDestroy() { + stop(); + super.onDestroy(); + } + + private Location mGpsLoc; + + @Override + public void onLocationChanged(Location location) { + mGpsLoc = location; + updateNotification(); + } + + @Override + public void onProviderEnabled(String provider) { + mLastUpdate = 0; + updateNotification(); + } + + @Override + public void onProviderDisabled(String provider) { + mLastUpdate = 0; + updateNotification(); + } + + @Override + public void onStatusChanged(String provider, int status, Bundle extras) { + mLastUpdate = 0; + updateNotification(); + } + + private void stop() { + mIsRunning = false; + stopGpsLocListener(); + if (mFuture != null) { + mFuture.cancel(true); + } + stopSelf(); + } + + @Override + public void onGpsStatusChanged(int event) { + updateGpsSats(); + } + + private WakeLock mWakeLock; + private Builder mNotifBuilder; + + private static final int NOTIF_ID = Utils.getInteger(R.integer.channel_gps_lock); + private static final String CHANNEL_ID = "channel_gps_lock"; + private static final String CHANNEL_NAME = Utils.getString(R.string.channel_gps_lock); + + private void showNotif() { + mWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, getClass().getName()); + mWakeLock.acquire(30 * 60 * 1000L); + + Utils.createNotifChannel(CHANNEL_ID, CHANNEL_NAME, IMPORTANCE_DEFAULT); + + Intent intent = new Intent(App.getCxt(), MainActivity.class); + PendingIntent pi = + PendingIntent.getActivity(App.getCxt(), NOTIF_ID, intent, FLAG_UPDATE_CURRENT); + + mNotifBuilder = + new Builder(App.getCxt(), CHANNEL_ID) + .setSilent(true) + .setOnlyAlertOnce(true) + .setSmallIcon(R.drawable.notification_icon) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) // For N and below + .setContentIntent(pi) + .setAutoCancel(false) + .setOngoing(true) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setContentTitle(getString(R.string.channel_gps_lock)); + + if (VERSION.SDK_INT >= VERSION_CODES.Q) { + startForeground(NOTIF_ID, mNotifBuilder.build(), FOREGROUND_SERVICE_TYPE_MANIFEST); + } else { + startForeground(NOTIF_ID, mNotifBuilder.build()); + } + + updateGpsSats(); + mLastUpdate = 0; + updateNotification(); + } + + public static final long MIN_DELAY = 5000; + + @SuppressLint("MissingPermission") + private void startGpsLocListener() { + mLocManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, MIN_DELAY, 0, this); + mLocManager.addGpsStatusListener(this); + } + + private void stopGpsLocListener() { + if (mWakeLock != null) { + mWakeLock.release(); + mWakeLock = null; + } + mLocManager.removeUpdates(this); + mLocManager.removeGpsStatusListener(this); + } + + private final Object UPDATE_GPS_SATS_LOCK = new Object(); + private int mTotalSats, mSatsStrongSig, mUsedSats; + + @SuppressLint("MissingPermission") + private void updateGpsSats() { + synchronized (UPDATE_GPS_SATS_LOCK) { + if (!hasFineLocPerm()) { + stop(); + return; + } + GpsStatus gpsStatus = mLocManager.getGpsStatus(null); + mTotalSats = mSatsStrongSig = mUsedSats = 0; + for (GpsSatellite gpsSat : gpsStatus.getSatellites()) { + mTotalSats++; + if (gpsSat.getSnr() != 0) { + mSatsStrongSig++; + } + if (gpsSat.usedInFix()) { + mUsedSats++; + } + } + updateNotification(); + } + } + + private Future mFuture; + + private synchronized void updateNotification() { + if (mFuture != null) { + mFuture.cancel(true); + } + mFuture = Utils.runBg(this::updateNotifBg); + } + + private final Object NOTIF_UPDATE_LOCK = new Object(); + private long mLastUpdate; + + private void updateNotifBg() { + synchronized (NOTIF_UPDATE_LOCK) { + long sleep = 5000 + mLastUpdate - System.currentTimeMillis(); + if (sleep > 0) { + try { + NOTIF_UPDATE_LOCK.wait(sleep); + } catch (InterruptedException e) { + return; + } + } + mLastUpdate = System.currentTimeMillis(); + + String sText, bText; + long when = 0; + if (!mLocManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { + sText = bText = getString(R.string.turned_off); + } else { + sText = bText = getString(R.string.satellites_count, mTotalSats, mSatsStrongSig, mUsedSats); + double lat, lng; + if (mGpsLoc != null + && (lat = mGpsLoc.getLatitude()) != 0 + && (lng = mGpsLoc.getLongitude()) != 0) { + sText = + getString( + R.string.location, + formatLatLng(lat), + formatLatLng(lng), + formatLocAccuracy(mGpsLoc.getAccuracy())); + bText += "\n" + sText; + } + if (mGpsLoc != null && mGpsLoc.getTime() != 0) { + when = mGpsLoc.getTime(); + } + } + mNotifBuilder.setContentText(sText); + mNotifBuilder.setStyle(new BigTextStyle().bigText(bText)); + if (when != 0) { + mNotifBuilder.setWhen(when); + mNotifBuilder.setShowWhen(true); + } else { + mNotifBuilder.setShowWhen(false); + } + mNotifManager.notify(NOTIF_ID, mNotifBuilder.build()); + } + } +} diff --git a/app/src/main/java/com/mirfatif/mylocation/LicenseChecker.java b/app/src/main/java/com/mirfatif/mylocation/LicenseChecker.java new file mode 100644 index 0000000..15ba0af --- /dev/null +++ b/app/src/main/java/com/mirfatif/mylocation/LicenseChecker.java @@ -0,0 +1,17 @@ +package com.mirfatif.mylocation; + +import androidx.fragment.app.FragmentActivity; + +public class LicenseChecker { + + @SuppressWarnings("UnusedParameters") + LicenseChecker(FragmentActivity activity) {} + + void check() {} + + void onDestroy() {} + + public boolean isVerified() { + return true; + } +} diff --git a/app/src/main/java/com/mirfatif/mylocation/MainActivity.java b/app/src/main/java/com/mirfatif/mylocation/MainActivity.java new file mode 100644 index 0000000..d7e7139 --- /dev/null +++ b/app/src/main/java/com/mirfatif/mylocation/MainActivity.java @@ -0,0 +1,750 @@ +package com.mirfatif.mylocation; + +import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.location.LocationManager.GPS_PROVIDER; +import static android.location.LocationManager.NETWORK_PROVIDER; +import static android.os.Build.VERSION.SDK_INT; +import static com.mirfatif.mylocation.GpsSvc.ACTION_STOP_SERVICE; +import static com.mirfatif.mylocation.GpsSvc.MIN_DELAY; +import static com.mirfatif.mylocation.MySettings.SETTINGS; +import static com.mirfatif.mylocation.Utils.copyLoc; +import static com.mirfatif.mylocation.Utils.hasCoarseLocPerm; +import static com.mirfatif.mylocation.Utils.hasFineLocPerm; +import static com.mirfatif.mylocation.Utils.isNaN; +import static com.mirfatif.mylocation.Utils.openMap; +import static com.mirfatif.mylocation.Utils.setNightTheme; + +import android.Manifest.permission; +import android.annotation.SuppressLint; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ResolveInfo; +import android.location.GpsSatellite; +import android.location.GpsStatus; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.provider.Settings; +import android.text.format.DateUtils; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import androidx.annotation.NonNull; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.view.menu.MenuBuilder; +import androidx.core.app.ActivityCompat; +import androidx.core.view.MenuCompat; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.LinearLayoutManager; +import com.mirfatif.mylocation.NlpAdapter.NlpClickListener; +import com.mirfatif.mylocation.databinding.ActivityMainBinding; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.locks.ReentrantLock; + +public class MainActivity extends AppCompatActivity { + + private ActivityMainBinding mB; + + private final LocationManager mLocManager = + (LocationManager) App.getCxt().getSystemService(Context.LOCATION_SERVICE); + + private LicenseChecker mLicenseChecker; + private boolean mGpsProviderSupported = false; + private boolean mNetProviderSupported = false; + + @Override + protected void onCreate(Bundle savedInstanceState) { + setTheme(R.style.AppTheme); + super.onCreate(savedInstanceState); + if (setNightTheme(this)) { + return; + } + mB = ActivityMainBinding.inflate(getLayoutInflater()); + setContentView(mB.getRoot()); + + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setDisplayUseLogoEnabled(true); + actionBar.setDisplayShowHomeEnabled(true); + actionBar.setIcon(R.drawable.action_bar_icon); + } + + for (String provider : mLocManager.getAllProviders()) { + if (provider.equals(GPS_PROVIDER)) { + mGpsProviderSupported = true; + } + if (provider.equals(NETWORK_PROVIDER)) { + mNetProviderSupported = true; + } + } + + setupGps(); + updateGpsUi(); + setupNetwork(); + updateNetUi(); + setupUnifiedNlp(); + checkPerms(); + + mB.grantPerm.setOnClickListener(v -> Utils.openAppSettings(this, getPackageName())); + + mLicenseChecker = new LicenseChecker(this); + } + + @Override + protected void onStart() { + super.onStart(); + startLocListeners(); + setTimer(); + setGrantPermButtonState(); + } + + @Override + protected void onStop() { + stopTimer(); + stopLocListeners(); + super.onStop(); + } + + @Override + protected void onResume() { + super.onResume(); + checkLicense(); + } + + @Override + protected void onDestroy() { + if (mLicenseChecker != null) { + mLicenseChecker.onDestroy(); + } + super.onDestroy(); + } + + @Override + protected void onSaveInstanceState(@NonNull Bundle outState) { + FragmentManager fm = getSupportFragmentManager(); + Fragment frag = fm.findFragmentByTag(SATS_DIALOG_TAG); + if (frag != null) { + fm.beginTransaction().remove(frag).commitNowAllowingStateLoss(); + } + super.onSaveInstanceState(outState); + } + + @Override + public void onBackPressed() { + if (VERSION.SDK_INT == VERSION_CODES.Q) { + // Bug: https://issuetracker.google.com/issues/139738913 + finishAfterTransition(); + } else { + super.onBackPressed(); + } + } + + @SuppressLint("RestrictedApi") + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.main_overflow, menu); + MenuCompat.setGroupDividerEnabled(menu, true); + if (menu instanceof MenuBuilder) { + ((MenuBuilder) menu).setOptionalIconsVisible(true); + } + menu.findItem(R.id.action_dark_theme).setChecked(SETTINGS.getForceDarkMode()); + if (BuildConfig.IS_PS) { + menu.findItem(R.id.action_donate).setVisible(false); + } + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int itemId = item.getItemId(); + if (itemId == R.id.action_loc_settings) { + try { + startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)); + } catch (ActivityNotFoundException ignored) { + Utils.showToast(R.string.failed_open_loc_settings); + } + return true; + } + if (itemId == R.id.action_dark_theme) { + SETTINGS.setForceDarkMode(!item.isChecked()); + setNightTheme(this); + return true; + } + if (itemId == R.id.action_donate) { + new DonateDialogFragment().show(getSupportFragmentManager(), "DONATE"); + return true; + } + if (itemId == R.id.action_about) { + new AboutDialogFragment().showNow(getSupportFragmentManager(), "ABOUT_DIALOG"); + return true; + } + return super.onOptionsItemSelected(item); + } + + ////////////////////////////////////////////////////////////////// + ////////////////////////// LOC PROVIDERS ///////////////////////// + ////////////////////////////////////////////////////////////////// + + private void setupGps() { + if (!mGpsProviderSupported) { + return; + } + + mB.clearAgps.setOnClickListener(v -> clearAGPSData()); + mB.lockGps.setOnClickListener( + v -> { + if (mB.lockGps.isChecked()) { + Intent intent = new Intent(App.getCxt(), GpsSvc.class); + // If startForeground() in Service is called on UI thread, it won't show notification + // unless Service is started with startForegroundService(). + if (SDK_INT >= VERSION_CODES.O) { + startForegroundService(intent); + } else { + startService(intent); + } + } else { + startService(new Intent(App.getCxt(), GpsSvc.class).setAction(ACTION_STOP_SERVICE)); + } + }); + + mB.gpsCont.map.setOnClickListener(v -> openMap(this, mGpsLocation)); + mB.gpsCont.copy.setOnClickListener(v -> copyLoc(mGpsLocation)); + + // setOnCheckedChangeListener() doesn't work well on screen rotation. + mB.gpsCont.switchV.setOnClickListener( + v -> { + if (SETTINGS.getGpsEnabled() != mB.gpsCont.switchV.isChecked()) { + SETTINGS.setGpsEnabled(mB.gpsCont.switchV.isChecked()); + startGpsLocListener(); + setTimer(); + } + }); + + if (GpsSvc.mIsRunning) { + mB.lockGps.setChecked(true); + } + + mB.gpsCont.satDetail.setOnClickListener(v -> showSatsDialog()); + + Utils.setTooltip(mB.gpsCont.map); + Utils.setTooltip(mB.gpsCont.copy); + Utils.setTooltip(mB.gpsCont.satDetail); + } + + private void setupNetwork() { + if (!mNetProviderSupported) { + return; + } + + mB.netCont.map.setOnClickListener(v -> openMap(this, mNetLocation)); + mB.netCont.copy.setOnClickListener(v -> copyLoc(mNetLocation)); + + mB.netCont.switchV.setOnClickListener( + v -> { + if (SETTINGS.getNetworkEnabled() != mB.netCont.switchV.isChecked()) { + SETTINGS.setNetworkEnabled(mB.netCont.switchV.isChecked()); + startNetLocListener(); + setTimer(); + } + }); + + Utils.setTooltip(mB.netCont.map); + Utils.setTooltip(mB.netCont.copy); + } + + private static final String ACTION_LOCATION_BACKEND = "org.microg.nlp.LOCATION_BACKEND"; + + private final List mBackends = new ArrayList<>(); + private NlpAdapter mNlpAdapter; + + private void setupUnifiedNlp() { + Intent intent = new Intent(ACTION_LOCATION_BACKEND); + List infoList = getPackageManager().queryIntentServices(intent, 0); + synchronized (mBackends) { + mBackends.clear(); + for (ResolveInfo info : infoList) { + mBackends.add(new NlpBackend(info.serviceInfo)); + } + } + + if (infoList.size() == 0) { + mB.nlpCont.stateV.setText(R.string.not_installed); + } + mB.nlpCont.switchV.setOnClickListener( + v -> { + if (SETTINGS.getNlpEnabled() != mB.nlpCont.switchV.isChecked()) { + SETTINGS.setNlpEnabled(mB.nlpCont.switchV.isChecked()); + startNlpBackends(); + setTimer(); + } + }); + + mNlpAdapter = + new NlpAdapter( + new NlpClickListener() { + @Override + public void mapClicked(Location loc) { + openMap(MainActivity.this, loc); + } + + @Override + public void copyClicked(Location loc) { + copyLoc(loc); + } + + @Override + public void settingsClicked(String pkg) { + Utils.openAppSettings(MainActivity.this, pkg); + } + }, + mBackends); + mB.nlpCont.rv.setAdapter(mNlpAdapter); + mB.nlpCont.rv.setLayoutManager(new LinearLayoutManager(this)); + mB.nlpCont.rv.addItemDecoration( + new DividerItemDecoration(this, DividerItemDecoration.VERTICAL)); + } + + private final Object LOC_LISTENER_LOCK = new Object(); + + private void startLocListeners() { + startGpsLocListener(); + startNetLocListener(); + startNlpBackends(); + } + + private LocListener mGpsLocListener; + private GpsStatus.Listener mGpsStatusListener; + + @SuppressLint("MissingPermission") + private void startGpsLocListener() { + synchronized (LOC_LISTENER_LOCK) { + stopGpsLocListener(); + if (SETTINGS.getGpsEnabled() && mGpsProviderSupported && hasFineLocPerm()) { + mGpsLocListener = new LocListener(true); + mLocManager.requestLocationUpdates(GPS_PROVIDER, MIN_DELAY, 0, mGpsLocListener); + mGpsStatusListener = new GpsStatusListener(); + mLocManager.addGpsStatusListener(mGpsStatusListener); + } + } + } + + private LocListener mNetLocListener; + + @SuppressLint("MissingPermission") + private void startNetLocListener() { + synchronized (LOC_LISTENER_LOCK) { + stopNetLocListener(); + if (SETTINGS.getNetworkEnabled() + && mNetProviderSupported + && (hasCoarseLocPerm() || hasFineLocPerm())) { + mNetLocListener = new LocListener(false); + mLocManager.requestLocationUpdates(NETWORK_PROVIDER, MIN_DELAY, 0, mNetLocListener); + } + } + } + + private void startNlpBackends() { + synchronized (mBackends) { + stopNlpBackends(); + if (SETTINGS.getNlpEnabled()) { + for (NlpBackend backend : mBackends) { + backend.start(); + } + } + } + } + + private void stopLocListeners() { + stopGpsLocListener(); + stopNetLocListener(); + stopNlpBackends(); + } + + private void stopGpsLocListener() { + synchronized (LOC_LISTENER_LOCK) { + if (mGpsLocListener != null) { + mLocManager.removeUpdates(mGpsLocListener); + mGpsLocListener = null; + } + if (mGpsStatusListener != null) { + mLocManager.removeGpsStatusListener(mGpsStatusListener); + mGpsStatusListener = null; + } + clearGpsData(); + } + } + + private void clearGpsData() { + mGpsLocation = null; + synchronized (mSats) { + mSats.clear(); + } + } + + private void stopNetLocListener() { + synchronized (LOC_LISTENER_LOCK) { + if (mNetLocListener != null) { + mLocManager.removeUpdates(mNetLocListener); + mNetLocListener = null; + } + mNetLocation = null; + } + } + + private void stopNlpBackends() { + synchronized (mBackends) { + for (NlpBackend backend : mBackends) { + backend.stop(); + } + } + } + + ////////////////////////////////////////////////////////////////// + /////////////////////////////// UI ////////////////////////////// + ////////////////////////////////////////////////////////////////// + + private final List mSats = new ArrayList<>(); + private final ReentrantLock UPDATE_SATS_LOCK = new ReentrantLock(); + + @SuppressLint("MissingPermission") + private void updateGpsSats() { + if (!UPDATE_SATS_LOCK.tryLock()) { + return; + } + if (hasFineLocPerm()) { + GpsStatus gpsStatus = mLocManager.getGpsStatus(null); + synchronized (mSats) { + mSats.clear(); + for (GpsSatellite gpsSat : gpsStatus.getSatellites()) { + mSats.add(new Sat(gpsSat.getPrn(), gpsSat.usedInFix(), gpsSat.getSnr())); + } + Collections.sort(mSats, (s1, s2) -> Float.compare(s2.mSnr, s1.mSnr)); + } + } + UPDATE_SATS_LOCK.unlock(); + } + + private Timer mTimer; + private long mPeriod = 1000; + private int mTickCount; + + private void setTimer() { + mPeriod = 1000; + mTickCount = 0; + startTimer(); + } + + private void startTimer() { + stopTimer(); + mTimer = new Timer(); + mTimer.scheduleAtFixedRate( + new TimerTask() { + @Override + public void run() { + Utils.runUi(() -> updateUi()); + mTickCount++; + if (mTickCount == 5) { + mPeriod = 5000; + startTimer(); + } + } + }, + 0, + mPeriod); + } + + private void stopTimer() { + if (mTimer != null) { + mTimer.cancel(); + mTimer = null; + } + } + + private Location mGpsLocation, mNetLocation; + + private void updateUi() { + if (mB != null && mLicenseChecker != null && mLicenseChecker.isVerified()) { + updateGpsUi(); + updateNetUi(); + updateNlpUi(); + } + } + + private void updateGpsUi() { + String state = null, lat = "--", lng = "--", acc = "--", time = "--"; + boolean hasFineLocPerm = false, showSats = false, locAvailable = false; + if (!mGpsProviderSupported) { + state = getString(R.string.not_supported); + } else { + hasFineLocPerm = hasFineLocPerm(); + if (!hasFineLocPerm) { + state = getString(R.string.perm_not_granted); + } else if (!mLocManager.isProviderEnabled(GPS_PROVIDER)) { + state = getString(R.string.turned_off); + } else { + showSats = SETTINGS.getGpsEnabled(); + if (mGpsLocation != null + && !isNaN(mGpsLocation.getLatitude()) + && !isNaN(mGpsLocation.getLongitude())) { + locAvailable = true; + lat = Utils.formatLatLng(mGpsLocation.getLatitude()); + lng = Utils.formatLatLng(mGpsLocation.getLongitude()); + if (!isNaN(mGpsLocation.getAccuracy()) && mGpsLocation.getAccuracy() != 0) { + acc = getString(R.string.acc_unit, Utils.formatLocAccuracy(mGpsLocation.getAccuracy())); + } + long curr = System.currentTimeMillis(); + long t = mGpsLocation.getTime(); + t = t - Math.max(0, t - curr); + time = DateUtils.getRelativeTimeSpanString(t).toString(); + } + } + } + mB.clearAgps.setEnabled(hasFineLocPerm); + mB.lockGps.setEnabled(hasFineLocPerm); + mB.gpsCont.map.setEnabled(locAvailable); + mB.gpsCont.copy.setEnabled(locAvailable); + mB.gpsCont.switchV.setEnabled(hasFineLocPerm); + mB.gpsCont.switchV.setChecked(hasFineLocPerm && SETTINGS.getGpsEnabled()); + mB.gpsCont.stateV.setText(state); + mB.gpsCont.latV.setText(lat); + mB.gpsCont.lngV.setText(lng); + mB.gpsCont.accV.setText(acc); + mB.gpsCont.timeV.setText(time); + mB.gpsCont.satDetail.setEnabled(showSats); + if (!showSats && mSatsDialog != null) { + mSatsDialog.dismissAllowingStateLoss(); + } + + int total, good = 0, used = 0; + synchronized (mSats) { + total = mSats.size(); + for (Sat sat : mSats) { + if (sat.mSnr != 0) { + good++; + } + if (sat.mUsed) { + used++; + } + } + } + + mB.gpsCont.totalSatV.setText(String.valueOf(total)); + mB.gpsCont.goodSatV.setText(String.valueOf(good)); + mB.gpsCont.usedSatV.setText(String.valueOf(used)); + + synchronized (SATS_DIALOG_TAG) { + if (mSatsDialog != null) { + mSatsDialog.submitList(mSats); + } + } + } + + private void updateNetUi() { + String state = null, lat = "--", lng = "--", acc = "--", time = "--"; + boolean hasLocPerm = false, locAvailable = false; + if (!mNetProviderSupported) { + state = getString(R.string.not_supported); + } else { + hasLocPerm = hasCoarseLocPerm() || hasFineLocPerm(); + if (!hasLocPerm) { + state = getString(R.string.perm_not_granted); + } else if (!mLocManager.isProviderEnabled(NETWORK_PROVIDER)) { + state = getString(R.string.turned_off); + } else if (mNetLocation != null + && !isNaN(mNetLocation.getLatitude()) + && !isNaN(mNetLocation.getLongitude())) { + locAvailable = true; + lat = Utils.formatLatLng(mNetLocation.getLatitude()); + lng = Utils.formatLatLng(mNetLocation.getLongitude()); + if (!isNaN(mNetLocation.getAccuracy()) && mNetLocation.getAccuracy() != 0) { + acc = getString(R.string.acc_unit, Utils.formatLocAccuracy(mNetLocation.getAccuracy())); + } + long curr = System.currentTimeMillis(); + long t = mNetLocation.getTime(); + t = t - Math.max(0, t - curr); + time = DateUtils.getRelativeTimeSpanString(t).toString(); + } + } + mB.netCont.map.setEnabled(locAvailable); + mB.netCont.copy.setEnabled(locAvailable); + mB.netCont.switchV.setEnabled(hasLocPerm); + mB.netCont.switchV.setChecked(hasLocPerm && SETTINGS.getNetworkEnabled()); + mB.netCont.stateV.setText(state); + mB.netCont.latV.setText(lat); + mB.netCont.lngV.setText(lng); + mB.netCont.accV.setText(acc); + mB.netCont.timeV.setText(time); + } + + private void updateNlpUi() { + boolean hasLocPerm = hasCoarseLocPerm(); + mB.nlpCont.switchV.setEnabled(hasLocPerm); + mB.nlpCont.switchV.setChecked(hasLocPerm && SETTINGS.getNlpEnabled()); + synchronized (mBackends) { + for (NlpBackend backend : mBackends) { + backend.refresh(); + if (mNlpAdapter != null) { + mNlpAdapter.notifyDataSetChanged(); + } + } + } + } + + private void setGrantPermButtonState() { + if (mB != null) { + if (hasFineLocPerm() && hasCoarseLocPerm()) { + mB.grantPerm.setVisibility(View.GONE); + } else { + mB.grantPerm.setVisibility(View.VISIBLE); + } + } + } + + private SatsDialogFragment mSatsDialog; + private static final String SATS_DIALOG_TAG = "SATELLITES_DETAIL"; + + private void showSatsDialog() { + mSatsDialog = new SatsDialogFragment(); + mSatsDialog.setOnDismissListener( + d -> { + synchronized (SATS_DIALOG_TAG) { + mSatsDialog = null; + } + }); + mSatsDialog.showNow(getSupportFragmentManager(), SATS_DIALOG_TAG); + mSatsDialog.submitList(mSats); + } + + ////////////////////////////////////////////////////////////////// + /////////////////////////// PERM REQUEST ///////////////////////// + ////////////////////////////////////////////////////////////////// + + private void checkPerms() { + List perms = new ArrayList<>(); + if (!hasFineLocPerm()) { + perms.add(permission.ACCESS_FINE_LOCATION); + } + if (!hasCoarseLocPerm()) { + perms.add(permission.ACCESS_COARSE_LOCATION); + } + if (!perms.isEmpty()) { + ActivityCompat.requestPermissions(this, perms.toArray(new String[] {}), 0); + } + } + + @Override + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + List results = new ArrayList<>(); + for (int i : grantResults) { + results.add(i); + } + if (results.contains(PERMISSION_GRANTED)) { + startLocListeners(); + setGrantPermButtonState(); + setTimer(); + } + } + + ////////////////////////////////////////////////////////////////// + ////////////////////////////// OTHER ///////////////////////////// + ////////////////////////////////////////////////////////////////// + + void checkLicense() { + if (mLicenseChecker != null) { + mLicenseChecker.check(); + } + } + + private void clearAGPSData() { + if (hasFineLocPerm()) { + mLocManager.sendExtraCommand(GPS_PROVIDER, "delete_aiding_data", null); + mLocManager.sendExtraCommand(GPS_PROVIDER, "force_time_injection", null); + String command = SDK_INT >= VERSION_CODES.Q ? "force_psds_injection" : "force_xtra_injection"; + mLocManager.sendExtraCommand(GPS_PROVIDER, command, null); + Utils.showShortToast(R.string.cleared); + } + } + + private class LocListener implements LocationListener { + + private final boolean mIsGps; + + private LocListener(boolean isGps) { + mIsGps = isGps; + } + + @Override + public void onLocationChanged(Location location) { + if (mIsGps) { + mGpsLocation = location; + } else { + mNetLocation = location; + } + } + + @Override + public void onProviderEnabled(String provider) { + setTimer(); + } + + @Override + public void onProviderDisabled(String provider) { + if (mIsGps) { + clearGpsData(); + } else { + mNetLocation = null; + } + setTimer(); + } + + @Override + public void onStatusChanged(String provider, int status, Bundle extras) { + setTimer(); + } + } + + private class GpsStatusListener implements GpsStatus.Listener { + + @Override + public void onGpsStatusChanged(int event) { + Utils.runBg(MainActivity.this::updateGpsSats); + } + } + + static class Sat { + + final int mPrn; + final boolean mUsed; + final float mSnr; + + Sat(int prn, boolean used, float snr) { + mPrn = prn; + mUsed = used; + mSnr = snr; + if (snr > maxSnr) { + maxSnr = snr + correction; + } else if (snr < minSnr) { + minSnr = snr; + if (minSnr < 0) { + correction = -minSnr; + } + } + } + + static float maxSnr; + private static float minSnr, correction; + } +} diff --git a/app/src/main/java/com/mirfatif/mylocation/MySettings.java b/app/src/main/java/com/mirfatif/mylocation/MySettings.java new file mode 100644 index 0000000..98e61bd --- /dev/null +++ b/app/src/main/java/com/mirfatif/mylocation/MySettings.java @@ -0,0 +1,96 @@ +package com.mirfatif.mylocation; + +import static com.mirfatif.mylocation.Utils.getString; + +import android.content.SharedPreferences; +import java.util.concurrent.TimeUnit; + +public class MySettings { + + public static final MySettings SETTINGS = new MySettings(); + + private final SharedPreferences mPrefs; + + private MySettings() { + mPrefs = Utils.getDefPrefs(); + } + + public boolean getBoolPref(int keyResId, boolean defValue) { + String prefKey = getString(keyResId); + return mPrefs.getBoolean(prefKey, defValue); + } + + public int getIntPref(int keyResId, int defValue) { + String prefKey = getString(keyResId); + return mPrefs.getInt(prefKey, defValue); + } + + @SuppressWarnings("SameParameterValue") + private long getLongPref(int keyResId) { + String prefKey = getString(keyResId); + return mPrefs.getLong(prefKey, 0); + } + + public void savePref(int key, boolean bool) { + String prefKey = getString(key); + mPrefs.edit().putBoolean(prefKey, bool).apply(); + } + + public void savePref(int key, int integer) { + String prefKey = getString(key); + mPrefs.edit().putInt(prefKey, integer).apply(); + } + + @SuppressWarnings("SameParameterValue") + private void savePref(int key, long _long) { + String prefKey = getString(key); + mPrefs.edit().putLong(prefKey, _long).apply(); + } + + public boolean getGpsEnabled() { + return mPrefs.getBoolean(getString(R.string.pref_main_gps_enabled_key), true); + } + + public void setGpsEnabled(boolean enabled) { + mPrefs.edit().putBoolean(getString(R.string.pref_main_gps_enabled_key), enabled).apply(); + } + + public boolean getNetworkEnabled() { + return mPrefs.getBoolean(getString(R.string.pref_main_network_enabled_key), true); + } + + public void setNetworkEnabled(boolean enabled) { + mPrefs.edit().putBoolean(getString(R.string.pref_main_network_enabled_key), enabled).apply(); + } + + public boolean getNlpEnabled() { + return mPrefs.getBoolean(getString(R.string.pref_main_nlp_enabled_key), true); + } + + public void setNlpEnabled(boolean enabled) { + mPrefs.edit().putBoolean(getString(R.string.pref_main_nlp_enabled_key), enabled).apply(); + } + + public boolean shouldAskToSendCrashReport() { + int crashCount = getIntPref(R.string.pref_main_crash_report_count_key, 1); + long lastTS = getLongPref(R.string.pref_main_crash_report_ts_key); + long currTime = System.currentTimeMillis(); + + if (crashCount >= 5 || (currTime - lastTS) >= TimeUnit.DAYS.toMillis(1)) { + savePref(R.string.pref_main_crash_report_ts_key, currTime); + savePref(R.string.pref_main_crash_report_count_key, 1); + return true; + } + + savePref(R.string.pref_main_crash_report_count_key, crashCount + 1); + return false; + } + + public boolean getForceDarkMode() { + return getBoolPref(R.string.pref_main_dark_theme_key, true); + } + + public void setForceDarkMode(boolean force) { + savePref(R.string.pref_main_dark_theme_key, force); + } +} diff --git a/app/src/main/java/com/mirfatif/mylocation/NlpAdapter.java b/app/src/main/java/com/mirfatif/mylocation/NlpAdapter.java new file mode 100644 index 0000000..187fc18 --- /dev/null +++ b/app/src/main/java/com/mirfatif/mylocation/NlpAdapter.java @@ -0,0 +1,138 @@ +package com.mirfatif.mylocation; + +import static com.mirfatif.mylocation.MySettings.SETTINGS; +import static com.mirfatif.mylocation.Utils.getString; +import static com.mirfatif.mylocation.Utils.hasCoarseLocPerm; +import static com.mirfatif.mylocation.Utils.isNaN; +import static com.mirfatif.mylocation.Utils.setTooltip; + +import android.location.Location; +import android.text.format.DateUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import com.mirfatif.mylocation.NlpAdapter.NlpViewHolder; +import com.mirfatif.mylocation.databinding.NlpItemBinding; +import java.util.List; + +public class NlpAdapter extends RecyclerView.Adapter { + + private final List mBackends; + private final NlpClickListener mListener; + + NlpAdapter(NlpClickListener listener, List backends) { + mListener = listener; + mBackends = backends; + } + + @NonNull + @Override + public NlpViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + return new NlpViewHolder(NlpItemBinding.inflate(inflater, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull NlpViewHolder holder, int position) { + holder.bind(position); + } + + @Override + public int getItemCount() { + return mBackends.size(); + } + + protected class NlpViewHolder extends RecyclerView.ViewHolder { + + private final NlpItemBinding mB; + + public NlpViewHolder(NlpItemBinding binding) { + super(binding.getRoot()); + mB = binding; + setTooltip(mB.map); + setTooltip(mB.copy); + mB.map.setOnClickListener( + v -> { + int pos = getBindingAdapterPosition(); + NlpBackend backend; + if (pos != RecyclerView.NO_POSITION + && pos < mBackends.size() + && (backend = mBackends.get(pos)) != null) + mListener.mapClicked(backend.getLocation()); + }); + mB.copy.setOnClickListener( + v -> { + int pos = getBindingAdapterPosition(); + NlpBackend backend; + if (pos != RecyclerView.NO_POSITION + && pos < mBackends.size() + && (backend = mBackends.get(pos)) != null) + mListener.copyClicked(backend.getLocation()); + }); + mB.settings.setOnClickListener( + v -> { + int pos = getBindingAdapterPosition(); + NlpBackend backend; + if (pos != RecyclerView.NO_POSITION + && pos < mBackends.size() + && (backend = mBackends.get(pos)) != null) + mListener.settingsClicked(backend.getPkgName()); + }); + } + + private void bind(int pos) { + NlpBackend backend; + if (pos >= mBackends.size() || (backend = mBackends.get(pos)) == null) { + return; + } + + String state = null, lat = "--", lng = "--", acc = "--", time = "--"; + boolean locAvailable = false; + if (SETTINGS.getNlpEnabled() && hasCoarseLocPerm()) { + if (backend.permsRequired()) { + state = getString(R.string.perm_not_granted); + } else if (backend.failed()) { + state = getString(R.string.failed); + } else { + Location loc = backend.getLocation(); + if (loc != null && !isNaN(loc.getLatitude()) && !isNaN(loc.getLongitude())) { + locAvailable = true; + lat = Utils.formatLatLng(loc.getLatitude()); + lng = Utils.formatLatLng(loc.getLongitude()); + if (!isNaN(loc.getAccuracy()) && loc.getAccuracy() != 0) { + acc = getString(R.string.acc_unit, Utils.formatLocAccuracy(loc.getAccuracy())); + } + long curr = System.currentTimeMillis(); + long t = loc.getTime(); + t = t - Math.max(0, t - curr); + time = DateUtils.getRelativeTimeSpanString(t).toString(); + } + } + } + mB.name.setText(backend.getLabel()); + int vis = state == null ? View.VISIBLE : View.GONE; + mB.map.setVisibility(vis); + mB.copy.setVisibility(vis); + mB.map.setEnabled(locAvailable); + mB.copy.setEnabled(locAvailable); + vis = state == null ? View.GONE : View.VISIBLE; + mB.settings.setVisibility(vis); + mB.stateV.setText(state); + mB.latV.setText(lat); + mB.lngV.setText(lng); + mB.accV.setText(acc); + mB.timeV.setText(time); + } + } + + interface NlpClickListener { + + void mapClicked(Location loc); + + void copyClicked(Location loc); + + void settingsClicked(String pkg); + } +} diff --git a/app/src/main/java/com/mirfatif/mylocation/NlpBackend.java b/app/src/main/java/com/mirfatif/mylocation/NlpBackend.java new file mode 100644 index 0000000..727642b --- /dev/null +++ b/app/src/main/java/com/mirfatif/mylocation/NlpBackend.java @@ -0,0 +1,183 @@ +package com.mirfatif.mylocation; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.ServiceInfo; +import android.location.Location; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; +import java.util.concurrent.Future; +import org.microg.nlp.api.LocationBackend; +import org.microg.nlp.api.LocationBackend.Stub; +import org.microg.nlp.api.LocationCallback; + +public class NlpBackend { + + private static final String TAG = "NlpBackend"; + + private final ServiceInfo mInfo; + private final String mLabel; + + NlpBackend(ServiceInfo info) { + mInfo = info; + mLabel = info.loadLabel(App.getCxt().getPackageManager()).toString(); + } + + void start() { + Utils.runBg(this::bind); + } + + private SvcConnection mConnection; + private boolean mBound; + private Future mInitializedSetter; + private final Object INITIALIZED_SETTER_WAITER = new Object(); + + private void bind() { + Intent intent = new Intent().setClassName(mInfo.packageName, mInfo.name); + mConnection = new SvcConnection(); + mBound = App.getCxt().bindService(intent, mConnection, Context.BIND_AUTO_CREATE); + if (!mBound) { + mConnection = null; + } + + if (mInitializedSetter != null) { + mInitializedSetter.cancel(true); + } + mInitializedSetter = + Utils.runBg( + () -> { + synchronized (INITIALIZED_SETTER_WAITER) { + try { + INITIALIZED_SETTER_WAITER.wait(5000); + mInitialized = true; + } catch (InterruptedException ignored) { + } + } + }); + } + + public String getPkgName() { + return mInfo.packageName; + } + + String getLabel() { + return mLabel; + } + + private Location mLoc; + + Location getLocation() { + return mLoc; + } + + private boolean mInitialized = false; + + boolean failed() { + return mInitialized && (!mBound || !mConnected); + } + + private boolean mPermsRequired; + + boolean permsRequired() { + return mPermsRequired; + } + + private long mLastCall; + + void refresh() { + long curr = System.currentTimeMillis(); + if (((mLoc == null && (curr - mLastCall) > 5000) + || (mLoc != null && (curr - mLastCall) > 30000)) + && mSvc != null + && !failed() + && !mPermsRequired) { + mLastCall = curr; + Utils.runBg( + () -> { + try { + Location loc = mSvc.update(); + if (loc != null) { + mLoc = loc; + } + } catch (RemoteException e) { + Log.e(TAG, mLabel + ": " + e.toString()); + cleanUp(); + } + }); + } + } + + void stop() { + cleanUp(); + if (mInitializedSetter != null) { + mInitializedSetter.cancel(true); + mInitializedSetter = null; + } + mInitialized = false; + } + + private void cleanUp() { + if (mSvc != null) { + try { + mSvc.close(); + } catch (RemoteException ignored) { + } + mSvc = null; + } + if (mConnection != null) { + App.getCxt().unbindService(mConnection); + mConnection = null; + } + mInitialized = true; + mBound = mConnected = false; + mLoc = null; + } + + private LocationBackend mSvc; + private boolean mConnected = false; + + private class SvcConnection implements ServiceConnection { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + mSvc = Stub.asInterface(service); + try { + mSvc.open(new Callback()); + mPermsRequired = mSvc.getInitIntent() != null; + Location loc = mSvc.update(); + if (loc != null) { + mLoc = loc; + } + mConnected = mInitialized = true; + } catch (RemoteException | SecurityException e) { + Log.e(TAG, mLabel + ": " + e.toString()); + cleanUp(); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + mConnected = false; + } + + @Override + public void onBindingDied(ComponentName name) { + cleanUp(); + } + + @Override + public void onNullBinding(ComponentName name) { + cleanUp(); + } + } + + private class Callback extends LocationCallback.Stub { + + @Override + public void report(Location location) { + mLoc = location; + } + } +} diff --git a/app/src/main/java/com/mirfatif/mylocation/NotifDismissSvc.java b/app/src/main/java/com/mirfatif/mylocation/NotifDismissSvc.java new file mode 100644 index 0000000..4bb1f6f --- /dev/null +++ b/app/src/main/java/com/mirfatif/mylocation/NotifDismissSvc.java @@ -0,0 +1,41 @@ +package com.mirfatif.mylocation; + +import android.app.NotificationManager; +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; + +public class NotifDismissSvc extends Service { + + public static final String EXTRA_INTENT_TYPE = BuildConfig.APPLICATION_ID + ".extra.INTENT_TYPE"; + public static final String EXTRA_NOTIF_ID = BuildConfig.APPLICATION_ID + ".extra.NOTIF_ID"; + public static final int INTENT_TYPE_ACTIVITY = 1; + public static final int INTENT_TYPE_SERVICE = 2; + private static final int NONE = -1; + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + int type = intent.getIntExtra(EXTRA_INTENT_TYPE, NONE); + int id = intent.getIntExtra(EXTRA_NOTIF_ID, NONE); + if (type != NONE && id != NONE) { + NotificationManager.from(App.getCxt()).cancel(id); + intent.setComponent(null); + intent.removeExtra(EXTRA_INTENT_TYPE); + intent.removeExtra(EXTRA_NOTIF_ID); + if (type == INTENT_TYPE_ACTIVITY) { + // FLAG_ACTIVITY_NEW_TASK is required to start Activity from outside an Activity + intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + } else if (type == INTENT_TYPE_SERVICE) { + startService(intent); + } + } + stopSelf(startId); // Stop if no pending requests + return Service.START_NOT_STICKY; + } +} diff --git a/app/src/main/java/com/mirfatif/mylocation/SatAdapter.java b/app/src/main/java/com/mirfatif/mylocation/SatAdapter.java new file mode 100644 index 0000000..19ac579 --- /dev/null +++ b/app/src/main/java/com/mirfatif/mylocation/SatAdapter.java @@ -0,0 +1,80 @@ +package com.mirfatif.mylocation; + +import android.content.res.ColorStateList; +import android.graphics.Color; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.core.graphics.ColorUtils; +import androidx.recyclerview.widget.RecyclerView; +import com.mirfatif.mylocation.MainActivity.Sat; +import com.mirfatif.mylocation.SatAdapter.SatViewHolder; +import com.mirfatif.mylocation.databinding.SatItemBinding; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +public class SatAdapter extends RecyclerView.Adapter { + + private final List mSats = new ArrayList<>(); + + void submitList(List sats) { + synchronized (mSats) { + mSats.clear(); + mSats.addAll(sats); + notifyDataSetChanged(); + } + } + + @Override + public int getItemCount() { + return mSats.size(); + } + + @NonNull + @Override + public SatViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + return new SatViewHolder(SatItemBinding.inflate(inflater, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull SatViewHolder holder, int position) { + holder.bind(position); + } + + protected class SatViewHolder extends RecyclerView.ViewHolder { + + private final SatItemBinding mB; + + public SatViewHolder(@NonNull SatItemBinding binding) { + super(binding.getRoot()); + mB = binding; + } + + private void bind(int pos) { + Sat sat; + if (pos >= mSats.size() || (sat = mSats.get(pos)) == null) { + return; + } + mB.idV.setText(String.valueOf(sat.mPrn)); + + int strength = Math.min((int) (100 * sat.mSnr / Sat.maxSnr), 100); + if (strength < 5) { + strength = ThreadLocalRandom.current().nextInt(0, 3); + } + mB.progV.setProgress(strength); + float ratio = Math.max(Math.min((float) strength / 100, 1), 0); + int color = ColorUtils.blendARGB(Color.RED, Color.GREEN, ratio); + mB.progV.setProgressTintList(ColorStateList.valueOf(color)); + + mB.signalV.setText(String.valueOf(sat.mSnr)); + if (sat.mUsed) { + mB.fixedV.setVisibility(View.VISIBLE); + } else { + mB.fixedV.setVisibility(View.GONE); + } + } + } +} diff --git a/app/src/main/java/com/mirfatif/mylocation/SatsDialogFragment.java b/app/src/main/java/com/mirfatif/mylocation/SatsDialogFragment.java new file mode 100644 index 0000000..357fbf3 --- /dev/null +++ b/app/src/main/java/com/mirfatif/mylocation/SatsDialogFragment.java @@ -0,0 +1,65 @@ +package com.mirfatif.mylocation; + +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnDismissListener; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AlertDialog.Builder; +import androidx.appcompat.app.AppCompatDialogFragment; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.LinearLayoutManager; +import com.mirfatif.mylocation.MainActivity.Sat; +import com.mirfatif.mylocation.databinding.RvSatsBinding; +import java.util.List; + +public class SatsDialogFragment extends AppCompatDialogFragment { + + public SatsDialogFragment() {} + + private MainActivity mA; + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + mA = (MainActivity) getActivity(); + } + + private SatAdapter mAdapter; + + synchronized void submitList(List satList) { + mAdapter.submitList(satList); + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + RvSatsBinding b = RvSatsBinding.inflate(mA.getLayoutInflater()); + + mAdapter = new SatAdapter(); + b.rv.setAdapter(mAdapter); + LinearLayoutManager layoutManager = new LinearLayoutManager(mA); + b.rv.setLayoutManager(layoutManager); + b.rv.addItemDecoration(new DividerItemDecoration(mA, DividerItemDecoration.VERTICAL)); + + AlertDialog d = new Builder(mA).setTitle(R.string.satellites).setView(b.getRoot()).create(); + return Utils.setDialogBg(d); + } + + // We cannot use Dialog's OnDismiss and OnCancel Listeners, DialogFragment owns them. + private OnDismissListener mDismissListener; + + public void setOnDismissListener(OnDismissListener dismissListener) { + mDismissListener = dismissListener; + } + + @Override + public void onDismiss(@NonNull DialogInterface dialog) { + super.onDismiss(dialog); + if (mDismissListener != null) { + mDismissListener.onDismiss(dialog); + } + } +} diff --git a/app/src/main/java/com/mirfatif/mylocation/Utils.java b/app/src/main/java/com/mirfatif/mylocation/Utils.java new file mode 100644 index 0000000..ea40fd1 --- /dev/null +++ b/app/src/main/java/com/mirfatif/mylocation/Utils.java @@ -0,0 +1,480 @@ +package com.mirfatif.mylocation; + +import static android.Manifest.permission.ACCESS_COARSE_LOCATION; +import static android.Manifest.permission.ACCESS_FINE_LOCATION; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static com.mirfatif.mylocation.MySettings.SETTINGS; + +import android.app.Activity; +import android.app.NotificationManager.Importance; +import android.app.PendingIntent; +import android.content.ActivityNotFoundException; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Configuration; +import android.location.Location; +import android.net.Uri; +import android.os.Build; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import android.provider.Settings; +import android.util.Log; +import android.view.Window; +import android.widget.ImageView; +import android.widget.Toast; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatDelegate; +import androidx.appcompat.widget.TooltipCompat; +import androidx.browser.customtabs.CustomTabColorSchemeParams; +import androidx.browser.customtabs.CustomTabsIntent; +import androidx.browser.customtabs.CustomTabsService; +import androidx.core.app.ActivityCompat; +import androidx.core.app.NotificationChannelCompat; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; +import androidx.core.content.FileProvider; +import androidx.security.crypto.EncryptedSharedPreferences; +import androidx.security.crypto.EncryptedSharedPreferences.PrefKeyEncryptionScheme; +import androidx.security.crypto.EncryptedSharedPreferences.PrefValueEncryptionScheme; +import androidx.security.crypto.MasterKey; +import androidx.security.crypto.MasterKey.KeyScheme; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.text.DecimalFormat; +import java.text.SimpleDateFormat; +import java.util.List; +import java.util.Locale; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +public class Utils { + + private static final String TAG = "Utils"; + + private Utils() {} + + public static String getString(int resId, Object... args) { + return App.getCxt().getString(resId, args); + } + + public static int getInteger(int resId) { + return App.getCxt().getResources().getInteger(resId); + } + + public static boolean isNaN(double d) { + return d != d; + } + + public static void copyLoc(Location location) { + if (location != null) { + ClipboardManager clipboard = + (ClipboardManager) App.getCxt().getSystemService(Context.CLIPBOARD_SERVICE); + String loc = location.getLatitude() + "," + location.getLongitude(); + ClipData data = ClipData.newPlainText("location", loc); + clipboard.setPrimaryClip(data); + Utils.showShortToast(R.string.copied); + } + } + + public static void openMap(Activity act, Location location) { + if (location != null) { + String loc = location.getLatitude() + "," + location.getLongitude(); + try { + act.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("geo:" + loc + "?q=" + loc))); + } catch (ActivityNotFoundException ignored) { + Utils.showToast(R.string.no_maps_installed); + } + } + } + + public static void openAppSettings(Activity act, String pkg) { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData(Uri.parse("package:" + pkg)); + try { + act.startActivity(intent); + } catch (ActivityNotFoundException ignored) { + Utils.showToast(R.string.failed_open_app_settings); + } + } + + public static boolean openWebUrl(Activity activity, String url) { + Intent intent = new Intent(CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION); + PackageManager pm = App.getCxt().getPackageManager(); + int flags = VERSION.SDK_INT >= VERSION_CODES.M ? PackageManager.MATCH_ALL : 0; + List infoList = pm.queryIntentServices(intent, flags); + boolean customTabsSupported = !infoList.isEmpty(); + + if (customTabsSupported) { + CustomTabColorSchemeParams colorSchemeParams = + new CustomTabColorSchemeParams.Builder() + .setToolbarColor(App.getRes().getColor(R.color.primary)) + .build(); + CustomTabsIntent customTabsIntent = + new CustomTabsIntent.Builder() + .setShareState(CustomTabsIntent.SHARE_STATE_ON) + .setDefaultColorSchemeParams(colorSchemeParams) + .build(); + customTabsIntent.launchUrl(activity, Uri.parse(url)); + return true; + } + + intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + intent.addCategory(Intent.CATEGORY_BROWSABLE).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + try { + activity.startActivity(intent); + return true; + } catch (ActivityNotFoundException ignored) { + } + + if (VERSION.SDK_INT >= VERSION_CODES.R) { + intent.setFlags(Intent.FLAG_ACTIVITY_REQUIRE_NON_BROWSER); + try { + activity.startActivity(intent); + return true; + } catch (ActivityNotFoundException ignored) { + } + } + + showToast(R.string.no_browser_installed); + return true; + } + + public static void sendMail(Activity activity, String body) { + Intent emailIntent = new Intent(Intent.ACTION_SENDTO).setData(Uri.parse("mailto:")); + emailIntent.putExtra(Intent.EXTRA_EMAIL, new String[] {getString(R.string.email_address)}); + emailIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.app_name)); + if (body != null) { + emailIntent.putExtra(Intent.EXTRA_TEXT, body); + } + try { + activity.startActivity(emailIntent); + } catch (ActivityNotFoundException e) { + showToast(R.string.no_email_app_installed); + } + } + + ////////////////////////////////////////////////////////////////// + /////////////////////////// FORMATTING /////////////////////////// + ////////////////////////////////////////////////////////////////// + + private static final DecimalFormat mLatLngFormat = new DecimalFormat(); + + static { + mLatLngFormat.setMaximumFractionDigits(5); + } + + public static String formatLatLng(double coordinate) { + return mLatLngFormat.format(coordinate); + } + + public static String formatLocAccuracy(float accuracy) { + return String.format(Locale.getDefault(), "%.0f", accuracy); + } + + public static String getDeviceInfo() { + return "Version: " + + BuildConfig.VERSION_NAME + + "\nSDK: " + + VERSION.SDK_INT + + "\nROM: " + + Build.DISPLAY + + "\nBuild: " + + Build.TYPE + + "\nDevice: " + + Build.DEVICE + + "\nManufacturer: " + + Build.MANUFACTURER + + "\nModel: " + + Build.MODEL + + "\nProduct: " + + Build.PRODUCT; + } + + public static String getCurrDateTime(boolean spaced) { + if (spaced) { + return new SimpleDateFormat("dd-MMM-yy HH:mm:ss", Locale.ENGLISH) + .format(System.currentTimeMillis()); + } else { + return new SimpleDateFormat("dd-MMM-yy_HH-mm-ss", Locale.ENGLISH) + .format(System.currentTimeMillis()); + } + } + + ////////////////////////////////////////////////////////////////// + ////////////////////////////// PERMS ///////////////////////////// + ////////////////////////////////////////////////////////////////// + + public static boolean hasFineLocPerm() { + return hasPerm(ACCESS_FINE_LOCATION); + } + + public static boolean hasCoarseLocPerm() { + return hasPerm(ACCESS_COARSE_LOCATION); + } + + public static boolean hasPerm(String perm) { + return ActivityCompat.checkSelfPermission(App.getCxt(), perm) == PERMISSION_GRANTED; + } + + public static SharedPreferences getDefPrefs() { + return App.getCxt().getSharedPreferences("def_prefs", Context.MODE_PRIVATE); + } + + private static SharedPreferences mEncPrefs; + private static final Object ENC_PREFS_LOCK = new Object(); + + @SuppressWarnings("UnusedReturnValue") + public static SharedPreferences getEncPrefs() { + synchronized (ENC_PREFS_LOCK) { + if (mEncPrefs != null) { + return mEncPrefs; + } + + for (int i = 0; i < 10; i++) { + try { + mEncPrefs = + EncryptedSharedPreferences.create( + App.getCxt(), + BuildConfig.APPLICATION_ID + "_nb_prefs", + new MasterKey.Builder(App.getCxt()).setKeyScheme(KeyScheme.AES256_GCM).build(), + PrefKeyEncryptionScheme.AES256_SIV, + PrefValueEncryptionScheme.AES256_GCM); + return mEncPrefs; + } catch (Exception e) { + if (i == 9) { + e.printStackTrace(); + } else { + Log.e(TAG, "getEncPrefs: " + e.toString()); + } + SystemClock.sleep(100); + } + } + + // Temp fix for https://github.com/google/tink/issues/413 + mEncPrefs = App.getCxt().getSharedPreferences("_nb_prefs2", Context.MODE_PRIVATE); + return mEncPrefs; + } + } + + ////////////////////////////////////////////////////////////////// + //////////////////////////// EXECUTORS /////////////////////////// + ////////////////////////////////////////////////////////////////// + + private static final Handler UI_EXECUTOR = new Handler(Looper.getMainLooper()); + + public static UiRunnable runUi(Runnable runnable) { + UiRunnable uiRunnable = new UiRunnable(runnable); + UI_EXECUTOR.post(uiRunnable); + return uiRunnable; + } + + public static class UiRunnable implements Runnable { + + private final Runnable mRunnable; + + UiRunnable(Runnable runnable) { + mRunnable = runnable; + } + + @Override + public void run() { + mRunnable.run(); + mDone = true; + synchronized (WAITER) { + WAITER.notify(); + } + } + + private boolean mDone = false; + private final Object WAITER = new Object(); + + public void waitForMe() { + if (Thread.currentThread() == UI_EXECUTOR.getLooper().getThread()) { + Log.e(TAG, "UiRunnable: waitForMe() called on main thread"); + return; + } + synchronized (WAITER) { + while (!mDone) { + try { + WAITER.wait(); + } catch (InterruptedException ignored) { + } + } + } + } + } + + private static final ExecutorService BG_EXECUTOR = Executors.newCachedThreadPool(); + + public static Future runBg(Runnable runnable) { + return BG_EXECUTOR.submit(runnable); + } + + ////////////////////////////////////////////////////////////////// + /////////////////////////////// UI /////////////////////////////// + ////////////////////////////////////////////////////////////////// + + public static void showToast(String msg) { + if (msg != null) { + runUi(() -> showToast(msg, Toast.LENGTH_LONG)); + } + } + + public static void showToast(int resId, Object... args) { + if (resId != 0) { + showToast(getString(resId, args)); + } + } + + public static void showShortToast(int resId, Object... args) { + if (resId != 0) { + runUi(() -> showToast(getString(resId, args), Toast.LENGTH_SHORT)); + } + } + + private static void showToast(String msg, int duration) { + Toast toast = Toast.makeText(App.getCxt(), msg, duration); + toast.show(); + } + + public static void createNotifChannel(String id, String name, @Importance int importance) { + NotificationManagerCompat nm = NotificationManagerCompat.from(App.getCxt()); + NotificationChannelCompat ch = nm.getNotificationChannelCompat(id); + if (ch == null) { + ch = new NotificationChannelCompat.Builder(id, importance).setName(name).build(); + nm.createNotificationChannel(ch); + } + } + + public static void setTooltip(ImageView v) { + TooltipCompat.setTooltipText(v, v.getContentDescription()); + } + + public static boolean isNightMode(Activity activity) { + int uiMode = activity.getResources().getConfiguration().uiMode; + return (uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; + } + + public static boolean setNightTheme(Activity activity) { + if (!SETTINGS.getForceDarkMode()) { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); + return false; + } + + // Dark Mode applied on whole device + if (isNightMode(activity)) { + return false; + } + + // Dark Mode already applied in app + int defMode = AppCompatDelegate.getDefaultNightMode(); + if (defMode == AppCompatDelegate.MODE_NIGHT_YES) { + return false; + } + + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); + return true; + } + + public static AlertDialog setDialogBg(AlertDialog dialog) { + Window window = dialog.getWindow(); + if (window != null) { + window.setBackgroundDrawableResource(R.drawable.alert_dialog_bg_bordered); + window.setWindowAnimations(android.R.style.Animation_Dialog); + } + return dialog; + } + + ////////////////////////////////////////////////////////////////// + ///////////////////////////// LOGGING //////////////////////////// + ////////////////////////////////////////////////////////////////// + + private static final Object CRASH_LOG_LOCK = new Object(); + + public static void writeCrashLog(String stackTrace) { + synchronized (CRASH_LOG_LOCK) { + File logFile = new File(App.getCxt().getExternalFilesDir(null), "MyLocation_crash.log"); + boolean append = true; + if (!logFile.exists() + || logFile.length() > 512 * 1024 + || logFile.lastModified() < System.currentTimeMillis() - TimeUnit.DAYS.toMillis(90)) { + append = false; + } + try { + PrintWriter writer = new PrintWriter(new FileWriter(logFile, append)); + writer.println("================================="); + writer.println(getDeviceInfo()); + writer.println("Time: " + getCurrDateTime(true)); + writer.println("Log ID: " + UUID.randomUUID().toString()); + writer.println("================================="); + writer.println(stackTrace); + writer.close(); + showCrashNotification(logFile); + } catch (IOException ignored) { + } + } + } + + private static void showCrashNotification(File logFile) { + if (!SETTINGS.shouldAskToSendCrashReport()) { + return; + } + + String authority = BuildConfig.APPLICATION_ID + ".FileProvider"; + Uri logFileUri = FileProvider.getUriForFile(App.getCxt(), authority, logFile); + + final String CHANNEL_ID = "channel_crash_report"; + final String CHANNEL_NAME = getString(R.string.channel_crash_report); + final int UNIQUE_ID = getInteger(R.integer.channel_crash_report); + + Intent intent = new Intent(Intent.ACTION_SEND); + intent + .setData(logFileUri) + .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + .setType("text/plain") + .putExtra(Intent.EXTRA_EMAIL, new String[] {getString(R.string.email_address)}) + .putExtra(Intent.EXTRA_SUBJECT, getString(R.string.app_name) + " - Crash Report") + .putExtra(Intent.EXTRA_TEXT, "Find attachment.") + .putExtra(Intent.EXTRA_STREAM, logFileUri); + + // Adding extra information to dismiss notification after the action is tapped + intent + .setClass(App.getCxt(), NotifDismissSvc.class) + .putExtra(NotifDismissSvc.EXTRA_INTENT_TYPE, NotifDismissSvc.INTENT_TYPE_ACTIVITY) + .putExtra(NotifDismissSvc.EXTRA_NOTIF_ID, UNIQUE_ID); + + PendingIntent pi = + PendingIntent.getService( + App.getCxt(), UNIQUE_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT); + createNotifChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManagerCompat.IMPORTANCE_HIGH); + + NotificationCompat.Builder nb = + new NotificationCompat.Builder(App.getCxt(), CHANNEL_ID) + .setSmallIcon(R.drawable.notification_icon) + .setContentTitle(getString(R.string.crash_report)) + .setContentText(getString(R.string.ask_to_report_crash_small)) + .setStyle( + new NotificationCompat.BigTextStyle() + .bigText(getString(R.string.ask_to_report_crash))) + .setContentIntent(pi) + .addAction(0, getString(R.string.send_report), pi) + .setDefaults(NotificationCompat.DEFAULT_LIGHTS) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setAutoCancel(true); + + NotificationManagerCompat.from(App.getCxt()).notify(UNIQUE_ID, nb.build()); + } +} diff --git a/app/src/main/res/drawable-hdpi/app_name.webp b/app/src/main/res/drawable-hdpi/app_name.webp new file mode 100644 index 0000000000000000000000000000000000000000..b5551657209cbd1313ce56cab0916b0b1e3e4bc1 GIT binary patch literal 1060 zcmV+<1l#*kNk&E-1ONb6MM6+kP&iBv1ONapr$7o2|HL?wq&9P=iZ{V2|4>xtfDjNh zlA1Xn;f=WZmr_KHq-G9Ccq6X~Q90Q=r!-?4V_ z8LS(vLJ~rNBuJ8g_{Swl0iXadBuViHN|1oF8iiJU(38NE1^f-@DoJMLfYh#x2W6xS z{r{U*E!cq_T@nv?Nfe}UW!Ea#Dp|LDMr&95PLOunwkbKgDzQrr_F{khT#R8ZYSrP!Wsb(=d>^ zzF-HyT}e}uK!dR z6Tpf<8dm{q3fmhsY&*D%0vmycEhr*#~-t|+w$P66rNiw-R-kVb2! z>%Mg%r;Y?9I{dU2I0EccBm+ahb*nD{9M(V*xan~c80waE!LKZrmjG?n39Bm-?ccIx zzjBxbi~zIpQS>b|&@N!GQ_=x|Y0@2Fc#ajkmyQ70m=s$bb{a_b<}jfIxD!a@9|6^h zCe%x{*j37h=LEGUfuf~{?gOll)KLuZ;QE!@+v=rr0K4a$t|P1kV6eJoosSLyY&^LC z2~c{e@Gd%DM5T*Ggzxw*Ni&}kS}(;n;9e1?wK#{of}4bwyilHuF=npMTw{z0Q8lpr zcTOvecaNlri?UW?-C4PNFIPCq4omvEcNnNR_sJMzI<5XAYb%T~Po%&YV`j^KyCZ3( z_qNMkXpC89y*0*|0Y!ZjIHZE?Zk;jpx4eJ0&G_6S_n(gGT`I)*mk%x9G&(1iL#n4R e{_3B$Y)t@1_0RhOdui<7r{nhRTZdcp-(mn1vI4RI literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/icon.webp b/app/src/main/res/drawable-hdpi/icon.webp new file mode 100644 index 0000000000000000000000000000000000000000..8165805c30277bc0ea9476ee2b401b814c91fed4 GIT binary patch literal 8548 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 Uh#;??0tjFf^UAR=5dLi?0CU{HPXGV_ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/action_bar_icon.webp b/app/src/main/res/drawable-xxhdpi/action_bar_icon.webp new file mode 100644 index 0000000000000000000000000000000000000000..58ee25029085a7982995ea01858f16c7113ff488 GIT binary patch literal 2652 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 KBdV$@2T&JkVnka2 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/app_name.webp b/app/src/main/res/drawable-xxhdpi/app_name.webp new file mode 100644 index 0000000000000000000000000000000000000000..a8ae64a78558bb03245d44c13c04f31736119e61 GIT binary patch literal 1810 zcmV+t2krP$Nk&Er2LJ$9MM6+kP&iEf1^@srPr(!r&!s4mq-OW-fH&gsFC}LTC~DhC zO6GhIxaALJ6}4?7C3C(9-13LA1|rGe=6i{*m^o_OwqcMmxxN7?K&SVJcZ6N63e7|_ zLxl=}{wod;04+e9W-ukV8i{y-pNBd}l>h-`j*$OviP%*ssvSqYrc?t^ts@a?rdDOx zHQovO+qNxFn-ShY5z2EOmJ~*16wut^u0R8{} z|90-)0DFT)Aq>mr76;45JG3~gEE}SudWK_vT>Q6|GyJD;GfTW@{;@gboRfbHV=T^@ zf4EL=I|qSMTrwfae>$!yxN|P!2AQq=r!!Y;8gnCKYH)}*p+=r}es@2$SC>cZc(P`J zy1|yZn#l6%&`qnHT1MWt*4I>5?5+y?=ViZFckaHX?co)Xp^w-7@K@iKnqB8=yIhJa zqK5{NJ56NKP{rcJ?^5m<78mHr@i@!G*mTmqNP&A@;S@_IWn5SeUSG~iVENhn%?nUJ zt3BWo!mv!v+mdHt$+5^UazhNmiCjCi-J_voTq}CMaO(HMa`s%E`u}!dd%Xu-+K8L%aWFHiv&-7 z0VAm_n`9feC@`hvUp^mpuDs}!2ThLzp7%aWnp5H=*wm&?Wv>c%nt<03wX-nIt>CLk z#BC@OM37$gGxU`95z*&MCcw8qA>*0y)h;6XxwgV4GqvmWFTw-20RQDPYPZ|%aUj(W z*DKn_IJeVC^xZxUIOVsB^lja}EqM}=DyUJEN0cpUBn5O8B181O)#6`^{<-bug%nmz)`1Rl4N zkYs^}g~=g`K6yl@A)l8l&LX{hV`wAUXu$nMG{|tqrR>Mn!Z=ph9cVFVsRZpu&$OH- zd=#)l3DpW({SeYxF{iAki%ohU{9xB})B8;V7K^K)E7rS;}yaBI-*5YzI#mdb$OD9>8NOB^K1! zeP|XW8%4&$tl$pdxBhV^1HNt)t+t?TdeW?l9>}G-14ZEqBBC?bS-cg^vY*ujran`lnyLa!tpokg~XSVzxV_&41;c4&O*D6;_ z`udTIn0c0EV}`-P3?wwOP!x_h;qaBcnI2ZsE+LLnWw?!)d6s3(T{QO@PRUptI-|h} z8EIQT;gN@OQ1oL8LC<{!hSLE;&U-S_rdKo%P_xV~(i;sPQ*z=n$^Ix(N3UnKY^zs5 zD`x_P99NaZtfhIuorXLE`Xl$h$(rKM7)fJWhBA5+D`nYCN~rICfDpZYsL5QmkOJ#h z6uhSDS`>6?p=T(c{Jtql{bxYrZ7HxxSPwV3NMXKQ+~!sc-%0ra5w?{rTfi`_q>nyfmLWETVJ@7N2EtW01(T z)g+4c27lIFzg}>Bn&CzY{?@W|0R@>d96_erKPi}$c&m+q!vh<9J3o2p|Ns97b-3MU AHUIzs literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/icon.webp b/app/src/main/res/drawable-xxhdpi/icon.webp new file mode 100644 index 0000000000000000000000000000000000000000..8c75d6fdffbef7383902f6d3bcff4c4d2160d814 GIT binary patch literal 14384 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/alert_dialog_bg_bordered.xml b/app/src/main/res/drawable/alert_dialog_bg_bordered.xml new file mode 100644 index 0000000..1779355 --- /dev/null +++ b/app/src/main/res/drawable/alert_dialog_bg_bordered.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/bitcoin.xml b/app/src/main/res/drawable/bitcoin.xml new file mode 100644 index 0000000..45b2e0f --- /dev/null +++ b/app/src/main/res/drawable/bitcoin.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/bitcoin_qr_code.webp b/app/src/main/res/drawable/bitcoin_qr_code.webp new file mode 100644 index 0000000000000000000000000000000000000000..06c5f9ef9cd17163a416ffe655cc21924adfd322 GIT binary patch literal 6822 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 _#3FiPKN1Bnp%OJwA_rh!N8{;qe+2~0hnfG= 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 z + + diff --git a/app/src/main/res/drawable/dark_mode.xml b/app/src/main/res/drawable/dark_mode.xml new file mode 100644 index 0000000..8e1ac01 --- /dev/null +++ b/app/src/main/res/drawable/dark_mode.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/dollar.xml b/app/src/main/res/drawable/dollar.xml new file mode 100644 index 0000000..a11312c --- /dev/null +++ b/app/src/main/res/drawable/dollar.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/donate.xml b/app/src/main/res/drawable/donate.xml new file mode 100644 index 0000000..7820841 --- /dev/null +++ b/app/src/main/res/drawable/donate.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/email.xml b/app/src/main/res/drawable/email.xml new file mode 100644 index 0000000..bfa837f --- /dev/null +++ b/app/src/main/res/drawable/email.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/github_mark.xml b/app/src/main/res/drawable/github_mark.xml new file mode 100644 index 0000000..c05a644 --- /dev/null +++ b/app/src/main/res/drawable/github_mark.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/app/src/main/res/drawable/info.xml b/app/src/main/res/drawable/info.xml new file mode 100644 index 0000000..8553aae --- /dev/null +++ b/app/src/main/res/drawable/info.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/loc_settings.xml b/app/src/main/res/drawable/loc_settings.xml new file mode 100644 index 0000000..9fff5f1 --- /dev/null +++ b/app/src/main/res/drawable/loc_settings.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/lock.xml b/app/src/main/res/drawable/lock.xml new file mode 100644 index 0000000..f0ab1ce --- /dev/null +++ b/app/src/main/res/drawable/lock.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/map.xml b/app/src/main/res/drawable/map.xml new file mode 100644 index 0000000..05b7419 --- /dev/null +++ b/app/src/main/res/drawable/map.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/notification_icon.xml b/app/src/main/res/drawable/notification_icon.xml new file mode 100644 index 0000000..370bcab --- /dev/null +++ b/app/src/main/res/drawable/notification_icon.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/play_store.xml b/app/src/main/res/drawable/play_store.xml new file mode 100644 index 0000000..33d7199 --- /dev/null +++ b/app/src/main/res/drawable/play_store.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/privacy_policy.xml b/app/src/main/res/drawable/privacy_policy.xml new file mode 100644 index 0000000..ed323e7 --- /dev/null +++ b/app/src/main/res/drawable/privacy_policy.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/progress.xml b/app/src/main/res/drawable/progress.xml new file mode 100644 index 0000000..0489cdf --- /dev/null +++ b/app/src/main/res/drawable/progress.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/drawable/rectangular_border.xml b/app/src/main/res/drawable/rectangular_border.xml new file mode 100644 index 0000000..f497137 --- /dev/null +++ b/app/src/main/res/drawable/rectangular_border.xml @@ -0,0 +1,7 @@ + + + + diff --git a/app/src/main/res/drawable/round_border.xml b/app/src/main/res/drawable/round_border.xml new file mode 100644 index 0000000..27bf79a --- /dev/null +++ b/app/src/main/res/drawable/round_border.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/app/src/main/res/drawable/round_button_bg.xml b/app/src/main/res/drawable/round_button_bg.xml new file mode 100644 index 0000000..127b967 --- /dev/null +++ b/app/src/main/res/drawable/round_button_bg.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/round_ripple.xml b/app/src/main/res/drawable/round_ripple.xml new file mode 100644 index 0000000..4abb309 --- /dev/null +++ b/app/src/main/res/drawable/round_ripple.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/satellite.xml b/app/src/main/res/drawable/satellite.xml new file mode 100644 index 0000000..c8b34f6 --- /dev/null +++ b/app/src/main/res/drawable/satellite.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/app/src/main/res/drawable/settings.xml b/app/src/main/res/drawable/settings.xml new file mode 100644 index 0000000..69aa928 --- /dev/null +++ b/app/src/main/res/drawable/settings.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/share.xml b/app/src/main/res/drawable/share.xml new file mode 100644 index 0000000..d62d94c --- /dev/null +++ b/app/src/main/res/drawable/share.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/splash_background.xml b/app/src/main/res/drawable/splash_background.xml new file mode 100644 index 0000000..a47c444 --- /dev/null +++ b/app/src/main/res/drawable/splash_background.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/star.xml b/app/src/main/res/drawable/star.xml new file mode 100644 index 0000000..0afc060 --- /dev/null +++ b/app/src/main/res/drawable/star.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/telegram.xml b/app/src/main/res/drawable/telegram.xml new file mode 100644 index 0000000..e863a37 --- /dev/null +++ b/app/src/main/res/drawable/telegram.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/update.xml b/app/src/main/res/drawable/update.xml new file mode 100644 index 0000000..e234018 --- /dev/null +++ b/app/src/main/res/drawable/update.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/layout/about_dialog.xml b/app/src/main/res/layout/about_dialog.xml new file mode 100644 index 0000000..b59f522 --- /dev/null +++ b/app/src/main/res/layout/about_dialog.xml @@ -0,0 +1,234 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..3adb466 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,68 @@ + + + + + +