From e57df974d6d90635c1a15c60e73c10deea68b775 Mon Sep 17 00:00:00 2001 From: Mohit Date: Sun, 7 Mar 2021 18:20:35 +0530 Subject: [PATCH] Initial Commit --- .gitignore | 13 + COPYING | 674 +++++++++ README.md | 43 + build.gradle | 128 ++ extra/launcher-make.sh | 27 + extra/launcher.svg | 273 ++++ gradle.properties | 1 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 55190 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 +++ gradlew.bat | 84 ++ metadata/en-US/changelogs/1.txt | 2 + metadata/en-US/changelogs/2.txt | 3 + metadata/en-US/changelogs/3.txt | 3 + metadata/en-US/changelogs/4.txt | 4 + metadata/en-US/full_description.txt | 10 + metadata/en-US/images/screenshots/1.png | Bin 0 -> 101883 bytes metadata/en-US/images/screenshots/2.png | Bin 0 -> 99808 bytes metadata/en-US/short_description.txt | 1 + proguard.pro | 5 + src/main/AndroidManifest.xml | 94 ++ src/main/ic_launcher-playstore.png | Bin 0 -> 32435 bytes src/main/kotlin/com/looker/droidify/Common.kt | 13 + .../com/looker/droidify/MainActivity.kt | 21 + .../com/looker/droidify/MainApplication.kt | 187 +++ .../com/looker/droidify/content/Cache.kt | 179 +++ .../looker/droidify/content/Preferences.kt | 151 ++ .../droidify/content/ProductPreferences.kt | 64 + .../looker/droidify/database/CursorOwner.kt | 101 ++ .../com/looker/droidify/database/Database.kt | 659 +++++++++ .../droidify/database/ObservableCursor.kt | 57 + .../looker/droidify/database/QueryBuilder.kt | 47 + .../looker/droidify/database/QueryLoader.kt | 94 ++ .../looker/droidify/entity/InstalledItem.kt | 3 + .../com/looker/droidify/entity/Product.kt | 227 +++ .../com/looker/droidify/entity/ProductItem.kt | 79 + .../droidify/entity/ProductPreference.kt | 31 + .../com/looker/droidify/entity/Release.kt | 156 ++ .../com/looker/droidify/entity/Repository.kt | 124 ++ .../droidify/graphics/DrawableWrapper.kt | 56 + .../droidify/graphics/PaddingDrawable.kt | 19 + .../com/looker/droidify/index/IndexHandler.kt | 265 ++++ .../com/looker/droidify/index/IndexMerger.kt | 83 ++ .../looker/droidify/index/IndexV1Parser.kt | 260 ++++ .../droidify/index/RepositoryUpdater.kt | 345 +++++ .../com/looker/droidify/network/Downloader.kt | 114 ++ .../droidify/network/PicassoDownloader.kt | 118 ++ .../droidify/screen/EditRepositoryFragment.kt | 484 ++++++ .../looker/droidify/screen/MessageDialog.kt | 231 +++ .../droidify/screen/PreferencesFragment.kt | 296 ++++ .../looker/droidify/screen/ProductAdapter.kt | 1305 +++++++++++++++++ .../looker/droidify/screen/ProductFragment.kt | 475 ++++++ .../looker/droidify/screen/ProductsAdapter.kt | 184 +++ .../droidify/screen/ProductsFragment.kt | 181 +++ .../droidify/screen/RepositoriesAdapter.kt | 61 + .../droidify/screen/RepositoriesFragment.kt | 73 + .../droidify/screen/RepositoryFragment.kt | 166 +++ .../looker/droidify/screen/ScreenActivity.kt | 250 ++++ .../looker/droidify/screen/ScreenFragment.kt | 10 + .../droidify/screen/ScreenshotsFragment.kt | 233 +++ .../looker/droidify/screen/TabsFragment.kt | 612 ++++++++ .../com/looker/droidify/service/Connection.kt | 41 + .../droidify/service/ConnectionService.kt | 19 + .../droidify/service/DownloadService.kt | 369 +++++ .../looker/droidify/service/SyncService.kt | 420 ++++++ .../looker/droidify/utility/KParcelable.kt | 18 + .../droidify/utility/PackageItemResolver.kt | 122 ++ .../droidify/utility/ProgressInputStream.kt | 29 + .../com/looker/droidify/utility/RxUtils.kt | 83 ++ .../com/looker/droidify/utility/Utils.kt | 100 ++ .../droidify/utility/extension/Android.kt | 76 + .../looker/droidify/utility/extension/Json.kt | 106 ++ .../droidify/utility/extension/Resources.kt | 94 ++ .../looker/droidify/utility/extension/Text.kt | 59 + .../widget/ClickableMovementMethod.kt | 48 + .../droidify/widget/CursorRecyclerAdapter.kt | 35 + .../droidify/widget/DividerItemDecoration.kt | 88 ++ .../droidify/widget/EnumRecyclerAdapter.kt | 28 + .../looker/droidify/widget/FocusSearchView.kt | 35 + .../droidify/widget/FragmentLinearLayout.kt | 22 + .../droidify/widget/RecyclerFastScroller.kt | 247 ++++ .../droidify/widget/StableRecyclerAdapter.kt | 27 + .../com/looker/droidify/widget/Toolbar.kt | 33 + src/main/res/animator/slide_in.xml | 19 + src/main/res/animator/slide_in_keep.xml | 4 + src/main/res/animator/slide_out.xml | 20 + src/main/res/color/accent_dark.xml | 13 + src/main/res/color/accent_light.xml | 13 + src/main/res/color/error_dark.xml | 13 + src/main/res/color/error_light.xml | 13 + src/main/res/drawable/background_border.xml | 5 + src/main/res/drawable/ic_add.xml | 13 + .../res/drawable/ic_application_default.xml | 20 + src/main/res/drawable/ic_archive.xml | 15 + src/main/res/drawable/ic_arrow_down.xml | 13 + src/main/res/drawable/ic_bug_report.xml | 16 + src/main/res/drawable/ic_code.xml | 13 + src/main/res/drawable/ic_copyright.xml | 20 + src/main/res/drawable/ic_delete.xml | 13 + src/main/res/drawable/ic_donate_bitcoin.xml | 21 + src/main/res/drawable/ic_donate_flattr.xml | 17 + src/main/res/drawable/ic_donate_liberapay.xml | 22 + src/main/res/drawable/ic_donate_litecoin.xml | 14 + .../res/drawable/ic_donate_opencollective.xml | 16 + src/main/res/drawable/ic_edit.xml | 14 + src/main/res/drawable/ic_email.xml | 14 + src/main/res/drawable/ic_history.xml | 15 + src/main/res/drawable/ic_launch.xml | 14 + src/main/res/drawable/ic_new_releases.xml | 15 + .../drawable/ic_perm_device_information.xml | 14 + src/main/res/drawable/ic_person.xml | 14 + src/main/res/drawable/ic_photo_camera.xml | 16 + src/main/res/drawable/ic_public.xml | 15 + src/main/res/drawable/ic_save.xml | 14 + src/main/res/drawable/ic_search.xml | 15 + src/main/res/drawable/ic_sort.xml | 13 + src/main/res/drawable/ic_sync.xml | 15 + src/main/res/drawable/ic_tune.xml | 14 + src/main/res/drawable/scrollbar_thumb.xml | 10 + src/main/res/drawable/scrollbar_track.xml | 10 + src/main/res/layout/edit_repository.xml | 218 +++ src/main/res/layout/fragment.xml | 42 + src/main/res/layout/link_item.xml | 48 + src/main/res/layout/permissions_item.xml | 32 + src/main/res/layout/preference_item.xml | 43 + src/main/res/layout/product_header_item.xml | 101 ++ src/main/res/layout/product_item.xml | 61 + src/main/res/layout/release_item.xml | 114 ++ src/main/res/layout/repository_item.xml | 27 + src/main/res/layout/section_item.xml | 31 + src/main/res/layout/switch_item.xml | 28 + src/main/res/layout/tabs_toolbar.xml | 59 + src/main/res/layout/title_text_item.xml | 27 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2125 bytes .../mipmap-hdpi/ic_launcher_background.png | Bin 0 -> 182 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 3212 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 4105 bytes src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1373 bytes .../mipmap-mdpi/ic_launcher_background.png | Bin 0 -> 125 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 1996 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2555 bytes src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 2967 bytes .../mipmap-xhdpi/ic_launcher_background.png | Bin 0 -> 261 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 4724 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 5938 bytes src/main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 4822 bytes .../mipmap-xxhdpi/ic_launcher_background.png | Bin 0 -> 487 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 8188 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 9591 bytes src/main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 6934 bytes .../mipmap-xxxhdpi/ic_launcher_background.png | Bin 0 -> 803 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 12529 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 13999 bytes src/main/res/values/attrs.xml | 6 + src/main/res/values/colors.xml | 17 + .../res/values/ic_launcher_background.xml | 4 + src/main/res/values/ids.xml | 11 + src/main/res/values/strings.xml | 167 +++ src/main/res/values/styles.xml | 47 + 161 files changed, 13284 insertions(+) create mode 100644 .gitignore create mode 100644 COPYING create mode 100644 README.md create mode 100644 build.gradle create mode 100644 extra/launcher-make.sh create mode 100644 extra/launcher.svg create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 metadata/en-US/changelogs/1.txt create mode 100644 metadata/en-US/changelogs/2.txt create mode 100644 metadata/en-US/changelogs/3.txt create mode 100644 metadata/en-US/changelogs/4.txt create mode 100644 metadata/en-US/full_description.txt create mode 100644 metadata/en-US/images/screenshots/1.png create mode 100644 metadata/en-US/images/screenshots/2.png create mode 100644 metadata/en-US/short_description.txt create mode 100644 proguard.pro create mode 100644 src/main/AndroidManifest.xml create mode 100644 src/main/ic_launcher-playstore.png create mode 100644 src/main/kotlin/com/looker/droidify/Common.kt create mode 100644 src/main/kotlin/com/looker/droidify/MainActivity.kt create mode 100644 src/main/kotlin/com/looker/droidify/MainApplication.kt create mode 100644 src/main/kotlin/com/looker/droidify/content/Cache.kt create mode 100644 src/main/kotlin/com/looker/droidify/content/Preferences.kt create mode 100644 src/main/kotlin/com/looker/droidify/content/ProductPreferences.kt create mode 100644 src/main/kotlin/com/looker/droidify/database/CursorOwner.kt create mode 100644 src/main/kotlin/com/looker/droidify/database/Database.kt create mode 100644 src/main/kotlin/com/looker/droidify/database/ObservableCursor.kt create mode 100644 src/main/kotlin/com/looker/droidify/database/QueryBuilder.kt create mode 100644 src/main/kotlin/com/looker/droidify/database/QueryLoader.kt create mode 100644 src/main/kotlin/com/looker/droidify/entity/InstalledItem.kt create mode 100644 src/main/kotlin/com/looker/droidify/entity/Product.kt create mode 100644 src/main/kotlin/com/looker/droidify/entity/ProductItem.kt create mode 100644 src/main/kotlin/com/looker/droidify/entity/ProductPreference.kt create mode 100644 src/main/kotlin/com/looker/droidify/entity/Release.kt create mode 100644 src/main/kotlin/com/looker/droidify/entity/Repository.kt create mode 100644 src/main/kotlin/com/looker/droidify/graphics/DrawableWrapper.kt create mode 100644 src/main/kotlin/com/looker/droidify/graphics/PaddingDrawable.kt create mode 100644 src/main/kotlin/com/looker/droidify/index/IndexHandler.kt create mode 100644 src/main/kotlin/com/looker/droidify/index/IndexMerger.kt create mode 100644 src/main/kotlin/com/looker/droidify/index/IndexV1Parser.kt create mode 100644 src/main/kotlin/com/looker/droidify/index/RepositoryUpdater.kt create mode 100644 src/main/kotlin/com/looker/droidify/network/Downloader.kt create mode 100644 src/main/kotlin/com/looker/droidify/network/PicassoDownloader.kt create mode 100644 src/main/kotlin/com/looker/droidify/screen/EditRepositoryFragment.kt create mode 100644 src/main/kotlin/com/looker/droidify/screen/MessageDialog.kt create mode 100644 src/main/kotlin/com/looker/droidify/screen/PreferencesFragment.kt create mode 100644 src/main/kotlin/com/looker/droidify/screen/ProductAdapter.kt create mode 100644 src/main/kotlin/com/looker/droidify/screen/ProductFragment.kt create mode 100644 src/main/kotlin/com/looker/droidify/screen/ProductsAdapter.kt create mode 100644 src/main/kotlin/com/looker/droidify/screen/ProductsFragment.kt create mode 100644 src/main/kotlin/com/looker/droidify/screen/RepositoriesAdapter.kt create mode 100644 src/main/kotlin/com/looker/droidify/screen/RepositoriesFragment.kt create mode 100644 src/main/kotlin/com/looker/droidify/screen/RepositoryFragment.kt create mode 100644 src/main/kotlin/com/looker/droidify/screen/ScreenActivity.kt create mode 100644 src/main/kotlin/com/looker/droidify/screen/ScreenFragment.kt create mode 100644 src/main/kotlin/com/looker/droidify/screen/ScreenshotsFragment.kt create mode 100644 src/main/kotlin/com/looker/droidify/screen/TabsFragment.kt create mode 100644 src/main/kotlin/com/looker/droidify/service/Connection.kt create mode 100644 src/main/kotlin/com/looker/droidify/service/ConnectionService.kt create mode 100644 src/main/kotlin/com/looker/droidify/service/DownloadService.kt create mode 100644 src/main/kotlin/com/looker/droidify/service/SyncService.kt create mode 100644 src/main/kotlin/com/looker/droidify/utility/KParcelable.kt create mode 100644 src/main/kotlin/com/looker/droidify/utility/PackageItemResolver.kt create mode 100644 src/main/kotlin/com/looker/droidify/utility/ProgressInputStream.kt create mode 100644 src/main/kotlin/com/looker/droidify/utility/RxUtils.kt create mode 100644 src/main/kotlin/com/looker/droidify/utility/Utils.kt create mode 100644 src/main/kotlin/com/looker/droidify/utility/extension/Android.kt create mode 100644 src/main/kotlin/com/looker/droidify/utility/extension/Json.kt create mode 100644 src/main/kotlin/com/looker/droidify/utility/extension/Resources.kt create mode 100644 src/main/kotlin/com/looker/droidify/utility/extension/Text.kt create mode 100644 src/main/kotlin/com/looker/droidify/widget/ClickableMovementMethod.kt create mode 100644 src/main/kotlin/com/looker/droidify/widget/CursorRecyclerAdapter.kt create mode 100644 src/main/kotlin/com/looker/droidify/widget/DividerItemDecoration.kt create mode 100644 src/main/kotlin/com/looker/droidify/widget/EnumRecyclerAdapter.kt create mode 100644 src/main/kotlin/com/looker/droidify/widget/FocusSearchView.kt create mode 100644 src/main/kotlin/com/looker/droidify/widget/FragmentLinearLayout.kt create mode 100644 src/main/kotlin/com/looker/droidify/widget/RecyclerFastScroller.kt create mode 100644 src/main/kotlin/com/looker/droidify/widget/StableRecyclerAdapter.kt create mode 100644 src/main/kotlin/com/looker/droidify/widget/Toolbar.kt create mode 100644 src/main/res/animator/slide_in.xml create mode 100644 src/main/res/animator/slide_in_keep.xml create mode 100644 src/main/res/animator/slide_out.xml create mode 100644 src/main/res/color/accent_dark.xml create mode 100644 src/main/res/color/accent_light.xml create mode 100644 src/main/res/color/error_dark.xml create mode 100644 src/main/res/color/error_light.xml create mode 100644 src/main/res/drawable/background_border.xml create mode 100644 src/main/res/drawable/ic_add.xml create mode 100644 src/main/res/drawable/ic_application_default.xml create mode 100644 src/main/res/drawable/ic_archive.xml create mode 100644 src/main/res/drawable/ic_arrow_down.xml create mode 100644 src/main/res/drawable/ic_bug_report.xml create mode 100644 src/main/res/drawable/ic_code.xml create mode 100644 src/main/res/drawable/ic_copyright.xml create mode 100644 src/main/res/drawable/ic_delete.xml create mode 100644 src/main/res/drawable/ic_donate_bitcoin.xml create mode 100644 src/main/res/drawable/ic_donate_flattr.xml create mode 100644 src/main/res/drawable/ic_donate_liberapay.xml create mode 100644 src/main/res/drawable/ic_donate_litecoin.xml create mode 100644 src/main/res/drawable/ic_donate_opencollective.xml create mode 100644 src/main/res/drawable/ic_edit.xml create mode 100644 src/main/res/drawable/ic_email.xml create mode 100644 src/main/res/drawable/ic_history.xml create mode 100644 src/main/res/drawable/ic_launch.xml create mode 100644 src/main/res/drawable/ic_new_releases.xml create mode 100644 src/main/res/drawable/ic_perm_device_information.xml create mode 100644 src/main/res/drawable/ic_person.xml create mode 100644 src/main/res/drawable/ic_photo_camera.xml create mode 100644 src/main/res/drawable/ic_public.xml create mode 100644 src/main/res/drawable/ic_save.xml create mode 100644 src/main/res/drawable/ic_search.xml create mode 100644 src/main/res/drawable/ic_sort.xml create mode 100644 src/main/res/drawable/ic_sync.xml create mode 100644 src/main/res/drawable/ic_tune.xml create mode 100644 src/main/res/drawable/scrollbar_thumb.xml create mode 100644 src/main/res/drawable/scrollbar_track.xml create mode 100644 src/main/res/layout/edit_repository.xml create mode 100644 src/main/res/layout/fragment.xml create mode 100644 src/main/res/layout/link_item.xml create mode 100644 src/main/res/layout/permissions_item.xml create mode 100644 src/main/res/layout/preference_item.xml create mode 100644 src/main/res/layout/product_header_item.xml create mode 100644 src/main/res/layout/product_item.xml create mode 100644 src/main/res/layout/release_item.xml create mode 100644 src/main/res/layout/repository_item.xml create mode 100644 src/main/res/layout/section_item.xml create mode 100644 src/main/res/layout/switch_item.xml create mode 100644 src/main/res/layout/tabs_toolbar.xml create mode 100644 src/main/res/layout/title_text_item.xml create mode 100644 src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 src/main/res/mipmap-hdpi/ic_launcher_background.png create mode 100644 src/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 src/main/res/mipmap-mdpi/ic_launcher_background.png create mode 100644 src/main/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 src/main/res/mipmap-xhdpi/ic_launcher_background.png create mode 100644 src/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 src/main/res/mipmap-xxhdpi/ic_launcher_background.png create mode 100644 src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 src/main/res/mipmap-xxxhdpi/ic_launcher_background.png create mode 100644 src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 src/main/res/values/attrs.xml create mode 100644 src/main/res/values/colors.xml create mode 100644 src/main/res/values/ic_launcher_background.xml create mode 100644 src/main/res/values/ids.xml create mode 100644 src/main/res/values/strings.xml create mode 100644 src/main/res/values/styles.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..4aea7f9b --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +/* +!/.gitignore +!/build.gradle +!/COPYING +!/extra +!/gradle +!/gradle.properties +!/gradlew +!/gradlew.bat +!/metadata +!/proguard.pro +!/README.md +!/src diff --git a/COPYING b/COPYING new file mode 100644 index 00000000..f288702d --- /dev/null +++ b/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 00000000..47e78458 --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# Foxy Droid + +Material-ify with Droid-ify. + +## Description + +Unofficial F-Droid client with Material UI. + +This app is an Direct Adaptation/Modification of [Foxy-Droid](https://github.com/kitsunyan/foxy-droid/) + +### Features + +* Material F-Droid style +* No cards or inappropriate animations +* Fast repository syncing +* Standard Android components and minimal dependencies + +### Screenshots + +

+ + +

+ +## Building and Installing + +Specify your Android SDK path either using the `ANDROID_HOME` environment variable, or by filling out the `sdk.dir` +property in `local.properties`. + +Signing can be done automatically using `keystore.properties` as follows: + +```properties +store.file=/path/to/keystore +store.password=key-store-password +key.alias=key-alias +key.password=key-password +``` + +Run `./gradlew assembleRelease` to build the package, which can be installed using the Android package manager. + +## License + +Droid-ify is available under the terms of the GNU General Public License v3 or later. Copyright © 2020 Iamlooker. diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..f2a8e03b --- /dev/null +++ b/build.gradle @@ -0,0 +1,128 @@ +buildscript { + ext.versions = [ + android: '3.4.1', + kotlin: '1.3.72' + ] + + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:' + versions.android + classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:' + versions.kotlin + } +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 29 + buildToolsVersion '29.0.3' + + defaultConfig { + archivesBaseName = 'droidify' + applicationId 'com.looker.droidify' + minSdkVersion 28 + targetSdkVersion 29 + versionCode 1 + versionName '0.1' + + def languages = [ 'en' ] + buildConfigField 'String[]', 'LANGUAGES', '{ "' + languages.join('", "') + '" }' + resConfigs languages + } + + sourceSets.all { + def javaDir = it.java.srcDirs.find { it.name == 'java' } + it.java.srcDirs += new File(javaDir.parentFile, 'kotlin') + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = compileOptions.sourceCompatibility.toString() + } + + buildTypes { + debug { + minifyEnabled false + shrinkResources false + } + release { + minifyEnabled true + shrinkResources false + } + all { + crunchPngs false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard.pro' + } + } + + lintOptions { + warning 'InvalidPackage' + ignore 'InvalidVectorPath' + } + + packagingOptions { + exclude '/DebugProbesKt.bin' + exclude '/kotlin/**.kotlin_builtins' + exclude '/kotlin/**.kotlin_metadata' + exclude '/META-INF/**.kotlin_module' + exclude '/META-INF/**.pro' + exclude '/META-INF/**.version' + exclude '/okhttp3/internal/publicsuffix/*' + } + + def keystorePropertiesFile = rootProject.file('keystore.properties') + if (keystorePropertiesFile.exists()) { + def keystoreProperties = new Properties() + keystoreProperties.load(keystorePropertiesFile.newDataInputStream()) + + def signing = [ + storeFile: keystoreProperties['store.file'], + storePassword: keystoreProperties['store.password'], + keyAlias: keystoreProperties['key.alias'], + keyPassword: keystoreProperties['key.password'] + ] + + if (!signing.any { _, v -> v == null }) { + signingConfigs { + primary { + storeFile file(signing.storeFile) + storePassword signing.storePassword + keyAlias signing.keyAlias + keyPassword signing.keyPassword + v2SigningEnabled false + } + } + + buildTypes { + debug.signingConfig signingConfigs.primary + release.signingConfig signingConfigs.primary + } + } + } +} + +repositories { + google() + jcenter() +} + +dependencies { + implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:' + versions.kotlin + implementation 'androidx.fragment:fragment:1.3.0' + implementation 'androidx.viewpager2:viewpager2:1.0.0' + implementation 'androidx.vectordrawable:vectordrawable:1.1.0' + implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.2' + implementation 'io.reactivex.rxjava3:rxjava:3.0.4' + implementation 'io.reactivex.rxjava3:rxandroid:3.0.0' + implementation 'com.fasterxml.jackson.core:jackson-core:2.12.1' + implementation 'com.squareup.picasso:picasso:2.71828' +} diff --git a/extra/launcher-make.sh b/extra/launcher-make.sh new file mode 100644 index 00000000..3cfaff5e --- /dev/null +++ b/extra/launcher-make.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +set -e +cd "`dirname "$0"`" + +dimensions=(mdpi:1 hdpi:1.5 xhdpi:2 xxhdpi:3 xxxhdpi:4) +res='../src/main/res' + +cp 'launcher.svg' 'launcher-foreground.svg' +inkscape --select circle --verb EditDelete --verb=FileSave --verb=FileQuit \ +'launcher-foreground.svg' + +for dimension in ${dimensions[@]}; do + resource="${dimension%:*}" + scale="${dimension#*:}" + mkdir -p "$res/mipmap-$resource" "$res/drawable-$resource" + size="`bc <<< "48 * $scale"`" + inkscape 'launcher.svg' -a 15:15:93:93 -w "$size" -h "$size" \ + -e "$res/mipmap-$resource/ic_launcher.png" + optipng "$res/mipmap-$resource/ic_launcher.png" + size="`bc <<< "108 * $scale"`" + inkscape 'launcher-foreground.svg' -w "$size" -h "$size" \ + -e "$res/drawable-$resource/ic_launcher_foreground.png" + optipng "$res/drawable-$resource/ic_launcher_foreground.png" +done + +rm 'launcher-foreground.svg' diff --git a/extra/launcher.svg b/extra/launcher.svg new file mode 100644 index 00000000..e9f11504 --- /dev/null +++ b/extra/launcher.svg @@ -0,0 +1,273 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..5bac8ac5 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +android.useAndroidX=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..87b738cbd051603d91cc39de6cb000dd98fe6b02 GIT binary patch literal 55190 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 z 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..6d57edc7 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/metadata/en-US/changelogs/1.txt b/metadata/en-US/changelogs/1.txt new file mode 100644 index 00000000..6d7f9b8a --- /dev/null +++ b/metadata/en-US/changelogs/1.txt @@ -0,0 +1,2 @@ +* Changed Primary color to Google Green +* Added Credits to kitsunyan diff --git a/metadata/en-US/changelogs/2.txt b/metadata/en-US/changelogs/2.txt new file mode 100644 index 00000000..d4c0757c --- /dev/null +++ b/metadata/en-US/changelogs/2.txt @@ -0,0 +1,3 @@ +* Allow to sort by date added and last update +* Display more compatibility information for packages +* Drop AppCompat and reduce APK file size diff --git a/metadata/en-US/changelogs/3.txt b/metadata/en-US/changelogs/3.txt new file mode 100644 index 00000000..ec6dd6a8 --- /dev/null +++ b/metadata/en-US/changelogs/3.txt @@ -0,0 +1,3 @@ +* Persist sorting order across application restarts +* Display Open Collective donation links +* Rework text strings diff --git a/metadata/en-US/changelogs/4.txt b/metadata/en-US/changelogs/4.txt new file mode 100644 index 00000000..32059a02 --- /dev/null +++ b/metadata/en-US/changelogs/4.txt @@ -0,0 +1,4 @@ +* Full text search +* Search results ranking +* Allow to view packages per repository +* Automatic day/night theme for Android 10 diff --git a/metadata/en-US/full_description.txt b/metadata/en-US/full_description.txt new file mode 100644 index 00000000..3a393ba1 --- /dev/null +++ b/metadata/en-US/full_description.txt @@ -0,0 +1,10 @@ +Unofficial F-Droid client with Material UI. + +This app is an Direct Adaptation/Modification of Foxy-Droid + +Features: + +* Material F-Droid style +* No cards or inappropriate animations +* Fast repository syncing +* Standard Android components and minimal dependencies diff --git a/metadata/en-US/images/screenshots/1.png b/metadata/en-US/images/screenshots/1.png new file mode 100644 index 0000000000000000000000000000000000000000..48703f8fb4de2e8eb1b20b5818474a88eb28bc8c GIT binary patch literal 101883 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 literal 0 HcmV?d00001 diff --git a/metadata/en-US/images/screenshots/2.png b/metadata/en-US/images/screenshots/2.png new file mode 100644 index 0000000000000000000000000000000000000000..3b0da1ee8085e17b0326c379b9f7a24a0d502363 GIT binary patch literal 99808 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 literal 0 HcmV?d00001 diff --git a/metadata/en-US/short_description.txt b/metadata/en-US/short_description.txt new file mode 100644 index 00000000..a3162860 --- /dev/null +++ b/metadata/en-US/short_description.txt @@ -0,0 +1 @@ +Material-ify with Droid-ify. \ No newline at end of file diff --git a/proguard.pro b/proguard.pro new file mode 100644 index 00000000..c4b40244 --- /dev/null +++ b/proguard.pro @@ -0,0 +1,5 @@ +-dontobfuscate + +# Disable ServiceLoader reproducibility-breaking optimizations +-keep class kotlinx.coroutines.CoroutineExceptionHandler +-keep class kotlinx.coroutines.internal.MainDispatcherFactory diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml new file mode 100644 index 00000000..d5b12596 --- /dev/null +++ b/src/main/AndroidManifest.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/ic_launcher-playstore.png b/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000000000000000000000000000000000000..28716da8d5b38f6038cddf60664fe7e6e736d176 GIT binary patch literal 32435 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 handleSpecialIntent(SpecialIntent.Updates) + ACTION_INSTALL -> handleSpecialIntent(SpecialIntent.Install(intent.packageName, + intent.getStringExtra(EXTRA_CACHE_FILE_NAME))) + else -> super.handleIntent(intent) + } + } +} diff --git a/src/main/kotlin/com/looker/droidify/MainApplication.kt b/src/main/kotlin/com/looker/droidify/MainApplication.kt new file mode 100644 index 00000000..2585c5bd --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/MainApplication.kt @@ -0,0 +1,187 @@ +package com.looker.droidify + +import android.annotation.SuppressLint +import android.app.Application +import android.app.job.JobInfo +import android.app.job.JobScheduler +import android.content.BroadcastReceiver +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageInfo +import com.squareup.picasso.OkHttp3Downloader +import com.squareup.picasso.Picasso +import com.looker.droidify.content.Cache +import com.looker.droidify.content.Preferences +import com.looker.droidify.content.ProductPreferences +import com.looker.droidify.database.Database +import com.looker.droidify.entity.InstalledItem +import com.looker.droidify.index.RepositoryUpdater +import com.looker.droidify.network.Downloader +import com.looker.droidify.network.PicassoDownloader +import com.looker.droidify.service.Connection +import com.looker.droidify.service.SyncService +import com.looker.droidify.utility.Utils +import com.looker.droidify.utility.extension.android.* +import java.net.InetSocketAddress +import java.net.Proxy + +@Suppress("unused") +class MainApplication: Application() { + private fun PackageInfo.toInstalledItem(): InstalledItem { + val signatureString = singleSignature?.let(Utils::calculateHash).orEmpty() + return InstalledItem(packageName, versionName.orEmpty(), versionCodeCompat, signatureString) + } + + override fun attachBaseContext(base: Context) { + super.attachBaseContext(Utils.configureLocale(base)) + } + + override fun onCreate() { + super.onCreate() + + val databaseUpdated = Database.init(this) + Preferences.init(this) + ProductPreferences.init(this) + RepositoryUpdater.init(this) + listenApplications() + listenPreferences() + + Picasso.setSingletonInstance(Picasso.Builder(this) + .downloader(OkHttp3Downloader(PicassoDownloader.Factory(Cache.getImagesDir(this)))).build()) + + if (databaseUpdated) { + forceSyncAll() + } + + Cache.cleanup(this) + updateSyncJob(false) + } + + private fun listenApplications() { + registerReceiver(object: BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val packageName = intent.data?.let { if (it.scheme == "package") it.schemeSpecificPart else null } + if (packageName != null) { + when (intent.action.orEmpty()) { + Intent.ACTION_PACKAGE_ADDED, + Intent.ACTION_PACKAGE_REMOVED -> { + val packageInfo = try { + packageManager.getPackageInfo(packageName, Android.PackageManager.signaturesFlag) + } catch (e: Exception) { + null + } + if (packageInfo != null) { + Database.InstalledAdapter.put(packageInfo.toInstalledItem()) + } else { + Database.InstalledAdapter.delete(packageName) + } + } + } + } + } + }, IntentFilter().apply { + addAction(Intent.ACTION_PACKAGE_ADDED) + addAction(Intent.ACTION_PACKAGE_REMOVED) + addDataScheme("package") + }) + val installedItems = packageManager.getInstalledPackages(Android.PackageManager.signaturesFlag) + .map { it.toInstalledItem() } + Database.InstalledAdapter.putAll(installedItems) + } + + private fun listenPreferences() { + updateProxy() + var lastAutoSync = Preferences[Preferences.Key.AutoSync] + var lastUpdateUnstable = Preferences[Preferences.Key.UpdateUnstable] + Preferences.observable.subscribe { + if (it == Preferences.Key.ProxyType || it == Preferences.Key.ProxyHost || it == Preferences.Key.ProxyPort) { + updateProxy() + } else if (it == Preferences.Key.AutoSync) { + val autoSync = Preferences[Preferences.Key.AutoSync] + if (lastAutoSync != autoSync) { + lastAutoSync = autoSync + updateSyncJob(true) + } + } else if (it == Preferences.Key.UpdateUnstable) { + val updateUnstable = Preferences[Preferences.Key.UpdateUnstable] + if (lastUpdateUnstable != updateUnstable) { + lastUpdateUnstable = updateUnstable + forceSyncAll() + } + } + } + } + + private fun updateSyncJob(force: Boolean) { + val jobScheduler = getSystemService(JOB_SCHEDULER_SERVICE) as JobScheduler + val reschedule = force || !jobScheduler.allPendingJobs.any { it.id == Common.JOB_ID_SYNC } + if (reschedule) { + val autoSync = Preferences[Preferences.Key.AutoSync] + when (autoSync) { + Preferences.AutoSync.Never -> { + jobScheduler.cancel(Common.JOB_ID_SYNC) + } + Preferences.AutoSync.Wifi, Preferences.AutoSync.Always -> { + val period = 12 * 60 * 60 * 1000L // 12 hours + val wifiOnly = autoSync == Preferences.AutoSync.Wifi + jobScheduler.schedule(JobInfo + .Builder(Common.JOB_ID_SYNC, ComponentName(this, SyncService.Job::class.java)) + .setRequiredNetworkType(if (wifiOnly) JobInfo.NETWORK_TYPE_UNMETERED else JobInfo.NETWORK_TYPE_ANY) + .apply { + if (Android.sdk(26)) { + setRequiresBatteryNotLow(true) + setRequiresStorageNotLow(true) + } + if (Android.sdk(24)) { + setPeriodic(period, JobInfo.getMinFlexMillis()) + } else { + setPeriodic(period) + } + } + .build()) + Unit + } + }::class.java + } + } + + private fun updateProxy() { + val type = Preferences[Preferences.Key.ProxyType].proxyType + val host = Preferences[Preferences.Key.ProxyHost] + val port = Preferences[Preferences.Key.ProxyPort] + val socketAddress = when (type) { + Proxy.Type.DIRECT -> { + null + } + Proxy.Type.HTTP, Proxy.Type.SOCKS -> { + try { + InetSocketAddress.createUnresolved(host, port) + } catch (e: Exception) { + e.printStackTrace() + null + } + } + } + val proxy = socketAddress?.let { Proxy(type, socketAddress) } + Downloader.proxy = proxy + } + + private fun forceSyncAll() { + Database.RepositoryAdapter.getAll(null).forEach { + if (it.lastModified.isNotEmpty() || it.entityTag.isNotEmpty()) { + Database.RepositoryAdapter.put(it.copy(lastModified = "", entityTag = "")) + } + } + Connection(SyncService::class.java, onBind = { connection, binder -> + binder.sync(SyncService.SyncRequest.FORCE) + connection.unbind(this) + }).bind(this) + } + + class BootReceiver: BroadcastReceiver() { + @SuppressLint("UnsafeProtectedBroadcastReceiver") + override fun onReceive(context: Context, intent: Intent) = Unit + } +} diff --git a/src/main/kotlin/com/looker/droidify/content/Cache.kt b/src/main/kotlin/com/looker/droidify/content/Cache.kt new file mode 100644 index 00000000..79d69ca0 --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/content/Cache.kt @@ -0,0 +1,179 @@ +package com.looker.droidify.content + +import android.content.ContentProvider +import android.content.ContentValues +import android.content.Context +import android.content.pm.PackageManager +import android.database.Cursor +import android.database.MatrixCursor +import android.net.Uri +import android.os.ParcelFileDescriptor +import android.provider.OpenableColumns +import android.system.Os +import com.looker.droidify.utility.extension.android.* +import java.io.File +import java.util.UUID +import kotlin.concurrent.thread + +object Cache { + private fun ensureCacheDir(context: Context, name: String): File { + return File(context.cacheDir, name).apply { isDirectory || mkdirs() || throw RuntimeException() } + } + + private fun applyOrMode(file: File, mode: Int) { + val oldMode = Os.stat(file.path).st_mode and 0b111111111111 + val newMode = oldMode or mode + if (newMode != oldMode) { + Os.chmod(file.path, newMode) + } + } + + private fun subPath(dir: File, file: File): String { + val dirPath = "${dir.path}/" + val filePath = file.path + filePath.startsWith(dirPath) || throw RuntimeException() + return filePath.substring(dirPath.length) + } + + fun getImagesDir(context: Context): File { + return ensureCacheDir(context, "images") + } + + fun getPartialReleaseFile(context: Context, cacheFileName: String): File { + return File(ensureCacheDir(context, "partial"), cacheFileName) + } + + fun getReleaseFile(context: Context, cacheFileName: String): File { + return File(ensureCacheDir(context, "releases"), cacheFileName).apply { + if (!Android.sdk(24)) { + // Make readable for package installer + val cacheDir = context.cacheDir.parentFile!!.parentFile!! + generateSequence(this) { it.parentFile!! }.takeWhile { it != cacheDir }.forEach { + when { + it.isDirectory -> applyOrMode(it, 0b001001001) + it.isFile -> applyOrMode(it, 0b100100100) + } + } + } + } + } + + fun getReleaseUri(context: Context, cacheFileName: String): Uri { + val file = getReleaseFile(context, cacheFileName) + val packageInfo = context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_PROVIDERS) + val authority = packageInfo.providers.find { it.name == Provider::class.java.name }!!.authority + return Uri.Builder().scheme("content").authority(authority) + .encodedPath(subPath(context.cacheDir, file)).build() + } + + fun getTemporaryFile(context: Context): File { + return File(ensureCacheDir(context, "temporary"), UUID.randomUUID().toString()) + } + + fun cleanup(context: Context) { + thread { cleanup(context, Pair("images", 0), Pair("partial", 24), Pair("releases", 24), Pair("temporary", 1)) } + } + + private fun cleanup(context: Context, vararg dirHours: Pair) { + val knownNames = dirHours.asSequence().map { it.first }.toSet() + val files = context.cacheDir.listFiles().orEmpty() + files.asSequence().filter { it.name !in knownNames }.forEach { + if (it.isDirectory) { + cleanupDir(it, 0) + it.delete() + } else { + it.delete() + } + } + dirHours.forEach { (name, hours) -> + if (hours > 0) { + val file = File(context.cacheDir, name) + if (file.exists()) { + if (file.isDirectory) { + cleanupDir(file, hours) + } else { + file.delete() + } + } + } + } + } + + private fun cleanupDir(dir: File, hours: Int) { + dir.listFiles()?.forEach { + val older = hours <= 0 || run { + val olderThan = System.currentTimeMillis() / 1000L - hours * 60 * 60 + try { + val stat = Os.lstat(it.path) + stat.st_atime < olderThan + } catch (e: Exception) { + false + } + } + if (older) { + if (it.isDirectory) { + cleanupDir(it, hours) + if (it.isDirectory) { + it.delete() + } + } else { + it.delete() + } + } + } + } + + class Provider: ContentProvider() { + companion object { + private val defaultColumns = arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE) + } + + private fun getFileAndTypeForUri(uri: Uri): Pair { + return when (uri.pathSegments?.firstOrNull()) { + "releases" -> Pair(File(context!!.cacheDir, uri.encodedPath!!), "application/vnd.android.package-archive") + else -> throw SecurityException() + } + } + + override fun onCreate(): Boolean = true + + override fun query(uri: Uri, projection: Array?, + selection: String?, selectionArgs: Array?, sortOrder: String?): Cursor? { + val file = getFileAndTypeForUri(uri).first + val columns = (projection ?: defaultColumns).mapNotNull { + when (it) { + OpenableColumns.DISPLAY_NAME -> Pair(it, file.name) + OpenableColumns.SIZE -> Pair(it, file.length()) + else -> null + } + }.unzip() + return MatrixCursor(columns.first.toTypedArray()).apply { addRow(columns.second.toTypedArray()) } + } + + override fun getType(uri: Uri): String? = getFileAndTypeForUri(uri).second + + private val unsupported: Nothing + get() = throw UnsupportedOperationException() + + override fun insert(uri: Uri, contentValues: ContentValues?): Uri? = unsupported + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int = unsupported + override fun update(uri: Uri, contentValues: ContentValues?, + selection: String?, selectionArgs: Array?): Int = unsupported + + override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? { + val openMode = when (mode) { + "r" -> ParcelFileDescriptor.MODE_READ_ONLY + "w", "wt" -> ParcelFileDescriptor.MODE_WRITE_ONLY or ParcelFileDescriptor.MODE_CREATE or + ParcelFileDescriptor.MODE_TRUNCATE + "wa" -> ParcelFileDescriptor.MODE_WRITE_ONLY or ParcelFileDescriptor.MODE_CREATE or + ParcelFileDescriptor.MODE_APPEND + "rw" -> ParcelFileDescriptor.MODE_READ_WRITE or ParcelFileDescriptor.MODE_CREATE + "rwt" -> ParcelFileDescriptor.MODE_READ_WRITE or ParcelFileDescriptor.MODE_CREATE or + ParcelFileDescriptor.MODE_TRUNCATE + else -> throw IllegalArgumentException() + } + val file = getFileAndTypeForUri(uri).first + return ParcelFileDescriptor.open(file, openMode) + } + } +} diff --git a/src/main/kotlin/com/looker/droidify/content/Preferences.kt b/src/main/kotlin/com/looker/droidify/content/Preferences.kt new file mode 100644 index 00000000..ad4f9a65 --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/content/Preferences.kt @@ -0,0 +1,151 @@ +package com.looker.droidify.content + +import android.content.Context +import android.content.SharedPreferences +import android.content.res.Configuration +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.subjects.PublishSubject +import com.looker.droidify.R +import com.looker.droidify.entity.ProductItem +import com.looker.droidify.utility.extension.android.* +import java.net.Proxy + +object Preferences { + private lateinit var preferences: SharedPreferences + + private val subject = PublishSubject.create>() + + private val keys = sequenceOf(Key.AutoSync, Key.IncompatibleVersions, Key.ProxyHost, Key.ProxyPort, Key.ProxyType, + Key.SortOrder, Key.Theme, Key.UpdateNotify, Key.UpdateUnstable).map { Pair(it.name, it) }.toMap() + + fun init(context: Context) { + preferences = context.getSharedPreferences("${context.packageName}_preferences", Context.MODE_PRIVATE) + preferences.registerOnSharedPreferenceChangeListener { _, keyString -> keys[keyString]?.let(subject::onNext) } + } + + val observable: Observable> + get() = subject + + sealed class Value { + abstract val value: T + + internal abstract fun get(preferences: SharedPreferences, key: String, defaultValue: Value): T + internal abstract fun set(preferences: SharedPreferences, key: String, value: T) + + class BooleanValue(override val value: Boolean): Value() { + override fun get(preferences: SharedPreferences, key: String, defaultValue: Value): Boolean { + return preferences.getBoolean(key, defaultValue.value) + } + + override fun set(preferences: SharedPreferences, key: String, value: Boolean) { + preferences.edit().putBoolean(key, value).apply() + } + } + + class IntValue(override val value: Int): Value() { + override fun get(preferences: SharedPreferences, key: String, defaultValue: Value): Int { + return preferences.getInt(key, defaultValue.value) + } + + override fun set(preferences: SharedPreferences, key: String, value: Int) { + preferences.edit().putInt(key, value).apply() + } + } + + class StringValue(override val value: String): Value() { + override fun get(preferences: SharedPreferences, key: String, defaultValue: Value): String { + return preferences.getString(key, defaultValue.value) ?: defaultValue.value + } + + override fun set(preferences: SharedPreferences, key: String, value: String) { + preferences.edit().putString(key, value).apply() + } + } + + class EnumerationValue>(override val value: T): Value() { + override fun get(preferences: SharedPreferences, key: String, defaultValue: Value): T { + val value = preferences.getString(key, defaultValue.value.valueString) + return defaultValue.value.values.find { it.valueString == value } ?: defaultValue.value + } + + override fun set(preferences: SharedPreferences, key: String, value: T) { + preferences.edit().putString(key, value.valueString).apply() + } + } + } + + interface Enumeration { + val values: List + val valueString: String + } + + sealed class Key(val name: String, val default: Value) { + object AutoSync: Key("auto_sync", Value.EnumerationValue(Preferences.AutoSync.Wifi)) + object IncompatibleVersions: Key("incompatible_versions", Value.BooleanValue(false)) + object ProxyHost: Key("proxy_host", Value.StringValue("localhost")) + object ProxyPort: Key("proxy_port", Value.IntValue(9050)) + object ProxyType: Key("proxy_type", Value.EnumerationValue(Preferences.ProxyType.Direct)) + object SortOrder: Key("sort_order", Value.EnumerationValue(Preferences.SortOrder.Update)) + object Theme: Key("theme", Value.EnumerationValue(if (Android.sdk(29)) + Preferences.Theme.System else Preferences.Theme.Light)) + object UpdateNotify: Key("update_notify", Value.BooleanValue(true)) + object UpdateUnstable: Key("update_unstable", Value.BooleanValue(false)) + } + + sealed class AutoSync(override val valueString: String): Enumeration { + override val values: List + get() = listOf(Never, Wifi, Always) + + object Never: AutoSync("never") + object Wifi: AutoSync("wifi") + object Always: AutoSync("always") + } + + sealed class ProxyType(override val valueString: String, val proxyType: Proxy.Type): Enumeration { + override val values: List + get() = listOf(Direct, Http, Socks) + + object Direct: ProxyType("direct", Proxy.Type.DIRECT) + object Http: ProxyType("http", Proxy.Type.HTTP) + object Socks: ProxyType("socks", Proxy.Type.SOCKS) + } + + sealed class SortOrder(override val valueString: String, val order: ProductItem.Order): Enumeration { + override val values: List + get() = listOf(Name, Added, Update) + + object Name: SortOrder("name", ProductItem.Order.NAME) + object Added: SortOrder("added", ProductItem.Order.DATE_ADDED) + object Update: SortOrder("update", ProductItem.Order.LAST_UPDATE) + } + + sealed class Theme(override val valueString: String): Enumeration { + override val values: List + get() = if (Android.sdk(29)) listOf(System, Light, Dark) else listOf(Light, Dark) + + abstract fun getResId(configuration: Configuration): Int + + object System: Theme("system") { + override fun getResId(configuration: Configuration): Int { + return if ((configuration.uiMode and Configuration.UI_MODE_NIGHT_YES) != 0) + R.style.Theme_Main_Dark else R.style.Theme_Main_Light + } + } + + object Light: Theme("light") { + override fun getResId(configuration: Configuration): Int = R.style.Theme_Main_Light + } + + object Dark: Theme("dark") { + override fun getResId(configuration: Configuration): Int = R.style.Theme_Main_Dark + } + } + + operator fun get(key: Key): T { + return key.default.get(preferences, key.name, key.default) + } + + operator fun set(key: Key, value: T) { + key.default.set(preferences, key.name, value) + } +} diff --git a/src/main/kotlin/com/looker/droidify/content/ProductPreferences.kt b/src/main/kotlin/com/looker/droidify/content/ProductPreferences.kt new file mode 100644 index 00000000..aa0ddb77 --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/content/ProductPreferences.kt @@ -0,0 +1,64 @@ +package com.looker.droidify.content + +import android.content.Context +import android.content.SharedPreferences +import io.reactivex.rxjava3.schedulers.Schedulers +import io.reactivex.rxjava3.subjects.PublishSubject +import com.looker.droidify.database.Database +import com.looker.droidify.entity.ProductPreference +import com.looker.droidify.utility.extension.json.* +import java.io.ByteArrayOutputStream +import java.nio.charset.Charset + +object ProductPreferences { + private val defaultProductPreference = ProductPreference(false, 0L) + private lateinit var preferences: SharedPreferences + private val subject = PublishSubject.create>() + + fun init(context: Context) { + preferences = context.getSharedPreferences("product_preferences", Context.MODE_PRIVATE) + Database.LockAdapter.putAll(preferences.all.keys + .mapNotNull { packageName -> this[packageName].databaseVersionCode?.let { Pair(packageName, it) } }) + subject + .observeOn(Schedulers.io()) + .subscribe { (packageName, versionCode) -> + if (versionCode != null) { + Database.LockAdapter.put(Pair(packageName, versionCode)) + } else { + Database.LockAdapter.delete(packageName) + } + } + } + + private val ProductPreference.databaseVersionCode: Long? + get() = when { + ignoreUpdates -> 0L + ignoreVersionCode > 0L -> ignoreVersionCode + else -> null + } + + operator fun get(packageName: String): ProductPreference { + return if (preferences.contains(packageName)) { + try { + Json.factory.createParser(preferences.getString(packageName, "{}")) + .use { it.parseDictionary(ProductPreference.Companion::deserialize) } + } catch (e: Exception) { + e.printStackTrace() + defaultProductPreference + } + } else { + defaultProductPreference + } + } + + operator fun set(packageName: String, productPreference: ProductPreference) { + val oldProductPreference = this[packageName] + preferences.edit().putString(packageName, ByteArrayOutputStream() + .apply { Json.factory.createGenerator(this).use { it.writeDictionary(productPreference::serialize) } } + .toByteArray().toString(Charset.defaultCharset())).apply() + if (oldProductPreference.ignoreUpdates != productPreference.ignoreUpdates || + oldProductPreference.ignoreVersionCode != productPreference.ignoreVersionCode) { + subject.onNext(Pair(packageName, productPreference.databaseVersionCode)) + } + } +} diff --git a/src/main/kotlin/com/looker/droidify/database/CursorOwner.kt b/src/main/kotlin/com/looker/droidify/database/CursorOwner.kt new file mode 100644 index 00000000..d0ab2d12 --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/database/CursorOwner.kt @@ -0,0 +1,101 @@ +package com.looker.droidify.database + +import android.database.Cursor +import android.os.Bundle +import androidx.fragment.app.Fragment +import androidx.loader.app.LoaderManager +import androidx.loader.content.Loader +import com.looker.droidify.entity.ProductItem + +class CursorOwner: Fragment(), LoaderManager.LoaderCallbacks { + sealed class Request { + internal abstract val id: Int + + data class ProductsAvailable(val searchQuery: String, val section: ProductItem.Section, + val order: ProductItem.Order): Request() { + override val id: Int + get() = 1 + } + + data class ProductsInstalled(val searchQuery: String, val section: ProductItem.Section, + val order: ProductItem.Order): Request() { + override val id: Int + get() = 2 + } + + data class ProductsUpdates(val searchQuery: String, val section: ProductItem.Section, + val order: ProductItem.Order): Request() { + override val id: Int + get() = 3 + } + + object Repositories: Request() { + override val id: Int + get() = 4 + } + } + + interface Callback { + fun onCursorData(request: Request, cursor: Cursor?) + } + + private data class ActiveRequest(val request: Request, val callback: Callback?, val cursor: Cursor?) + + init { + retainInstance = true + } + + private val activeRequests = mutableMapOf() + + fun attach(callback: Callback, request: Request) { + val oldActiveRequest = activeRequests[request.id] + if (oldActiveRequest?.callback != null && + oldActiveRequest.callback != callback && oldActiveRequest.cursor != null) { + oldActiveRequest.callback.onCursorData(oldActiveRequest.request, null) + } + val cursor = if (oldActiveRequest?.request == request && oldActiveRequest.cursor != null) { + callback.onCursorData(request, oldActiveRequest.cursor) + oldActiveRequest.cursor + } else { + null + } + activeRequests[request.id] = ActiveRequest(request, callback, cursor) + if (cursor == null) { + LoaderManager.getInstance(this).restartLoader(request.id, null, this) + } + } + + fun detach(callback: Callback) { + for (id in activeRequests.keys) { + val activeRequest = activeRequests[id]!! + if (activeRequest.callback == callback) { + activeRequests[id] = activeRequest.copy(callback = null) + } + } + } + + override fun onCreateLoader(id: Int, args: Bundle?): Loader { + val request = activeRequests[id]!!.request + return QueryLoader(requireContext()) { + when (request) { + is Request.ProductsAvailable -> Database.ProductAdapter + .query(false, false, request.searchQuery, request.section, request.order, it) + is Request.ProductsInstalled -> Database.ProductAdapter + .query(true, false, request.searchQuery, request.section, request.order, it) + is Request.ProductsUpdates -> Database.ProductAdapter + .query(true, true, request.searchQuery, request.section, request.order, it) + is Request.Repositories -> Database.RepositoryAdapter.query(it) + } + } + } + + override fun onLoadFinished(loader: Loader, data: Cursor?) { + val activeRequest = activeRequests[loader.id] + if (activeRequest != null) { + activeRequests[loader.id] = activeRequest.copy(cursor = data) + activeRequest.callback?.onCursorData(activeRequest.request, data) + } + } + + override fun onLoaderReset(loader: Loader) = onLoadFinished(loader, null) +} diff --git a/src/main/kotlin/com/looker/droidify/database/Database.kt b/src/main/kotlin/com/looker/droidify/database/Database.kt new file mode 100644 index 00000000..96e12def --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/database/Database.kt @@ -0,0 +1,659 @@ +package com.looker.droidify.database + +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import android.os.CancellationSignal +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParser +import io.reactivex.rxjava3.core.Observable +import com.looker.droidify.entity.InstalledItem +import com.looker.droidify.entity.Product +import com.looker.droidify.entity.ProductItem +import com.looker.droidify.entity.Repository +import com.looker.droidify.utility.extension.android.* +import com.looker.droidify.utility.extension.json.* +import java.io.ByteArrayOutputStream + +object Database { + fun init(context: Context): Boolean { + val helper = Helper(context) + db = helper.writableDatabase + if (helper.created) { + for (repository in Repository.defaultRepositories) { + RepositoryAdapter.put(repository) + } + } + return helper.created || helper.updated + } + + private lateinit var db: SQLiteDatabase + + private interface Table { + val memory: Boolean + val innerName: String + val createTable: String + val createIndex: String? + get() = null + + val databasePrefix: String + get() = if (memory) "memory." else "" + + val name: String + get() = "$databasePrefix$innerName" + + fun formatCreateTable(name: String): String { + return "CREATE TABLE $name (${QueryBuilder.trimQuery(createTable)})" + } + + val createIndexPairFormatted: Pair? + get() = createIndex?.let { Pair("CREATE INDEX ${innerName}_index ON $innerName ($it)", + "CREATE INDEX ${name}_index ON $innerName ($it)") } + } + + private object Schema { + object Repository: Table { + const val ROW_ID = "_id" + const val ROW_ENABLED = "enabled" + const val ROW_DELETED = "deleted" + const val ROW_DATA = "data" + + override val memory = false + override val innerName = "repository" + override val createTable = """ + $ROW_ID INTEGER PRIMARY KEY AUTOINCREMENT, + $ROW_ENABLED INTEGER NOT NULL, + $ROW_DELETED INTEGER NOT NULL, + $ROW_DATA BLOB NOT NULL + """ + } + + object Product: Table { + const val ROW_REPOSITORY_ID = "repository_id" + const val ROW_PACKAGE_NAME = "package_name" + const val ROW_NAME = "name" + const val ROW_SUMMARY = "summary" + const val ROW_DESCRIPTION = "description" + const val ROW_ADDED = "added" + const val ROW_UPDATED = "updated" + const val ROW_VERSION_CODE = "version_code" + const val ROW_SIGNATURES = "signatures" + const val ROW_COMPATIBLE = "compatible" + const val ROW_DATA = "data" + const val ROW_DATA_ITEM = "data_item" + + override val memory = false + override val innerName = "product" + override val createTable = """ + $ROW_REPOSITORY_ID INTEGER NOT NULL, + $ROW_PACKAGE_NAME TEXT NOT NULL, + $ROW_NAME TEXT NOT NULL, + $ROW_SUMMARY TEXT NOT NULL, + $ROW_DESCRIPTION TEXT NOT NULL, + $ROW_ADDED INTEGER NOT NULL, + $ROW_UPDATED INTEGER NOT NULL, + $ROW_VERSION_CODE INTEGER NOT NULL, + $ROW_SIGNATURES TEXT NOT NULL, + $ROW_COMPATIBLE INTEGER NOT NULL, + $ROW_DATA BLOB NOT NULL, + $ROW_DATA_ITEM BLOB NOT NULL, + PRIMARY KEY ($ROW_REPOSITORY_ID, $ROW_PACKAGE_NAME) + """ + override val createIndex = ROW_PACKAGE_NAME + } + + object Category: Table { + const val ROW_REPOSITORY_ID = "repository_id" + const val ROW_PACKAGE_NAME = "package_name" + const val ROW_NAME = "name" + + override val memory = false + override val innerName = "category" + override val createTable = """ + $ROW_REPOSITORY_ID INTEGER NOT NULL, + $ROW_PACKAGE_NAME TEXT NOT NULL, + $ROW_NAME TEXT NOT NULL, + PRIMARY KEY ($ROW_REPOSITORY_ID, $ROW_PACKAGE_NAME, $ROW_NAME) + """ + override val createIndex = "$ROW_PACKAGE_NAME, $ROW_NAME" + } + + object Installed: Table { + const val ROW_PACKAGE_NAME = "package_name" + const val ROW_VERSION = "version" + const val ROW_VERSION_CODE = "version_code" + const val ROW_SIGNATURE = "signature" + + override val memory = true + override val innerName = "installed" + override val createTable = """ + $ROW_PACKAGE_NAME TEXT PRIMARY KEY, + $ROW_VERSION TEXT NOT NULL, + $ROW_VERSION_CODE INTEGER NOT NULL, + $ROW_SIGNATURE TEXT NOT NULL + """ + } + + object Lock: Table { + const val ROW_PACKAGE_NAME = "package_name" + const val ROW_VERSION_CODE = "version_code" + + override val memory = true + override val innerName = "lock" + override val createTable = """ + $ROW_PACKAGE_NAME TEXT PRIMARY KEY, + $ROW_VERSION_CODE INTEGER NOT NULL + """ + } + + object Synthetic { + const val ROW_CAN_UPDATE = "can_update" + const val ROW_MATCH_RANK = "match_rank" + } + } + + private class Helper(context: Context): SQLiteOpenHelper(context, "droidify", null, 1) { + var created = false + private set + var updated = false + private set + + override fun onCreate(db: SQLiteDatabase) = Unit + override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) = onVersionChange(db) + override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) = onVersionChange(db) + + private fun onVersionChange(db: SQLiteDatabase) { + handleTables(db, true, Schema.Product, Schema.Category) + this.updated = true + } + + override fun onOpen(db: SQLiteDatabase) { + val create = handleTables(db, false, Schema.Repository) + val updated = handleTables(db, create, Schema.Product, Schema.Category) + db.execSQL("ATTACH DATABASE ':memory:' AS memory") + handleTables(db, false, Schema.Installed, Schema.Lock) + handleIndexes(db, Schema.Repository, Schema.Product, Schema.Category, Schema.Installed, Schema.Lock) + dropOldTables(db, Schema.Repository, Schema.Product, Schema.Category) + this.created = this.created || create + this.updated = this.updated || create || updated + } + } + + private fun handleTables(db: SQLiteDatabase, recreate: Boolean, vararg tables: Table): Boolean { + val shouldRecreate = recreate || tables.any { + val sql = db.query("${it.databasePrefix}sqlite_master", columns = arrayOf("sql"), + selection = Pair("type = ? AND name = ?", arrayOf("table", it.innerName))) + .use { it.firstOrNull()?.getString(0) }.orEmpty() + it.formatCreateTable(it.innerName) != sql + } + return shouldRecreate && run { + val shouldVacuum = tables.map { + db.execSQL("DROP TABLE IF EXISTS ${it.name}") + db.execSQL(it.formatCreateTable(it.name)) + !it.memory + } + if (shouldVacuum.any { it } && !db.inTransaction()) { + db.execSQL("VACUUM") + } + true + } + } + + private fun handleIndexes(db: SQLiteDatabase, vararg tables: Table) { + val shouldVacuum = tables.map { + val sqls = db.query("${it.databasePrefix}sqlite_master", columns = arrayOf("name", "sql"), + selection = Pair("type = ? AND tbl_name = ?", arrayOf("index", it.innerName))) + .use { it.asSequence().mapNotNull { it.getString(1)?.let { sql -> Pair(it.getString(0), sql) } }.toList() } + .filter { !it.first.startsWith("sqlite_") } + val createIndexes = it.createIndexPairFormatted?.let { listOf(it) }.orEmpty() + createIndexes.map { it.first } != sqls.map { it.second } && run { + for (name in sqls.map { it.first }) { + db.execSQL("DROP INDEX IF EXISTS $name") + } + for (createIndexPair in createIndexes) { + db.execSQL(createIndexPair.second) + } + !it.memory + } + } + if (shouldVacuum.any { it } && !db.inTransaction()) { + db.execSQL("VACUUM") + } + } + + private fun dropOldTables(db: SQLiteDatabase, vararg neededTables: Table) { + val tables = db.query("sqlite_master", columns = arrayOf("name"), + selection = Pair("type = ?", arrayOf("table"))) + .use { it.asSequence().mapNotNull { it.getString(0) }.toList() } + .filter { !it.startsWith("sqlite_") && !it.startsWith("android_") } + .toSet() - neededTables.mapNotNull { if (it.memory) null else it.name } + if (tables.isNotEmpty()) { + for (table in tables) { + db.execSQL("DROP TABLE IF EXISTS $table") + } + if (!db.inTransaction()) { + db.execSQL("VACUUM") + } + } + } + + sealed class Subject { + object Repositories: Subject() + data class Repository(val id: Long): Subject() + object Products: Subject() + } + + private val observers = mutableMapOf Unit>>() + + private fun dataObservable(subject: Subject): (Boolean, () -> Unit) -> Unit = { register, observer -> + synchronized(observers) { + val set = observers[subject] ?: run { + val set = mutableSetOf<() -> Unit>() + observers[subject] = set + set + } + if (register) { + set += observer + } else { + set -= observer + } + } + } + + fun observable(subject: Subject): Observable { + return Observable.create { + val callback: () -> Unit = { it.onNext(Unit) } + val dataObservable = dataObservable(subject) + dataObservable(true, callback) + it.setCancellable { dataObservable(false, callback) } + } + } + + private fun notifyChanged(vararg subjects: Subject) { + synchronized(observers) { + subjects.asSequence().mapNotNull { observers[it] }.flatten().forEach { it() } + } + } + + private fun SQLiteDatabase.insertOrReplace(replace: Boolean, table: String, contentValues: ContentValues): Long { + return if (replace) replace(table, null, contentValues) else insert(table, null, contentValues) + } + + private fun SQLiteDatabase.query(table: String, columns: Array? = null, + selection: Pair>? = null, orderBy: String? = null, + signal: CancellationSignal? = null): Cursor { + return query(false, table, columns, selection?.first, selection?.second, null, null, orderBy, null, signal) + } + + private fun Cursor.observable(subject: Subject): ObservableCursor { + return ObservableCursor(this, dataObservable(subject)) + } + + private fun ByteArray.jsonParse(callback: (JsonParser) -> T): T { + return Json.factory.createParser(this).use { it.parseDictionary(callback) } + } + + private fun jsonGenerate(callback: (JsonGenerator) -> Unit): ByteArray { + val outputStream = ByteArrayOutputStream() + Json.factory.createGenerator(outputStream).use { it.writeDictionary(callback) } + return outputStream.toByteArray() + } + + object RepositoryAdapter { + internal fun putWithoutNotification(repository: Repository, shouldReplace: Boolean): Long { + return db.insertOrReplace(shouldReplace, Schema.Repository.name, ContentValues().apply { + if (shouldReplace) { + put(Schema.Repository.ROW_ID, repository.id) + } + put(Schema.Repository.ROW_ENABLED, if (repository.enabled) 1 else 0) + put(Schema.Repository.ROW_DELETED, 0) + put(Schema.Repository.ROW_DATA, jsonGenerate(repository::serialize)) + }) + } + + fun put(repository: Repository): Repository { + val shouldReplace = repository.id >= 0L + val newId = putWithoutNotification(repository, shouldReplace) + val id = if (shouldReplace) repository.id else newId + notifyChanged(Subject.Repositories, Subject.Repository(id), Subject.Products) + return if (newId != repository.id) repository.copy(id = newId) else repository + } + + fun get(id: Long): Repository? { + return db.query(Schema.Repository.name, + selection = Pair("${Schema.Repository.ROW_ID} = ? AND ${Schema.Repository.ROW_DELETED} == 0", + arrayOf(id.toString()))) + .use { it.firstOrNull()?.let(::transform) } + } + + fun getAll(signal: CancellationSignal?): List { + return db.query(Schema.Repository.name, + selection = Pair("${Schema.Repository.ROW_DELETED} == 0", emptyArray()), + signal = signal).use { it.asSequence().map(::transform).toList() } + } + + fun getAllDisabledDeleted(signal: CancellationSignal?): Set> { + return db.query(Schema.Repository.name, + columns = arrayOf(Schema.Repository.ROW_ID, Schema.Repository.ROW_DELETED), + selection = Pair("${Schema.Repository.ROW_ENABLED} == 0 OR ${Schema.Repository.ROW_DELETED} != 0", emptyArray()), + signal = signal).use { it.asSequence().map { Pair(it.getLong(it.getColumnIndex(Schema.Repository.ROW_ID)), + it.getInt(it.getColumnIndex(Schema.Repository.ROW_DELETED)) != 0) }.toSet() } + } + + fun markAsDeleted(id: Long) { + db.update(Schema.Repository.name, ContentValues().apply { + put(Schema.Repository.ROW_DELETED, 1) + }, "${Schema.Repository.ROW_ID} = ?", arrayOf(id.toString())) + notifyChanged(Subject.Repositories, Subject.Repository(id), Subject.Products) + } + + fun cleanup(pairs: Set>) { + val result = pairs.windowed(10, 10, true).map { + val idsString = it.joinToString(separator = ", ") { it.first.toString() } + val productsCount = db.delete(Schema.Product.name, + "${Schema.Product.ROW_REPOSITORY_ID} IN ($idsString)", null) + val categoriesCount = db.delete(Schema.Category.name, + "${Schema.Category.ROW_REPOSITORY_ID} IN ($idsString)", null) + val deleteIdsString = it.asSequence().filter { it.second } + .joinToString(separator = ", ") { it.first.toString() } + if (deleteIdsString.isNotEmpty()) { + db.delete(Schema.Repository.name, "${Schema.Repository.ROW_ID} IN ($deleteIdsString)", null) + } + productsCount != 0 || categoriesCount != 0 + } + if (result.any { it }) { + notifyChanged(Subject.Products) + } + } + + fun query(signal: CancellationSignal?): Cursor { + return db.query(Schema.Repository.name, + selection = Pair("${Schema.Repository.ROW_DELETED} == 0", emptyArray()), + signal = signal).observable(Subject.Repositories) + } + + fun transform(cursor: Cursor): Repository { + return cursor.getBlob(cursor.getColumnIndex(Schema.Repository.ROW_DATA)) + .jsonParse { Repository.deserialize(cursor.getLong(cursor.getColumnIndex(Schema.Repository.ROW_ID)), it) } + } + } + + object ProductAdapter { + fun get(packageName: String, signal: CancellationSignal?): List { + return db.query(Schema.Product.name, + columns = arrayOf(Schema.Product.ROW_REPOSITORY_ID, Schema.Product.ROW_DESCRIPTION, Schema.Product.ROW_DATA), + selection = Pair("${Schema.Product.ROW_PACKAGE_NAME} = ?", arrayOf(packageName)), + signal = signal).use { it.asSequence().map(::transform).toList() } + } + + fun getCount(repositoryId: Long): Int { + return db.query(Schema.Product.name, columns = arrayOf("COUNT (*)"), + selection = Pair("${Schema.Product.ROW_REPOSITORY_ID} = ?", arrayOf(repositoryId.toString()))) + .use { it.firstOrNull()?.getInt(0) ?: 0 } + } + + fun query(installed: Boolean, updates: Boolean, searchQuery: String, + section: ProductItem.Section, order: ProductItem.Order, signal: CancellationSignal?): Cursor { + val builder = QueryBuilder() + + val signatureMatches = """installed.${Schema.Installed.ROW_SIGNATURE} IS NOT NULL AND + product.${Schema.Product.ROW_SIGNATURES} LIKE ('%.' || installed.${Schema.Installed.ROW_SIGNATURE} || '.%') AND + product.${Schema.Product.ROW_SIGNATURES} != ''""" + + builder += """SELECT product.rowid AS _id, product.${Schema.Product.ROW_REPOSITORY_ID}, + product.${Schema.Product.ROW_PACKAGE_NAME}, product.${Schema.Product.ROW_NAME}, + product.${Schema.Product.ROW_SUMMARY}, installed.${Schema.Installed.ROW_VERSION}, + (COALESCE(lock.${Schema.Lock.ROW_VERSION_CODE}, -1) NOT IN (0, product.${Schema.Product.ROW_VERSION_CODE}) AND + product.${Schema.Product.ROW_COMPATIBLE} != 0 AND product.${Schema.Product.ROW_VERSION_CODE} > + COALESCE(installed.${Schema.Installed.ROW_VERSION_CODE}, 0xffffffff) AND $signatureMatches) + AS ${Schema.Synthetic.ROW_CAN_UPDATE}, product.${Schema.Product.ROW_COMPATIBLE}, + product.${Schema.Product.ROW_DATA_ITEM},""" + + if (searchQuery.isNotEmpty()) { + builder += """(((product.${Schema.Product.ROW_NAME} LIKE ? OR + product.${Schema.Product.ROW_SUMMARY} LIKE ?) * 7) | + ((product.${Schema.Product.ROW_PACKAGE_NAME} LIKE ?) * 3) | + (product.${Schema.Product.ROW_DESCRIPTION} LIKE ?)) AS ${Schema.Synthetic.ROW_MATCH_RANK},""" + builder %= List(4) { "%$searchQuery%" } + } else { + builder += "0 AS ${Schema.Synthetic.ROW_MATCH_RANK}," + } + + builder += """MAX((product.${Schema.Product.ROW_COMPATIBLE} AND + (installed.${Schema.Installed.ROW_SIGNATURE} IS NULL OR $signatureMatches)) || + PRINTF('%016X', product.${Schema.Product.ROW_VERSION_CODE})) FROM ${Schema.Product.name} AS product""" + + builder += """JOIN ${Schema.Repository.name} AS repository + ON product.${Schema.Product.ROW_REPOSITORY_ID} = repository.${Schema.Repository.ROW_ID}""" + builder += """LEFT JOIN ${Schema.Lock.name} AS lock + ON product.${Schema.Product.ROW_PACKAGE_NAME} = lock.${Schema.Lock.ROW_PACKAGE_NAME}""" + if (!installed && !updates) { + builder += "LEFT" + } + builder += """JOIN ${Schema.Installed.name} AS installed + ON product.${Schema.Product.ROW_PACKAGE_NAME} = installed.${Schema.Installed.ROW_PACKAGE_NAME}""" + if (section is ProductItem.Section.Category) { + builder += """JOIN ${Schema.Category.name} AS category + ON product.${Schema.Product.ROW_PACKAGE_NAME} = category.${Schema.Product.ROW_PACKAGE_NAME}""" + } + + builder += """WHERE repository.${Schema.Repository.ROW_ENABLED} != 0 AND + repository.${Schema.Repository.ROW_DELETED} == 0""" + if (section is ProductItem.Section.Category) { + builder += "AND category.${Schema.Category.ROW_NAME} = ?" + builder %= section.name + } else if (section is ProductItem.Section.Repository) { + builder += "AND product.${Schema.Product.ROW_REPOSITORY_ID} = ?" + builder %= section.id.toString() + } + if (searchQuery.isNotEmpty()) { + builder += """AND ${Schema.Synthetic.ROW_MATCH_RANK} > 0""" + } + + builder += "GROUP BY product.${Schema.Product.ROW_PACKAGE_NAME} HAVING 1" + if (updates) { + builder += "AND ${Schema.Synthetic.ROW_CAN_UPDATE}" + } + builder += "ORDER BY" + if (searchQuery.isNotEmpty()) { + builder += """${Schema.Synthetic.ROW_MATCH_RANK} DESC,""" + } + when (order) { + ProductItem.Order.NAME -> Unit + ProductItem.Order.DATE_ADDED -> builder += "product.${Schema.Product.ROW_ADDED} DESC," + ProductItem.Order.LAST_UPDATE -> builder += "product.${Schema.Product.ROW_UPDATED} DESC," + }::class + builder += "product.${Schema.Product.ROW_NAME} COLLATE LOCALIZED ASC" + + return builder.query(db, signal).observable(Subject.Products) + } + + private fun transform(cursor: Cursor): Product { + return cursor.getBlob(cursor.getColumnIndex(Schema.Product.ROW_DATA)) + .jsonParse { Product.deserialize(cursor.getLong(cursor.getColumnIndex(Schema.Product.ROW_REPOSITORY_ID)), + cursor.getString(cursor.getColumnIndex(Schema.Product.ROW_DESCRIPTION)), it) } + } + + fun transformItem(cursor: Cursor): ProductItem { + return cursor.getBlob(cursor.getColumnIndex(Schema.Product.ROW_DATA_ITEM)) + .jsonParse { ProductItem.deserialize(cursor.getLong(cursor.getColumnIndex(Schema.Product.ROW_REPOSITORY_ID)), + cursor.getString(cursor.getColumnIndex(Schema.Product.ROW_PACKAGE_NAME)), + cursor.getString(cursor.getColumnIndex(Schema.Product.ROW_NAME)), + cursor.getString(cursor.getColumnIndex(Schema.Product.ROW_SUMMARY)), + cursor.getString(cursor.getColumnIndex(Schema.Installed.ROW_VERSION)).orEmpty(), + cursor.getInt(cursor.getColumnIndex(Schema.Product.ROW_COMPATIBLE)) != 0, + cursor.getInt(cursor.getColumnIndex(Schema.Synthetic.ROW_CAN_UPDATE)) != 0, + cursor.getInt(cursor.getColumnIndex(Schema.Synthetic.ROW_MATCH_RANK)), it) } + } + } + + object CategoryAdapter { + fun getAll(signal: CancellationSignal?): Set { + val builder = QueryBuilder() + + builder += """SELECT DISTINCT category.${Schema.Category.ROW_NAME} + FROM ${Schema.Category.name} AS category + JOIN ${Schema.Repository.name} AS repository + ON category.${Schema.Category.ROW_REPOSITORY_ID} = repository.${Schema.Repository.ROW_ID} + WHERE repository.${Schema.Repository.ROW_ENABLED} != 0 AND + repository.${Schema.Repository.ROW_DELETED} == 0""" + + return builder.query(db, signal).use { it.asSequence() + .map { it.getString(it.getColumnIndex(Schema.Category.ROW_NAME)) }.toSet() } + } + } + + object InstalledAdapter { + fun get(packageName: String, signal: CancellationSignal?): InstalledItem? { + return db.query(Schema.Installed.name, + columns = arrayOf(Schema.Installed.ROW_PACKAGE_NAME, Schema.Installed.ROW_VERSION, + Schema.Installed.ROW_VERSION_CODE, Schema.Installed.ROW_SIGNATURE), + selection = Pair("${Schema.Installed.ROW_PACKAGE_NAME} = ?", arrayOf(packageName)), + signal = signal).use { it.firstOrNull()?.let(::transform) } + } + + private fun put(installedItem: InstalledItem, notify: Boolean) { + db.insertOrReplace(true, Schema.Installed.name, ContentValues().apply { + put(Schema.Installed.ROW_PACKAGE_NAME, installedItem.packageName) + put(Schema.Installed.ROW_VERSION, installedItem.version) + put(Schema.Installed.ROW_VERSION_CODE, installedItem.versionCode) + put(Schema.Installed.ROW_SIGNATURE, installedItem.signature) + }) + if (notify) { + notifyChanged(Subject.Products) + } + } + + fun put(installedItem: InstalledItem) = put(installedItem, true) + + fun putAll(installedItems: List) { + db.beginTransaction() + try { + db.delete(Schema.Installed.name, null, null) + installedItems.forEach { put(it, false) } + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + } + + fun delete(packageName: String) { + val count = db.delete(Schema.Installed.name, "${Schema.Installed.ROW_PACKAGE_NAME} = ?", arrayOf(packageName)) + if (count > 0) { + notifyChanged(Subject.Products) + } + } + + private fun transform(cursor: Cursor): InstalledItem { + return InstalledItem(cursor.getString(cursor.getColumnIndex(Schema.Installed.ROW_PACKAGE_NAME)), + cursor.getString(cursor.getColumnIndex(Schema.Installed.ROW_VERSION)), + cursor.getLong(cursor.getColumnIndex(Schema.Installed.ROW_VERSION_CODE)), + cursor.getString(cursor.getColumnIndex(Schema.Installed.ROW_SIGNATURE))) + } + } + + object LockAdapter { + private fun put(lock: Pair, notify: Boolean) { + db.insertOrReplace(true, Schema.Lock.name, ContentValues().apply { + put(Schema.Lock.ROW_PACKAGE_NAME, lock.first) + put(Schema.Lock.ROW_VERSION_CODE, lock.second) + }) + if (notify) { + notifyChanged(Subject.Products) + } + } + + fun put(lock: Pair) = put(lock, true) + + fun putAll(locks: List>) { + db.beginTransaction() + try { + db.delete(Schema.Lock.name, null, null) + locks.forEach { put(it, false) } + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + } + + fun delete(packageName: String) { + db.delete(Schema.Lock.name, "${Schema.Lock.ROW_PACKAGE_NAME} = ?", arrayOf(packageName)) + notifyChanged(Subject.Products) + } + } + + object UpdaterAdapter { + private val Table.temporaryName: String + get() = "${name}_temporary" + + fun createTemporaryTable() { + db.execSQL("DROP TABLE IF EXISTS ${Schema.Product.temporaryName}") + db.execSQL("DROP TABLE IF EXISTS ${Schema.Category.temporaryName}") + db.execSQL(Schema.Product.formatCreateTable(Schema.Product.temporaryName)) + db.execSQL(Schema.Category.formatCreateTable(Schema.Category.temporaryName)) + } + + fun putTemporary(products: List) { + db.beginTransaction() + try { + for (product in products) { + // Format signatures like ".signature1.signature2." for easier select + val signatures = product.signatures.joinToString { ".$it" } + .let { if (it.isNotEmpty()) "$it." else "" } + db.insertOrReplace(true, Schema.Product.temporaryName, ContentValues().apply { + put(Schema.Product.ROW_REPOSITORY_ID, product.repositoryId) + put(Schema.Product.ROW_PACKAGE_NAME, product.packageName) + put(Schema.Product.ROW_NAME, product.name) + put(Schema.Product.ROW_SUMMARY, product.summary) + put(Schema.Product.ROW_DESCRIPTION, product.description) + put(Schema.Product.ROW_ADDED, product.added) + put(Schema.Product.ROW_UPDATED, product.updated) + put(Schema.Product.ROW_VERSION_CODE, product.versionCode) + put(Schema.Product.ROW_SIGNATURES, signatures) + put(Schema.Product.ROW_COMPATIBLE, if (product.compatible) 1 else 0) + put(Schema.Product.ROW_DATA, jsonGenerate(product::serialize)) + put(Schema.Product.ROW_DATA_ITEM, jsonGenerate(product.item()::serialize)) + }) + for (category in product.categories) { + db.insertOrReplace(true, Schema.Category.temporaryName, ContentValues().apply { + put(Schema.Category.ROW_REPOSITORY_ID, product.repositoryId) + put(Schema.Category.ROW_PACKAGE_NAME, product.packageName) + put(Schema.Category.ROW_NAME, category) + }) + } + } + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + } + + fun finishTemporary(repository: Repository, success: Boolean) { + if (success) { + db.beginTransaction() + try { + db.delete(Schema.Product.name, "${Schema.Product.ROW_REPOSITORY_ID} = ?", + arrayOf(repository.id.toString())) + db.delete(Schema.Category.name, "${Schema.Category.ROW_REPOSITORY_ID} = ?", + arrayOf(repository.id.toString())) + db.execSQL("INSERT INTO ${Schema.Product.name} SELECT * FROM ${Schema.Product.temporaryName}") + db.execSQL("INSERT INTO ${Schema.Category.name} SELECT * FROM ${Schema.Category.temporaryName}") + RepositoryAdapter.putWithoutNotification(repository, true) + db.execSQL("DROP TABLE IF EXISTS ${Schema.Product.temporaryName}") + db.execSQL("DROP TABLE IF EXISTS ${Schema.Category.temporaryName}") + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + if (success) { + notifyChanged(Subject.Repositories, Subject.Repository(repository.id), Subject.Products) + } + } else { + db.execSQL("DROP TABLE IF EXISTS ${Schema.Product.temporaryName}") + db.execSQL("DROP TABLE IF EXISTS ${Schema.Category.temporaryName}") + } + } + } +} diff --git a/src/main/kotlin/com/looker/droidify/database/ObservableCursor.kt b/src/main/kotlin/com/looker/droidify/database/ObservableCursor.kt new file mode 100644 index 00000000..6de7e114 --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/database/ObservableCursor.kt @@ -0,0 +1,57 @@ +package com.looker.droidify.database + +import android.database.ContentObservable +import android.database.ContentObserver +import android.database.Cursor +import android.database.CursorWrapper + +class ObservableCursor(cursor: Cursor, private val observable: (register: Boolean, + observer: () -> Unit) -> Unit): CursorWrapper(cursor) { + private var registered = false + private val contentObservable = ContentObservable() + + private val onChange: () -> Unit = { + contentObservable.dispatchChange(false, null) + } + + init { + observable(true, onChange) + registered = true + } + + override fun registerContentObserver(observer: ContentObserver) { + super.registerContentObserver(observer) + contentObservable.registerObserver(observer) + } + + override fun unregisterContentObserver(observer: ContentObserver) { + super.unregisterContentObserver(observer) + contentObservable.unregisterObserver(observer) + } + + @Suppress("DEPRECATION") + override fun requery(): Boolean { + if (!registered) { + observable(true, onChange) + registered = true + } + return super.requery() + } + + @Suppress("DEPRECATION") + override fun deactivate() { + super.deactivate() + deactivateOrClose() + } + + override fun close() { + super.close() + contentObservable.unregisterAll() + deactivateOrClose() + } + + private fun deactivateOrClose() { + observable(false, onChange) + registered = false + } +} diff --git a/src/main/kotlin/com/looker/droidify/database/QueryBuilder.kt b/src/main/kotlin/com/looker/droidify/database/QueryBuilder.kt new file mode 100644 index 00000000..11d9bec5 --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/database/QueryBuilder.kt @@ -0,0 +1,47 @@ +package com.looker.droidify.database + +import android.database.Cursor +import android.database.sqlite.SQLiteDatabase +import android.os.CancellationSignal +import com.looker.droidify.BuildConfig +import com.looker.droidify.utility.extension.android.* +import com.looker.droidify.utility.extension.text.* + +class QueryBuilder { + companion object { + fun trimQuery(query: String): String { + return query.lines().map { it.trim() }.filter { it.isNotEmpty() }.joinToString(separator = " ") + } + } + + private val builder = StringBuilder() + private val arguments = mutableListOf() + + operator fun plusAssign(query: String) { + if (builder.isNotEmpty()) { + builder.append(" ") + } + builder.append(trimQuery(query)) + } + + operator fun remAssign(argument: String) { + this.arguments += argument + } + + operator fun remAssign(arguments: List) { + this.arguments += arguments + } + + fun query(db: SQLiteDatabase, signal: CancellationSignal?): Cursor { + val query = builder.toString() + val arguments = arguments.toTypedArray() + if (BuildConfig.DEBUG) { + synchronized(QueryBuilder::class.java) { + debug(query) + db.rawQuery("EXPLAIN QUERY PLAN $query", arguments).use { it.asSequence() + .forEach { debug(":: ${it.getString(it.getColumnIndex("detail"))}") } } + } + } + return db.rawQuery(query, arguments, signal) + } +} diff --git a/src/main/kotlin/com/looker/droidify/database/QueryLoader.kt b/src/main/kotlin/com/looker/droidify/database/QueryLoader.kt new file mode 100644 index 00000000..b648aa24 --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/database/QueryLoader.kt @@ -0,0 +1,94 @@ +package com.looker.droidify.database + +import android.content.Context +import android.database.Cursor +import android.os.CancellationSignal +import android.os.OperationCanceledException +import androidx.loader.content.AsyncTaskLoader + +class QueryLoader(context: Context, private val query: (CancellationSignal) -> Cursor?): + AsyncTaskLoader(context) { + private val observer = ForceLoadContentObserver() + private var cancellationSignal: CancellationSignal? = null + private var cursor: Cursor? = null + + override fun loadInBackground(): Cursor? { + val cancellationSignal = synchronized(this) { + if (isLoadInBackgroundCanceled) { + throw OperationCanceledException() + } + val cancellationSignal = CancellationSignal() + this.cancellationSignal = cancellationSignal + cancellationSignal + } + try { + val cursor = query(cancellationSignal) + if (cursor != null) { + try { + cursor.count // Ensure the cursor window is filled + cursor.registerContentObserver(observer) + } catch (e: Exception) { + cursor.close() + throw e + } + } + return cursor + } finally { + synchronized(this) { + this.cancellationSignal = null + } + } + } + + override fun cancelLoadInBackground() { + super.cancelLoadInBackground() + + synchronized(this) { + cancellationSignal?.cancel() + } + } + + override fun deliverResult(data: Cursor?) { + if (isReset) { + data?.close() + } else { + val oldCursor = cursor + cursor = data + if (isStarted) { + super.deliverResult(data) + } + if (oldCursor != data) { + oldCursor.closeIfNeeded() + } + } + } + + override fun onStartLoading() { + cursor?.let(this::deliverResult) + if (takeContentChanged() || cursor == null) { + forceLoad() + } + } + + override fun onStopLoading() { + cancelLoad() + } + + override fun onCanceled(data: Cursor?) { + data.closeIfNeeded() + } + + override fun onReset() { + super.onReset() + + stopLoading() + cursor.closeIfNeeded() + cursor = null + } + + private fun Cursor?.closeIfNeeded() { + if (this != null && !isClosed) { + close() + } + } +} diff --git a/src/main/kotlin/com/looker/droidify/entity/InstalledItem.kt b/src/main/kotlin/com/looker/droidify/entity/InstalledItem.kt new file mode 100644 index 00000000..2608f68c --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/entity/InstalledItem.kt @@ -0,0 +1,3 @@ +package com.looker.droidify.entity + +class InstalledItem(val packageName: String, val version: String, val versionCode: Long, val signature: String) diff --git a/src/main/kotlin/com/looker/droidify/entity/Product.kt b/src/main/kotlin/com/looker/droidify/entity/Product.kt new file mode 100644 index 00000000..c61a73e8 --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/entity/Product.kt @@ -0,0 +1,227 @@ +package com.looker.droidify.entity + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.core.JsonToken +import com.looker.droidify.utility.extension.json.* +import com.looker.droidify.utility.extension.text.* + +data class Product(val repositoryId: Long, val packageName: String, val name: String, val summary: String, + val description: String, val whatsNew: String, val icon: String, val metadataIcon: String, val author: Author, + val source: String, val changelog: String, val web: String, val tracker: String, + val added: Long, val updated: Long, val suggestedVersionCode: Long, + val categories: List, val antiFeatures: List, val licenses: List, + val donates: List, val screenshots: List, val releases: List) { + data class Author(val name: String, val email: String, val web: String) + + sealed class Donate { + data class Regular(val url: String): Donate() + data class Bitcoin(val address: String): Donate() + data class Litecoin(val address: String): Donate() + data class Flattr(val id: String): Donate() + data class Liberapay(val id: String): Donate() + data class OpenCollective(val id: String): Donate() + } + + class Screenshot(val locale: String, val type: Type, val path: String) { + enum class Type(val jsonName: String) { + PHONE("phone"), + SMALL_TABLET("smallTablet"), + LARGE_TABLET("largeTablet") + } + + val identifier: String + get() = "$locale.${type.name}.$path" + } + + // Same releases with different signatures + val selectedReleases: List + get() = releases.filter { it.selected } + + val displayRelease: Release? + get() = selectedReleases.firstOrNull() ?: releases.firstOrNull() + + val version: String + get() = displayRelease?.version.orEmpty() + + val versionCode: Long + get() = selectedReleases.firstOrNull()?.versionCode ?: 0L + + val compatible: Boolean + get() = selectedReleases.firstOrNull()?.incompatibilities?.isEmpty() == true + + val signatures: List + get() = selectedReleases.mapNotNull { it.signature.nullIfEmpty() }.distinct().toList() + + fun item(): ProductItem { + return ProductItem(repositoryId, packageName, name, summary, icon, metadataIcon, version, "", compatible, false, 0) + } + + fun canUpdate(installedItem: InstalledItem?): Boolean { + return installedItem != null && compatible && versionCode > installedItem.versionCode && + installedItem.signature in signatures + } + + fun serialize(generator: JsonGenerator) { + generator.writeNumberField("serialVersion", 1) + generator.writeStringField("packageName", packageName) + generator.writeStringField("name", name) + generator.writeStringField("summary", summary) + generator.writeStringField("whatsNew", whatsNew) + generator.writeStringField("icon", icon) + generator.writeStringField("metadataIcon", metadataIcon) + generator.writeStringField("authorName", author.name) + generator.writeStringField("authorEmail", author.email) + generator.writeStringField("authorWeb", author.web) + generator.writeStringField("source", source) + generator.writeStringField("changelog", changelog) + generator.writeStringField("web", web) + generator.writeStringField("tracker", tracker) + generator.writeNumberField("added", added) + generator.writeNumberField("updated", updated) + generator.writeNumberField("suggestedVersionCode", suggestedVersionCode) + generator.writeArray("categories") { categories.forEach(::writeString) } + generator.writeArray("antiFeatures") { antiFeatures.forEach(::writeString) } + generator.writeArray("licenses") { licenses.forEach(::writeString) } + generator.writeArray("donates") { + donates.forEach { + writeDictionary { + when (it) { + is Donate.Regular -> { + writeStringField("type", "") + writeStringField("url", it.url) + } + is Donate.Bitcoin -> { + writeStringField("type", "bitcoin") + writeStringField("address", it.address) + } + is Donate.Litecoin -> { + writeStringField("type", "litecoin") + writeStringField("address", it.address) + } + is Donate.Flattr -> { + writeStringField("type", "flattr") + writeStringField("id", it.id) + } + is Donate.Liberapay -> { + writeStringField("type", "liberapay") + writeStringField("id", it.id) + } + is Donate.OpenCollective -> { + writeStringField("type", "openCollective") + writeStringField("id", it.id) + } + }::class + } + } + } + generator.writeArray("screenshots") { + screenshots.forEach { + writeDictionary { + writeStringField("locale", it.locale) + writeStringField("type", it.type.jsonName) + writeStringField("path", it.path) + } + } + } + generator.writeArray("releases") { releases.forEach { writeDictionary { it.serialize(this) } } } + } + + companion object { + fun findSuggested(products: List, installedItem: InstalledItem?, extract: (T) -> Product): T? { + return products.maxWith(compareBy({ extract(it).compatible && + (installedItem == null || installedItem.signature in extract(it).signatures) }, { extract(it).versionCode })) + } + + fun deserialize(repositoryId: Long, description: String, parser: JsonParser): Product { + var packageName = "" + var name = "" + var summary = "" + var whatsNew = "" + var icon = "" + var metadataIcon = "" + var authorName = "" + var authorEmail = "" + var authorWeb = "" + var source = "" + var changelog = "" + var web = "" + var tracker = "" + var added = 0L + var updated = 0L + var suggestedVersionCode = 0L + var categories = emptyList() + var antiFeatures = emptyList() + var licenses = emptyList() + var donates = emptyList() + var screenshots = emptyList() + var releases = emptyList() + parser.forEachKey { + when { + it.string("packageName") -> packageName = valueAsString + it.string("name") -> name = valueAsString + it.string("summary") -> summary = valueAsString + it.string("whatsNew") -> whatsNew = valueAsString + it.string("icon") -> icon = valueAsString + it.string("metadataIcon") -> metadataIcon = valueAsString + it.string("authorName") -> authorName = valueAsString + it.string("authorEmail") -> authorEmail = valueAsString + it.string("authorWeb") -> authorWeb = valueAsString + it.string("source") -> source = valueAsString + it.string("changelog") -> changelog = valueAsString + it.string("web") -> web = valueAsString + it.string("tracker") -> tracker = valueAsString + it.number("added") -> added = valueAsLong + it.number("updated") -> updated = valueAsLong + it.number("suggestedVersionCode") -> suggestedVersionCode = valueAsLong + it.array("categories") -> categories = collectNotNullStrings() + it.array("antiFeatures") -> antiFeatures = collectNotNullStrings() + it.array("licenses") -> licenses = collectNotNullStrings() + it.array("donates") -> donates = collectNotNull(JsonToken.START_OBJECT) { + var type = "" + var url = "" + var address = "" + var id = "" + forEachKey { + when { + it.string("type") -> type = valueAsString + it.string("url") -> url = valueAsString + it.string("address") -> address = valueAsString + it.string("id") -> id = valueAsString + else -> skipChildren() + } + } + when (type) { + "" -> Donate.Regular(url) + "bitcoin" -> Donate.Bitcoin(address) + "litecoin" -> Donate.Litecoin(address) + "flattr" -> Donate.Flattr(id) + "liberapay" -> Donate.Liberapay(id) + "openCollective" -> Donate.OpenCollective(id) + else -> null + } + } + it.array("screenshots") -> screenshots = collectNotNull(JsonToken.START_OBJECT) { + var locale = "" + var type = "" + var path = "" + forEachKey { + when { + it.string("locale") -> locale = valueAsString + it.string("type") -> type = valueAsString + it.string("path") -> path = valueAsString + else -> skipChildren() + } + } + Screenshot.Type.values().find { it.jsonName == type }?.let { Screenshot(locale, it, path) } + } + it.array("releases") -> releases = collectNotNull(JsonToken.START_OBJECT, Release.Companion::deserialize) + else -> skipChildren() + } + } + return Product(repositoryId, packageName, name, summary, description, whatsNew, icon, metadataIcon, + Author(authorName, authorEmail, authorWeb), source, changelog, web, tracker, added, updated, + suggestedVersionCode, categories, antiFeatures, licenses, donates, screenshots, releases) + } + } +} diff --git a/src/main/kotlin/com/looker/droidify/entity/ProductItem.kt b/src/main/kotlin/com/looker/droidify/entity/ProductItem.kt new file mode 100644 index 00000000..2d53aa4f --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/entity/ProductItem.kt @@ -0,0 +1,79 @@ +package com.looker.droidify.entity + +import android.os.Parcel +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParser +import com.looker.droidify.R +import com.looker.droidify.utility.KParcelable +import com.looker.droidify.utility.extension.json.* + +data class ProductItem(val repositoryId: Long, val packageName: String, val name: String, val summary: String, + val icon: String, val metadataIcon: String, val version: String, val installedVersion: String, + val compatible: Boolean, val canUpdate: Boolean, val matchRank: Int) { + sealed class Section: KParcelable { + object All: Section() { + @Suppress("unused") @JvmField val CREATOR = KParcelable.creator { All } + } + + data class Category(val name: String): Section() { + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeString(name) + } + + companion object { + @Suppress("unused") @JvmField val CREATOR = KParcelable.creator { + val name = it.readString()!! + Category(name) + } + } + } + + data class Repository(val id: Long, val name: String): Section() { + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeLong(id) + dest.writeString(name) + } + + companion object { + @Suppress("unused") @JvmField val CREATOR = KParcelable.creator { + val id = it.readLong() + val name = it.readString()!! + Repository(id, name) + } + } + } + } + + enum class Order(val titleResId: Int) { + NAME(R.string.name), + DATE_ADDED(R.string.date_added), + LAST_UPDATE(R.string.last_update) + } + + fun serialize(generator: JsonGenerator) { + generator.writeNumberField("serialVersion", 1) + generator.writeStringField("icon", icon) + generator.writeStringField("metadataIcon", metadataIcon) + generator.writeStringField("version", version) + } + + companion object { + fun deserialize(repositoryId: Long, packageName: String, name: String, summary: String, + installedVersion: String, compatible: Boolean, canUpdate: Boolean, matchRank: Int, + parser: JsonParser): ProductItem { + var icon = "" + var metadataIcon = "" + var version = "" + parser.forEachKey { + when { + it.string("icon") -> icon = valueAsString + it.string("metadataIcon") -> metadataIcon = valueAsString + it.string("version") -> version = valueAsString + else -> skipChildren() + } + } + return ProductItem(repositoryId, packageName, name, summary, icon, metadataIcon, + version, installedVersion, compatible, canUpdate, matchRank) + } + } +} diff --git a/src/main/kotlin/com/looker/droidify/entity/ProductPreference.kt b/src/main/kotlin/com/looker/droidify/entity/ProductPreference.kt new file mode 100644 index 00000000..700b14e8 --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/entity/ProductPreference.kt @@ -0,0 +1,31 @@ +package com.looker.droidify.entity + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParser +import com.looker.droidify.utility.extension.json.* + +data class ProductPreference(val ignoreUpdates: Boolean, val ignoreVersionCode: Long) { + fun shouldIgnoreUpdate(versionCode: Long): Boolean { + return ignoreUpdates || ignoreVersionCode == versionCode + } + + fun serialize(generator: JsonGenerator) { + generator.writeBooleanField("ignoreUpdates", ignoreUpdates) + generator.writeNumberField("ignoreVersionCode", ignoreVersionCode) + } + + companion object { + fun deserialize(parser: JsonParser): ProductPreference { + var ignoreUpdates = false + var ignoreVersionCode = 0L + parser.forEachKey { + when { + it.boolean("ignoreUpdates") -> ignoreUpdates = valueAsBoolean + it.number("ignoreVersionCode") -> ignoreVersionCode = valueAsLong + else -> skipChildren() + } + } + return ProductPreference(ignoreUpdates, ignoreVersionCode) + } + } +} diff --git a/src/main/kotlin/com/looker/droidify/entity/Release.kt b/src/main/kotlin/com/looker/droidify/entity/Release.kt new file mode 100644 index 00000000..6a0ad1a2 --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/entity/Release.kt @@ -0,0 +1,156 @@ +package com.looker.droidify.entity + +import android.net.Uri +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.core.JsonToken +import com.looker.droidify.utility.extension.json.* + +data class Release(val selected: Boolean, val version: String, val versionCode: Long, + val added: Long, val size: Long, val minSdkVersion: Int, val targetSdkVersion: Int, val maxSdkVersion: Int, + val source: String, val release: String, val hash: String, val hashType: String, val signature: String, + val obbMain: String, val obbMainHash: String, val obbMainHashType: String, + val obbPatch: String, val obbPatchHash: String, val obbPatchHashType: String, + val permissions: List, val features: List, val platforms: List, + val incompatibilities: List) { + sealed class Incompatibility { + object MinSdk: Incompatibility() + object MaxSdk: Incompatibility() + object Platform: Incompatibility() + data class Feature(val feature: String): Incompatibility() + } + + val identifier: String + get() = "$versionCode.$hash" + + fun getDownloadUrl(repository: Repository): String { + return Uri.parse(repository.address).buildUpon().appendPath(release).build().toString() + } + + val cacheFileName: String + get() = "${hash.replace('/', '-')}.apk" + + fun serialize(generator: JsonGenerator) { + generator.writeNumberField("serialVersion", 1) + generator.writeBooleanField("selected", selected) + generator.writeStringField("version", version) + generator.writeNumberField("versionCode", versionCode) + generator.writeNumberField("added", added) + generator.writeNumberField("size", size) + generator.writeNumberField("minSdkVersion", minSdkVersion) + generator.writeNumberField("targetSdkVersion", targetSdkVersion) + generator.writeNumberField("maxSdkVersion", maxSdkVersion) + generator.writeStringField("source", source) + generator.writeStringField("release", release) + generator.writeStringField("hash", hash) + generator.writeStringField("hashType", hashType) + generator.writeStringField("signature", signature) + generator.writeStringField("obbMain", obbMain) + generator.writeStringField("obbMainHash", obbMainHash) + generator.writeStringField("obbMainHashType", obbMainHashType) + generator.writeStringField("obbPatch", obbPatch) + generator.writeStringField("obbPatchHash", obbPatchHash) + generator.writeStringField("obbPatchHashType", obbPatchHashType) + generator.writeArray("permissions") { permissions.forEach { writeString(it) } } + generator.writeArray("features") { features.forEach { writeString(it) } } + generator.writeArray("platforms") { platforms.forEach { writeString(it) } } + generator.writeArray("incompatibilities") { + incompatibilities.forEach { + writeDictionary { + when (it) { + is Incompatibility.MinSdk -> { + writeStringField("type", "minSdk") + } + is Incompatibility.MaxSdk -> { + writeStringField("type", "maxSdk") + } + is Incompatibility.Platform -> { + writeStringField("type", "platform") + } + is Incompatibility.Feature -> { + writeStringField("type", "feature") + writeStringField("feature", it.feature) + } + }::class + } + } + } + } + + companion object { + fun deserialize(parser: JsonParser): Release { + var selected = false + var version = "" + var versionCode = 0L + var added = 0L + var size = 0L + var minSdkVersion = 0 + var targetSdkVersion = 0 + var maxSdkVersion = 0 + var source = "" + var release = "" + var hash = "" + var hashType = "" + var signature = "" + var obbMain = "" + var obbMainHash = "" + var obbMainHashType = "" + var obbPatch = "" + var obbPatchHash = "" + var obbPatchHashType = "" + var permissions = emptyList() + var features = emptyList() + var platforms = emptyList() + var incompatibilities = emptyList() + parser.forEachKey { + when { + it.boolean("selected") -> selected = valueAsBoolean + it.string("version") -> version = valueAsString + it.number("versionCode") -> versionCode = valueAsLong + it.number("added") -> added = valueAsLong + it.number("size") -> size = valueAsLong + it.number("minSdkVersion") -> minSdkVersion = valueAsInt + it.number("targetSdkVersion") -> targetSdkVersion = valueAsInt + it.number("maxSdkVersion") -> maxSdkVersion = valueAsInt + it.string("source") -> source = valueAsString + it.string("release") -> release = valueAsString + it.string("hash") -> hash = valueAsString + it.string("hashType") -> hashType = valueAsString + it.string("signature") -> signature = valueAsString + it.string("obbMain") -> obbMain = valueAsString + it.string("obbMainHash") -> obbMainHash = valueAsString + it.string("obbMainHashType") -> obbMainHashType = valueAsString + it.string("obbPatch") -> obbPatch = valueAsString + it.string("obbPatchHash") -> obbPatchHash = valueAsString + it.string("obbPatchHashType") -> obbPatchHashType = valueAsString + it.array("permissions") -> permissions = collectNotNullStrings() + it.array("features") -> features = collectNotNullStrings() + it.array("platforms") -> platforms = collectNotNullStrings() + it.array("incompatibilities") -> incompatibilities = collectNotNull(JsonToken.START_OBJECT) { + var type = "" + var feature = "" + forEachKey { + when { + it.string("type") -> type = valueAsString + it.string("feature") -> feature = valueAsString + else -> skipChildren() + } + } + when (type) { + "minSdk" -> Incompatibility.MinSdk + "maxSdk" -> Incompatibility.MaxSdk + "platform" -> Incompatibility.Platform + "feature" -> Incompatibility.Feature(feature) + else -> null + } + } + else -> skipChildren() + } + } + return Release(selected, version, versionCode, added, size, + minSdkVersion, targetSdkVersion, maxSdkVersion, source, release, hash, hashType, signature, + obbMain, obbMainHash, obbMainHashType, obbPatch, obbPatchHash, obbPatchHashType, + permissions, features, platforms, incompatibilities) + } + } +} diff --git a/src/main/kotlin/com/looker/droidify/entity/Repository.kt b/src/main/kotlin/com/looker/droidify/entity/Repository.kt new file mode 100644 index 00000000..eeb9374c --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/entity/Repository.kt @@ -0,0 +1,124 @@ +package com.looker.droidify.entity + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParser +import com.looker.droidify.utility.extension.json.* +import java.net.URL + +data class Repository(val id: Long, val address: String, val mirrors: List, + val name: String, val description: String, val version: Int, val enabled: Boolean, + val fingerprint: String, val lastModified: String, val entityTag: String, + val updated: Long, val timestamp: Long, val authentication: String) { + fun edit(address: String, fingerprint: String, authentication: String): Repository { + val addressChanged = this.address != address + val fingerprintChanged = this.fingerprint != fingerprint + val changed = addressChanged || fingerprintChanged + return copy(address = address, fingerprint = fingerprint, lastModified = if (changed) "" else lastModified, + entityTag = if (changed) "" else entityTag, authentication = authentication) + } + + fun update(mirrors: List, name: String, description: String, version: Int, + lastModified: String, entityTag: String, timestamp: Long): Repository { + return copy(mirrors = mirrors, name = name, description = description, + version = if (version >= 0) version else this.version, lastModified = lastModified, + entityTag = entityTag, updated = System.currentTimeMillis(), timestamp = timestamp) + } + + fun enable(enabled: Boolean): Repository { + return copy(enabled = enabled, lastModified = "", entityTag = "") + } + + fun serialize(generator: JsonGenerator) { + generator.writeNumberField("serialVersion", 1) + generator.writeStringField("address", address) + generator.writeArray("mirrors") { mirrors.forEach { writeString(it) } } + generator.writeStringField("name", name) + generator.writeStringField("description", description) + generator.writeNumberField("version", version) + generator.writeBooleanField("enabled", enabled) + generator.writeStringField("fingerprint", fingerprint) + generator.writeStringField("lastModified", lastModified) + generator.writeStringField("entityTag", entityTag) + generator.writeNumberField("updated", updated) + generator.writeNumberField("timestamp", timestamp) + generator.writeStringField("authentication", authentication) + } + + companion object { + fun deserialize(id: Long, parser: JsonParser): Repository { + var address = "" + var mirrors = emptyList() + var name = "" + var description = "" + var version = 0 + var enabled = false + var fingerprint = "" + var lastModified = "" + var entityTag = "" + var updated = 0L + var timestamp = 0L + var authentication = "" + parser.forEachKey { + when { + it.string("address") -> address = valueAsString + it.array("mirrors") -> mirrors = collectNotNullStrings() + it.string("name") -> name = valueAsString + it.string("description") -> description = valueAsString + it.number("version") -> version = valueAsInt + it.boolean("enabled") -> enabled = valueAsBoolean + it.string("fingerprint") -> fingerprint = valueAsString + it.string("lastModified") -> lastModified = valueAsString + it.string("entityTag") -> entityTag = valueAsString + it.number("updated") -> updated = valueAsLong + it.number("timestamp") -> timestamp = valueAsLong + it.string("authentication") -> authentication = valueAsString + else -> skipChildren() + } + } + return Repository(id, address, mirrors, name, description, version, enabled, fingerprint, + lastModified, entityTag, updated, timestamp, authentication) + } + + fun newRepository(address: String, fingerprint: String, authentication: String): Repository { + val name = try { + URL(address).let { "${it.host}${it.path}" } + } catch (e: Exception) { + address + } + return defaultRepository(address, name, "", 0, true, fingerprint, authentication) + } + + private fun defaultRepository(address: String, name: String, description: String, + version: Int, enabled: Boolean, fingerprint: String, authentication: String): Repository { + return Repository(-1, address, emptyList(), name, description, version, enabled, + fingerprint, "", "", 0L, 0L, authentication) + } + + val defaultRepositories = listOf(run { + defaultRepository("https://f-droid.org/repo", "F-Droid", "The official F-Droid Free Software repository. " + + "Everything in this repository is always built from the source code.", + 21, true, "43238D512C1E5EB2D6569F4A3AFBF5523418B82E0A3ED1552770ABB9A9C9CCAB", "") + }, run { + defaultRepository("https://f-droid.org/archive", "F-Droid Archive", "The archive of the official F-Droid Free " + + "Software repository. Apps here are old and can contain known vulnerabilities and security issues!", + 21, false, "43238D512C1E5EB2D6569F4A3AFBF5523418B82E0A3ED1552770ABB9A9C9CCAB", "") + }, run { + defaultRepository("https://guardianproject.info/fdroid/repo", "Guardian Project Official Releases", "The " + + "official repository of The Guardian Project apps for use with the F-Droid client. Applications in this " + + "repository are official binaries built by the original application developers and signed by the same key as " + + "the APKs that are released in the Google Play Store.", + 21, false, "B7C2EEFD8DAC7806AF67DFCD92EB18126BC08312A7F2D6F3862E46013C7A6135", "") + }, run { + defaultRepository("https://guardianproject.info/fdroid/archive", "Guardian Project Archive", "The official " + + "repository of The Guardian Project apps for use with the F-Droid client. This contains older versions of " + + "applications from the main repository.", 21, false, + "B7C2EEFD8DAC7806AF67DFCD92EB18126BC08312A7F2D6F3862E46013C7A6135", "") + }, run { + defaultRepository("https://apt.izzysoft.de/fdroid/repo", "IzzyOnDroid F-Droid Repo", "This is a " + + "repository of apps to be used with F-Droid the original application developers, taken from the resp. " + + "repositories (mostly GitHub). At this moment I cannot give guarantees on regular updates for all of them, " + + "though most are checked multiple times a week ", 21, true, + "3BF0D6ABFEAE2F401707B6D966BE743BF0EEE49C2561B9BA39073711F628937A", "") + }) + } +} diff --git a/src/main/kotlin/com/looker/droidify/graphics/DrawableWrapper.kt b/src/main/kotlin/com/looker/droidify/graphics/DrawableWrapper.kt new file mode 100644 index 00000000..472e4f53 --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/graphics/DrawableWrapper.kt @@ -0,0 +1,56 @@ +package com.looker.droidify.graphics + +import android.graphics.Canvas +import android.graphics.ColorFilter +import android.graphics.Rect +import android.graphics.drawable.Drawable + +open class DrawableWrapper(val drawable: Drawable): Drawable() { + init { + drawable.callback = object: Callback { + override fun invalidateDrawable(who: Drawable) { + callback?.invalidateDrawable(who) + } + + override fun scheduleDrawable(who: Drawable, what: Runnable, `when`: Long) { + callback?.scheduleDrawable(who, what, `when`) + } + + override fun unscheduleDrawable(who: Drawable, what: Runnable) { + callback?.unscheduleDrawable(who, what) + } + } + } + + override fun onBoundsChange(bounds: Rect) { + drawable.bounds = bounds + } + + override fun getIntrinsicWidth(): Int = drawable.intrinsicWidth + override fun getIntrinsicHeight(): Int = drawable.intrinsicHeight + override fun getMinimumWidth(): Int = drawable.minimumWidth + override fun getMinimumHeight(): Int = drawable.minimumHeight + + override fun draw(canvas: Canvas) { + drawable.draw(canvas) + } + + override fun getAlpha(): Int { + return drawable.alpha + } + + override fun setAlpha(alpha: Int) { + drawable.alpha = alpha + } + + override fun getColorFilter(): ColorFilter? { + return drawable.colorFilter + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + drawable.colorFilter = colorFilter + } + + @Suppress("DEPRECATION") + override fun getOpacity(): Int = drawable.opacity +} diff --git a/src/main/kotlin/com/looker/droidify/graphics/PaddingDrawable.kt b/src/main/kotlin/com/looker/droidify/graphics/PaddingDrawable.kt new file mode 100644 index 00000000..3d1cc5d3 --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/graphics/PaddingDrawable.kt @@ -0,0 +1,19 @@ +package com.looker.droidify.graphics + +import android.graphics.Rect +import android.graphics.drawable.Drawable +import kotlin.math.* + +class PaddingDrawable(drawable: Drawable, private val factor: Float): DrawableWrapper(drawable) { + override fun getIntrinsicWidth(): Int = (factor * super.getIntrinsicWidth()).roundToInt() + override fun getIntrinsicHeight(): Int = (factor * super.getIntrinsicHeight()).roundToInt() + + override fun onBoundsChange(bounds: Rect) { + val width = (bounds.width() / factor).roundToInt() + val height = (bounds.height() / factor).roundToInt() + val left = (bounds.width() - width) / 2 + val top = (bounds.height() - height) / 2 + drawable.setBounds(bounds.left + left, bounds.top + top, + bounds.left + left + width, bounds.top + top + height) + } +} diff --git a/src/main/kotlin/com/looker/droidify/index/IndexHandler.kt b/src/main/kotlin/com/looker/droidify/index/IndexHandler.kt new file mode 100644 index 00000000..4f19f525 --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/index/IndexHandler.kt @@ -0,0 +1,265 @@ +package com.looker.droidify.index + +import com.looker.droidify.entity.Product +import com.looker.droidify.entity.Release +import com.looker.droidify.utility.extension.android.* +import org.xml.sax.Attributes +import org.xml.sax.helpers.DefaultHandler +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.TimeZone + +class IndexHandler(private val repositoryId: Long, private val callback: Callback): DefaultHandler() { + companion object { + private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.US) + .apply { timeZone = TimeZone.getTimeZone("UTC") } + + private fun String.parseDate(): Long { + return try { + dateFormat.parse(this)?.time ?: 0L + } catch (e: Exception) { + 0L + } + } + + internal fun validateIcon(icon: String): String { + return if (icon.endsWith(".xml")) "" else icon + } + } + + interface Callback { + fun onRepository(mirrors: List, name: String, description: String, + certificate: String, version: Int, timestamp: Long) + fun onProduct(product: Product) + } + + internal object DonateComparator: Comparator { + private val classes = listOf(Product.Donate.Regular::class, Product.Donate.Bitcoin::class, + Product.Donate.Litecoin::class, Product.Donate.Flattr::class, Product.Donate.Liberapay::class, + Product.Donate.OpenCollective::class) + + override fun compare(donate1: Product.Donate, donate2: Product.Donate): Int { + val index1 = classes.indexOf(donate1::class) + val index2 = classes.indexOf(donate2::class) + return when { + index1 >= 0 && index2 == -1 -> -1 + index2 >= 0 && index1 == -1 -> 1 + else -> index1.compareTo(index2) + } + } + } + + private class RepositoryBuilder { + var address = "" + val mirrors = mutableListOf() + var name = "" + var description = "" + var certificate = "" + var version = -1 + var timestamp = 0L + } + + private class ProductBuilder(val repositoryId: Long, val packageName: String) { + var name = "" + var summary = "" + var description = "" + var icon = "" + var authorName = "" + var authorEmail = "" + var source = "" + var changelog = "" + var web = "" + var tracker = "" + var added = 0L + var updated = 0L + var suggestedVersionCode = 0L + val categories = linkedSetOf() + val antiFeatures = linkedSetOf() + val licenses = mutableListOf() + val donates = mutableListOf() + val releases = mutableListOf() + + fun build(): Product { + return Product(repositoryId, packageName, name, summary, description, "", icon, "", + Product.Author(authorName, authorEmail, ""), source, changelog, web, tracker, added, updated, + suggestedVersionCode, categories.toList(), antiFeatures.toList(), + licenses, donates.sortedWith(DonateComparator), emptyList(), releases) + } + } + + private class ReleaseBuilder { + var version = "" + var versionCode = 0L + var added = 0L + var size = 0L + var minSdkVersion = 0 + var targetSdkVersion = 0 + var maxSdkVersion = 0 + var source = "" + var release = "" + var hash = "" + var hashType = "" + var signature = "" + var obbMain = "" + var obbMainHash = "" + var obbPatch = "" + var obbPatchHash = "" + val permissions = linkedSetOf() + val features = linkedSetOf() + val platforms = linkedSetOf() + + fun build(): Release { + val hashType = if (hash.isNotEmpty() && hashType.isEmpty()) "sha256" else hashType + val obbMainHashType = if (obbMainHash.isNotEmpty()) "sha256" else "" + val obbPatchHashType = if (obbPatchHash.isNotEmpty()) "sha256" else "" + return Release(false, version, versionCode, added, size, + minSdkVersion, targetSdkVersion, maxSdkVersion, source, release, hash, hashType, signature, + obbMain, obbMainHash, obbMainHashType, obbPatch, obbPatchHash, obbPatchHashType, + permissions.toList(), features.toList(), platforms.toList(), emptyList()) + } + } + + private val contentBuilder = StringBuilder() + + private var repositoryBuilder: RepositoryBuilder? = RepositoryBuilder() + private var productBuilder: ProductBuilder? = null + private var releaseBuilder: ReleaseBuilder? = null + + private fun Attributes.get(localName: String): String = getValue("", localName).orEmpty() + private fun String.cleanWhiteSpace(): String = replace("\\s".toRegex(), " ") + + override fun startElement(uri: String, localName: String, qName: String, attributes: Attributes) { + super.startElement(uri, localName, qName, attributes) + + val repositoryBuilder = repositoryBuilder + val productBuilder = productBuilder + val releaseBuilder = releaseBuilder + contentBuilder.setLength(0) + + when { + localName == "repo" -> { + if (repositoryBuilder != null) { + repositoryBuilder.address = attributes.get("url").cleanWhiteSpace() + repositoryBuilder.name = attributes.get("name").cleanWhiteSpace() + repositoryBuilder.description = attributes.get("description").cleanWhiteSpace() + repositoryBuilder.certificate = attributes.get("pubkey") + repositoryBuilder.version = attributes.get("version").toIntOrNull() ?: 0 + repositoryBuilder.timestamp = (attributes.get("timestamp").toLongOrNull() ?: 0L) * 1000L + } + } + localName == "application" && productBuilder == null -> { + this.productBuilder = ProductBuilder(repositoryId, attributes.get("id")) + } + localName == "package" && productBuilder != null && releaseBuilder == null -> { + this.releaseBuilder = ReleaseBuilder() + } + localName == "hash" && releaseBuilder != null -> { + releaseBuilder.hashType = attributes.get("type") + } + (localName == "uses-permission" || localName.startsWith("uses-permission-")) && releaseBuilder != null -> { + val minSdkVersion = if (localName != "uses-permission") { + "uses-permission-sdk-(\\d+)".toRegex().matchEntire(localName) + ?.destructured?.let { (version) -> version.toIntOrNull() } + } else { + null + } ?: 0 + val maxSdkVersion = attributes.get("maxSdkVersion").toIntOrNull() ?: Int.MAX_VALUE + if (Android.sdk in minSdkVersion .. maxSdkVersion) { + releaseBuilder.permissions.add(attributes.get("name")) + } else { + releaseBuilder.permissions.remove(attributes.get("name")) + } + } + } + } + + override fun endElement(uri: String, localName: String, qName: String) { + super.endElement(uri, localName, qName) + + val repositoryBuilder = repositoryBuilder + val productBuilder = productBuilder + val releaseBuilder = releaseBuilder + val content = contentBuilder.toString() + + when { + localName == "repo" -> { + if (repositoryBuilder != null) { + val mirrors = (listOf(repositoryBuilder.address) + repositoryBuilder.mirrors) + .filter { it.isNotEmpty() }.distinct() + callback.onRepository(mirrors, repositoryBuilder.name, repositoryBuilder.description, + repositoryBuilder.certificate, repositoryBuilder.version, repositoryBuilder.timestamp) + this.repositoryBuilder = null + } + } + localName == "application" && productBuilder != null -> { + val product = productBuilder.build() + this.productBuilder = null + callback.onProduct(product) + } + localName == "package" && productBuilder != null && releaseBuilder != null -> { + productBuilder.releases.add(releaseBuilder.build()) + this.releaseBuilder = null + } + repositoryBuilder != null -> { + when (localName) { + "description" -> repositoryBuilder.description = content.cleanWhiteSpace() + "mirror" -> repositoryBuilder.mirrors += content + } + } + productBuilder != null && releaseBuilder != null -> { + when (localName) { + "version" -> releaseBuilder.version = content + "versioncode" -> releaseBuilder.versionCode = content.toLongOrNull() ?: 0L + "added" -> releaseBuilder.added = content.parseDate() + "size" -> releaseBuilder.size = content.toLongOrNull() ?: 0 + "sdkver" -> releaseBuilder.minSdkVersion = content.toIntOrNull() ?: 0 + "targetSdkVersion" -> releaseBuilder.targetSdkVersion = content.toIntOrNull() ?: 0 + "maxsdkver" -> releaseBuilder.maxSdkVersion = content.toIntOrNull() ?: 0 + "srcname" -> releaseBuilder.source = content + "apkname" -> releaseBuilder.release = content + "hash" -> releaseBuilder.hash = content + "sig" -> releaseBuilder.signature = content + "obbMainFile" -> releaseBuilder.obbMain = content + "obbMainFileSha256" -> releaseBuilder.obbMainHash = content + "obbPatchFile" -> releaseBuilder.obbPatch = content + "obbPatchFileSha256" -> releaseBuilder.obbPatchHash = content + "permissions" -> releaseBuilder.permissions += content.split(',').filter { it.isNotEmpty() } + "features" -> releaseBuilder.features += content.split(',').filter { it.isNotEmpty() } + "nativecode" -> releaseBuilder.platforms += content.split(',').filter { it.isNotEmpty() } + } + } + productBuilder != null -> { + when (localName) { + "name" -> productBuilder.name = content + "summary" -> productBuilder.summary = content + "description" -> productBuilder.description = "

$content

" + "desc" -> productBuilder.description = content.replace("\n", "
") + "icon" -> productBuilder.icon = validateIcon(content) + "author" -> productBuilder.authorName = content + "email" -> productBuilder.authorEmail = content + "source" -> productBuilder.source = content + "changelog" -> productBuilder.changelog = content + "web" -> productBuilder.web = content + "tracker" -> productBuilder.tracker = content + "added" -> productBuilder.added = content.parseDate() + "lastupdated" -> productBuilder.updated = content.parseDate() + "marketvercode" -> productBuilder.suggestedVersionCode = content.toLongOrNull() ?: 0L + "categories" -> productBuilder.categories += content.split(',').filter { it.isNotEmpty() } + "antifeatures" -> productBuilder.antiFeatures += content.split(',').filter { it.isNotEmpty() } + "license" -> productBuilder.licenses += content.split(',').filter { it.isNotEmpty() } + "donate" -> productBuilder.donates += Product.Donate.Regular(content) + "bitcoin" -> productBuilder.donates += Product.Donate.Bitcoin(content) + "litecoin" -> productBuilder.donates += Product.Donate.Litecoin(content) + "flattr" -> productBuilder.donates += Product.Donate.Flattr(content) + "liberapay" -> productBuilder.donates += Product.Donate.Liberapay(content) + "openCollective" -> productBuilder.donates += Product.Donate.OpenCollective(content) + } + } + } + } + + override fun characters(ch: CharArray, start: Int, length: Int) { + super.characters(ch, start, length) + contentBuilder.append(ch, start, length) + } +} diff --git a/src/main/kotlin/com/looker/droidify/index/IndexMerger.kt b/src/main/kotlin/com/looker/droidify/index/IndexMerger.kt new file mode 100644 index 00000000..c629c7bf --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/index/IndexMerger.kt @@ -0,0 +1,83 @@ +package com.looker.droidify.index + +import android.content.ContentValues +import android.database.sqlite.SQLiteDatabase +import com.fasterxml.jackson.core.JsonToken +import com.looker.droidify.entity.Product +import com.looker.droidify.entity.Release +import com.looker.droidify.utility.extension.android.* +import com.looker.droidify.utility.extension.json.* +import java.io.ByteArrayOutputStream +import java.io.Closeable +import java.io.File + +class IndexMerger(file: File): Closeable { + private val db = SQLiteDatabase.openOrCreateDatabase(file, null) + + init { + db.execWithResult("PRAGMA synchronous = OFF") + db.execWithResult("PRAGMA journal_mode = OFF") + db.execSQL("CREATE TABLE product (package_name TEXT PRIMARY KEY, description TEXT NOT NULL, data BLOB NOT NULL)") + db.execSQL("CREATE TABLE releases (package_name TEXT PRIMARY KEY, data BLOB NOT NULL)") + db.beginTransaction() + } + + fun addProducts(products: List) { + for (product in products) { + val outputStream = ByteArrayOutputStream() + Json.factory.createGenerator(outputStream).use { it.writeDictionary(product::serialize) } + db.insert("product", null, ContentValues().apply { + put("package_name", product.packageName) + put("description", product.description) + put("data", outputStream.toByteArray()) + }) + } + } + + fun addReleases(pairs: List>>) { + for (pair in pairs) { + val (packageName, releases) = pair + val outputStream = ByteArrayOutputStream() + Json.factory.createGenerator(outputStream).use { + it.writeStartArray() + for (release in releases) { + it.writeDictionary(release::serialize) + } + it.writeEndArray() + } + db.insert("releases", null, ContentValues().apply { + put("package_name", packageName) + put("data", outputStream.toByteArray()) + }) + } + } + + private fun closeTransaction() { + if (db.inTransaction()) { + db.setTransactionSuccessful() + db.endTransaction() + } + } + + fun forEach(repositoryId: Long, windowSize: Int, callback: (List, Int) -> Unit) { + closeTransaction() + db.rawQuery("""SELECT product.description, product.data AS pd, releases.data AS rd FROM product + LEFT JOIN releases ON product.package_name = releases.package_name""", null) + ?.use { it.asSequence().map { + val description = it.getString(0) + val product = Json.factory.createParser(it.getBlob(1)).use { + it.nextToken() + Product.deserialize(repositoryId, description, it) + } + val releases = it.getBlob(2)?.let { Json.factory.createParser(it).use { + it.nextToken() + it.collectNotNull(JsonToken.START_OBJECT, Release.Companion::deserialize) + } }.orEmpty() + product.copy(releases = releases) + }.windowed(windowSize, windowSize, true).forEach { products -> callback(products, it.count) } } + } + + override fun close() { + db.use { closeTransaction() } + } +} diff --git a/src/main/kotlin/com/looker/droidify/index/IndexV1Parser.kt b/src/main/kotlin/com/looker/droidify/index/IndexV1Parser.kt new file mode 100644 index 00000000..2f527f34 --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/index/IndexV1Parser.kt @@ -0,0 +1,260 @@ +package com.looker.droidify.index + +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.core.JsonToken +import com.looker.droidify.entity.Product +import com.looker.droidify.entity.Release +import com.looker.droidify.utility.extension.android.* +import com.looker.droidify.utility.extension.json.* +import com.looker.droidify.utility.extension.text.* +import java.io.InputStream + +object IndexV1Parser { + interface Callback { + fun onRepository(mirrors: List, name: String, description: String, version: Int, timestamp: Long) + fun onProduct(product: Product) + fun onReleases(packageName: String, releases: List) + } + + private class Screenshots(val phone: List, val smallTablet: List, val largeTablet: List) + private class Localized(val name: String, val summary: String, val description: String, + val whatsNew: String, val metadataIcon: String, val screenshots: Screenshots?) + + private fun Map.getAndCall(key: String, callback: (String, Localized) -> T?): T? { + return this[key]?.let { callback(key, it) } + } + + private fun Map.find(callback: (String, Localized) -> T?): T? { + return getAndCall("en-US", callback) ?: getAndCall("en_US", callback) ?: getAndCall("en", callback) + } + + private fun Map.findString(fallback: String, callback: (Localized) -> String): String { + return (find { _, localized -> callback(localized).nullIfEmpty() } ?: fallback).trim() + } + + fun parse(repositoryId: Long, inputStream: InputStream, callback: Callback) { + val jsonParser = Json.factory.createParser(inputStream) + if (jsonParser.nextToken() != JsonToken.START_OBJECT) { + jsonParser.illegal() + } else { + jsonParser.forEachKey { + when { + it.dictionary("repo") -> { + var address = "" + var mirrors = emptyList() + var name = "" + var description = "" + var version = 0 + var timestamp = 0L + forEachKey { + when { + it.string("address") -> address = valueAsString + it.array("mirrors") -> mirrors = collectDistinctNotEmptyStrings() + it.string("name") -> name = valueAsString + it.string("description") -> description = valueAsString + it.number("version") -> version = valueAsInt + it.number("timestamp") -> timestamp = valueAsLong + else -> skipChildren() + } + } + val realMirrors = ((if (address.isNotEmpty()) listOf(address) else emptyList()) + mirrors).distinct() + callback.onRepository(realMirrors, name, description, version, timestamp) + } + it.array("apps") -> forEach(JsonToken.START_OBJECT) { + val product = parseProduct(repositoryId) + callback.onProduct(product) + } + it.dictionary("packages") -> forEachKey { + if (it.token == JsonToken.START_ARRAY) { + val packageName = it.key + val releases = collectNotNull(JsonToken.START_OBJECT) { parseRelease() } + callback.onReleases(packageName, releases) + } else { + skipChildren() + } + } + else -> skipChildren() + } + } + } + } + + private fun JsonParser.parseProduct(repositoryId: Long): Product { + var packageName = "" + var nameFallback = "" + var summaryFallback = "" + var descriptionFallback = "" + var icon = "" + var authorName = "" + var authorEmail = "" + var authorWeb = "" + var source = "" + var changelog = "" + var web = "" + var tracker = "" + var added = 0L + var updated = 0L + var suggestedVersionCode = 0L + var categories = emptyList() + var antiFeatures = emptyList() + val licenses = mutableListOf() + val donates = mutableListOf() + val localizedMap = mutableMapOf() + forEachKey { + when { + it.string("packageName") -> packageName = valueAsString + it.string("name") -> nameFallback = valueAsString + it.string("summary") -> summaryFallback = valueAsString + it.string("description") -> descriptionFallback = valueAsString + it.string("icon") -> icon = IndexHandler.validateIcon(valueAsString) + it.string("authorName") -> authorName = valueAsString + it.string("authorEmail") -> authorEmail = valueAsString + it.string("authorWebSite") -> authorWeb = valueAsString + it.string("sourceCode") -> source = valueAsString + it.string("changelog") -> changelog = valueAsString + it.string("webSite") -> web = valueAsString + it.string("issueTracker") -> tracker = valueAsString + it.number("added") -> added = valueAsLong + it.number("lastUpdated") -> updated = valueAsLong + it.string("suggestedVersionCode") -> suggestedVersionCode = valueAsString.toLongOrNull() ?: 0L + it.array("categories") -> categories = collectDistinctNotEmptyStrings() + it.array("antiFeatures") -> antiFeatures = collectDistinctNotEmptyStrings() + it.string("license") -> licenses += valueAsString.split(',').filter { it.isNotEmpty() } + it.string("donate") -> donates += Product.Donate.Regular(valueAsString) + it.string("bitcoin") -> donates += Product.Donate.Bitcoin(valueAsString) + it.string("flattrID") -> donates += Product.Donate.Flattr(valueAsString) + it.string("liberapayID") -> donates += Product.Donate.Liberapay(valueAsString) + it.string("openCollective") -> donates += Product.Donate.OpenCollective(valueAsString) + it.dictionary("localized") -> forEachKey { + if (it.token == JsonToken.START_OBJECT) { + val locale = it.key + var name = "" + var summary = "" + var description = "" + var whatsNew = "" + var metadataIcon = "" + var phone = emptyList() + var smallTablet = emptyList() + var largeTablet = emptyList() + forEachKey { + when { + it.string("name") -> name = valueAsString + it.string("summary") -> summary = valueAsString + it.string("description") -> description = valueAsString + it.string("whatsNew") -> whatsNew = valueAsString + it.string("icon") -> metadataIcon = valueAsString + it.array("phoneScreenshots") -> phone = collectDistinctNotEmptyStrings() + it.array("sevenInchScreenshots") -> smallTablet = collectDistinctNotEmptyStrings() + it.array("tenInchScreenshots") -> largeTablet = collectDistinctNotEmptyStrings() + else -> skipChildren() + } + } + val screenshots = if (sequenceOf(phone, smallTablet, largeTablet).any { it.isNotEmpty() }) + Screenshots(phone, smallTablet, largeTablet) else null + localizedMap[locale] = Localized(name, summary, description, whatsNew, + metadataIcon.nullIfEmpty()?.let { "$locale/$it" }.orEmpty(), screenshots) + } else { + skipChildren() + } + } + else -> skipChildren() + } + } + val name = localizedMap.findString(nameFallback) { it.name } + val summary = localizedMap.findString(summaryFallback) { it.summary } + val description = localizedMap.findString(descriptionFallback) { it.description }.replace("\n", "
") + val whatsNew = localizedMap.findString("") { it.whatsNew }.replace("\n", "
") + val metadataIcon = localizedMap.findString("") { it.metadataIcon } + val screenshotPairs = localizedMap.find { key, localized -> localized.screenshots?.let { Pair(key, it) } } + val screenshots = screenshotPairs + ?.let { (key, screenshots) -> screenshots.phone.asSequence() + .map { Product.Screenshot(key, Product.Screenshot.Type.PHONE, it) } + + screenshots.smallTablet.asSequence() + .map { Product.Screenshot(key, Product.Screenshot.Type.SMALL_TABLET, it) } + + screenshots.largeTablet.asSequence() + .map { Product.Screenshot(key, Product.Screenshot.Type.LARGE_TABLET, it) } } + .orEmpty().toList() + return Product(repositoryId, packageName, name, summary, description, whatsNew, icon, metadataIcon, + Product.Author(authorName, authorEmail, authorWeb), source, changelog, web, tracker, added, updated, + suggestedVersionCode, categories, antiFeatures, licenses, + donates.sortedWith(IndexHandler.DonateComparator), screenshots, emptyList()) + } + + private fun JsonParser.parseRelease(): Release { + var version = "" + var versionCode = 0L + var added = 0L + var size = 0L + var minSdkVersion = 0 + var targetSdkVersion = 0 + var maxSdkVersion = 0 + var source = "" + var release = "" + var hash = "" + var hashTypeCandidate = "" + var signature = "" + var obbMain = "" + var obbMainHash = "" + var obbPatch = "" + var obbPatchHash = "" + val permissions = linkedSetOf() + var features = emptyList() + var platforms = emptyList() + forEachKey { + when { + it.string("versionName") -> version = valueAsString + it.number("versionCode") -> versionCode = valueAsLong + it.number("added") -> added = valueAsLong + it.number("size") -> size = valueAsLong + it.number("minSdkVersion") -> minSdkVersion = valueAsInt + it.number("targetSdkVersion") -> targetSdkVersion = valueAsInt + it.number("maxSdkVersion") -> maxSdkVersion = valueAsInt + it.string("srcname") -> source = valueAsString + it.string("apkName") -> release = valueAsString + it.string("hash") -> hash = valueAsString + it.string("hashType") -> hashTypeCandidate = valueAsString + it.string("sig") -> signature = valueAsString + it.string("obbMainFile") -> obbMain = valueAsString + it.string("obbMainFileSha256") -> obbMainHash = valueAsString + it.string("obbPatchFile") -> obbPatch = valueAsString + it.string("obbPatchFileSha256") -> obbPatchHash = valueAsString + it.array("uses-permission") -> collectPermissions(permissions, 0) + it.array("uses-permission-sdk-23") -> collectPermissions(permissions, 23) + it.array("features") -> features = collectDistinctNotEmptyStrings() + it.array("nativecode") -> platforms = collectDistinctNotEmptyStrings() + else -> skipChildren() + } + } + val hashType = if (hash.isNotEmpty() && hashTypeCandidate.isEmpty()) "sha256" else hashTypeCandidate + val obbMainHashType = if (obbMainHash.isNotEmpty()) "sha256" else "" + val obbPatchHashType = if (obbPatchHash.isNotEmpty()) "sha256" else "" + return Release(false, version, versionCode, added, size, + minSdkVersion, targetSdkVersion, maxSdkVersion, source, release, hash, hashType, signature, + obbMain, obbMainHash, obbMainHashType, obbPatch, obbPatchHash, obbPatchHashType, + permissions.toList(), features, platforms, emptyList()) + } + + private fun JsonParser.collectPermissions(permissions: LinkedHashSet, minSdk: Int) { + forEach(JsonToken.START_ARRAY) { + val firstToken = nextToken() + val permission = if (firstToken == JsonToken.VALUE_STRING) valueAsString else "" + if (firstToken != JsonToken.END_ARRAY) { + val secondToken = nextToken() + val maxSdk = if (secondToken == JsonToken.VALUE_NUMBER_INT) valueAsInt else 0 + if (permission.isNotEmpty() && Android.sdk >= minSdk && (maxSdk <= 0 || Android.sdk <= maxSdk)) { + permissions.add(permission) + } + if (secondToken != JsonToken.END_ARRAY) { + while (true) { + val token = nextToken() + if (token == JsonToken.END_ARRAY) { + break + } else if (token.isStructStart) { + skipChildren() + } + } + } + } + } + } +} diff --git a/src/main/kotlin/com/looker/droidify/index/RepositoryUpdater.kt b/src/main/kotlin/com/looker/droidify/index/RepositoryUpdater.kt new file mode 100644 index 00000000..5dec9d14 --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/index/RepositoryUpdater.kt @@ -0,0 +1,345 @@ +package com.looker.droidify.index + +import android.content.Context +import android.net.Uri +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.schedulers.Schedulers +import com.looker.droidify.content.Cache +import com.looker.droidify.database.Database +import com.looker.droidify.entity.Product +import com.looker.droidify.entity.Release +import com.looker.droidify.entity.Repository +import com.looker.droidify.network.Downloader +import com.looker.droidify.utility.ProgressInputStream +import com.looker.droidify.utility.RxUtils +import com.looker.droidify.utility.Utils +import com.looker.droidify.utility.extension.android.* +import com.looker.droidify.utility.extension.text.* +import org.xml.sax.InputSource +import java.io.File +import java.security.cert.X509Certificate +import java.util.Locale +import java.util.jar.JarEntry +import java.util.jar.JarFile +import javax.xml.parsers.SAXParserFactory + +object RepositoryUpdater { + enum class Stage { + DOWNLOAD, PROCESS, MERGE, COMMIT + } + + private enum class IndexType(val jarName: String, val contentName: String, val certificateFromIndex: Boolean) { + INDEX("index.jar", "index.xml", true), + INDEX_V1("index-v1.jar", "index-v1.json", false) + } + + enum class ErrorType { + NETWORK, HTTP, VALIDATION, PARSING + } + + class UpdateException: Exception { + val errorType: ErrorType + + constructor(errorType: ErrorType, message: String): super(message) { + this.errorType = errorType + } + + constructor(errorType: ErrorType, message: String, cause: Exception): super(message, cause) { + this.errorType = errorType + } + } + + private lateinit var context: Context + private val updaterLock = Any() + private val cleanupLock = Any() + + fun init(context: Context) { + this.context = context + + var lastDisabled = setOf() + Observable.just(Unit) + .concatWith(Database.observable(Database.Subject.Repositories)) + .observeOn(Schedulers.io()) + .flatMapSingle { RxUtils.querySingle { Database.RepositoryAdapter.getAllDisabledDeleted(it) } } + .forEach { + val newDisabled = it.asSequence().filter { !it.second }.map { it.first }.toSet() + val disabled = newDisabled - lastDisabled + lastDisabled = newDisabled + val deleted = it.asSequence().filter { it.second }.map { it.first }.toSet() + if (disabled.isNotEmpty() || deleted.isNotEmpty()) { + val pairs = (disabled.asSequence().map { Pair(it, false) } + + deleted.asSequence().map { Pair(it, true) }).toSet() + synchronized(cleanupLock) { Database.RepositoryAdapter.cleanup(pairs) } + } + } + } + + fun await() { + synchronized(updaterLock) { } + } + + fun update(repository: Repository, unstable: Boolean, + callback: (Stage, Long, Long?) -> Unit): Single { + return update(repository, listOf(IndexType.INDEX_V1, IndexType.INDEX), unstable, callback) + } + + private fun update(repository: Repository, indexTypes: List, unstable: Boolean, + callback: (Stage, Long, Long?) -> Unit): Single { + val indexType = indexTypes[0] + return downloadIndex(repository, indexType, callback) + .flatMap { (result, file) -> + when { + result.isNotChanged -> { + file.delete() + Single.just(false) + } + !result.success -> { + file.delete() + if (result.code == 404 && indexTypes.isNotEmpty()) { + update(repository, indexTypes.subList(1, indexTypes.size), unstable, callback) + } else { + Single.error(UpdateException(ErrorType.HTTP, "Invalid response: HTTP ${result.code}")) + } + } + else -> { + RxUtils.managedSingle { processFile(repository, indexType, unstable, + file, result.lastModified, result.entityTag, callback) } + } + } + } + } + + private fun downloadIndex(repository: Repository, indexType: IndexType, + callback: (Stage, Long, Long?) -> Unit): Single> { + return Single.just(Unit) + .map { Cache.getTemporaryFile(context) } + .flatMap { file -> Downloader + .download(Uri.parse(repository.address).buildUpon() + .appendPath(indexType.jarName).build().toString(), file, repository.lastModified, repository.entityTag, + repository.authentication) { read, total -> callback(Stage.DOWNLOAD, read, total) } + .subscribeOn(Schedulers.io()) + .map { Pair(it, file) } + .onErrorResumeNext { + file.delete() + when (it) { + is InterruptedException, is RuntimeException, is Error -> Single.error(it) + is Exception -> Single.error(UpdateException(ErrorType.NETWORK, "Network error", it)) + else -> Single.error(it) + } + } } + } + + private fun processFile(repository: Repository, indexType: IndexType, unstable: Boolean, + file: File, lastModified: String, entityTag: String, callback: (Stage, Long, Long?) -> Unit): Boolean { + var rollback = true + return synchronized(updaterLock) { + try { + val jarFile = JarFile(file, true) + val indexEntry = jarFile.getEntry(indexType.contentName) as JarEntry + val total = indexEntry.size + Database.UpdaterAdapter.createTemporaryTable() + val features = context.packageManager.systemAvailableFeatures + .asSequence().map { it.name }.toSet() + setOf("android.hardware.touchscreen") + + val (changedRepository, certificateFromIndex) = when (indexType) { + IndexType.INDEX -> { + val factory = SAXParserFactory.newInstance() + factory.isNamespaceAware = true + val parser = factory.newSAXParser() + val reader = parser.xmlReader + var changedRepository: Repository? = null + var certificateFromIndex: String? = null + val products = mutableListOf() + + reader.contentHandler = IndexHandler(repository.id, object: IndexHandler.Callback { + override fun onRepository(mirrors: List, name: String, description: String, + certificate: String, version: Int, timestamp: Long) { + changedRepository = repository.update(mirrors, name, description, version, + lastModified, entityTag, timestamp) + certificateFromIndex = certificate.toLowerCase(Locale.US) + } + + override fun onProduct(product: Product) { + if (Thread.interrupted()) { + throw InterruptedException() + } + products += transformProduct(product, features, unstable) + if (products.size >= 50) { + Database.UpdaterAdapter.putTemporary(products) + products.clear() + } + } + }) + + ProgressInputStream(jarFile.getInputStream(indexEntry)) { callback(Stage.PROCESS, it, total) } + .use { reader.parse(InputSource(it)) } + if (Thread.interrupted()) { + throw InterruptedException() + } + if (products.isNotEmpty()) { + Database.UpdaterAdapter.putTemporary(products) + products.clear() + } + Pair(changedRepository, certificateFromIndex) + } + IndexType.INDEX_V1 -> { + var changedRepository: Repository? = null + + val mergerFile = Cache.getTemporaryFile(context) + try { + val unmergedProducts = mutableListOf() + val unmergedReleases = mutableListOf>>() + IndexMerger(mergerFile).use { indexMerger -> + ProgressInputStream(jarFile.getInputStream(indexEntry)) { callback(Stage.PROCESS, it, total) }.use { + IndexV1Parser.parse(repository.id, it, object: IndexV1Parser.Callback { + override fun onRepository(mirrors: List, name: String, description: String, + version: Int, timestamp: Long) { + changedRepository = repository.update(mirrors, name, description, version, + lastModified, entityTag, timestamp) + } + + override fun onProduct(product: Product) { + if (Thread.interrupted()) { + throw InterruptedException() + } + unmergedProducts += product + if (unmergedProducts.size >= 50) { + indexMerger.addProducts(unmergedProducts) + unmergedProducts.clear() + } + } + + override fun onReleases(packageName: String, releases: List) { + if (Thread.interrupted()) { + throw InterruptedException() + } + unmergedReleases += Pair(packageName, releases) + if (unmergedReleases.size >= 50) { + indexMerger.addReleases(unmergedReleases) + unmergedReleases.clear() + } + } + }) + + if (Thread.interrupted()) { + throw InterruptedException() + } + if (unmergedProducts.isNotEmpty()) { + indexMerger.addProducts(unmergedProducts) + unmergedProducts.clear() + } + if (unmergedReleases.isNotEmpty()) { + indexMerger.addReleases(unmergedReleases) + unmergedReleases.clear() + } + var progress = 0 + indexMerger.forEach(repository.id, 50) { products, totalCount -> + if (Thread.interrupted()) { + throw InterruptedException() + } + progress += products.size + callback(Stage.MERGE, progress.toLong(), totalCount.toLong()) + Database.UpdaterAdapter.putTemporary(products + .map { transformProduct(it, features, unstable) }) + } + } + } + } finally { + mergerFile.delete() + } + Pair(changedRepository, null) + } + } + + val workRepository = changedRepository ?: repository + if (workRepository.timestamp < repository.timestamp) { + throw UpdateException(ErrorType.VALIDATION, "New index is older than current index: " + + "${workRepository.timestamp} < ${repository.timestamp}") + } else { + val fingerprint = run { + val certificateFromJar = run { + val codeSigners = indexEntry.codeSigners + if (codeSigners == null || codeSigners.size != 1) { + throw UpdateException(ErrorType.VALIDATION, "index.jar must be signed by a single code signer") + } else { + val certificates = codeSigners[0].signerCertPath?.certificates.orEmpty() + if (certificates.size != 1) { + throw UpdateException(ErrorType.VALIDATION, "index.jar code signer should have only one certificate") + } else { + certificates[0] as X509Certificate + } + } + } + val fingerprintFromJar = Utils.calculateFingerprint(certificateFromJar) + if (indexType.certificateFromIndex) { + val fingerprintFromIndex = certificateFromIndex?.unhex()?.let(Utils::calculateFingerprint) + if (fingerprintFromIndex == null || fingerprintFromJar != fingerprintFromIndex) { + throw UpdateException(ErrorType.VALIDATION, "index.xml contains invalid public key") + } + fingerprintFromIndex + } else { + fingerprintFromJar + } + } + + val commitRepository = if (workRepository.fingerprint != fingerprint) { + if (workRepository.fingerprint.isEmpty()) { + workRepository.copy(fingerprint = fingerprint) + } else { + throw UpdateException(ErrorType.VALIDATION, "Certificate fingerprints do not match") + } + } else { + workRepository + } + if (Thread.interrupted()) { + throw InterruptedException() + } + callback(Stage.COMMIT, 0, null) + synchronized(cleanupLock) { Database.UpdaterAdapter.finishTemporary(commitRepository, true) } + rollback = false + true + } + } catch (e: Exception) { + throw when (e) { + is UpdateException, is InterruptedException -> e + else -> UpdateException(ErrorType.PARSING, "Error parsing index", e) + } + } finally { + file.delete() + if (rollback) { + Database.UpdaterAdapter.finishTemporary(repository, false) + } + } + } + } + + private fun transformProduct(product: Product, features: Set, unstable: Boolean): Product { + val releasePairs = product.releases.distinctBy { it.identifier }.sortedByDescending { it.versionCode }.map { + val incompatibilities = mutableListOf() + if (it.minSdkVersion > 0 && Android.sdk < it.minSdkVersion) { + incompatibilities += Release.Incompatibility.MinSdk + } + if (it.maxSdkVersion > 0 && Android.sdk > it.maxSdkVersion) { + incompatibilities += Release.Incompatibility.MaxSdk + } + if (it.platforms.isNotEmpty() && it.platforms.intersect(Android.platforms).isEmpty()) { + incompatibilities += Release.Incompatibility.Platform + } + incompatibilities += (it.features - features).sorted().map { Release.Incompatibility.Feature(it) } + Pair(it, incompatibilities as List) + }.toMutableList() + + val predicate: (Release) -> Boolean = { unstable || product.suggestedVersionCode <= 0 || + it.versionCode <= product.suggestedVersionCode } + val firstCompatibleReleaseIndex = releasePairs.indexOfFirst { it.second.isEmpty() && predicate(it.first) } + val firstReleaseIndex = if (firstCompatibleReleaseIndex >= 0) firstCompatibleReleaseIndex else + releasePairs.indexOfFirst { predicate(it.first) } + val firstSelected = if (firstReleaseIndex >= 0) releasePairs[firstReleaseIndex] else null + + val releases = releasePairs.map { (release, incompatibilities) -> release + .copy(incompatibilities = incompatibilities, selected = firstSelected + ?.let { it.first.versionCode == release.versionCode && it.second == incompatibilities } == true) } + return product.copy(releases = releases) + } +} diff --git a/src/main/kotlin/com/looker/droidify/network/Downloader.kt b/src/main/kotlin/com/looker/droidify/network/Downloader.kt new file mode 100644 index 00000000..d4c1323e --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/network/Downloader.kt @@ -0,0 +1,114 @@ +package com.looker.droidify.network + +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.schedulers.Schedulers +import com.looker.droidify.utility.ProgressInputStream +import com.looker.droidify.utility.RxUtils +import okhttp3.Cache +import okhttp3.Call +import okhttp3.OkHttpClient +import okhttp3.Request +import java.io.File +import java.io.FileOutputStream +import java.net.InetSocketAddress +import java.net.Proxy +import java.util.concurrent.TimeUnit + +object Downloader { + private data class ClientConfiguration(val cache: Cache?, val onion: Boolean) + + private val clients = mutableMapOf() + private val onionProxy = Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", 9050)) + + var proxy: Proxy? = null + set(value) { + if (field != value) { + synchronized(clients) { + field = value + clients.keys.removeAll { !it.onion } + } + } + } + + private fun createClient(proxy: Proxy?, cache: Cache?): OkHttpClient { + return OkHttpClient.Builder() + .connectTimeout(30L, TimeUnit.SECONDS) + .readTimeout(15L, TimeUnit.SECONDS) + .writeTimeout(15L, TimeUnit.SECONDS) + .proxy(proxy).cache(cache).build() + } + + class Result(val code: Int, val lastModified: String, val entityTag: String) { + val success: Boolean + get() = code == 200 || code == 206 + + val isNotChanged: Boolean + get() = code == 304 + } + + fun createCall(request: Request.Builder, authentication: String, cache: Cache?): Call { + val oldRequest = request.build() + val newRequest = if (authentication.isNotEmpty()) { + request.addHeader("Authorization", authentication).build() + } else { + request.build() + } + val onion = oldRequest.url.host.endsWith(".onion") + val client = synchronized(clients) { + val proxy = if (onion) onionProxy else proxy + val clientConfiguration = ClientConfiguration(cache, onion) + clients[clientConfiguration] ?: run { + val client = createClient(proxy, cache) + clients[clientConfiguration] = client + client + } + } + return client.newCall(newRequest) + } + + fun download(url: String, target: File, lastModified: String, entityTag: String, authentication: String, + callback: ((read: Long, total: Long?) -> Unit)?): Single { + val start = if (target.exists()) target.length().let { if (it > 0L) it else null } else null + val request = Request.Builder().url(url) + .apply { + if (entityTag.isNotEmpty()) { + addHeader("If-None-Match", entityTag) + } else if (lastModified.isNotEmpty()) { + addHeader("If-Modified-Since", lastModified) + } + if (start != null) { + addHeader("Range", "bytes=$start-") + } + } + + return RxUtils + .callSingle { createCall(request, authentication, null) } + .subscribeOn(Schedulers.io()) + .flatMap { result -> RxUtils + .managedSingle { result.use { + if (result.code == 304) { + Result(it.code, lastModified, entityTag) + } else { + val body = it.body!! + val append = start != null && it.header("Content-Range") != null + val progressStart = if (append && start != null) start else 0L + val progressTotal = body.contentLength().let { if (it >= 0L) it else null } + ?.let { progressStart + it } + val inputStream = ProgressInputStream(body.byteStream()) { + if (Thread.interrupted()) { + throw InterruptedException() + } + callback?.invoke(progressStart + it, progressTotal) + } + inputStream.use { input -> + val outputStream = if (append) FileOutputStream(target, true) else FileOutputStream(target) + outputStream.use { output -> + input.copyTo(output) + output.fd.sync() + } + } + Result(it.code, it.header("Last-Modified").orEmpty(), it.header("ETag").orEmpty()) + } + } } } + } +} diff --git a/src/main/kotlin/com/looker/droidify/network/PicassoDownloader.kt b/src/main/kotlin/com/looker/droidify/network/PicassoDownloader.kt new file mode 100644 index 00000000..2845561d --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/network/PicassoDownloader.kt @@ -0,0 +1,118 @@ +package com.looker.droidify.network + +import android.content.Context +import android.net.Uri +import android.view.View +import com.looker.droidify.entity.Product +import com.looker.droidify.entity.Repository +import com.looker.droidify.utility.extension.text.* +import okhttp3.Cache +import okhttp3.Call +import okhttp3.HttpUrl.Companion.toHttpUrl +import java.io.File +import kotlin.math.* + +object PicassoDownloader { + private const val HOST_ICON = "icon" + private const val HOST_SCREENSHOT = "screenshot" + private const val QUERY_ADDRESS = "address" + private const val QUERY_AUTHENTICATION = "authentication" + private const val QUERY_PACKAGE_NAME = "packageName" + private const val QUERY_ICON = "icon" + private const val QUERY_METADATA_ICON = "metadataIcon" + private const val QUERY_LOCALE = "locale" + private const val QUERY_DEVICE = "device" + private const val QUERY_SCREENSHOT = "screenshot" + private const val QUERY_DPI = "dpi" + + private val supportedDpis = listOf(120, 160, 240, 320, 480, 640) + + class Factory(cacheDir: File): Call.Factory { + private val cache = Cache(cacheDir, 50_000_000L) + + override fun newCall(request: okhttp3.Request): Call { + return when (request.url.host) { + HOST_ICON -> { + val address = request.url.queryParameter(QUERY_ADDRESS)?.nullIfEmpty() + val authentication = request.url.queryParameter(QUERY_AUTHENTICATION) + val path = run { + val packageName = request.url.queryParameter(QUERY_PACKAGE_NAME)?.nullIfEmpty() + val icon = request.url.queryParameter(QUERY_ICON)?.nullIfEmpty() + val metadataIcon = request.url.queryParameter(QUERY_METADATA_ICON)?.nullIfEmpty() + val dpi = request.url.queryParameter(QUERY_DPI)?.nullIfEmpty() + when { + icon != null -> "${if (dpi != null) "icons-$dpi" else "icons"}/$icon" + packageName != null && metadataIcon != null -> "$packageName/$metadataIcon" + else -> null + } + } + if (address == null || path == null) { + Downloader.createCall(request.newBuilder(), "", null) + } else { + Downloader.createCall(request.newBuilder().url(address.toHttpUrl() + .newBuilder().addPathSegments(path).build()), authentication.orEmpty(), cache) + } + } + HOST_SCREENSHOT -> { + val address = request.url.queryParameter(QUERY_ADDRESS) + val authentication = request.url.queryParameter(QUERY_AUTHENTICATION) + val packageName = request.url.queryParameter(QUERY_PACKAGE_NAME) + val locale = request.url.queryParameter(QUERY_LOCALE) + val device = request.url.queryParameter(QUERY_DEVICE) + val screenshot = request.url.queryParameter(QUERY_SCREENSHOT) + if (screenshot.isNullOrEmpty() || address.isNullOrEmpty()) { + Downloader.createCall(request.newBuilder(), "", null) + } else { + Downloader.createCall(request.newBuilder().url(address.toHttpUrl() + .newBuilder().addPathSegment(packageName.orEmpty()).addPathSegment(locale.orEmpty()) + .addPathSegment(device.orEmpty()).addPathSegment(screenshot.orEmpty()).build()), + authentication.orEmpty(), cache) + } + } + else -> { + Downloader.createCall(request.newBuilder(), "", null) + } + } + } + } + + fun createScreenshotUri(repository: Repository, packageName: String, screenshot: Product.Screenshot): Uri { + return Uri.Builder().scheme("https").authority(HOST_SCREENSHOT) + .appendQueryParameter(QUERY_ADDRESS, repository.address) + .appendQueryParameter(QUERY_AUTHENTICATION, repository.authentication) + .appendQueryParameter(QUERY_PACKAGE_NAME, packageName) + .appendQueryParameter(QUERY_LOCALE, screenshot.locale) + .appendQueryParameter(QUERY_DEVICE, when (screenshot.type) { + Product.Screenshot.Type.PHONE -> "phoneScreenshots" + Product.Screenshot.Type.SMALL_TABLET -> "sevenInchScreenshots" + Product.Screenshot.Type.LARGE_TABLET -> "tenInchScreenshots" + }) + .appendQueryParameter(QUERY_SCREENSHOT, screenshot.path) + .build() + } + + fun createIconUri(view: View, packageName: String, icon: String, metadataIcon: String, repository: Repository): Uri { + val size = (view.layoutParams.let { min(it.width, it.height) } / + view.resources.displayMetrics.density).roundToInt() + return createIconUri(view.context, packageName, icon, metadataIcon, size, repository) + } + + private fun createIconUri(context: Context, packageName: String, icon: String, metadataIcon: String, + targetSizeDp: Int, repository: Repository): Uri { + return Uri.Builder().scheme("https").authority(HOST_ICON) + .appendQueryParameter(QUERY_ADDRESS, repository.address) + .appendQueryParameter(QUERY_AUTHENTICATION, repository.authentication) + .appendQueryParameter(QUERY_PACKAGE_NAME, packageName) + .appendQueryParameter(QUERY_ICON, icon) + .appendQueryParameter(QUERY_METADATA_ICON, metadataIcon) + .apply { + if (repository.version >= 11) { + val displayDpi = context.resources.displayMetrics.densityDpi + val requiredDpi = displayDpi * targetSizeDp / 48 + val iconDpi = supportedDpis.find { it >= requiredDpi } ?: supportedDpis.last() + appendQueryParameter(QUERY_DPI, iconDpi.toString()) + } + } + .build() + } +} diff --git a/src/main/kotlin/com/looker/droidify/screen/EditRepositoryFragment.kt b/src/main/kotlin/com/looker/droidify/screen/EditRepositoryFragment.kt new file mode 100644 index 00000000..d12baf41 --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/screen/EditRepositoryFragment.kt @@ -0,0 +1,484 @@ +package com.looker.droidify.screen + +import android.app.AlertDialog +import android.content.ClipboardManager +import android.content.Context +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.net.Uri +import android.os.Bundle +import android.text.Editable +import android.text.Selection +import android.text.TextWatcher +import android.util.Base64 +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.EditText +import android.widget.FrameLayout +import android.widget.TextView +import android.widget.Toolbar +import androidx.fragment.app.DialogFragment +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.disposables.Disposable +import io.reactivex.rxjava3.schedulers.Schedulers +import com.looker.droidify.R +import com.looker.droidify.database.Database +import com.looker.droidify.entity.Repository +import com.looker.droidify.network.Downloader +import com.looker.droidify.service.Connection +import com.looker.droidify.service.SyncService +import com.looker.droidify.utility.RxUtils +import com.looker.droidify.utility.Utils +import com.looker.droidify.utility.extension.resources.* +import com.looker.droidify.utility.extension.text.* +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import java.net.URI +import java.net.URL +import java.nio.charset.Charset +import java.util.Locale +import kotlin.math.* + +class EditRepositoryFragment(): ScreenFragment() { + companion object { + private const val EXTRA_REPOSITORY_ID = "repositoryId" + + private val checkPaths = listOf("", "fdroid/repo", "repo") + } + + constructor(repositoryId: Long?): this() { + arguments = Bundle().apply { + repositoryId?.let { putLong(EXTRA_REPOSITORY_ID, it) } + } + } + + private class Layout(view: View) { + val address = view.findViewById(R.id.address)!! + val addressMirror = view.findViewById(R.id.address_mirror)!! + val addressError = view.findViewById(R.id.address_error)!! + val fingerprint = view.findViewById(R.id.fingerprint)!! + val fingerprintError = view.findViewById(R.id.fingerprint_error)!! + val username = view.findViewById(R.id.username)!! + val usernameError = view.findViewById(R.id.username_error)!! + val password = view.findViewById(R.id.password)!! + val passwordError = view.findViewById(R.id.password_error)!! + val overlay = view.findViewById(R.id.overlay)!! + val skip = view.findViewById(R.id.skip)!! + } + + private val repositoryId: Long? + get() = requireArguments().let { if (it.containsKey(EXTRA_REPOSITORY_ID)) + it.getLong(EXTRA_REPOSITORY_ID) else null } + + private lateinit var errorColorFilter: PorterDuffColorFilter + + private var saveMenuItem: MenuItem? = null + private var layout: Layout? = null + + private val syncConnection = Connection(SyncService::class.java) + private var repositoriesDisposable: Disposable? = null + private var checkDisposable: Disposable? = null + + private var takenAddresses = emptySet() + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return inflater.inflate(R.layout.fragment, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + syncConnection.bind(requireContext()) + + val toolbar = view.findViewById(R.id.toolbar)!! + screenActivity.onToolbarCreated(toolbar) + if (repositoryId != null) { + toolbar.setTitle(R.string.edit_repository) + } else { + toolbar.setTitle(R.string.add_repository) + } + + toolbar.menu.apply { + saveMenuItem = add(R.string.save) + .setIcon(Utils.getToolbarIcon(toolbar.context, R.drawable.ic_save)) + .setEnabled(false) + .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS) + .setOnMenuItemClickListener { + onSaveRepositoryClick(true) + true + } + } + + val content = view.findViewById(R.id.fragment_content)!! + errorColorFilter = PorterDuffColorFilter(content.context + .getColorFromAttr(R.attr.colorError).defaultColor, PorterDuff.Mode.SRC_IN) + + content.addView(content.inflate(R.layout.edit_repository)) + val layout = Layout(content) + this.layout = layout + + layout.fingerprint.hint = generateSequence { "FF" }.take(32).joinToString(separator = " ") + layout.fingerprint.addTextChangedListener(object: TextWatcher { + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit + override fun onTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit + + private val validChar: (Char) -> Boolean = { it in '0' .. '9' || it in 'a' .. 'f' || it in 'A' .. 'F' } + + private fun logicalPosition(s: String, position: Int): Int { + return if (position > 0) s.asSequence().take(position).count(validChar) else position + } + + private fun realPosition(s: String, position: Int): Int { + return if (position > 0) { + var left = position + val index = s.indexOfFirst { + validChar(it) && run { + left -= 1 + left <= 0 + } + } + if (index >= 0) min(index + 1, s.length) else s.length + } else { + position + } + } + + override fun afterTextChanged(s: Editable) { + val inputString = s.toString() + val outputString = inputString.toUpperCase(Locale.US) + .filter(validChar).windowed(2, 2, true).take(32).joinToString(separator = " ") + if (inputString != outputString) { + val inputStart = logicalPosition(inputString, Selection.getSelectionStart(s)) + val inputEnd = logicalPosition(inputString, Selection.getSelectionEnd(s)) + s.replace(0, s.length, outputString) + Selection.setSelection(s, realPosition(outputString, inputStart), realPosition(outputString, inputEnd)) + } + } + }) + + if (savedInstanceState == null) { + val repository = repositoryId?.let(Database.RepositoryAdapter::get) + if (repository == null) { + val clipboardManager = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val text = clipboardManager.primaryClip + ?.let { if (it.itemCount > 0) it else null } + ?.getItemAt(0)?.text?.toString().orEmpty() + val (addressText, fingerprintText) = try { + val uri = Uri.parse(URL(text).toString()) + val fingerprintText = uri.getQueryParameter("fingerprint")?.nullIfEmpty() + ?: uri.getQueryParameter("FINGERPRINT")?.nullIfEmpty() + Pair(uri.buildUpon().path(uri.path?.pathCropped) + .query(null).fragment(null).build().toString(), fingerprintText) + } catch (e: Exception) { + Pair(null, null) + } + layout.address.setText(addressText?.nullIfEmpty() ?: layout.address.hint) + layout.fingerprint.setText(fingerprintText) + } else { + layout.address.setText(repository.address) + val mirrors = repository.mirrors.map { it.withoutKnownPath } + if (mirrors.isNotEmpty()) { + layout.addressMirror.visibility = View.VISIBLE + layout.address.apply { setPaddingRelative(paddingStart, paddingTop, + paddingEnd + layout.addressMirror.layoutParams.width, paddingBottom) } + layout.addressMirror.setOnClickListener { SelectMirrorDialog(mirrors) + .show(childFragmentManager, SelectMirrorDialog::class.java.name) } + } + layout.fingerprint.setText(repository.fingerprint) + val (usernameText, passwordText) = repository.authentication.nullIfEmpty() + ?.let { if (it.startsWith("Basic ")) it.substring(6) else null } + ?.let { + try { + Base64.decode(it, Base64.NO_WRAP).toString(Charset.defaultCharset()) + } catch (e: Exception) { + e.printStackTrace() + null + } + } + ?.let { + val index = it.indexOf(':') + if (index >= 0) Pair(it.substring(0, index), it.substring(index + 1)) else null + } + ?: Pair(null, null) + layout.username.setText(usernameText) + layout.password.setText(passwordText) + } + } + + layout.address.addTextChangedListener(SimpleTextWatcher { invalidateAddress() }) + layout.fingerprint.addTextChangedListener(SimpleTextWatcher { invalidateFingerprint() }) + layout.username.addTextChangedListener(SimpleTextWatcher { invalidateUsernamePassword() }) + layout.password.addTextChangedListener(SimpleTextWatcher { invalidateUsernamePassword() }) + + (layout.overlay.parent as ViewGroup).layoutTransition?.setDuration(200L) + layout.overlay.background!!.apply { + mutate() + alpha = 0xcc + } + layout.skip.setOnClickListener { + if (checkDisposable != null) { + checkDisposable?.dispose() + checkDisposable = null + onSaveRepositoryClick(false) + } + } + + repositoriesDisposable = Observable.just(Unit) + .concatWith(Database.observable(Database.Subject.Repositories)) + .observeOn(Schedulers.io()) + .flatMapSingle { RxUtils.querySingle { Database.RepositoryAdapter.getAll(it) } } + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + takenAddresses = it.asSequence().filter { it.id != repositoryId } + .flatMap { (it.mirrors + it.address).asSequence() } + .map { it.withoutKnownPath }.toSet() + invalidateAddress() + } + } + + override fun onDestroyView() { + super.onDestroyView() + + saveMenuItem = null + layout = null + + syncConnection.unbind(requireContext()) + repositoriesDisposable?.dispose() + repositoriesDisposable = null + checkDisposable?.dispose() + checkDisposable = null + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + invalidateAddress() + invalidateFingerprint() + invalidateUsernamePassword() + } + + private var addressError = false + private var fingerprintError = false + private var usernamePasswordError = false + + private fun invalidateAddress() { + invalidateAddress(layout!!.address.text.toString()) + } + + private fun invalidateAddress(addressText: String) { + val layout = layout!! + val normalizedAddress = normalizeAddress(addressText) + val addressErrorResId = if (normalizedAddress != null) { + if (normalizedAddress.withoutKnownPath in takenAddresses) { + R.string.already_exists + } else { + null + } + } else { + R.string.invalid_address + } + layout.address.setError(addressErrorResId != null) + layout.addressError.visibility = if (addressErrorResId != null) View.VISIBLE else View.GONE + if (addressErrorResId != null) { + layout.addressError.setText(addressErrorResId) + } + addressError = addressErrorResId != null + invalidateState() + } + + private fun invalidateFingerprint() { + val layout = layout!! + val fingerprint = layout.fingerprint.text.toString().replace(" ", "") + val fingerprintInvalid = fingerprint.isNotEmpty() && fingerprint.length != 64 + layout.fingerprintError.visibility = if (fingerprintInvalid) View.VISIBLE else View.GONE + if (fingerprintInvalid) { + layout.fingerprintError.setText(R.string.invalid_fingerprint_format) + } + layout.fingerprint.setError(fingerprintInvalid) + fingerprintError = fingerprintInvalid + invalidateState() + } + + private fun invalidateUsernamePassword() { + val layout = layout!! + val username = layout.username.text.toString() + val password = layout.password.text.toString() + val usernameInvalid = username.contains(':') + val usernameEmpty = username.isEmpty() && password.isNotEmpty() + val passwordEmpty = username.isNotEmpty() && password.isEmpty() + layout.usernameError.visibility = if (usernameInvalid || usernameEmpty) View.VISIBLE else View.GONE + layout.passwordError.visibility = if (passwordEmpty) View.VISIBLE else View.GONE + if (usernameInvalid) { + layout.usernameError.setText(R.string.invalid_username_format) + } else if (usernameEmpty) { + layout.usernameError.setText(R.string.username_missing) + } + layout.username.setError(usernameEmpty) + if (passwordEmpty) { + layout.passwordError.setText(R.string.password_missing) + } + layout.password.setError(passwordEmpty) + usernamePasswordError = usernameInvalid || usernameEmpty || passwordEmpty + invalidateState() + } + + private fun invalidateState() { + val layout = layout!! + saveMenuItem!!.isEnabled = !addressError && !fingerprintError && + !usernamePasswordError && checkDisposable == null + layout.apply { sequenceOf(address, addressMirror, fingerprint, username, password) + .forEach { it.isEnabled = checkDisposable == null } } + layout.overlay.visibility = if (checkDisposable != null) View.VISIBLE else View.GONE + } + + private val String.pathCropped: String + get() { + val index = indexOfLast { it != '/' } + return if (index >= 0 && index < length - 1) substring(0, index + 1) else this + } + + private val String.withoutKnownPath: String + get() { + val cropped = pathCropped + val endsWith = checkPaths.asSequence().filter { it.isNotEmpty() } + .sortedByDescending { it.length }.find { cropped.endsWith("/$it") } + return if (endsWith != null) cropped.substring(0, cropped.length - endsWith.length - 1) else cropped + } + + private fun normalizeAddress(address: String): String? { + val uri = try { + val uri = URI(address) + if (uri.isAbsolute) uri.normalize() else null + } catch (e: Exception) { + null + } + val path = uri?.path?.pathCropped + return if (uri != null && path != null) { + try { + URI(uri.scheme, uri.userInfo, uri.host, uri.port, path, uri.query, uri.fragment).toString() + } catch (e: Exception) { + null + } + } else { + null + } + } + + private fun setMirror(address: String) { + layout?.address?.setText(address) + } + + private fun EditText.setError(error: Boolean) { + val drawable = background.mutate() + drawable.colorFilter = if (error) errorColorFilter else null + } + + private fun onSaveRepositoryClick(check: Boolean) { + if (checkDisposable == null) { + val layout = layout!! + val address = normalizeAddress(layout.address.text.toString())!! + val fingerprint = layout.fingerprint.text.toString().replace(" ", "") + val username = layout.username.text.toString().nullIfEmpty() + val password = layout.password.text.toString().nullIfEmpty() + val paths = sequenceOf("", "fdroid/repo", "repo") + val authentication = username?.let { u -> password + ?.let { p -> Base64.encodeToString("$u:$p".toByteArray(Charset.defaultCharset()), Base64.NO_WRAP) } } + ?.let { "Basic $it" }.orEmpty() + + if (check) { + checkDisposable = paths + .fold(Single.just("")) { oldAddressSingle, checkPath -> oldAddressSingle + .flatMap { oldAddress -> + if (oldAddress.isEmpty()) { + val builder = Uri.parse(address).buildUpon() + .let { if (checkPath.isEmpty()) it else it.appendEncodedPath(checkPath) } + val newAddress = builder.build() + val indexAddress = builder.appendPath("index.jar").build() + RxUtils + .callSingle { Downloader + .createCall(Request.Builder().method("HEAD", null) + .url(indexAddress.toString().toHttpUrl()), authentication, null) } + .subscribeOn(Schedulers.io()) + .map { if (it.code == 200) newAddress.toString() else "" } + } else { + Single.just(oldAddress) + } + } + } + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { result, throwable -> + checkDisposable = null + throwable?.printStackTrace() + val resultAddress = result?.let { if (it.isEmpty()) null else it } ?: address + val allow = resultAddress == address || run { + layout.address.setText(resultAddress) + invalidateAddress(resultAddress) + !addressError + } + if (allow) { + onSaveRepositoryProceedInvalidate(resultAddress, fingerprint, authentication) + } else { + invalidateState() + } + } + invalidateState() + } else { + onSaveRepositoryProceedInvalidate(address, fingerprint, authentication) + } + } + } + + private fun onSaveRepositoryProceedInvalidate(address: String, fingerprint: String, authentication: String) { + val binder = syncConnection.binder + if (binder != null) { + val repositoryId = repositoryId + if (repositoryId != null && binder.isCurrentlySyncing(repositoryId)) { + MessageDialog(MessageDialog.Message.CantEditSyncing).show(childFragmentManager) + invalidateState() + } else { + val repository = repositoryId?.let(Database.RepositoryAdapter::get) + ?.edit(address, fingerprint, authentication) + ?: Repository.newRepository(address, fingerprint, authentication) + val changedRepository = Database.RepositoryAdapter.put(repository) + if (repositoryId == null && changedRepository.enabled) { + binder.sync(changedRepository) + } + requireActivity().onBackPressed() + } + } else { + invalidateState() + } + } + + private class SimpleTextWatcher(private val callback: (Editable) -> Unit): TextWatcher { + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit + override fun onTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit + override fun afterTextChanged(s: Editable) = callback(s) + } + + class SelectMirrorDialog(): DialogFragment() { + companion object { + private const val EXTRA_MIRRORS = "mirrors" + } + + constructor(mirrors: List): this() { + arguments = Bundle().apply { + putStringArrayList(EXTRA_MIRRORS, ArrayList(mirrors)) + } + } + + override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog { + val mirrors = requireArguments().getStringArrayList(EXTRA_MIRRORS)!! + return AlertDialog.Builder(requireContext()) + .setTitle(R.string.select_mirror) + .setItems(mirrors.toTypedArray()) { _, position -> (parentFragment as EditRepositoryFragment) + .setMirror(mirrors[position]) } + .setNegativeButton(R.string.cancel, null) + .create() + } + } +} diff --git a/src/main/kotlin/com/looker/droidify/screen/MessageDialog.kt b/src/main/kotlin/com/looker/droidify/screen/MessageDialog.kt new file mode 100644 index 00000000..37b4342f --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/screen/MessageDialog.kt @@ -0,0 +1,231 @@ +package com.looker.droidify.screen + +import android.app.AlertDialog +import android.content.ActivityNotFoundException +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.os.Parcel +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.FragmentManager +import com.looker.droidify.R +import com.looker.droidify.entity.Release +import com.looker.droidify.utility.KParcelable +import com.looker.droidify.utility.PackageItemResolver +import com.looker.droidify.utility.extension.android.* +import com.looker.droidify.utility.extension.text.* + +class MessageDialog(): DialogFragment() { + companion object { + private const val EXTRA_MESSAGE = "message" + } + + sealed class Message: KParcelable { + object DeleteRepositoryConfirm: Message() { + @Suppress("unused") @JvmField val CREATOR = KParcelable.creator { DeleteRepositoryConfirm } + } + + object CantEditSyncing: Message() { + @Suppress("unused") @JvmField val CREATOR = KParcelable.creator { CantEditSyncing } + } + + class Link(val uri: Uri): Message() { + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeString(uri.toString()) + } + + companion object { + @Suppress("unused") @JvmField val CREATOR = KParcelable.creator { + val uri = Uri.parse(it.readString()!!) + Link(uri) + } + } + } + + class Permissions(val group: String?, val permissions: List): Message() { + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeString(group) + dest.writeStringList(permissions) + } + + companion object { + @Suppress("unused") @JvmField val CREATOR = KParcelable.creator { + val group = it.readString() + val permissions = it.createStringArrayList()!! + Permissions(group, permissions) + } + } + } + + class ReleaseIncompatible(val incompatibilities: List, + val platforms: List, val minSdkVersion: Int, val maxSdkVersion: Int): Message() { + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeInt(incompatibilities.size) + for (incompatibility in incompatibilities) { + when (incompatibility) { + is Release.Incompatibility.MinSdk -> { + dest.writeInt(0) + } + is Release.Incompatibility.MaxSdk -> { + dest.writeInt(1) + } + is Release.Incompatibility.Platform -> { + dest.writeInt(2) + } + is Release.Incompatibility.Feature -> { + dest.writeInt(3) + dest.writeString(incompatibility.feature) + } + }::class + } + dest.writeStringList(platforms) + dest.writeInt(minSdkVersion) + dest.writeInt(maxSdkVersion) + } + + companion object { + @Suppress("unused") @JvmField val CREATOR = KParcelable.creator { + val count = it.readInt() + val incompatibilities = generateSequence { + when (it.readInt()) { + 0 -> Release.Incompatibility.MinSdk + 1 -> Release.Incompatibility.MaxSdk + 2 -> Release.Incompatibility.Platform + 3 -> Release.Incompatibility.Feature(it.readString()!!) + else -> throw RuntimeException() + } + }.take(count).toList() + val platforms = it.createStringArrayList()!! + val minSdkVersion = it.readInt() + val maxSdkVersion = it.readInt() + ReleaseIncompatible(incompatibilities, platforms, minSdkVersion, maxSdkVersion) + } + } + } + + object ReleaseOlder: Message() { + @Suppress("unused") @JvmField val CREATOR = KParcelable.creator { ReleaseOlder } + } + + object ReleaseSignatureMismatch: Message() { + @Suppress("unused") @JvmField val CREATOR = KParcelable.creator { ReleaseSignatureMismatch } + } + } + + constructor(message: Message): this() { + arguments = Bundle().apply { + putParcelable(EXTRA_MESSAGE, message) + } + } + + fun show(fragmentManager: FragmentManager) { + show(fragmentManager, this::class.java.name) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog { + val dialog = AlertDialog.Builder(requireContext()) + when (val message = requireArguments().getParcelable(EXTRA_MESSAGE)!!) { + is Message.DeleteRepositoryConfirm -> { + dialog.setTitle(R.string.confirmation) + dialog.setMessage(R.string.delete_repository_DESC) + dialog.setPositiveButton(R.string.delete) { _, _ -> (parentFragment as RepositoryFragment).onDeleteConfirm() } + dialog.setNegativeButton(R.string.cancel, null) + } + is Message.CantEditSyncing -> { + dialog.setTitle(R.string.action_failed) + dialog.setMessage(R.string.cant_edit_sync_DESC) + dialog.setPositiveButton(R.string.ok, null) + } + is Message.Link -> { + dialog.setTitle(R.string.confirmation) + dialog.setMessage(getString(R.string.open_DESC_FORMAT, message.uri.toString())) + dialog.setPositiveButton(R.string.ok) { _, _ -> + try { + startActivity(Intent(Intent.ACTION_VIEW, message.uri)) + } catch (e: ActivityNotFoundException) { + e.printStackTrace() + } + } + dialog.setNegativeButton(R.string.cancel, null) + } + is Message.Permissions -> { + val packageManager = requireContext().packageManager + val builder = StringBuilder() + val localCache = PackageItemResolver.LocalCache() + val title = if (message.group != null) { + val name = try { + val permissionGroupInfo = packageManager.getPermissionGroupInfo(message.group, 0) + PackageItemResolver.loadLabel(requireContext(), localCache, permissionGroupInfo) + ?.nullIfEmpty()?.let { if (it == message.group) null else it } + } catch (e: Exception) { + null + } + name ?: getString(R.string.unknown) + } else { + getString(R.string.other) + } + for (permission in message.permissions) { + val description = try { + val permissionInfo = packageManager.getPermissionInfo(permission, 0) + PackageItemResolver.loadDescription(requireContext(), localCache, permissionInfo) + ?.nullIfEmpty()?.let { if (it == permission) null else it } + } catch (e: Exception) { + null + } + description?.let { builder.append(it).append("\n\n") } + } + if (builder.isNotEmpty()) { + builder.delete(builder.length - 2, builder.length) + } else { + builder.append(getString(R.string.no_description_available_DESC)) + } + dialog.setTitle(title) + dialog.setMessage(builder) + dialog.setPositiveButton(R.string.ok, null) + } + is Message.ReleaseIncompatible -> { + val builder = StringBuilder() + val minSdkVersion = if (Release.Incompatibility.MinSdk in message.incompatibilities) + message.minSdkVersion else null + val maxSdkVersion = if (Release.Incompatibility.MaxSdk in message.incompatibilities) + message.maxSdkVersion else null + if (minSdkVersion != null || maxSdkVersion != null) { + val versionMessage = minSdkVersion?.let { getString(R.string.incompatible_api_min_DESC_FORMAT, it) } + ?: maxSdkVersion?.let { getString(R.string.incompatible_api_max_DESC_FORMAT, it) } + builder.append(getString(R.string.incompatible_api_DESC_FORMAT, + Android.name, Android.sdk, versionMessage.orEmpty())).append("\n\n") + } + if (Release.Incompatibility.Platform in message.incompatibilities) { + builder.append(getString(R.string.incompatible_platforms_DESC_FORMAT, + Android.primaryPlatform ?: getString(R.string.unknown), + message.platforms.joinToString(separator = ", "))).append("\n\n") + } + val features = message.incompatibilities.mapNotNull { it as? Release.Incompatibility.Feature } + if (features.isNotEmpty()) { + builder.append(getString(R.string.incompatible_features_DESC)) + for (feature in features) { + builder.append("\n\u2022 ").append(feature.feature) + } + builder.append("\n\n") + } + if (builder.isNotEmpty()) { + builder.delete(builder.length - 2, builder.length) + } + dialog.setTitle(R.string.incompatible_version) + dialog.setMessage(builder) + dialog.setPositiveButton(R.string.ok, null) + } + is Message.ReleaseOlder -> { + dialog.setTitle(R.string.incompatible_version) + dialog.setMessage(R.string.incompatible_older_DESC) + dialog.setPositiveButton(R.string.ok, null) + } + is Message.ReleaseSignatureMismatch -> { + dialog.setTitle(R.string.incompatible_version) + dialog.setMessage(R.string.incompatible_signature_DESC) + dialog.setPositiveButton(R.string.ok, null) + } + }::class + return dialog.create() + } +} diff --git a/src/main/kotlin/com/looker/droidify/screen/PreferencesFragment.kt b/src/main/kotlin/com/looker/droidify/screen/PreferencesFragment.kt new file mode 100644 index 00000000..ddfbf50b --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/screen/PreferencesFragment.kt @@ -0,0 +1,296 @@ +package com.looker.droidify.screen + +import android.app.AlertDialog +import android.app.Dialog +import android.content.Context +import android.content.Intent +import android.graphics.Typeface +import android.net.Uri +import android.os.Bundle +import android.text.InputFilter +import android.text.InputType +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import android.widget.* +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.Fragment +import com.looker.droidify.R +import com.looker.droidify.content.Preferences +import com.looker.droidify.utility.extension.resources.* +import io.reactivex.rxjava3.disposables.Disposable + + +class PreferencesFragment: ScreenFragment() { + private val preferences = mutableMapOf, Preference<*>>() + private var disposable: Disposable? = null + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return inflater.inflate(R.layout.fragment, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val toolbar = view.findViewById(R.id.toolbar)!! + screenActivity.onToolbarCreated(toolbar) + toolbar.setTitle(R.string.preferences) + + val content = view.findViewById(R.id.fragment_content)!! + val scroll = ScrollView(content.context) + scroll.id = R.id.preferences_list + scroll.isFillViewport = true + content.addView(scroll, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) + val scrollLayout = FrameLayout(content.context) + scroll.addView(scrollLayout, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + val preferences = LinearLayout(scrollLayout.context) + preferences.orientation = LinearLayout.VERTICAL + scrollLayout.addView(preferences, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + + preferences.addCategory(getString(R.string.updates)) { + addEnumeration(Preferences.Key.AutoSync, getString(R.string.sync_repositories_automatically)) { + when (it) { + Preferences.AutoSync.Never -> getString(R.string.never) + Preferences.AutoSync.Wifi -> getString(R.string.only_on_wifi) + Preferences.AutoSync.Always -> getString(R.string.always) + } + } + addSwitch(Preferences.Key.UpdateNotify, getString(R.string.notify_about_updates), + getString(R.string.notify_about_updates_summary)) + addSwitch(Preferences.Key.UpdateUnstable, getString(R.string.unstable_updates), + getString(R.string.unstable_updates_summary)) + } + preferences.addCategory(getString(R.string.proxy)) { + addEnumeration(Preferences.Key.ProxyType, getString(R.string.proxy_type)) { + when (it) { + is Preferences.ProxyType.Direct -> getString(R.string.no_proxy) + is Preferences.ProxyType.Http -> getString(R.string.http_proxy) + is Preferences.ProxyType.Socks -> getString(R.string.socks_proxy) + } + } + addEditString(Preferences.Key.ProxyHost, getString(R.string.proxy_host)) + addEditInt(Preferences.Key.ProxyPort, getString(R.string.proxy_port), 1..65535) + } + preferences.addCategory(getString(R.string.other)) { + addEnumeration(Preferences.Key.Theme, getString(R.string.theme)) { + when (it) { + is Preferences.Theme.System -> getString(R.string.system) + is Preferences.Theme.Light -> getString(R.string.light) + is Preferences.Theme.Dark -> getString(R.string.dark) + } + } + addSwitch(Preferences.Key.IncompatibleVersions, getString(R.string.incompatible_versions), + getString(R.string.incompatible_versions_summary)) + } + // Adding Credits to Foxy + //TODO "Fix Linking" + var number = 0 + + preferences.addCategory("Credits") { + addText(title = "Based on an App by kitsunyan", summary = "FoxyDroid").also { setOnClickListener { number = 1 ; openURI(urlToSite = "https://github.com/kitsunyan/foxy-droid/") } } + } + + // End Credits + + + disposable = Preferences.observable.subscribe(this::updatePreference) + updatePreference(null) + } + + // Add Text for Credits + + private fun LinearLayout.addText(title: String, summary: String){ + val text = TextView(context) + val subText = TextView(context) + text.text = title + subText.text = summary + text.setTypeface(null, Typeface.BOLD) + resources.sizeScaled(16).let { text.setPadding(it, it, 5, 5) } + resources.sizeScaled(16).let { subText.setPadding(it, 5, 5, 5) } + addView(text, LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT) + addView(subText, LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT) + } + + private fun openURI(urlToSite: String){ + val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(urlToSite)) + startActivity(browserIntent) + } + // End Add Text for Credits + + override fun onDestroyView() { + super.onDestroyView() + + preferences.clear() + disposable?.dispose() + disposable = null + } + + private fun updatePreference(key: Preferences.Key<*>?) { + if (key != null) { + preferences[key]?.update() + } + if (key == null || key == Preferences.Key.ProxyType) { + val enabled = when (Preferences[Preferences.Key.ProxyType]) { + is Preferences.ProxyType.Direct -> false + is Preferences.ProxyType.Http, is Preferences.ProxyType.Socks -> true + } + preferences[Preferences.Key.ProxyHost]?.setEnabled(enabled) + preferences[Preferences.Key.ProxyPort]?.setEnabled(enabled) + } + if (key == Preferences.Key.Theme) { + requireActivity().recreate() + } + } + + private fun LinearLayout.addCategory(title: String, callback: LinearLayout.() -> Unit) { + val text = TextView(context) + text.typeface = TypefaceExtra.medium + text.setTextSizeScaled(14) + text.setTextColor(text.context.getColorFromAttr(android.R.attr.colorAccent)) + text.text = title + resources.sizeScaled(16).let { text.setPadding(it, it, it, 0) } + addView(text, LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT) + callback() + } + + private fun LinearLayout.addPreference(key: Preferences.Key, title: String, + summaryProvider: () -> String, dialogProvider: ((Context) -> AlertDialog)?): Preference { + val preference = Preference(key, this@PreferencesFragment, this, title, summaryProvider, dialogProvider) + preferences[key] = preference + return preference + } + + private fun LinearLayout.addSwitch(key: Preferences.Key, title: String, summary: String) { + val preference = addPreference(key, title, { summary }, null) + preference.check.visibility = View.VISIBLE + preference.view.setOnClickListener { Preferences[key] = !Preferences[key] } + preference.setCallback { preference.check.isChecked = Preferences[key] } + } + + private fun LinearLayout.addEdit(key: Preferences.Key, title: String, valueToString: (T) -> String, + stringToValue: (String) -> T?, configureEdit: (EditText) -> Unit) { + addPreference(key, title, { valueToString(Preferences[key]) }) { + val scroll = ScrollView(it) + scroll.resources.sizeScaled(20).let { scroll.setPadding(it, 0, it, 0) } + val edit = EditText(it) + configureEdit(edit) + edit.id = android.R.id.edit + edit.setTextSizeScaled(16) + edit.resources.sizeScaled(16).let { edit.setPadding(edit.paddingLeft, it, edit.paddingRight, it) } + edit.setText(valueToString(Preferences[key])) + edit.hint = edit.text.toString() + edit.setSelection(edit.text.length) + edit.requestFocus() + scroll.addView(edit, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + AlertDialog.Builder(it) + .setTitle(title) + .setView(scroll) + .setPositiveButton(R.string.ok) { _, _ -> + val value = stringToValue(edit.text.toString()) ?: key.default.value + post { Preferences[key] = value } + } + .setNegativeButton(R.string.cancel, null) + .create() + .apply { + window!!.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE) + } + } + } + + private fun LinearLayout.addEditString(key: Preferences.Key, title: String) { + addEdit(key, title, { it }, { it }, { }) + } + + private fun LinearLayout.addEditInt(key: Preferences.Key, title: String, range: IntRange?) { + addEdit(key, title, { it.toString() }, { it.toIntOrNull() }) { + it.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL + if (range != null) { + it.filters = arrayOf(InputFilter { source, start, end, dest, dstart, dend -> + val value = (dest.substring(0, dstart) + source.substring(start, end) + + dest.substring(dend, dest.length)).toIntOrNull() + if (value != null && value in range) null else "" + }) + } + } + } + + private fun > LinearLayout + .addEnumeration(key: Preferences.Key, title: String, valueToString: (T) -> String) { + addPreference(key, title, { valueToString(Preferences[key]) }) { + val values = key.default.value.values + AlertDialog.Builder(it) + .setTitle(title) + .setSingleChoiceItems(values.map(valueToString).toTypedArray(), + values.indexOf(Preferences[key])) { dialog, which -> + dialog.dismiss() + post { Preferences[key] = values[which] } + } + .setNegativeButton(R.string.cancel, null) + .create() + } + } + + private class Preference(private val key: Preferences.Key, + fragment: Fragment, parent: ViewGroup, titleText: String, + private val summaryProvider: () -> String, private val dialogProvider: ((Context) -> AlertDialog)?) { + val view = parent.inflate(R.layout.preference_item) + val title = view.findViewById(R.id.title)!! + val summary = view.findViewById(R.id.summary)!! + val check = view.findViewById(R.id.check)!! + + private var callback: (() -> Unit)? = null + + init { + title.text = titleText + parent.addView(view) + if (dialogProvider != null) { + view.setOnClickListener { PreferenceDialog(key.name) + .show(fragment.childFragmentManager, "${PreferenceDialog::class.java.name}.${key.name}") } + } + update() + } + + fun setCallback(callback: () -> Unit) { + this.callback = callback + update() + } + + fun setEnabled(enabled: Boolean) { + view.isEnabled = enabled + title.isEnabled = enabled + summary.isEnabled = enabled + check.isEnabled = enabled + } + + fun update() { + summary.text = summaryProvider() + summary.visibility = if (summary.text.isNotEmpty()) View.VISIBLE else View.GONE + callback?.invoke() + } + + fun createDialog(context: Context): AlertDialog { + return dialogProvider!!(context) + } + } + + class PreferenceDialog(): DialogFragment() { + companion object { + private const val EXTRA_KEY = "key" + } + + constructor(key: String): this() { + arguments = Bundle().apply { + putString(EXTRA_KEY, key) + } + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val preferences = (parentFragment as PreferencesFragment).preferences + val key = requireArguments().getString(EXTRA_KEY)!! + .let { name -> preferences.keys.find { it.name == name }!! } + val preference = preferences[key]!! + return preference.createDialog(requireContext()) + } + } +} diff --git a/src/main/kotlin/com/looker/droidify/screen/ProductAdapter.kt b/src/main/kotlin/com/looker/droidify/screen/ProductAdapter.kt new file mode 100644 index 00000000..11010d7d --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/screen/ProductAdapter.kt @@ -0,0 +1,1305 @@ +package com.looker.droidify.screen + +import android.annotation.SuppressLint +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.pm.PermissionGroupInfo +import android.content.pm.PermissionInfo +import android.content.res.Resources +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable +import android.net.Uri +import android.os.Parcel +import android.text.SpannableStringBuilder +import android.text.format.DateFormat +import android.text.style.BulletSpan +import android.text.style.ClickableSpan +import android.text.style.RelativeSizeSpan +import android.text.style.ReplacementSpan +import android.text.style.TypefaceSpan +import android.text.style.URLSpan +import android.text.util.Linkify +import android.view.Gravity +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.ProgressBar +import android.widget.Switch +import android.widget.TextView +import android.widget.Toast +import androidx.core.graphics.ColorUtils +import androidx.core.text.HtmlCompat +import androidx.core.text.util.LinkifyCompat +import androidx.recyclerview.widget.RecyclerView +import com.looker.droidify.R +import com.looker.droidify.content.Preferences +import com.looker.droidify.content.ProductPreferences +import com.looker.droidify.entity.InstalledItem +import com.looker.droidify.entity.Product +import com.looker.droidify.entity.ProductPreference +import com.looker.droidify.entity.Release +import com.looker.droidify.entity.Repository +import com.looker.droidify.graphics.PaddingDrawable +import com.looker.droidify.network.PicassoDownloader +import com.looker.droidify.utility.KParcelable +import com.looker.droidify.utility.PackageItemResolver +import com.looker.droidify.utility.Utils +import com.looker.droidify.utility.extension.android.* +import com.looker.droidify.utility.extension.resources.* +import com.looker.droidify.utility.extension.text.* +import com.looker.droidify.widget.ClickableMovementMethod +import com.looker.droidify.widget.DividerItemDecoration +import com.looker.droidify.widget.StableRecyclerAdapter +import java.lang.ref.WeakReference +import java.util.Locale +import kotlin.math.* + +class ProductAdapter(private val callbacks: Callbacks, private val columns: Int): + StableRecyclerAdapter() { + companion object { + private const val GRID_SPACING_OUTER_DP = 16 + private const val GRID_SPACING_INNER_DP = 4 + } + + interface Callbacks { + fun onActionClick(action: Action) + fun onPreferenceChanged(preference: ProductPreference) + fun onPermissionsClick(group: String?, permissions: List) + fun onScreenshotClick(screenshot: Product.Screenshot) + fun onReleaseClick(release: Release) + fun onUriClick(uri: Uri, shouldConfirm: Boolean): Boolean + } + + enum class Action(val titleResId: Int) { + INSTALL(R.string.install), + UPDATE(R.string.update), + LAUNCH(R.string.launch), + DETAILS(R.string.details), + UNINSTALL(R.string.uninstall), + CANCEL(R.string.cancel) + } + + sealed class Status { + object Pending: Status() + object Connecting: Status() + data class Downloading(val read: Long, val total: Long?): Status() + } + + enum class ViewType { HEADER, SWITCH, SECTION, EXPAND, TEXT, LINK, PERMISSIONS, SCREENSHOT, RELEASE, EMPTY } + + private enum class SwitchType(val titleResId: Int) { + IGNORE_ALL_UPDATES(R.string.ignore_all_updates), + IGNORE_THIS_UPDATE(R.string.ignore_this_update) + } + + private enum class SectionType(val titleResId: Int, val colorAttrResId: Int) { + ANTI_FEATURES(R.string.anti_features, R.attr.colorError), + CHANGES(R.string.changes, android.R.attr.colorAccent), + LINKS(R.string.links, android.R.attr.colorAccent), + DONATE(R.string.donate, android.R.attr.colorAccent), + PERMISSIONS(R.string.permissions, android.R.attr.colorAccent), + SCREENSHOTS(R.string.screenshots, android.R.attr.colorAccent), + VERSIONS(R.string.versions, android.R.attr.colorAccent) + } + + internal enum class ExpandType { NOTHING, DESCRIPTION, CHANGES, + LINKS, DONATES, PERMISSIONS, SCREENSHOTS, VERSIONS } + private enum class TextType { DESCRIPTION, ANTI_FEATURES, CHANGES } + + private enum class LinkType(val iconResId: Int, val titleResId: Int, + val format: ((Context, String) -> String)? = null) { + AUTHOR(R.drawable.ic_person, R.string.author_website), + EMAIL(R.drawable.ic_email, R.string.author_email), + LICENSE(R.drawable.ic_copyright, R.string.license, + format = { context, text -> context.getString(R.string.license_FORMAT, text) }), + SOURCE(R.drawable.ic_code, R.string.source_code), + TRACKER(R.drawable.ic_bug_report, R.string.bug_tracker), + CHANGELOG(R.drawable.ic_history, R.string.changelog), + WEB(R.drawable.ic_public, R.string.project_website) + } + + private sealed class Item { + abstract val descriptor: String + abstract val viewType: ViewType + + class HeaderItem(val repository: Repository, val product: Product): Item() { + override val descriptor: String + get() = "header" + + override val viewType: ViewType + get() = ViewType.HEADER + } + + class SwitchItem(val switchType: SwitchType, val packageName: String, val versionCode: Long): Item() { + override val descriptor: String + get() = "switch.${switchType.name}" + + override val viewType: ViewType + get() = ViewType.SWITCH + } + + class SectionItem(val sectionType: SectionType, val expandType: ExpandType, + val items: List, val collapseCount: Int): Item() { + constructor(sectionType: SectionType): this(sectionType, ExpandType.NOTHING, emptyList(), 0) + + override val descriptor: String + get() = "section.${sectionType.name}" + + override val viewType: ViewType + get() = ViewType.SECTION + } + + class ExpandItem(val expandType: ExpandType, val replace: Boolean, val items: List): Item() { + override val descriptor: String + get() = "expand.${expandType.name}" + + override val viewType: ViewType + get() = ViewType.EXPAND + } + + class TextItem(val textType: TextType, val text: CharSequence): Item() { + override val descriptor: String + get() = "text.${textType.name}" + + override val viewType: ViewType + get() = ViewType.TEXT + } + + sealed class LinkItem: Item() { + override val viewType: ViewType + get() = ViewType.LINK + + abstract val iconResId: Int + abstract fun getTitle(context: Context): String + abstract val uri: Uri? + + val displayLink: String? + get() = uri?.schemeSpecificPart?.nullIfEmpty() + ?.let { if (it.startsWith("//")) null else it } ?: uri?.toString() + + class Typed(val linkType: LinkType, val text: String, override val uri: Uri?): LinkItem() { + override val descriptor: String + get() = "link.typed.${linkType.name}" + + override val iconResId: Int + get() = linkType.iconResId + + override fun getTitle(context: Context): String { + return text.nullIfEmpty()?.let { linkType.format?.invoke(context, it) ?: it } + ?: context.getString(linkType.titleResId) + } + } + + class Donate(val donate: Product.Donate): LinkItem() { + override val descriptor: String + get() = "link.donate.$donate" + + override val iconResId: Int + get() = when (donate) { + is Product.Donate.Regular -> R.drawable.ic_public + is Product.Donate.Bitcoin -> R.drawable.ic_donate_bitcoin + is Product.Donate.Litecoin -> R.drawable.ic_donate_litecoin + is Product.Donate.Flattr -> R.drawable.ic_donate_flattr + is Product.Donate.Liberapay -> R.drawable.ic_donate_liberapay + is Product.Donate.OpenCollective -> R.drawable.ic_donate_opencollective + } + + override fun getTitle(context: Context): String = when (donate) { + is Product.Donate.Regular -> context.getString(R.string.website) + is Product.Donate.Bitcoin -> "Bitcoin" + is Product.Donate.Litecoin -> "Litecoin" + is Product.Donate.Flattr -> "Flattr" + is Product.Donate.Liberapay -> "Liberapay" + is Product.Donate.OpenCollective -> "Open Collective" + } + + override val uri: Uri? = when (donate) { + is Product.Donate.Regular -> Uri.parse(donate.url) + is Product.Donate.Bitcoin -> Uri.parse("bitcoin:${donate.address}") + is Product.Donate.Litecoin -> Uri.parse("litecoin:${donate.address}") + is Product.Donate.Flattr -> Uri.parse("https://flattr.com/thing/${donate.id}") + is Product.Donate.Liberapay -> Uri.parse("https://liberapay.com/~${donate.id}") + is Product.Donate.OpenCollective -> Uri.parse("https://opencollective.com/${donate.id}") + } + } + } + + class PermissionsItem(val group: PermissionGroupInfo?, val permissions: List): Item() { + override val descriptor: String + get() = "permissions.${group?.name}.${permissions.joinToString(separator = ".") { it.name }}" + + override val viewType: ViewType + get() = ViewType.PERMISSIONS + } + + class ScreenshotItem(val repository: Repository, val packageName: String, + val screenshot: Product.Screenshot): Item() { + override val descriptor: String + get() = "screenshot.${repository.id}.${screenshot.identifier}" + + override val viewType: ViewType + get() = ViewType.SCREENSHOT + } + + class ReleaseItem(val repository: Repository, val release: Release, val selectedRepository: Boolean, + val showSignature: Boolean): Item() { + override val descriptor: String + get() = "release.${repository.id}.${release.identifier}" + + override val viewType: ViewType + get() = ViewType.RELEASE + } + + class EmptyItem(val packageName: String): Item() { + override val descriptor: String + get() = "empty" + + override val viewType: ViewType + get() = ViewType.EMPTY + } + } + + private class Measurement { + private var density = 0f + private var scaledDensity = 0f + private lateinit var metric: T + + fun measure(view: View) { + View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED).let { view.measure(it, it) } + } + + fun invalidate(resources: Resources, callback: () -> T): T { + val (density, scaledDensity) = resources.displayMetrics.let { Pair(it.density, it.scaledDensity) } + if (this.density != density || this.scaledDensity != scaledDensity) { + this.density = density + this.scaledDensity = scaledDensity + metric = callback() + } + return metric + } + } + + private enum class Payload { REFRESH, STATUS } + + private class HeaderViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) { + val icon = itemView.findViewById(R.id.icon)!! + val name = itemView.findViewById(R.id.name)!! + val version = itemView.findViewById(R.id.version)!! + val packageName = itemView.findViewById(R.id.package_name)!! + val action = itemView.findViewById