From 5dddd6290bc59bd986c54cc49690d07fe1aad468 Mon Sep 17 00:00:00 2001 From: NekoInverter <42698724+NekoInverter@users.noreply.github.com> Date: Mon, 3 Feb 2020 18:57:01 +0800 Subject: [PATCH 001/420] first commit --- .gitignore | 14 + .idea/codeStyles/Project.xml | 116 +++ .idea/gradle.xml | 16 + .idea/misc.xml | 53 ++ .idea/runConfigurations.xml | 12 + app/.gitignore | 1 + app/build.gradle | 41 + app/libs/AndroidHiddenAPI.jar | Bin 0 -> 3191 bytes app/proguard-rules.pro | 21 + app/src/main/AndroidManifest.xml | 112 +++ .../android/xposed/installer/XposedApp.java | 56 ++ .../xposed/installer/util/InstallZipUtil.java | 57 ++ .../edxposed/manager/AboutActivity.java | 108 +++ .../edxposed/manager/BaseActivity.java | 267 ++++++ .../manager/BaseAdvancedInstaller.java | 241 ++++++ .../edxposed/manager/BlackListActivity.java | 122 +++ .../manager/CompileDialogFragment.java | 126 +++ .../edxposed/manager/DownloadActivity.java | 439 ++++++++++ .../manager/DownloadDetailsActivity.java | 298 +++++++ .../manager/DownloadDetailsFragment.java | 86 ++ .../DownloadDetailsSettingsFragment.java | 60 ++ .../DownloadDetailsVersionsFragment.java | 261 ++++++ .../edxposed/manager/EdDownloadActivity.java | 175 ++++ .../edxposed/manager/LogsActivity.java | 312 +++++++ .../edxposed/manager/MainActivity.java | 179 ++++ .../edxposed/manager/ModulesActivity.java | 654 ++++++++++++++ .../edxposed/manager/SettingsActivity.java | 371 ++++++++ .../manager/StatusInstallerFragment.java | 327 +++++++ .../meowcat/edxposed/manager/XposedApp.java | 244 ++++++ .../edxposed/manager/adapters/AppAdapter.java | 271 ++++++ .../edxposed/manager/adapters/AppHelper.java | 362 ++++++++ .../manager/adapters/BlackListAdapter.java | 63 ++ .../adapters/CursorRecyclerViewAdapter.java | 127 +++ .../manager/receivers/BootReceiver.java | 58 ++ .../manager/receivers/DownloadReceiver.java | 19 + .../receivers/PackageChangeReceiver.java | 80 ++ .../meowcat/edxposed/manager/repo/Module.java | 28 + .../edxposed/manager/repo/ModuleVersion.java | 17 + .../edxposed/manager/repo/ReleaseType.java | 40 + .../meowcat/edxposed/manager/repo/RepoDb.java | 492 +++++++++++ .../manager/repo/RepoDbDefinitions.java | 216 +++++ .../edxposed/manager/repo/RepoParser.java | 322 +++++++ .../edxposed/manager/repo/Repository.java | 12 + .../edxposed/manager/util/CompileUtil.java | 43 + .../edxposed/manager/util/DownloadsUtil.java | 672 +++++++++++++++ .../edxposed/manager/util/HashUtil.java | 64 ++ .../edxposed/manager/util/InstallApkUtil.java | 131 +++ .../edxposed/manager/util/LocaleUtil.java | 16 + .../edxposed/manager/util/ModuleUtil.java | 400 +++++++++ .../edxposed/manager/util/NavUtil.java | 59 ++ .../manager/util/NotificationUtil.java | 313 +++++++ .../util/PrefixedSharedPreferences.java | 161 ++++ .../edxposed/manager/util/RepoLoader.java | 437 ++++++++++ .../edxposed/manager/util/ToastUtil.java | 18 + .../util/chrome/CustomTabsURLSpan.java | 26 + .../util/chrome/LinkTransformationMethod.java | 50 ++ .../edxposed/manager/util/json/JSONUtils.java | 97 +++ .../edxposed/manager/util/json/XposedTab.java | 91 ++ .../edxposed/manager/util/json/XposedZip.java | 66 ++ .../edxposed/manager/widget/DownloadView.java | 257 ++++++ .../manager/widget/IntegerListPreference.java | 60 ++ .../widget/ListPreferenceSummaryFix.java | 22 + .../drawable-v24/ic_launcher_foreground.xml | 34 + app/src/main/res/drawable/ic_android.xml | 10 + app/src/main/res/drawable/ic_apps.xml | 10 + app/src/main/res/drawable/ic_assignment.xml | 10 + app/src/main/res/drawable/ic_bug.xml | 10 + app/src/main/res/drawable/ic_check_circle.xml | 10 + app/src/main/res/drawable/ic_chip.xml | 10 + app/src/main/res/drawable/ic_description.xml | 10 + app/src/main/res/drawable/ic_donate.xml | 10 + app/src/main/res/drawable/ic_error.xml | 10 + app/src/main/res/drawable/ic_framework.xml | 13 + app/src/main/res/drawable/ic_get_app.xml | 10 + app/src/main/res/drawable/ic_github.xml | 11 + app/src/main/res/drawable/ic_help.xml | 10 + app/src/main/res/drawable/ic_history.xml | 10 + app/src/main/res/drawable/ic_info.xml | 10 + app/src/main/res/drawable/ic_language.xml | 10 + .../res/drawable/ic_launcher_background.xml | 170 ++++ app/src/main/res/drawable/ic_manager.xml | 11 + app/src/main/res/drawable/ic_modules.xml | 11 + app/src/main/res/drawable/ic_notification.xml | 10 + app/src/main/res/drawable/ic_person.xml | 10 + app/src/main/res/drawable/ic_phone.xml | 10 + app/src/main/res/drawable/ic_refresh.xml | 10 + app/src/main/res/drawable/ic_save.xml | 10 + app/src/main/res/drawable/ic_send.xml | 10 + app/src/main/res/drawable/ic_settings.xml | 10 + app/src/main/res/drawable/ic_share.xml | 10 + app/src/main/res/drawable/ic_sort.xml | 10 + app/src/main/res/drawable/ic_update.xml | 10 + app/src/main/res/drawable/ic_verified.xml | 10 + app/src/main/res/drawable/ic_warning.xml | 10 + app/src/main/res/drawable/outline_list_24.xml | 10 + .../res/drawable/shortcut_ic_downloads.xml | 15 + .../main/res/drawable/shortcut_ic_modules.xml | 13 + app/src/main/res/layout/activity_about.xml | 796 ++++++++++++++++++ .../main/res/layout/activity_black_list.xml | 25 + app/src/main/res/layout/activity_download.xml | 24 + .../res/layout/activity_download_details.xml | 37 + .../activity_download_details_not_found.xml | 30 + .../main/res/layout/activity_ed_download.xml | 36 + app/src/main/res/layout/activity_logs.xml | 39 + app/src/main/res/layout/activity_main.xml | 295 +++++++ app/src/main/res/layout/activity_modules.xml | 25 + app/src/main/res/layout/activity_settings.xml | 15 + app/src/main/res/layout/appbar_layout.xml | 14 + .../res/layout/dialog_install_warning.xml | 26 + app/src/main/res/layout/download_details.xml | 46 + app/src/main/res/layout/download_moreinfo.xml | 20 + app/src/main/res/layout/download_view.xml | 73 ++ .../res/layout/fragment_compile_dialog.xml | 33 + app/src/main/res/layout/item_app.xml | 158 ++++ app/src/main/res/layout/item_download.xml | 46 + app/src/main/res/layout/item_module.xml | 144 ++++ app/src/main/res/layout/item_version.xml | 86 ++ .../main/res/layout/single_installer_view.xml | 341 ++++++++ app/src/main/res/layout/status_installer.xml | 355 ++++++++ .../res/layout/sticky_header_download.xml | 18 + .../main/res/menu/context_menu_modules.xml | 23 + app/src/main/res/menu/menu_app_item.xml | 27 + app/src/main/res/menu/menu_app_list.xml | 11 + app/src/main/res/menu/menu_download.xml | 17 + .../main/res/menu/menu_download_details.xml | 22 + app/src/main/res/menu/menu_installer.xml | 49 ++ app/src/main/res/menu/menu_logs.xml | 56 ++ app/src/main/res/menu/menu_main.xml | 10 + app/src/main/res/menu/menu_modules.xml | 77 ++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2963 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 4905 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2060 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2783 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4490 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 6895 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6387 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10413 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9128 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 15132 bytes app/src/main/res/values-night/bool.xml | 6 + app/src/main/res/values-night/colors.xml | 12 + app/src/main/res/values-v23/colors.xml | 2 + app/src/main/res/values-v27/colors.xml | 4 + app/src/main/res/values-zh-rCN/strings.xml | 316 +++++++ app/src/main/res/values/arrays.xml | 90 ++ app/src/main/res/values/attrs.xml | 4 + app/src/main/res/values/bool.xml | 6 + app/src/main/res/values/colors.xml | 12 + app/src/main/res/values/dimens.xml | 1 + app/src/main/res/values/strings.xml | 350 ++++++++ app/src/main/res/values/styles.xml | 31 + app/src/main/res/xml/file_paths.xml | 17 + app/src/main/res/xml/module_prefs.xml | 14 + app/src/main/res/xml/prefs.xml | 213 +++++ app/src/main/res/xml/shortcuts.xml | 32 + build.gradle | 27 + gradle.properties | 20 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 ++++ gradlew.bat | 84 ++ settings.gradle | 2 + 164 files changed, 14812 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/runConfigurations.xml create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/libs/AndroidHiddenAPI.jar create mode 100644 app/proguard-rules.pro create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/de/robv/android/xposed/installer/XposedApp.java create mode 100644 app/src/main/java/de/robv/android/xposed/installer/util/InstallZipUtil.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/AboutActivity.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/BaseActivity.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/BaseAdvancedInstaller.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/BlackListActivity.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/CompileDialogFragment.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/DownloadActivity.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/DownloadDetailsActivity.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/DownloadDetailsFragment.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/DownloadDetailsSettingsFragment.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/DownloadDetailsVersionsFragment.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/EdDownloadActivity.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/LogsActivity.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/MainActivity.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/ModulesActivity.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/SettingsActivity.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/StatusInstallerFragment.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/XposedApp.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/adapters/AppAdapter.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/adapters/AppHelper.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/adapters/BlackListAdapter.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/adapters/CursorRecyclerViewAdapter.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/receivers/BootReceiver.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/receivers/DownloadReceiver.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/receivers/PackageChangeReceiver.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/repo/Module.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/repo/ModuleVersion.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/repo/ReleaseType.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/repo/RepoDb.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/repo/RepoDbDefinitions.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/repo/RepoParser.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/repo/Repository.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/util/CompileUtil.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/util/DownloadsUtil.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/util/HashUtil.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/util/InstallApkUtil.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/util/LocaleUtil.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/util/ModuleUtil.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/util/NavUtil.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/util/NotificationUtil.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/util/PrefixedSharedPreferences.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/util/RepoLoader.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/util/ToastUtil.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/util/chrome/CustomTabsURLSpan.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/util/chrome/LinkTransformationMethod.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/util/json/JSONUtils.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/util/json/XposedTab.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/util/json/XposedZip.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/widget/DownloadView.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/widget/IntegerListPreference.java create mode 100644 app/src/main/java/org/meowcat/edxposed/manager/widget/ListPreferenceSummaryFix.java create mode 100644 app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/ic_android.xml create mode 100644 app/src/main/res/drawable/ic_apps.xml create mode 100644 app/src/main/res/drawable/ic_assignment.xml create mode 100644 app/src/main/res/drawable/ic_bug.xml create mode 100644 app/src/main/res/drawable/ic_check_circle.xml create mode 100644 app/src/main/res/drawable/ic_chip.xml create mode 100644 app/src/main/res/drawable/ic_description.xml create mode 100644 app/src/main/res/drawable/ic_donate.xml create mode 100644 app/src/main/res/drawable/ic_error.xml create mode 100644 app/src/main/res/drawable/ic_framework.xml create mode 100644 app/src/main/res/drawable/ic_get_app.xml create mode 100644 app/src/main/res/drawable/ic_github.xml create mode 100644 app/src/main/res/drawable/ic_help.xml create mode 100644 app/src/main/res/drawable/ic_history.xml create mode 100644 app/src/main/res/drawable/ic_info.xml create mode 100644 app/src/main/res/drawable/ic_language.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/ic_manager.xml create mode 100644 app/src/main/res/drawable/ic_modules.xml create mode 100644 app/src/main/res/drawable/ic_notification.xml create mode 100644 app/src/main/res/drawable/ic_person.xml create mode 100644 app/src/main/res/drawable/ic_phone.xml create mode 100644 app/src/main/res/drawable/ic_refresh.xml create mode 100644 app/src/main/res/drawable/ic_save.xml create mode 100644 app/src/main/res/drawable/ic_send.xml create mode 100644 app/src/main/res/drawable/ic_settings.xml create mode 100644 app/src/main/res/drawable/ic_share.xml create mode 100644 app/src/main/res/drawable/ic_sort.xml create mode 100644 app/src/main/res/drawable/ic_update.xml create mode 100644 app/src/main/res/drawable/ic_verified.xml create mode 100644 app/src/main/res/drawable/ic_warning.xml create mode 100644 app/src/main/res/drawable/outline_list_24.xml create mode 100644 app/src/main/res/drawable/shortcut_ic_downloads.xml create mode 100644 app/src/main/res/drawable/shortcut_ic_modules.xml create mode 100644 app/src/main/res/layout/activity_about.xml create mode 100644 app/src/main/res/layout/activity_black_list.xml create mode 100644 app/src/main/res/layout/activity_download.xml create mode 100644 app/src/main/res/layout/activity_download_details.xml create mode 100644 app/src/main/res/layout/activity_download_details_not_found.xml create mode 100644 app/src/main/res/layout/activity_ed_download.xml create mode 100644 app/src/main/res/layout/activity_logs.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/layout/activity_modules.xml create mode 100644 app/src/main/res/layout/activity_settings.xml create mode 100644 app/src/main/res/layout/appbar_layout.xml create mode 100644 app/src/main/res/layout/dialog_install_warning.xml create mode 100644 app/src/main/res/layout/download_details.xml create mode 100644 app/src/main/res/layout/download_moreinfo.xml create mode 100644 app/src/main/res/layout/download_view.xml create mode 100644 app/src/main/res/layout/fragment_compile_dialog.xml create mode 100644 app/src/main/res/layout/item_app.xml create mode 100644 app/src/main/res/layout/item_download.xml create mode 100644 app/src/main/res/layout/item_module.xml create mode 100644 app/src/main/res/layout/item_version.xml create mode 100644 app/src/main/res/layout/single_installer_view.xml create mode 100644 app/src/main/res/layout/status_installer.xml create mode 100644 app/src/main/res/layout/sticky_header_download.xml create mode 100644 app/src/main/res/menu/context_menu_modules.xml create mode 100644 app/src/main/res/menu/menu_app_item.xml create mode 100644 app/src/main/res/menu/menu_app_list.xml create mode 100644 app/src/main/res/menu/menu_download.xml create mode 100644 app/src/main/res/menu/menu_download_details.xml create mode 100644 app/src/main/res/menu/menu_installer.xml create mode 100644 app/src/main/res/menu/menu_logs.xml create mode 100644 app/src/main/res/menu/menu_main.xml create mode 100644 app/src/main/res/menu/menu_modules.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/values-night/bool.xml create mode 100644 app/src/main/res/values-night/colors.xml create mode 100644 app/src/main/res/values-v23/colors.xml create mode 100644 app/src/main/res/values-v27/colors.xml create mode 100644 app/src/main/res/values-zh-rCN/strings.xml create mode 100644 app/src/main/res/values/arrays.xml create mode 100644 app/src/main/res/values/attrs.xml create mode 100644 app/src/main/res/values/bool.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/dimens.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/main/res/xml/file_paths.xml create mode 100644 app/src/main/res/xml/module_prefs.xml create mode 100644 app/src/main/res/xml/prefs.xml create mode 100644 app/src/main/res/xml/shortcuts.xml create mode 100644 build.gradle 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 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..603b14077 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 000000000..681f41ae2 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,116 @@ + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+
+
\ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 000000000..d291b3d7c --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 000000000..bed10fbdc --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 000000000..7f68460d8 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 000000000..0e1ab904b --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,41 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 28 + buildToolsVersion "29.0.2" + defaultConfig { + applicationId "org.meowcat.edxposed.manager" + minSdkVersion 21 + targetSdkVersion 27 + versionCode 45401 + versionName "4.5.4" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'com.google.android.material:material:1.2.0-alpha04' + implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0' + implementation 'com.github.bumptech.glide:glide:4.11.0' + implementation "com.github.topjohnwu.libsu:core:2.5.0" + implementation 'androidx.browser:browser:1.2.0' + implementation 'com.timehop.stickyheadersrecyclerview:library:0.4.3@aar' + implementation 'com.takisoft.preferencex:preferencex:1.1.0' + implementation 'com.takisoft.preferencex:preferencex-simplemenu:1.1.0' + implementation "androidx.recyclerview:recyclerview:1.2.0-alpha01" + implementation 'com.annimon:stream:1.2.0' + implementation 'com.google.code.gson:gson:2.8.6' + implementation 'de.psdev.licensesdialog:licensesdialog:1.8.3' +} diff --git a/app/libs/AndroidHiddenAPI.jar b/app/libs/AndroidHiddenAPI.jar new file mode 100644 index 0000000000000000000000000000000000000000..113acc1e39b8151c067fc694a108aa613ae76541 GIT binary patch literal 3191 zcmZ{ncTf}97REsgp)MT+LR1h#U3v|Qh}1|2DS{dx8hY1IqzWuZuOd~15J8Hdp#@M9 zL`WcXP>Z`A{#pEWFq|md z)7jU{#aYJmKQ4!VxS0Ru^78xB$W6m=k~jZh6!>qWu8TXu%HPG^?{_D6xSwCNO`yN! zF#nH9()B445q9D2wV0GzMrS3aQ}=0ULk zb)(wH57So5BA?6CJyrRMwkHX-*vhhiqLRn1F-Vv+Tsx zX8U5kn=6abZWq*vX>a-rvR+0vbs};_%WaT;<2;-JcD1}uox1EW?3>t;p~@*s>*<=N z7igb7w33pCN5##jr$bmr0a*I-bX_rfq0!*o)-Ou5_`dmm#HmK2c7KXbT=?_XFCE_6 z3a$n34v;!ls_|kBtt_Js-?Ty>FZ@+)dx4WY2@D`aIozBtH z`ncbDHZ!63T9~4ksbi|P+@Q1Hz>1N8^B`R+0%2XJze|Spnor}JWH8?Z{I%)xWxt#L(i*Y;+)#3#cTORi+#0fd{F!!W7eM#voEW!yEhV~f>pVf@NhEc3?mYOq0Ex|W zBrvyOFMN?dGj<+cvqOn-1gV&B*h6YLW6JHLe%tiV8OX zZRqF3D7f42n95hq8kn1F(9kAwcD&;2A}hSt=F7-sMf<255#qK1qRl;Y6W^$ir~+0G&DIq|Lm5?#pVVvs3;6tKMw7@ zTw?#uTk1S8CBJZ4Z-ZHM0Xfqm3cQ^S9$^g>?db7YmYA$Dzr|Z6=R8wT~fIp+1tfjky946a^}4-wZD22^hf=dQOF+B^lWaZ^49y;rpi5u zP6tvw*j(TC57`{Dj^N{oD_+%L1jg#)deyHz(G_jez6(kguzsh6%wl@|kRadIEH!zyaEblrEZ`oY7 zzK#&^B0g5srduTRRS0x;hj64E!oRhs90J&axl>9BGIg^`1nW#@zCxsy8?cCVV*L_)4RN&!t|)Kg6k1~ngIIVzN=pNCh3oN@f%>_GIYY77K_`aV3s zViUUu@&WtLxcW_oCr*6xb`R?}$ELa^g=A%&jj}7aYRvT8@|`HBzM&$yIAd5C$6f@j zI=>A#s_ULCfTd`%WRaa-dDhOtlk?PyqBNi%VJi@WA2MCCkvnA#%qM%4m#fksbgY=x zX_292r$c)CY0W{BcUjEOu|+@8eda>yP*fQD$l^mP^~D@w+1*6$K?6s3RCIu9qv>8E zYSr&G=v%^RVJb4kbb4(67hrjO zKl6yAuVu-9+V0r^w5jaoM@Xi4o<&aY22GyP zX$b3`nCZ+{8{5lcvZB2t(+Bh_k6unC-kr0X zK^F+S#>qMO*{Bm#Q(A+M^mVCA6@Y#=>octRDkJ1`0N^r&jA`e_3=8t5>+Ku8 z;}Ub`GB$=AGFbp2yZfUaEe=<_RC0x|nQ|ZK!{;5g7cDN_!?o$otYHLnQ>P6omwyl7 z*|@5eO7t7`{36gx1JG3W6?(wApCbktZ(rwW?I2d%!eurGPJ%)|ixiL-vRh1udF z^$>j4O>NTMoE%!+ObN|MC~Cl$sp8h*Bl1zV0znE=u3Ihyjtqa6c|YYKCI#E?IpU=5 z1*xexHV|y0eS;%(cv&sRewlNfx)UP7ztd0#jx8H2!b=Fo3dmPhNx6_^bSf{2J z-3ke6IYBQ~5zh0V8yQ@brL+x=HuIGQ#x(xTz?VZCx~(P$GXZ6?WLWZ|}wMY16WIfmc3B5CVJTM{`A!^L{vV< z`zIW&1S8u_-XjW=XUx^yMf9Uwu$24@FoodO0Jxo5uVtJRkR<9~5%}F;#f`)Vtfr{6 zq&^Rdr$9}9sHpm^r-bpG&rT0oC#VcU3bQ~H5$>M>+Y8ysH+)i@M=AZz-ue z_ims1O>8qPwGsVTJwJ*LRWQV-AI6c+Uy!}Zay*j~mxGw%5T)==MlwFkO##6z)V{Q8 zOW&_=eY{aR$tj?{n=+sr$+Z6f5rqV4S#^$~=JI_)oz8pC=9_i3R!&7sM39elI%RCW zEOZU5>_;!|MV z{Z>`GKOt{kk>_dnBfRsG=Ed*VDIDvyz8S80{%0n*Y!0e_{Jy@{8Pmb^R}P|J8Nz wzXAQ{IBuF>EdTeX{ABrGVRBObd)&zi@DH*#g#qY)dg)HG{zS`vZVWX40^qsIEdT%j literal 0 HcmV?d00001 diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 000000000..f1b424510 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..723e37be2 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/de/robv/android/xposed/installer/XposedApp.java b/app/src/main/java/de/robv/android/xposed/installer/XposedApp.java new file mode 100644 index 000000000..0a98c2de7 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/XposedApp.java @@ -0,0 +1,56 @@ +package de.robv.android.xposed.installer; + +import android.annotation.SuppressLint; +import android.app.Application; +import android.util.Log; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +import de.robv.android.xposed.installer.util.InstallZipUtil; + +import static de.robv.android.xposed.installer.util.InstallZipUtil.parseXposedProp; + +@SuppressLint("Registered") +public class XposedApp extends Application { + public static final String TAG = "XposedApp"; + private static final File EDXPOSED_PROP_FILE = new File("/system/framework/edconfig.jar"); + private static XposedApp mInstance = null; + public InstallZipUtil.XposedProp mXposedProp; + + public static XposedApp getInstance() { + return mInstance; + } + + // This method is hooked by XposedBridge to return the current version + public static Integer getActiveXposedVersion() { + Log.d(TAG, "EdXposed is not active"); + return -1; + } + + public void onCreate() { + super.onCreate(); + mInstance = this; + } + + public void reloadXposedProp() { + InstallZipUtil.XposedProp prop = null; + File file = null; + + if (EDXPOSED_PROP_FILE.canRead()) { + file = EDXPOSED_PROP_FILE; + } + + if (file != null) { + try (FileInputStream is = new FileInputStream(file)) { + prop = parseXposedProp(is); + } catch (IOException e) { + Log.e(TAG, "Could not read " + file.getPath(), e); + } + } + synchronized (this) { + mXposedProp = prop; + } + } +} diff --git a/app/src/main/java/de/robv/android/xposed/installer/util/InstallZipUtil.java b/app/src/main/java/de/robv/android/xposed/installer/util/InstallZipUtil.java new file mode 100644 index 000000000..b27971987 --- /dev/null +++ b/app/src/main/java/de/robv/android/xposed/installer/util/InstallZipUtil.java @@ -0,0 +1,57 @@ +package de.robv.android.xposed.installer.util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +import org.meowcat.edxposed.manager.util.ModuleUtil; + +public final class InstallZipUtil { + + public static XposedProp parseXposedProp(InputStream is) throws IOException { + XposedProp prop = new XposedProp(); + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + String line; + while ((line = reader.readLine()) != null) { + String[] parts = line.split("=", 2); + if (parts.length != 2) { + continue; + } + + String key = parts[0].trim(); + if (key.charAt(0) == '#') { + continue; + } + + String value = parts[1].trim(); + + if ("version".equals(key)) { + prop.mVersion = value; + prop.mVersionInt = ModuleUtil.extractIntPart(value); + } + } + reader.close(); + return prop.isComplete() ? prop : null; + } + + public static class XposedProp { + private String mVersion = null; + private int mVersionInt = 0; + //private Set mRequires = new HashSet<>(); + + private boolean isComplete() { + return mVersion != null + && mVersionInt > 0; + } + + public String getVersion() { + return mVersion; + } + +// public int getVersionInt() { +// return mVersionInt; +// } + + } +} diff --git a/app/src/main/java/org/meowcat/edxposed/manager/AboutActivity.java b/app/src/main/java/org/meowcat/edxposed/manager/AboutActivity.java new file mode 100644 index 000000000..031714535 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/AboutActivity.java @@ -0,0 +1,108 @@ +package org.meowcat.edxposed.manager; + +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.text.Html; +import android.view.View; +import android.widget.TextView; + +import androidx.appcompat.app.ActionBar; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import org.meowcat.edxposed.manager.util.NavUtil; + +import de.psdev.licensesdialog.LicensesDialog; +import de.psdev.licensesdialog.licenses.ApacheSoftwareLicense20; +import de.psdev.licensesdialog.licenses.MITLicense; +import de.psdev.licensesdialog.model.Notice; +import de.psdev.licensesdialog.model.Notices; + +public class AboutActivity extends BaseActivity { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_about); + setSupportActionBar(findViewById(R.id.toolbar)); + ActionBar bar = getSupportActionBar(); + if (bar != null) { + bar.setDisplayHomeAsUpEnabled(true); + } + View changelogView = findViewById(R.id.changelogView); + View licensesView = findViewById(R.id.licensesView); + View translatorsView = findViewById(R.id.translatorsView); + View sourceCodeView = findViewById(R.id.sourceCodeView); + View tgChannelView = findViewById(R.id.tgChannelView); + View installerSupportView = findViewById(R.id.installerSupportView); + View faqView = findViewById(R.id.faqView); + View donateView = findViewById(R.id.donateView); + TextView txtModuleSupport = findViewById(R.id.tab_support_module_description); + View qqGroupView = findViewById(R.id.qqGroupView); + View tgGroupView = findViewById(R.id.tgGroupView); + + String packageName = getPackageName(); + String translator = getResources().getString(R.string.translator); + + SharedPreferences prefs = getSharedPreferences(packageName + "_preferences", MODE_PRIVATE); + + final String changes = prefs.getString("changelog", null); + + if (changes == null) { + changelogView.setVisibility(View.GONE); + } else { + changelogView.setOnClickListener(v1 -> new MaterialAlertDialogBuilder(this) + .setTitle(R.string.changes) + .setMessage(Html.fromHtml(changes)) + .setPositiveButton(android.R.string.ok, null).show()); + } + + try { + String version = getPackageManager().getPackageInfo(packageName, 0).versionName; + ((TextView) findViewById(R.id.app_version)).setText(version); + } catch (PackageManager.NameNotFoundException ignored) { + } + + licensesView.setOnClickListener(v12 -> createLicenseDialog()); + + txtModuleSupport.setText(getString(R.string.support_modules_description, + getString(R.string.module_support))); + + setupView(installerSupportView, R.string.support_material_xda); + setupView(faqView, R.string.support_faq_url); + setupView(tgGroupView, R.string.group_telegram_link); + setupView(qqGroupView, R.string.group_qq_link); + setupView(donateView, R.string.support_donate_url); + setupView(sourceCodeView, R.string.about_source); + setupView(tgChannelView, R.string.group_telegram_channel_link); + + if (translator.isEmpty()) { + translatorsView.setVisibility(View.GONE); + } + } + + void setupView(View v, final int url) { + v.setOnClickListener(v1 -> NavUtil.startURL(this, getString(url))); + } + + private void createLicenseDialog() { + Notices notices = new Notices(); + notices.addNotice(new Notice("material-dialogs", "https://github.com/afollestad/material-dialogs", "Copyright (c) 2014-2016 Aidan Michael Follestad", new MITLicense())); + notices.addNotice(new Notice("StickyListHeaders", "https://github.com/emilsjolander/StickyListHeaders", "Emil Sjölander", new ApacheSoftwareLicense20())); + notices.addNotice(new Notice("PreferenceFragment-Compat", "https://github.com/Machinarius/PreferenceFragment-Compat", "machinarius", new ApacheSoftwareLicense20())); + notices.addNotice(new Notice("libsuperuser", "https://github.com/Chainfire/libsuperuser", "Copyright (C) 2012-2015 Jorrit \"Chainfire\" Jongma", new ApacheSoftwareLicense20())); + notices.addNotice(new Notice("picasso", "https://github.com/square/picasso", "Copyright 2013 Square, Inc.", new ApacheSoftwareLicense20())); + + new LicensesDialog.Builder(this) + .setNotices(notices) + .setIncludeOwnLicense(true) + .build() + .show(); + } + + public void openLink(View view) { + NavUtil.startURL(this, view.getTag().toString()); + } + +} diff --git a/app/src/main/java/org/meowcat/edxposed/manager/BaseActivity.java b/app/src/main/java/org/meowcat/edxposed/manager/BaseActivity.java new file mode 100644 index 000000000..22a41e33c --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/BaseActivity.java @@ -0,0 +1,267 @@ +package org.meowcat.edxposed.manager; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.os.Bundle; +import android.os.Looper; +import android.text.TextUtils; +import android.view.MenuItem; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StyleRes; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatDelegate; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.topjohnwu.superuser.Shell; + +import org.meowcat.edxposed.manager.util.NavUtil; + +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; + +@SuppressLint("Registered") +public class BaseActivity extends AppCompatActivity { + + private static final String THEME_DEFAULT = "DEFAULT"; + private static final String THEME_BLACK = "BLACK"; + private String mTheme; + + public static boolean isBlackNightTheme() { + return XposedApp.getPreferences().getBoolean("black_dark_theme", false); + } + + public static String getTheme(Context context) { + if (isBlackNightTheme() + && isNightMode(context.getResources().getConfiguration())) + return THEME_BLACK; + + return THEME_DEFAULT; + } + + @StyleRes + public static int getThemeStyleRes(Context context) { + switch (getTheme(context)) { + case THEME_BLACK: + return R.style.ThemeOverlay_Black; + case THEME_DEFAULT: + default: + return R.style.ThemeOverlay; + } + } + + public static boolean isNightMode(Configuration configuration) { + return (configuration.uiMode & Configuration.UI_MODE_NIGHT_YES) > 0; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + AppCompatDelegate.setDefaultNightMode(XposedApp.getPreferences().getInt("theme", 0)); + mTheme = getTheme(this); + } + + @Override + protected void onResume() { + super.onResume(); + if (!Objects.equals(mTheme, getTheme(this))) { + recreate(); + } + } + + @Override + protected void onApplyThemeResource(Resources.Theme theme, int resid, boolean first) { + // apply real style and our custom style + if (getParent() == null) { + theme.applyStyle(resid, true); + } else { + try { + theme.setTo(getParent().getTheme()); + } catch (Exception e) { + // Empty + } + theme.applyStyle(resid, false); + } + theme.applyStyle(getThemeStyleRes(this), true); + // only pass theme style to super, so styled theme will not be overwritten + super.onApplyThemeResource(theme, R.style.ThemeOverlay, first); + } + + private void areYouSure(int contentTextId, DialogInterface.OnClickListener listener) { + new MaterialAlertDialogBuilder(this).setTitle(R.string.areyousure) + .setMessage(contentTextId) + .setPositiveButton(android.R.string.yes, listener) + .setNegativeButton(android.R.string.no, null) + .show(); + } + + void softReboot() { + if (startShell()) + return; + + List messages = new LinkedList<>(); + Shell.Result result = Shell.su("setprop ctl.restart surfaceflinger; setprop ctl.restart zygote").exec(); + if (result.getCode() != 0) { + messages.add(result.getOut().toString()); + messages.add(""); + messages.add(getString(R.string.reboot_failed)); + showAlert(TextUtils.join("\n", messages).trim()); + } + } + + private boolean startShell() { + if (Shell.rootAccess()) + return false; + + showAlert(getString(R.string.root_failed)); + return true; + } + + void showAlert(final String result) { + if (Looper.myLooper() != Looper.getMainLooper()) { + runOnUiThread(() -> showAlert(result)); + return; + } + + AlertDialog dialog = new MaterialAlertDialogBuilder(this).setMessage(result).setPositiveButton(android.R.string.ok, null).create(); + dialog.show(); + + TextView txtMessage = dialog + .findViewById(android.R.id.message); + try { + txtMessage.setTextSize(14); + } catch (NullPointerException ignored) { + } + } + + void reboot(String mode) { + if (startShell()) + return; + + List messages = new LinkedList<>(); + + String command = "/system/bin/svc power reboot"; + if (mode != null) { + command += " " + mode; + if (mode.equals("recovery")) + // create a flag used by some kernels to boot into recovery + Shell.su("touch /cache/recovery/boot").exec(); + } + Shell.Result result = Shell.su(command).exec(); + if (result.getCode() != 0) { + messages.add(result.getOut().toString()); + messages.add(""); + messages.add(getString(R.string.reboot_failed)); + showAlert(TextUtils.join("\n", messages).trim()); + } + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + switch (item.getItemId()) { + case R.id.dexopt_all: + areYouSure(R.string.take_while_cannot_resore, (dialog, which) -> { + new MaterialAlertDialogBuilder(this) + .setTitle(R.string.dexopt_now) + .setMessage(R.string.this_may_take_a_while) + .setCancelable(false) + .show(); + new Thread("dexopt") { + @Override + public void run() { + if (!Shell.rootAccess()) { + dialog.dismiss(); + NavUtil.showMessage(BaseActivity.this, getString(R.string.root_failed)); + return; + } + Shell.su("cmd package bg-dexopt-job").exec(); + + dialog.dismiss(); + XposedApp.runOnUiThread(() -> Toast.makeText(BaseActivity.this, R.string.done, Toast.LENGTH_LONG).show()); + } + }.start(); + } + ); + + break; + case R.id.speed_all: + areYouSure(R.string.take_while_cannot_resore, (dialog, which) -> { + + new MaterialAlertDialogBuilder(this) + .setTitle(R.string.dexopt_now) + .setMessage(R.string.this_may_take_a_while) + .setCancelable(false) + .show(); + new Thread("dex2oat") { + @Override + public void run() { + if (!Shell.rootAccess()) { + dialog.dismiss(); + NavUtil.showMessage(BaseActivity.this, getString(R.string.root_failed)); + return; + } + + Shell.su("cmd package compile -m speed -a").exec(); + + dialog.dismiss(); + XposedApp.runOnUiThread(() -> Toast.makeText(BaseActivity.this, R.string.done, Toast.LENGTH_LONG).show()); + } + + }; + }); + break; + case R.id.reboot: + if (XposedApp.getPreferences().getBoolean("confirm_reboots", true)) { + areYouSure(R.string.reboot, (dialog, which) -> reboot(null)); + } else { + reboot(null); + } + break; + case R.id.soft_reboot: + if (XposedApp.getPreferences().getBoolean("confirm_reboots", true)) { + areYouSure(R.string.soft_reboot, (dialog, which) -> softReboot()); + } else { + softReboot(); + } + break; + case R.id.reboot_recovery: + if (XposedApp.getPreferences().getBoolean("confirm_reboots", true)) { + areYouSure(R.string.reboot_recovery, (dialog, which) -> reboot("recovery")); + } else { + reboot("recovery"); + } + break; + case R.id.reboot_bootloader: + if (XposedApp.getPreferences().getBoolean("confirm_reboots", true)) { + areYouSure(R.string.reboot_bootloader, (dialog, which) -> reboot("bootloader")); + } else { + reboot("bootloader"); + } + break; + case R.id.reboot_download: + if (XposedApp.getPreferences().getBoolean("confirm_reboots", true)) { + areYouSure(R.string.reboot_download, (dialog, which) -> reboot("download")); + } else { + reboot("download"); + } + break; + case R.id.reboot_edl: + if (XposedApp.getPreferences().getBoolean("confirm_reboots", true)) { + areYouSure(R.string.reboot_download, (dialog, which) -> reboot("edl")); + } else { + reboot("edl"); + } + break; + } + + return super.onOptionsItemSelected(item); + } +} diff --git a/app/src/main/java/org/meowcat/edxposed/manager/BaseAdvancedInstaller.java b/app/src/main/java/org/meowcat/edxposed/manager/BaseAdvancedInstaller.java new file mode 100644 index 000000000..54ab3e991 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/BaseAdvancedInstaller.java @@ -0,0 +1,241 @@ +package org.meowcat.edxposed.manager; + +import android.Manifest; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.text.Html; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.app.ActivityCompat; +import androidx.fragment.app.Fragment; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import org.meowcat.edxposed.manager.util.NavUtil; +import org.meowcat.edxposed.manager.util.json.XposedTab; +import org.meowcat.edxposed.manager.util.json.XposedZip; + +import java.util.List; +import java.util.Objects; + +import static org.meowcat.edxposed.manager.XposedApp.WRITE_EXTERNAL_PERMISSION; + +public class BaseAdvancedInstaller extends Fragment { + + // private static final String JAR_PATH = XposedApp.BASE_DIR + "bin/XposedBridge.jar"; +// private static final int INSTALL_MODE_NORMAL = 0; +// private static final int INSTALL_MODE_RECOVERY_AUTO = 1; +// private static final int INSTALL_MODE_RECOVERY_MANUAL = 2; +// private static String APP_PROCESS_NAME = null; + //private List messages = new ArrayList<>(); + private View mClickedButton; + + static BaseAdvancedInstaller newInstance(XposedTab tab) { + BaseAdvancedInstaller myFragment = new BaseAdvancedInstaller(); + + Bundle args = new Bundle(); + args.putParcelable("tab", tab); + myFragment.setArguments(args); + + return myFragment; + } + + private List installers() { + XposedTab tab = Objects.requireNonNull(getArguments()).getParcelable("tab"); + return Objects.requireNonNull(tab).installers; + } + + private List uninstallers() { + XposedTab tab = Objects.requireNonNull(getArguments()).getParcelable("tab"); + return Objects.requireNonNull(tab).uninstallers; + } + + private String notice() { + XposedTab tab = Objects.requireNonNull(getArguments()).getParcelable("tab"); + return Objects.requireNonNull(tab).notice; + } + +// private String compatibility() { +// XposedTab tab = Objects.requireNonNull(getArguments()).getParcelable("tab"); +// return Objects.requireNonNull(tab).getCompatibility(); +// } + +// private String incompatibility() { +// XposedTab tab = Objects.requireNonNull(getArguments()).getParcelable("tab"); +// return Objects.requireNonNull(tab).getIncompatibility(); +// } + + protected String author() { + XposedTab tab = Objects.requireNonNull(getArguments()).getParcelable("tab"); + return Objects.requireNonNull(tab).author; + } + + private String supportUrl() { + XposedTab tab = Objects.requireNonNull(getArguments()).getParcelable("tab"); + return Objects.requireNonNull(tab).support; + } + + protected boolean isStable() { + XposedTab tab = Objects.requireNonNull(getArguments()).getParcelable("tab"); + return Objects.requireNonNull(tab).stable; + } + + private boolean isOfficial() { + XposedTab tab = Objects.requireNonNull(getArguments()).getParcelable("tab"); + return Objects.requireNonNull(tab).official; + } + + private String description() { + XposedTab tab = Objects.requireNonNull(getArguments()).getParcelable("tab"); + return Objects.requireNonNull(tab).description; + } + + private boolean checkPermissions() { + if (Build.VERSION.SDK_INT < 23) return false; + + if (ActivityCompat.checkSelfPermission(Objects.requireNonNull(getActivity()), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, WRITE_EXTERNAL_PERMISSION); + return true; + } + return false; + } + + @Override + public void onDestroy() { + super.onDestroy(); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.single_installer_view, container, false); + + final Spinner chooserInstallers = view.findViewById(R.id.chooserInstallers); + final Spinner chooserUninstallers = view.findViewById(R.id.chooserUninstallers); + final Button btnInstall = view.findViewById(R.id.btnInstall); + final Button btnUninstall = view.findViewById(R.id.btnUninstall); + ImageView infoInstaller = view.findViewById(R.id.infoInstaller); + ImageView infoUninstaller = view.findViewById(R.id.infoUninstaller); + TextView noticeTv = view.findViewById(R.id.noticeTv); + TextView author = view.findViewById(R.id.author); + View showOnXda = view.findViewById(R.id.show_on_xda); + View updateDescription = view.findViewById(R.id.updateDescription); + + try { + chooserInstallers.setAdapter(new XposedZip.MyAdapter(getContext(), installers())); + chooserUninstallers.setAdapter(new XposedZip.MyAdapter(getContext(), uninstallers())); + } catch (Exception ignored) { + } + infoInstaller.setOnClickListener(v -> { + XposedZip selectedInstaller = (XposedZip) chooserInstallers.getSelectedItem(); + String s = getString(R.string.infoInstaller, + selectedInstaller.name, + selectedInstaller.version); + + new MaterialAlertDialogBuilder(Objects.requireNonNull(getContext())).setTitle(R.string.info) + .setMessage(s).setPositiveButton(android.R.string.ok, null).show(); + }); + infoUninstaller.setOnClickListener(v -> { + XposedZip selectedUninstaller = (XposedZip) chooserUninstallers.getSelectedItem(); + String s = getString(R.string.infoUninstaller, + selectedUninstaller.name, + selectedUninstaller.version); + + new MaterialAlertDialogBuilder(Objects.requireNonNull(getContext())).setTitle(R.string.info) + .setMessage(s).setPositiveButton(android.R.string.ok, null).show(); + }); + + btnInstall.setOnClickListener(v -> { + mClickedButton = v; + if (checkPermissions()) return; + + areYouSure(R.string.warningArchitecture, + (dialog, which) -> { + XposedZip selectedInstaller = (XposedZip) chooserInstallers.getSelectedItem(); + Uri uri = Uri.parse(selectedInstaller.link); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + startActivity(intent); + }); + }); + + btnUninstall.setOnClickListener(v -> { + mClickedButton = v; + if (checkPermissions()) return; + + areYouSure(R.string.warningArchitecture, + (dialog, which) -> { + XposedZip selectedUninstaller = (XposedZip) chooserUninstallers.getSelectedItem(); + Uri uri = Uri.parse(selectedUninstaller.link); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + startActivity(intent); + }); + }); + + noticeTv.setText(Html.fromHtml(notice())); + author.setText(getString(R.string.download_author, author())); + + try { + if (uninstallers().size() == 0) { + infoUninstaller.setVisibility(View.GONE); + chooserUninstallers.setVisibility(View.GONE); + btnUninstall.setVisibility(View.GONE); + } + } catch (Exception ignored) { + } + + if (!isStable()) { + view.findViewById(R.id.warning_unstable).setVisibility(View.VISIBLE); + } + + if (!isOfficial()) { + view.findViewById(R.id.warning_unofficial).setVisibility(View.VISIBLE); + } + + showOnXda.setOnClickListener(v -> NavUtil.startURL(getActivity(), supportUrl())); + updateDescription.setOnClickListener(v -> new MaterialAlertDialogBuilder(Objects.requireNonNull(getContext())) + .setTitle(R.string.changes) + .setMessage(Html.fromHtml(description())) + .setPositiveButton(android.R.string.ok, null).show()); + + return view; + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == WRITE_EXTERNAL_PERMISSION) { + if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + if (mClickedButton != null) { + new Handler().postDelayed(() -> mClickedButton.performClick(), 500); + } + } else { + Toast.makeText(getActivity(), R.string.permissionNotGranted, Toast.LENGTH_LONG).show(); + } + } + } + + + @SuppressWarnings("SameParameterValue") + private void areYouSure(int contentTextId, DialogInterface.OnClickListener listener) { + new MaterialAlertDialogBuilder(Objects.requireNonNull(getActivity())).setTitle(R.string.areyousure) + .setMessage(contentTextId) + .setPositiveButton(android.R.string.yes, listener) + .setNegativeButton(android.R.string.no, null) + .show(); + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/meowcat/edxposed/manager/BlackListActivity.java b/app/src/main/java/org/meowcat/edxposed/manager/BlackListActivity.java new file mode 100644 index 000000000..a53e3072e --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/BlackListActivity.java @@ -0,0 +1,122 @@ +package org.meowcat.edxposed.manager; + +import android.content.pm.ApplicationInfo; +import android.os.Bundle; +import android.view.Menu; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.widget.SearchView; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +import org.meowcat.edxposed.manager.adapters.AppAdapter; +import org.meowcat.edxposed.manager.adapters.AppHelper; +import org.meowcat.edxposed.manager.adapters.BlackListAdapter; + +public class BlackListActivity extends BaseActivity implements AppAdapter.Callback { + private SwipeRefreshLayout mSwipeRefreshLayout; + private SearchView mSearchView; + private BlackListAdapter mAppAdapter; + + private SearchView.OnQueryTextListener mSearchListener; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_black_list); + setSupportActionBar(findViewById(R.id.toolbar)); + ActionBar bar = getSupportActionBar(); + if (bar != null) { + bar.setDisplayHomeAsUpEnabled(true); + } + mSwipeRefreshLayout = findViewById(R.id.swipeRefreshLayout); + RecyclerView mRecyclerView = findViewById(R.id.recyclerView); + mRecyclerView.setLayoutManager(new LinearLayoutManager(this)); + final boolean isWhiteListMode = isWhiteListMode(); + mAppAdapter = new BlackListAdapter(this, isWhiteListMode); + mRecyclerView.setAdapter(mAppAdapter); + mAppAdapter.setCallback(this); + DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(mRecyclerView.getContext(), + DividerItemDecoration.VERTICAL); + mRecyclerView.addItemDecoration(dividerItemDecoration); + mSwipeRefreshLayout.setRefreshing(true); + mSwipeRefreshLayout.setOnRefreshListener(mAppAdapter::refresh); + mSearchListener = new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + mAppAdapter.filter(query); + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + mAppAdapter.filter(newText); + return false; + } + }; + } + + @Override + public boolean onCreateOptionsMenu(@NonNull Menu menu) { + getMenuInflater().inflate(R.menu.menu_app_list, menu); + mSearchView = (SearchView) menu.findItem(R.id.app_search).getActionView(); + mSearchView.setOnQueryTextListener(mSearchListener); + return super.onCreateOptionsMenu(menu); + } + + @Override + public void onResume() { + super.onResume(); + changeTitle(isBlackListMode(), isWhiteListMode()); + } + + + private void changeTitle(boolean isBlackListMode, boolean isWhiteListMode) { + if (isBlackListMode) { + setTitle(isWhiteListMode ? R.string.title_white_list : R.string.title_black_list); + } else { + setTitle(R.string.nav_title_black_list); + } + } + + private boolean isWhiteListMode() { + return AppHelper.isWhiteListMode(); + } + + private boolean isBlackListMode() { + return AppHelper.isBlackListMode(); + } + + @Override + public void onDataReady() { + mSwipeRefreshLayout.setRefreshing(false); + String queryStr = mSearchView != null ? mSearchView.getQuery().toString() : ""; + mAppAdapter.filter(queryStr); + } + + @SuppressWarnings("deprecation") + @Override + public void onItemClick(View v, ApplicationInfo info) { + getSupportFragmentManager(); + AppHelper.showMenu(this, getSupportFragmentManager(), v, info); + } + + @Override + public void onPointerCaptureChanged(boolean hasCapture) { + + } + + @Override + public void onBackPressed() { + if (mSearchView.isIconified()) { + super.onBackPressed(); + } else { + mSearchView.setIconified(true); + } + } +} diff --git a/app/src/main/java/org/meowcat/edxposed/manager/CompileDialogFragment.java b/app/src/main/java/org/meowcat/edxposed/manager/CompileDialogFragment.java new file mode 100644 index 000000000..35087a392 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/CompileDialogFragment.java @@ -0,0 +1,126 @@ +package org.meowcat.edxposed.manager; + +import android.annotation.SuppressLint; +import android.app.Dialog; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.AsyncTask; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatDialogFragment; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.topjohnwu.superuser.Shell; + +import org.meowcat.edxposed.manager.util.ToastUtil; + +import java.lang.ref.WeakReference; + +public class CompileDialogFragment extends AppCompatDialogFragment { + + private static final String KEY_APP_INFO = "app_info"; + private static final String KEY_MSG = "msg"; + private static final String KEY_COMMANDS = "commands"; + private ApplicationInfo appInfo; + + + public CompileDialogFragment() { + } + + public static CompileDialogFragment newInstance(ApplicationInfo appInfo, + String msg, String[] commands) { + Bundle arguments = new Bundle(); + arguments.putParcelable(KEY_APP_INFO, appInfo); + arguments.putString(KEY_MSG, msg); + arguments.putStringArray(KEY_COMMANDS, commands); + CompileDialogFragment fragment = new CompileDialogFragment(); + fragment.setArguments(arguments); + fragment.setCancelable(false); + return fragment; + } + + @Override + @NonNull + public Dialog onCreateDialog(Bundle savedInstanceState) { + Bundle arguments = getArguments(); + if (arguments == null) { + throw new IllegalStateException("arguments should not be null."); + } + appInfo = arguments.getParcelable(KEY_APP_INFO); + if (appInfo == null) { + throw new IllegalStateException("appInfo should not be null."); + } + String msg = arguments.getString(KEY_MSG, getString(R.string.compile_speed_msg)); + final PackageManager pm = requireContext().getPackageManager(); + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext()) + .setIcon(appInfo.loadIcon(pm)) + .setTitle(appInfo.loadLabel(pm)) + .setCancelable(false); + @SuppressLint("InflateParams") View customView = LayoutInflater.from(requireContext()).inflate(R.layout.fragment_compile_dialog, null); + builder.setView(customView); + TextView msgView = customView.findViewById(R.id.message); + //ProgressBar progressView = customView.findViewById(R.id.progress); + msgView.setText(msg); + AlertDialog alertDialog = builder.create(); + alertDialog.setCanceledOnTouchOutside(false); + return alertDialog; + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + Bundle arguments = getArguments(); + if (arguments != null) { + String[] commandPrefixes = arguments.getStringArray(KEY_COMMANDS); + appInfo = arguments.getParcelable(KEY_APP_INFO); + if (commandPrefixes == null || commandPrefixes.length == 0 || appInfo == null) { + ToastUtil.showShortToast(context, R.string.empty_param); + dismissAllowingStateLoss(); + return; + } + String[] commands = new String[commandPrefixes.length]; + for (int i = 0; i < commandPrefixes.length; i++) { + commands[i] = commandPrefixes[i] + appInfo.packageName; + } + new CompileTask(this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, commands); + } else { + dismissAllowingStateLoss(); + } + } + + private static class CompileTask extends AsyncTask { + + WeakReference outerRef; + + CompileTask(CompileDialogFragment fragment) { + outerRef = new WeakReference<>(fragment); + } + + @Override + protected String doInBackground(String... commands) { + if (outerRef.get() == null) { + return outerRef.get().requireContext().getString(R.string.compile_failed); + } + return Shell.su(commands).exec().getOut().toString(); + } + + @Override + protected void onPostExecute(String result) { + if (outerRef.get() == null || !outerRef.get().isAdded()) { + return; + } + if ("".equals(result.substring(1, result.length() - 1))) { + ToastUtil.showLongToast(outerRef.get().requireContext(), R.string.compile_failed); + } else { + ToastUtil.showLongToast(outerRef.get().requireContext(), R.string.done); + } + outerRef.get().dismissAllowingStateLoss(); + } + } +} diff --git a/app/src/main/java/org/meowcat/edxposed/manager/DownloadActivity.java b/app/src/main/java/org/meowcat/edxposed/manager/DownloadActivity.java new file mode 100644 index 000000000..170fdb4d7 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/DownloadActivity.java @@ -0,0 +1,439 @@ +package org.meowcat.edxposed.manager; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.database.Cursor; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.widget.SearchView; +import androidx.core.view.MenuItemCompat; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.timehop.stickyheadersrecyclerview.StickyRecyclerHeadersAdapter; +import com.timehop.stickyheadersrecyclerview.StickyRecyclerHeadersDecoration; + +import org.meowcat.edxposed.manager.adapters.CursorRecyclerViewAdapter; +import org.meowcat.edxposed.manager.repo.RepoDb; +import org.meowcat.edxposed.manager.repo.RepoDbDefinitions; +import org.meowcat.edxposed.manager.util.ModuleUtil; +import org.meowcat.edxposed.manager.util.RepoLoader; + +import java.text.DateFormat; +import java.util.Date; + +public class DownloadActivity extends BaseActivity implements RepoLoader.RepoListener, ModuleUtil.ModuleListener, SharedPreferences.OnSharedPreferenceChangeListener { + private SharedPreferences mPref; + private DownloadsAdapter mAdapter; + private String mFilterText; + private RepoLoader mRepoLoader; + private ModuleUtil mModuleUtil; + private int mSortingOrder; + private SearchView mSearchView; + private SharedPreferences mIgnoredUpdatesPref; + private boolean changed = false; + private BroadcastReceiver connectionListener = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = cm.getActiveNetworkInfo(); + + if (mRepoLoader != null) { + /*if (networkInfo == null) { + ((TextView) backgroundList.findViewById(R.id.list_status)).setText(R.string.no_connection_available); + backgroundList.findViewById(R.id.progress).setVisibility(View.GONE); + } else { + ((TextView) backgroundList.findViewById(R.id.list_status)).setText(R.string.update_download_list); + backgroundList.findViewById(R.id.progress).setVisibility(View.VISIBLE); + } +*/ + mRepoLoader.triggerReload(true); + } + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_download); + setSupportActionBar(findViewById(R.id.toolbar)); + ActionBar bar = getSupportActionBar(); + if (bar != null) { + bar.setDisplayHomeAsUpEnabled(true); + } + + mPref = XposedApp.getPreferences(); + mRepoLoader = RepoLoader.getInstance(); + mModuleUtil = ModuleUtil.getInstance(); + mAdapter = new DownloadsAdapter(this, RepoDb.queryModuleOverview(mSortingOrder, mFilterText)); + /*mAdapter.setFilterQueryProvider(new FilterQueryProvider() { + @Override + public Cursor runQuery(CharSequence constraint) { + return RepoDb.queryModuleOverview(mSortingOrder, constraint); + } + });*/ + + mSortingOrder = mPref.getInt("download_sorting_order", + RepoDb.SORT_STATUS); + + mIgnoredUpdatesPref = getSharedPreferences("update_ignored", MODE_PRIVATE); + RecyclerView mListView = findViewById(R.id.recyclerView); + if (Build.VERSION.SDK_INT >= 26) { + mListView.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS); + } + final SwipeRefreshLayout refreshLayout = findViewById(R.id.swipeRefreshLayout); + refreshLayout.setOnRefreshListener(() -> { + mRepoLoader.setSwipeRefreshLayout(refreshLayout); + mRepoLoader.triggerReload(true); + }); + mRepoLoader.addListener(this, true); + mModuleUtil.addListener(this); + mListView.setAdapter(mAdapter); + + mListView.setLayoutManager(new LinearLayoutManager(this)); + mListView.addItemDecoration(new StickyRecyclerHeadersDecoration(mAdapter)); + DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(mListView.getContext(), + DividerItemDecoration.VERTICAL); + mListView.addItemDecoration(dividerItemDecoration); + /*mListView.setOnScrollListener(new AbsListView.OnScrollListener() { + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { + if (mListView.getChildAt(0) != null) { + refreshLayout.setEnabled(mListView.getFirstVisiblePosition() == 0 && mListView.getChildAt(0).getTop() == 0); + } + } + });*/ + + /*mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + Cursor cursor = (Cursor) mAdapter.getItem(position); + String packageName = cursor.getString(OverviewColumnsIndexes.PKGNAME); + + Intent detailsIntent = new Intent(getActivity(), DownloadDetailsActivity.class); + detailsIntent.setData(Uri.fromParts("package", packageName, null)); + startActivity(detailsIntent); + } + });*/ + mListView.setOnKeyListener(new View.OnKeyListener() { + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + // Expand the search view when the SEARCH key is triggered + if (keyCode == KeyEvent.KEYCODE_SEARCH && event.getAction() == KeyEvent.ACTION_UP && (event.getFlags() & KeyEvent.FLAG_CANCELED) == 0) { + if (mSearchView != null) + mSearchView.setIconified(false); + return true; + } + return false; + } + }); + + } + + + @Override + public void onResume() { + super.onResume(); + + mIgnoredUpdatesPref.registerOnSharedPreferenceChangeListener(this); + if (changed) { + reloadItems(); + changed = !changed; + } + + registerReceiver(connectionListener, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); + } + + @Override + public void onPause() { + super.onPause(); + + unregisterReceiver(connectionListener); + } + + @Override + public void onDestroy() { + super.onDestroy(); + + mRepoLoader.removeListener(this); + mModuleUtil.removeListener(this); + mIgnoredUpdatesPref.unregisterOnSharedPreferenceChangeListener(this); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_download, menu); + + // Setup search button + final MenuItem searchItem = menu.findItem(R.id.menu_search); + mSearchView = (SearchView) searchItem.getActionView(); + mSearchView.setIconifiedByDefault(true); + mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + setFilter(query); + mSearchView.clearFocus(); + return true; + } + + @Override + public boolean onQueryTextChange(String newText) { + setFilter(newText); + return true; + } + }); + MenuItemCompat.setOnActionExpandListener(searchItem, new MenuItemCompat.OnActionExpandListener() { + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + setFilter(null); + return true; // Return true to collapse action view + } + + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + return true; // Return true to expand action view + } + }); + return super.onCreateOptionsMenu(menu); + } + + private void setFilter(String filterText) { + mFilterText = filterText; + reloadItems(); + } + + private void reloadItems() { + mAdapter.swapCursor(RepoDb.queryModuleOverview(mSortingOrder, mFilterText)); + mAdapter.notifyDataSetChanged(); + //mAdapter.getFilter().filter(mFilterText); + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_sort: + new MaterialAlertDialogBuilder(this) + .setTitle(R.string.download_sorting_title) + .setSingleChoiceItems(R.array.download_sort_order, mSortingOrder, (dialog, which) -> { + mSortingOrder = which; + mPref.edit().putInt("download_sorting_order", mSortingOrder).apply(); + reloadItems(); + dialog.dismiss(); + }) + .show(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onRepoReloaded(final RepoLoader loader) { + reloadItems(); + } + + @Override + public void onSingleInstalledModuleReloaded(ModuleUtil moduleUtil, String packageName, ModuleUtil.InstalledModule module) { + reloadItems(); + } + + @Override + public void onInstalledModulesReloaded(ModuleUtil moduleUtil) { + reloadItems(); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + changed = true; + } + + @Override + public void onBackPressed() { + if (mSearchView.isIconified()) { + super.onBackPressed(); + } else { + mSearchView.setIconified(true); + } + } + + private class DownloadsAdapter extends CursorRecyclerViewAdapter implements StickyRecyclerHeadersAdapter { + private final Context mContext; + private final DateFormat mDateFormatter = DateFormat.getDateInstance(DateFormat.SHORT); + private final SharedPreferences mPrefs; + private String[] sectionHeadersStatus; + private String[] sectionHeadersDate; + + DownloadsAdapter(Context context, Cursor cursor) { + super(context, cursor); + mContext = context; + mPrefs = context.getSharedPreferences("update_ignored", MODE_PRIVATE); + + Resources res = context.getResources(); + sectionHeadersStatus = new String[]{ + res.getString(R.string.download_section_framework), + res.getString(R.string.download_section_update_available), + res.getString(R.string.download_section_installed), + res.getString(R.string.download_section_not_installed),}; + sectionHeadersDate = new String[]{ + res.getString(R.string.download_section_24h), + res.getString(R.string.download_section_7d), + res.getString(R.string.download_section_30d), + res.getString(R.string.download_section_older)}; + } + + @Override + public long getHeaderId(int position) { + Cursor cursor = getCursor(); + cursor.moveToPosition(position); + long created = cursor.getLong(RepoDbDefinitions.OverviewColumnsIndexes.CREATED); + long updated = cursor.getLong(RepoDbDefinitions.OverviewColumnsIndexes.UPDATED); + boolean isFramework = cursor.getInt(RepoDbDefinitions.OverviewColumnsIndexes.IS_FRAMEWORK) > 0; + boolean isInstalled = cursor.getInt(RepoDbDefinitions.OverviewColumnsIndexes.IS_INSTALLED) > 0; + boolean updateIgnored = mPrefs.getBoolean(cursor.getString(RepoDbDefinitions.OverviewColumnsIndexes.PKGNAME), false); + boolean updateIgnorePreference = XposedApp.getPreferences().getBoolean("ignore_updates", false); + boolean hasUpdate = cursor.getInt(RepoDbDefinitions.OverviewColumnsIndexes.HAS_UPDATE) > 0; + + if (hasUpdate && updateIgnored && updateIgnorePreference) { + hasUpdate = false; + } + + if (mSortingOrder != RepoDb.SORT_STATUS) { + long timestamp = (mSortingOrder == RepoDb.SORT_UPDATED) ? updated : created; + long age = System.currentTimeMillis() - timestamp; + final long mSecsPerDay = 24 * 60 * 60 * 1000L; + if (age < mSecsPerDay) + return 0; + if (age < 7 * mSecsPerDay) + return 1; + if (age < 30 * mSecsPerDay) + return 2; + return 3; + } else { + if (isFramework) + return 0; + + if (hasUpdate) + return 1; + else if (isInstalled) + return 2; + else + return 3; + } + } + + @Override + public RecyclerView.ViewHolder onCreateHeaderViewHolder(ViewGroup parent) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.sticky_header_download, parent, false); + return new RecyclerView.ViewHolder(view) { + }; + } + + @Override + public void onBindHeaderViewHolder(RecyclerView.ViewHolder viewHolder, int position) { + long section = getHeaderId(position); + TextView tv = viewHolder.itemView.findViewById(android.R.id.title); + tv.setText(mSortingOrder == RepoDb.SORT_STATUS + ? sectionHeadersStatus[(int) section] + : sectionHeadersDate[(int) section]); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_download, parent, false); + return new ViewHolder(v); + } + + @Override + public void onBindViewHolder(ViewHolder holder, Cursor cursor) { + String title = cursor.getString(RepoDbDefinitions.OverviewColumnsIndexes.TITLE); + String summary = cursor.getString(RepoDbDefinitions.OverviewColumnsIndexes.SUMMARY); + String installedVersion = cursor.getString(RepoDbDefinitions.OverviewColumnsIndexes.INSTALLED_VERSION); + String latestVersion = cursor.getString(RepoDbDefinitions.OverviewColumnsIndexes.LATEST_VERSION); + long created = cursor.getLong(RepoDbDefinitions.OverviewColumnsIndexes.CREATED); + long updated = cursor.getLong(RepoDbDefinitions.OverviewColumnsIndexes.UPDATED); + boolean isInstalled = cursor.getInt(RepoDbDefinitions.OverviewColumnsIndexes.IS_INSTALLED) > 0; + boolean updateIgnored = mPrefs.getBoolean(cursor.getString(RepoDbDefinitions.OverviewColumnsIndexes.PKGNAME), false); + boolean updateIgnorePreference = XposedApp.getPreferences().getBoolean("ignore_updates", false); + boolean hasUpdate = cursor.getInt(RepoDbDefinitions.OverviewColumnsIndexes.HAS_UPDATE) > 0; + + if (hasUpdate && updateIgnored && updateIgnorePreference) { + hasUpdate = false; + } + + TextView txtTitle = holder.appName; + txtTitle.setText(title); + + TextView txtSummary = holder.appDescription; + txtSummary.setText(summary); + + TextView txtStatus = holder.downloadStatus; + if (hasUpdate) { + txtStatus.setText(mContext.getString( + R.string.download_status_update_available, + installedVersion, latestVersion)); + txtStatus.setTextColor(getResources().getColor(R.color.download_status_update_available)); + txtStatus.setVisibility(View.VISIBLE); + } else if (isInstalled) { + txtStatus.setText(mContext.getString( + R.string.download_status_installed, installedVersion)); + //txtStatus.setTextColor(ThemeUtil.getThemeColor(mContext, R.attr.download_status_installed)); + txtStatus.setVisibility(View.VISIBLE); + } else { + txtStatus.setVisibility(View.GONE); + } + + String creationDate = mDateFormatter.format(new Date(created)); + String updateDate = mDateFormatter.format(new Date(updated)); + holder.timestamps.setText(getString(R.string.download_timestamps, creationDate, updateDate)); + String packageName = cursor.getString(RepoDbDefinitions.OverviewColumnsIndexes.PKGNAME); + holder.itemView.setOnClickListener(v -> { + Intent detailsIntent = new Intent(DownloadActivity.this, DownloadDetailsActivity.class); + detailsIntent.setData(Uri.fromParts("package", packageName, null)); + startActivity(detailsIntent); + }); + } + + class ViewHolder extends RecyclerView.ViewHolder { + TextView appName; + TextView appDescription; + TextView downloadStatus; + TextView timestamps; + + ViewHolder(View itemView) { + super(itemView); + appName = itemView.findViewById(R.id.title); + appDescription = itemView.findViewById(R.id.description); + downloadStatus = itemView.findViewById(R.id.downloadStatus); + timestamps = itemView.findViewById(R.id.timestamps); + } + } + } +} + diff --git a/app/src/main/java/org/meowcat/edxposed/manager/DownloadDetailsActivity.java b/app/src/main/java/org/meowcat/edxposed/manager/DownloadDetailsActivity.java new file mode 100644 index 000000000..213e8546f --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/DownloadDetailsActivity.java @@ -0,0 +1,298 @@ +package org.meowcat.edxposed.manager; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.widget.Toolbar; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentPagerAdapter; +import androidx.viewpager.widget.ViewPager; + +import com.google.android.material.snackbar.Snackbar; +import com.google.android.material.tabs.TabLayout; + +import org.meowcat.edxposed.manager.repo.Module; +import org.meowcat.edxposed.manager.util.ModuleUtil; +import org.meowcat.edxposed.manager.util.RepoLoader; + +import java.util.List; +import java.util.Objects; + +public class DownloadDetailsActivity extends BaseActivity implements RepoLoader.RepoListener, ModuleUtil.ModuleListener { + + public static final int DOWNLOAD_DESCRIPTION = 0; + public static final int DOWNLOAD_VERSIONS = 1; + public static final int DOWNLOAD_SETTINGS = 2; + static final String XPOSED_REPO_LINK = "http://repo.xposed.info/module/%s"; + static final String PLAY_STORE_PACKAGE = "com.android.vending"; + static final String PLAY_STORE_LINK = "https://play.google.com/store/apps/details?id=%s"; + private static final String TAG = "DownloadDetailsActivity"; + private static final String NOT_ACTIVE_NOTE_TAG = "NOT_ACTIVE_NOTE"; + private static RepoLoader sRepoLoader = RepoLoader.getInstance(); + private static ModuleUtil sModuleUtil = ModuleUtil.getInstance(); + private ViewPager mPager; + private String mPackageName; + private Module mModule; + private ModuleUtil.InstalledModule mInstalledModule; + + @Override + public void onCreate(Bundle savedInstanceState) { + + mPackageName = getModulePackageName(); + try { + mModule = sRepoLoader.getModule(mPackageName); + } catch (Exception e) { + Log.i(TAG, "DownloadDetailsActivity -> " + e.getMessage()); + + mModule = null; + } + + mInstalledModule = ModuleUtil.getInstance().getModule(mPackageName); + + super.onCreate(savedInstanceState); + sRepoLoader.addListener(this, false); + sModuleUtil.addListener(this); + + if (mModule != null) { + setContentView(R.layout.activity_download_details); + + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + toolbar.setNavigationOnClickListener(view -> finish()); + + ActionBar ab = getSupportActionBar(); + + if (ab != null) { + ab.setTitle(R.string.nav_item_download); + ab.setDisplayHomeAsUpEnabled(true); + } + + setupTabs(); + + boolean directDownload = getIntent().getBooleanExtra("direct_download", false); + // Updates available => start on the versions page + if (mInstalledModule != null && mInstalledModule.isUpdate(sRepoLoader.getLatestVersion(mModule)) || directDownload) + mPager.setCurrentItem(DOWNLOAD_VERSIONS); + + } else { + setContentView(R.layout.activity_download_details_not_found); + + TextView txtMessage = findViewById(android.R.id.message); + txtMessage.setText(getResources().getString(R.string.download_details_not_found, mPackageName)); + + findViewById(R.id.reload).setOnClickListener(v -> { + v.setEnabled(false); + sRepoLoader.triggerReload(true); + }); + } + } + + private void setupTabs() { + mPager = findViewById(R.id.download_pager); + mPager.setAdapter(new SwipeFragmentPagerAdapter(getSupportFragmentManager())); + TabLayout mTabLayout = findViewById(R.id.sliding_tabs); + mTabLayout.setupWithViewPager(mPager); + mTabLayout.setBackgroundColor(XposedApp.getColor(this)); + } + + private String getModulePackageName() { + Uri uri = getIntent().getData(); + if (uri == null) + return null; + + String scheme = uri.getScheme(); + if (TextUtils.isEmpty(scheme)) { + return null; + } else switch (Objects.requireNonNull(scheme)) { + case "xposed": + case "package": + return uri.getSchemeSpecificPart().replace("//", ""); + case "http": + List segments = uri.getPathSegments(); + if (segments.size() > 1) + return segments.get(1); + break; + } + return null; + } + + @Override + protected void onDestroy() { + super.onDestroy(); + sRepoLoader.removeListener(this); + sModuleUtil.removeListener(this); + } + + public Module getModule() { + return mModule; + } + + public ModuleUtil.InstalledModule getInstalledModule() { + return mInstalledModule; + } + + public void gotoPage(int page) { + mPager.setCurrentItem(page); + } + + private void reload() { + runOnUiThread(this::recreate); + } + + @Override + public void onRepoReloaded(RepoLoader loader) { + reload(); + } + + @Override + public void onInstalledModulesReloaded(ModuleUtil moduleUtil) { + reload(); + } + + @Override + public void onSingleInstalledModuleReloaded(ModuleUtil moduleUtil, String packageName, ModuleUtil.InstalledModule module) { + if (packageName.equals(mPackageName)) + reload(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.menu_download_details, menu); + + boolean updateIgnorePreference = XposedApp.getPreferences().getBoolean("ignore_updates", false); + if (updateIgnorePreference) { + SharedPreferences prefs = getSharedPreferences("update_ignored", MODE_PRIVATE); + + boolean ignored = prefs.getBoolean(mModule.packageName, false); + menu.findItem(R.id.ignoreUpdate).setChecked(ignored); + } else { + menu.removeItem(R.id.ignoreUpdate); + } + setupBookmark(false); + return true; + } + + private void setupBookmark(boolean clicked) { + SharedPreferences myPref = getSharedPreferences("bookmarks", MODE_PRIVATE); + + boolean saved = myPref.getBoolean(mModule.packageName, false); + boolean newValue; + + if (clicked) { + newValue = !saved; + myPref.edit().putBoolean(mModule.packageName, newValue).apply(); + + int msg = newValue ? R.string.bookmark_added : R.string.bookmark_removed; + + Snackbar.make(findViewById(android.R.id.content), msg, Snackbar.LENGTH_SHORT).show(); + } + + saved = myPref.getBoolean(mModule.packageName, false); + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_refresh: + RepoLoader.getInstance().triggerReload(true); + return true; + case R.id.menu_share: + String text = mModule.name + " - "; + + if (isPackageInstalled(mPackageName, this)) { + String s = getPackageManager().getInstallerPackageName(mPackageName); + boolean playStore; + + try { + playStore = s.equals(PLAY_STORE_PACKAGE); + } catch (NullPointerException e) { + playStore = false; + } + + if (playStore) { + text += String.format(PLAY_STORE_LINK, mPackageName); + } else { + text += String.format(XPOSED_REPO_LINK, mPackageName); + } + } else { + text += String.format(XPOSED_REPO_LINK, + mPackageName); + } + + Intent sharingIntent = new Intent(Intent.ACTION_SEND); + sharingIntent.setType("text/plain"); + sharingIntent.putExtra(Intent.EXTRA_TEXT, text); + startActivity(Intent.createChooser(sharingIntent, getString(R.string.share))); + return true; + case R.id.ignoreUpdate: + SharedPreferences prefs = getSharedPreferences("update_ignored", MODE_PRIVATE); + + boolean ignored = prefs.getBoolean(mModule.packageName, false); + prefs.edit().putBoolean(mModule.packageName, !ignored).apply(); + item.setChecked(!ignored); + break; + } + return super.onOptionsItemSelected(item); + } + + private boolean isPackageInstalled(String packagename, Context context) { + PackageManager pm = context.getPackageManager(); + try { + pm.getPackageInfo(packagename, PackageManager.GET_ACTIVITIES); + return true; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + + class SwipeFragmentPagerAdapter extends FragmentPagerAdapter { + final int PAGE_COUNT = 3; + private String[] tabTitles = new String[]{getString(R.string.download_details_page_description), getString(R.string.download_details_page_versions), getString(R.string.download_details_page_settings),}; + + SwipeFragmentPagerAdapter(FragmentManager fm) { + super(fm); + } + + @Override + public int getCount() { + return PAGE_COUNT; + } + + @NonNull + @Override + public Fragment getItem(int position) { + switch (position) { + case DOWNLOAD_DESCRIPTION: + return new DownloadDetailsFragment(); + case DOWNLOAD_VERSIONS: + return new DownloadDetailsVersionsFragment(); + case DOWNLOAD_SETTINGS: + return new DownloadDetailsSettingsFragment(); + default: + //noinspection ConstantConditions + return null; + } + } + + @Override + public CharSequence getPageTitle(int position) { + // Generate title based on item position + return tabTitles[position]; + } + } +} diff --git a/app/src/main/java/org/meowcat/edxposed/manager/DownloadDetailsFragment.java b/app/src/main/java/org/meowcat/edxposed/manager/DownloadDetailsFragment.java new file mode 100644 index 000000000..a4152b917 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/DownloadDetailsFragment.java @@ -0,0 +1,86 @@ +package org.meowcat.edxposed.manager; + +import android.app.Activity; +import android.net.Uri; +import android.os.Bundle; +import android.text.method.LinkMovementMethod; +import android.util.Pair; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.fragment.app.Fragment; + +import org.meowcat.edxposed.manager.repo.Module; +import org.meowcat.edxposed.manager.repo.RepoParser; +import org.meowcat.edxposed.manager.util.NavUtil; +import org.meowcat.edxposed.manager.util.chrome.LinkTransformationMethod; + +public class DownloadDetailsFragment extends Fragment { + private DownloadDetailsActivity mActivity; + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + mActivity = (DownloadDetailsActivity) activity; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + final Module module = mActivity.getModule(); + if (module == null) + return null; + + final View view = inflater.inflate(R.layout.download_details, container, false); + + TextView title = view.findViewById(R.id.download_title); + title.setText(module.name); + title.setTextIsSelectable(true); + + TextView author = view.findViewById(R.id.download_author); + if (module.author != null && !module.author.isEmpty()) + author.setText(getString(R.string.download_author, module.author)); + else + author.setText(R.string.download_unknown_author); + + TextView description = view.findViewById(R.id.download_description); + if (module.description != null) { + if (module.descriptionIsHtml) { + description.setText(RepoParser.parseSimpleHtml(getActivity(), module.description, description)); + description.setTransformationMethod(new LinkTransformationMethod(getActivity())); + description.setMovementMethod(LinkMovementMethod.getInstance()); + } else { + description.setText(module.description); + } + description.setTextIsSelectable(true); + } else { + description.setVisibility(View.GONE); + } + + ViewGroup moreInfoContainer = view.findViewById(R.id.download_moreinfo_container); + for (Pair moreInfoEntry : module.moreInfo) { + View moreInfoView = inflater.inflate(R.layout.download_moreinfo, moreInfoContainer, false); + TextView txtTitle = moreInfoView.findViewById(android.R.id.title); + TextView txtValue = moreInfoView.findViewById(android.R.id.message); + + txtTitle.setText(moreInfoEntry.first + ":"); + txtValue.setText(moreInfoEntry.second); + + final Uri link = NavUtil.parseURL(moreInfoEntry.second); + if (link != null) { + txtValue.setTextColor(txtValue.getLinkTextColors()); + moreInfoView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + NavUtil.startURL(getActivity(), link); + } + }); + } + + moreInfoContainer.addView(moreInfoView); + } + + return view; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/meowcat/edxposed/manager/DownloadDetailsSettingsFragment.java b/app/src/main/java/org/meowcat/edxposed/manager/DownloadDetailsSettingsFragment.java new file mode 100644 index 000000000..acfcfc4f9 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/DownloadDetailsSettingsFragment.java @@ -0,0 +1,60 @@ +package org.meowcat.edxposed.manager; + +import android.app.Activity; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; + +import androidx.preference.PreferenceManager; + +import com.takisoft.preferencex.PreferenceFragmentCompat; + +import org.meowcat.edxposed.manager.repo.Module; +import org.meowcat.edxposed.manager.util.PrefixedSharedPreferences; +import org.meowcat.edxposed.manager.util.RepoLoader; + +import java.util.Map; + +public class DownloadDetailsSettingsFragment extends PreferenceFragmentCompat { + private DownloadDetailsActivity mActivity; + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + mActivity = (DownloadDetailsActivity) activity; + } + + @Override + public void onCreatePreferencesFix(Bundle savedInstanceState, String rootKey) { + final Module module = mActivity.getModule(); + if (module == null) + return; + + final String packageName = module.packageName; + + PreferenceManager prefManager = getPreferenceManager(); + prefManager.setSharedPreferencesName("module_settings"); + PrefixedSharedPreferences.injectToPreferenceManager(prefManager, module.packageName); + addPreferencesFromResource(R.xml.module_prefs); + + SharedPreferences prefs = getActivity().getSharedPreferences("module_settings", Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + + if (prefs.getBoolean("no_global", true)) { + for (Map.Entry k : prefs.getAll().entrySet()) { + if (prefs.getString(k.getKey(), "").equals("global")) { + editor.putString(k.getKey(), "").apply(); + } + } + + editor.putBoolean("no_global", false).apply(); + } + + findPreference("release_type").setOnPreferenceChangeListener( + (preference, newValue) -> { + RepoLoader.getInstance().setReleaseTypeLocal(packageName, (String) newValue); + return true; + }); + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/meowcat/edxposed/manager/DownloadDetailsVersionsFragment.java b/app/src/main/java/org/meowcat/edxposed/manager/DownloadDetailsVersionsFragment.java new file mode 100644 index 000000000..77602cdb5 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/DownloadDetailsVersionsFragment.java @@ -0,0 +1,261 @@ +package org.meowcat.edxposed.manager; + +import android.app.Activity; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.os.Bundle; +import android.text.method.LinkMovementMethod; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.core.content.ContextCompat; +import androidx.fragment.app.ListFragment; + +import org.meowcat.edxposed.manager.repo.Module; +import org.meowcat.edxposed.manager.repo.ModuleVersion; +import org.meowcat.edxposed.manager.repo.ReleaseType; +import org.meowcat.edxposed.manager.repo.RepoParser; +import org.meowcat.edxposed.manager.util.DownloadsUtil; +import org.meowcat.edxposed.manager.util.HashUtil; +import org.meowcat.edxposed.manager.util.InstallApkUtil; +import org.meowcat.edxposed.manager.util.ModuleUtil.InstalledModule; +import org.meowcat.edxposed.manager.util.RepoLoader; +import org.meowcat.edxposed.manager.util.chrome.LinkTransformationMethod; +import org.meowcat.edxposed.manager.widget.DownloadView; + +import java.io.File; +import java.text.DateFormat; +import java.util.Date; + +import static org.meowcat.edxposed.manager.XposedApp.WRITE_EXTERNAL_PERMISSION; + +public class DownloadDetailsVersionsFragment extends ListFragment { + private static VersionsAdapter sAdapter; + private DownloadDetailsActivity mActivity; + private Module module; + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + mActivity = (DownloadDetailsActivity) activity; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + module = mActivity.getModule(); + if (module == null) + return; + + if (module.versions.isEmpty()) { + setEmptyText(getString(R.string.download_no_versions)); + setListShown(true); + } else { + RepoLoader repoLoader = RepoLoader.getInstance(); + if (!repoLoader.isVersionShown(module.versions.get(0))) { + TextView txtHeader = new TextView(getActivity()); + txtHeader.setText(R.string.download_test_version_not_shown); + txtHeader.setTextColor(getResources().getColor(R.color.warning)); + txtHeader.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mActivity.gotoPage(DownloadDetailsActivity.DOWNLOAD_SETTINGS); + } + }); + getListView().addHeaderView(txtHeader); + } + + sAdapter = new VersionsAdapter(mActivity, mActivity.getInstalledModule()); + for (ModuleVersion version : module.versions) { + if (repoLoader.isVersionShown(version)) + sAdapter.add(version); + } + setListAdapter(sAdapter); + } + + getListView().setClipToPadding(false); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + setListAdapter(null); + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == WRITE_EXTERNAL_PERMISSION) { + if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + DownloadView.mClickedButton.performClick(); + } else { + Toast.makeText(getActivity(), R.string.permissionNotGranted, Toast.LENGTH_LONG).show(); + } + } + } + + static class ViewHolder { + TextView txtStatus; + TextView txtVersion; + TextView txtRelType; + TextView txtUploadDate; + DownloadView downloadView; + TextView txtChangesTitle; + TextView txtChanges; + } + + public static class DownloadModuleCallback implements DownloadsUtil.DownloadFinishedCallback { + private final ModuleVersion moduleVersion; + + DownloadModuleCallback(ModuleVersion moduleVersion) { + this.moduleVersion = moduleVersion; + } + + @Override + public void onDownloadFinished(Context context, DownloadsUtil.DownloadInfo info) { + File localFile = new File(info.localFilename); + + if (!localFile.isFile()) + return; + + if (moduleVersion.md5sum != null && !moduleVersion.md5sum.isEmpty()) { + try { + String actualMd5Sum = HashUtil.md5(localFile); + if (!moduleVersion.md5sum.equals(actualMd5Sum)) { + Toast.makeText(context, context.getString(R.string.download_md5sum_incorrect, actualMd5Sum, moduleVersion.md5sum), Toast.LENGTH_LONG).show(); + DownloadsUtil.removeById(context, info.id); + return; + } + } catch (Exception e) { + Toast.makeText(context, context.getString(R.string.download_could_not_read_file, e.getMessage()), Toast.LENGTH_LONG).show(); + DownloadsUtil.removeById(context, info.id); + return; + } + } + + PackageManager pm = context.getPackageManager(); + PackageInfo packageInfo = pm.getPackageArchiveInfo(info.localFilename, 0); + + if (packageInfo == null) { + Toast.makeText(context, R.string.download_no_valid_apk, Toast.LENGTH_LONG).show(); + DownloadsUtil.removeById(context, info.id); + return; + } + + if (!packageInfo.packageName.equals(moduleVersion.module.packageName)) { + Toast.makeText(context, context.getString(R.string.download_incorrect_package_name, packageInfo.packageName, moduleVersion.module.packageName), Toast.LENGTH_LONG).show(); + DownloadsUtil.removeById(context, info.id); + return; + } + + new InstallApkUtil(context, info).execute(); + } + } + + private class VersionsAdapter extends ArrayAdapter { + private final DateFormat mDateFormatter = DateFormat + .getDateInstance(DateFormat.SHORT); + private final int mColorRelTypeStable; + private final int mColorRelTypeOthers; + private final int mColorInstalled; + private final int mColorUpdateAvailable; + private final String mTextInstalled; + private final String mTextUpdateAvailable; + private final long mInstalledVersionCode; + + public VersionsAdapter(Context context, InstalledModule installed) { + super(context, R.layout.item_version); + TypedValue typedValue = new TypedValue(); + Resources.Theme theme = context.getTheme(); + theme.resolveAttribute(android.R.attr.textColorPrimary, typedValue, true); + int color = ContextCompat.getColor(context, typedValue.resourceId); + mColorRelTypeStable = color; + mColorRelTypeOthers = getResources().getColor(R.color.warning); + mColorInstalled = color; + mColorUpdateAvailable = getResources().getColor(R.color.download_status_update_available); + mTextInstalled = getString(R.string.download_section_installed) + ":"; + mTextUpdateAvailable = getString(R.string.download_section_update_available) + ":"; + mInstalledVersionCode = (installed != null) ? installed.versionCode : -1; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view = convertView; + if (view == null) { + LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + view = inflater.inflate(R.layout.item_version, null, true); + ViewHolder viewHolder = new ViewHolder(); + viewHolder.txtStatus = view.findViewById(R.id.txtStatus); + viewHolder.txtVersion = view.findViewById(R.id.txtVersion); + viewHolder.txtRelType = view.findViewById(R.id.txtRelType); + viewHolder.txtUploadDate = view.findViewById(R.id.txtUploadDate); + viewHolder.downloadView = view.findViewById(R.id.downloadView); + viewHolder.txtChangesTitle = view.findViewById(R.id.txtChangesTitle); + viewHolder.txtChanges = view.findViewById(R.id.txtChanges); + viewHolder.downloadView.fragment = DownloadDetailsVersionsFragment.this; + view.setTag(viewHolder); + } + + ViewHolder holder = (ViewHolder) view.getTag(); + ModuleVersion item = getItem(position); + + holder.txtVersion.setText(item.name); + holder.txtRelType.setText(item.relType.getTitleId()); + holder.txtRelType.setTextColor(item.relType == ReleaseType.STABLE + ? mColorRelTypeStable : mColorRelTypeOthers); + + if (item.uploaded > 0) { + holder.txtUploadDate.setText( + mDateFormatter.format(new Date(item.uploaded))); + holder.txtUploadDate.setVisibility(View.VISIBLE); + } else { + holder.txtUploadDate.setVisibility(View.GONE); + } + + if (item.code <= 0 || mInstalledVersionCode <= 0 + || item.code < mInstalledVersionCode) { + holder.txtStatus.setVisibility(View.GONE); + } else if (item.code == mInstalledVersionCode) { + holder.txtStatus.setText(mTextInstalled); + holder.txtStatus.setTextColor(mColorInstalled); + holder.txtStatus.setVisibility(View.VISIBLE); + } else { // item.code > mInstalledVersionCode + holder.txtStatus.setText(mTextUpdateAvailable); + holder.txtStatus.setTextColor(mColorUpdateAvailable); + holder.txtStatus.setVisibility(View.VISIBLE); + } + + holder.downloadView.setUrl(item.downloadLink); + holder.downloadView.setTitle(mActivity.getModule().name); + holder.downloadView.setDownloadFinishedCallback(new DownloadModuleCallback(item)); + + if (item.changelog != null && !item.changelog.isEmpty()) { + holder.txtChangesTitle.setVisibility(View.VISIBLE); + holder.txtChanges.setVisibility(View.VISIBLE); + + if (item.changelogIsHtml) { + holder.txtChanges.setText(RepoParser.parseSimpleHtml(getActivity(), item.changelog, holder.txtChanges)); + holder.txtChanges.setTransformationMethod(new LinkTransformationMethod(getActivity())); + holder.txtChanges.setMovementMethod(LinkMovementMethod.getInstance()); + } else { + holder.txtChanges.setText(item.changelog); + holder.txtChanges.setMovementMethod(null); + } + + } else { + holder.txtChangesTitle.setVisibility(View.GONE); + holder.txtChanges.setVisibility(View.GONE); + } + + return view; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/meowcat/edxposed/manager/EdDownloadActivity.java b/app/src/main/java/org/meowcat/edxposed/manager/EdDownloadActivity.java new file mode 100644 index 000000000..084b1b09f --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/EdDownloadActivity.java @@ -0,0 +1,175 @@ +package org.meowcat.edxposed.manager; + +import android.annotation.SuppressLint; +import android.content.SharedPreferences; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; +import android.view.Menu; +import android.view.View; +import android.widget.CheckBox; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.ActionBar; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentPagerAdapter; +import androidx.viewpager.widget.ViewPager; + +import com.annimon.stream.Stream; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.tabs.TabLayout; +import com.google.gson.Gson; + +import org.meowcat.edxposed.manager.util.json.JSONUtils; +import org.meowcat.edxposed.manager.util.json.XposedTab; + +import java.util.ArrayList; +import java.util.List; + +public class EdDownloadActivity extends BaseActivity { + + private TabsAdapter tabsAdapter; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_ed_download); + setSupportActionBar(findViewById(R.id.toolbar)); + ActionBar bar = getSupportActionBar(); + if (bar != null) { + bar.setDisplayHomeAsUpEnabled(true); + } + ViewPager mPager = findViewById(R.id.pager); + TabLayout mTabLayout = findViewById(R.id.tab_layout); + + tabsAdapter = new TabsAdapter(getSupportFragmentManager()); + tabsAdapter.notifyDataSetChanged(); + mPager.setAdapter(tabsAdapter); + mTabLayout.setupWithViewPager(mPager); + new JSONParser().execute(); + + if (!XposedApp.getPreferences().getBoolean("hide_install_warning", false)) { + @SuppressLint("InflateParams") final View dontShowAgainView = getLayoutInflater().inflate(R.layout.dialog_install_warning, null); + + new MaterialAlertDialogBuilder(this) + .setTitle(R.string.install_warning_title) + .setView(dontShowAgainView) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + CheckBox checkBox = dontShowAgainView.findViewById(android.R.id.checkbox); + if (checkBox.isChecked()) + XposedApp.getPreferences().edit().putBoolean("hide_install_warning", true).apply(); + }) + .setCancelable(false) + .show(); + } + + } + + @Override + public boolean onCreateOptionsMenu(@NonNull Menu menu) { + getMenuInflater().inflate(R.menu.menu_installer, menu); + if (Build.VERSION.SDK_INT < 26) { + menu.findItem(R.id.dexopt_all).setVisible(false); + menu.findItem(R.id.speed_all).setVisible(false); + } + return super.onCreateOptionsMenu(menu); + } + + @SuppressLint("StaticFieldLeak") + private class JSONParser extends AsyncTask { + + private String newApkVersion = null; + private String newApkLink = null; + private String newApkChangelog = null; + + @Override + protected Boolean doInBackground(Void... params) { + try { + String originalJson = JSONUtils.getFileContent(JSONUtils.JSON_LINK); + + final JSONUtils.XposedJson xposedJson = new Gson().fromJson(originalJson, JSONUtils.XposedJson.class); + + List tabs = Stream.of(xposedJson.tabs) + .filter(value -> value.sdks.contains(Build.VERSION.SDK_INT)).toList(); + + for (XposedTab tab : tabs) { + tabsAdapter.addFragment(tab.name, BaseAdvancedInstaller.newInstance(tab)); + } + + newApkVersion = xposedJson.apk.version; + newApkLink = xposedJson.apk.link; + newApkChangelog = xposedJson.apk.changelog; + + return true; + } catch (Exception e) { + e.printStackTrace(); + Log.e(XposedApp.TAG, "AdvancedInstallerFragment -> " + e.getMessage()); + return false; + } + } + + @Override + protected void onPostExecute(Boolean result) { + super.onPostExecute(result); + + try { + tabsAdapter.notifyDataSetChanged(); + + if (newApkVersion == null) return; + + SharedPreferences prefs; + try { + prefs = EdDownloadActivity.this.getSharedPreferences(EdDownloadActivity.this.getPackageName() + "_preferences", MODE_PRIVATE); + + prefs.edit().putString("changelog", newApkChangelog).apply(); + } catch (NullPointerException ignored) { + } + + Integer a = BuildConfig.VERSION_CODE; + Integer b = Integer.valueOf(newApkVersion); + + if (a.compareTo(b) < 0) { + StatusInstallerFragment.setUpdate(newApkLink, newApkChangelog, EdDownloadActivity.this); + } + + } catch (Exception ignored) { + } + + } + } + + private class TabsAdapter extends FragmentPagerAdapter { + + private final ArrayList titles = new ArrayList<>(); + private final ArrayList listFragment = new ArrayList<>(); + + @SuppressWarnings("deprecation") + TabsAdapter(FragmentManager mgr) { + super(mgr); + addFragment(getString(R.string.tabInstall), new StatusInstallerFragment()); + } + + void addFragment(String title, Fragment fragment) { + titles.add(title); + listFragment.add(fragment); + } + + @Override + public int getCount() { + return listFragment.size(); + } + + @NonNull + @Override + public Fragment getItem(int position) { + return listFragment.get(position); + } + + @Override + public String getPageTitle(int position) { + return titles.get(position); + } + } +} diff --git a/app/src/main/java/org/meowcat/edxposed/manager/LogsActivity.java b/app/src/main/java/org/meowcat/edxposed/manager/LogsActivity.java new file mode 100644 index 000000000..d63fada58 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/LogsActivity.java @@ -0,0 +1,312 @@ +package org.meowcat.edxposed.manager; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.app.ProgressDialog; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.CheckBox; +import android.widget.HorizontalScrollView; +import android.widget.ScrollView; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBar; +import androidx.core.app.ActivityCompat; +import androidx.core.content.FileProvider; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.util.Calendar; +import java.util.Objects; + +public class LogsActivity extends BaseActivity { + private boolean errorLog = false; + private File mFileErrorLog = new File(XposedApp.BASE_DIR + "log/error.log"); + private File mFileErrorLogOld = new File( + XposedApp.BASE_DIR + "log/error.log.old"); + private File mFileErrorLogError = new File(XposedApp.BASE_DIR + "log/all.log"); + private File mFileErrorLogOldError = new File(XposedApp.BASE_DIR + "log/all.log.old"); + private TextView mTxtLog; + private ScrollView mSVLog; + private HorizontalScrollView mHSVLog; + private MenuItem mClickedMenuItem = null; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_logs); + setSupportActionBar(findViewById(R.id.toolbar)); + ActionBar bar = getSupportActionBar(); + if (bar != null) { + bar.setDisplayHomeAsUpEnabled(true); + } + mTxtLog = findViewById(R.id.txtLog); + mTxtLog.setTextIsSelectable(true); + mSVLog = findViewById(R.id.svLog); + mHSVLog = findViewById(R.id.hsvLog); + + if (!XposedApp.getPreferences().getBoolean("hide_logcat_warning", false)) { + @SuppressLint("InflateParams") final View dontShowAgainView = getLayoutInflater().inflate(R.layout.dialog_install_warning, null); + + TextView message = dontShowAgainView.findViewById(android.R.id.message); + message.setText(R.string.not_logcat); + + new MaterialAlertDialogBuilder(this) + .setTitle(R.string.install_warning_title) + .setView(dontShowAgainView) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + CheckBox checkBox = dontShowAgainView.findViewById(android.R.id.checkbox); + if (checkBox.isChecked()) + XposedApp.getPreferences().edit().putBoolean("hide_logcat_warning", true).apply(); + }) + .setCancelable(false) + .show(); + } + } + + @Override + public void onResume() { + super.onResume(); + reloadErrorLog(); + } + + @Override + public boolean onCreateOptionsMenu(@NonNull Menu menu) { + getMenuInflater().inflate(R.menu.menu_logs, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + mClickedMenuItem = item; + switch (item.getItemId()) { + case R.id.menu_logs: + item.setChecked(true); + errorLog = false; + reloadErrorLog(); + break; + case R.id.menu_logs_err: + item.setChecked(true); + errorLog = true; + reloadErrorLog(); + scrollDown(); + break; + case R.id.menu_scroll_top: + scrollTop(); + break; + case R.id.menu_scroll_down: + scrollDown(); + break; + case R.id.menu_refresh: + reloadErrorLog(); + return true; + case R.id.menu_send: + try { + send(); + } catch (NullPointerException ignored) { + } + return true; + case R.id.menu_save: + save(); + return true; + case R.id.menu_clear: + clear(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void scrollTop() { + mSVLog.post(() -> mSVLog.scrollTo(0, 0)); + mHSVLog.post(() -> mHSVLog.scrollTo(0, 0)); + } + + private void scrollDown() { + mSVLog.post(() -> mSVLog.scrollTo(0, mTxtLog.getHeight())); + mHSVLog.post(() -> mHSVLog.scrollTo(0, 0)); + } + + private void reloadErrorLog() { + new LogsReader().execute(errorLog ? mFileErrorLogError : mFileErrorLog); + mSVLog.post(() -> mSVLog.scrollTo(0, mTxtLog.getHeight())); + mHSVLog.post(() -> mHSVLog.scrollTo(0, 0)); + } + + private void clear() { + try { + new FileOutputStream(errorLog ? mFileErrorLogError : mFileErrorLog).close(); + (errorLog ? mFileErrorLogOldError : mFileErrorLogOld).delete(); + mTxtLog.setText(R.string.log_is_empty); + Toast.makeText(this, R.string.logs_cleared, + Toast.LENGTH_SHORT).show(); + reloadErrorLog(); + } catch (IOException e) { + Toast.makeText(this, getResources().getString(R.string.logs_clear_failed) + "n" + e.getMessage(), Toast.LENGTH_LONG).show(); + } + } + + private void send() { + Uri uri = FileProvider.getUriForFile(Objects.requireNonNull(this), "org.meowcat.edxposed.manager.fileprovider", errorLog ? mFileErrorLogError : mFileErrorLog); + Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_STREAM, uri); + sendIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + sendIntent.setType("application/html"); + startActivity(Intent.createChooser(sendIntent, getResources().getString(R.string.menuSend))); + } + + @Override + public void onRequestPermissionsResult(int requestCode, + @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, + grantResults); + if (requestCode == XposedApp.WRITE_EXTERNAL_PERMISSION) { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + if (mClickedMenuItem != null) { + new Handler().postDelayed(() -> onOptionsItemSelected(mClickedMenuItem), 500); + } + } else { + Toast.makeText(this, R.string.permissionNotGranted, Toast.LENGTH_LONG).show(); + } + } + } + + @SuppressLint("DefaultLocale") + private void save() { + if (ActivityCompat.checkSelfPermission(Objects.requireNonNull(this), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, XposedApp.WRITE_EXTERNAL_PERMISSION); + return; + } + + if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + Toast.makeText(this, R.string.sdcard_not_writable, Toast.LENGTH_LONG).show(); + return; + } + + Calendar now = Calendar.getInstance(); + String filename = String.format( + "EdXposed_Verbose_%04d%02d%02d_%02d%02d%02d.log", + now.get(Calendar.YEAR), now.get(Calendar.MONTH) + 1, + now.get(Calendar.DAY_OF_MONTH), now.get(Calendar.HOUR_OF_DAY), + now.get(Calendar.MINUTE), now.get(Calendar.SECOND)); + + File targetFile = new File(XposedApp.createFolder(), filename); + + try { + FileInputStream in = new FileInputStream(errorLog ? mFileErrorLogError : mFileErrorLog); + FileOutputStream out = new FileOutputStream(targetFile); + byte[] buffer = new byte[1024]; + int len; + while ((len = in.read(buffer)) > 0) { + out.write(buffer, 0, len); + } + in.close(); + out.close(); + + Toast.makeText(this, targetFile.toString(), + Toast.LENGTH_LONG).show(); + } catch (IOException e) { + Toast.makeText(this, getResources().getString(R.string.logs_save_failed) + "\n" + e.getMessage(), Toast.LENGTH_LONG).show(); + } + } + + @SuppressLint("StaticFieldLeak") + private class LogsReader extends AsyncTask { + + private static final int MAX_LOG_SIZE = 1000 * 1024; // 1000 KB + private ProgressDialog mProgressDialog; + + private long skipLargeFile(BufferedReader is, long length) throws IOException { + if (length < MAX_LOG_SIZE) + return 0; + + long skipped = length - MAX_LOG_SIZE; + long yetToSkip = skipped; + do { + yetToSkip -= is.skip(yetToSkip); + } while (yetToSkip > 0); + + int c; + do { + c = is.read(); + if (c == -1) + break; + skipped++; + } while (c != '\n'); + + return skipped; + + } + + @Override + protected void onPreExecute() { + mTxtLog.setText(""); + mProgressDialog = new ProgressDialog(LogsActivity.this); + mProgressDialog.setMessage(getString(R.string.loading)); + mProgressDialog.setProgress(0); + mProgressDialog.show(); + } + + @Override + protected String doInBackground(File... log) { + Thread.currentThread().setPriority(Thread.NORM_PRIORITY + 2); + + StringBuilder llog = new StringBuilder(15 * 10 * 1024); + + if (XposedApp.getPreferences().getBoolean( + "disable_verbose_log", false) && errorLog) { + llog.append(LogsActivity.this.getResources().getString(R.string.logs_verbose_disabled)); + return llog.toString(); + } + try { + File logfile = log[0]; + BufferedReader br; + br = new BufferedReader(new FileReader(logfile)); + long skipped = skipLargeFile(br, logfile.length()); + if (skipped > 0) { + llog.append(LogsActivity.this.getResources().getString(R.string.logs_too_long)); + llog.append("\n-----------------\n"); + } + + char[] temp = new char[1024]; + int read; + while ((read = br.read(temp)) > 0) { + llog.append(temp, 0, read); + } + br.close(); + } catch (IOException e) { + llog.append(LogsActivity.this.getResources().getString(R.string.logs_cannot_read)); + llog.append(e.getMessage()); + } + + return llog.toString(); + } + + @Override + protected void onPostExecute(String llog) { + mProgressDialog.dismiss(); + mTxtLog.setText(llog); + + if (llog.length() == 0) + mTxtLog.setText(R.string.log_is_empty); + } + + } +} diff --git a/app/src/main/java/org/meowcat/edxposed/manager/MainActivity.java b/app/src/main/java/org/meowcat/edxposed/manager/MainActivity.java new file mode 100644 index 000000000..d98a13291 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/MainActivity.java @@ -0,0 +1,179 @@ +package org.meowcat.edxposed.manager; + +import android.annotation.SuppressLint; +import android.content.Intent; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; + +import com.google.android.material.card.MaterialCardView; + +import org.meowcat.edxposed.manager.util.ModuleUtil; +import org.meowcat.edxposed.manager.util.RepoLoader; + +public class MainActivity extends BaseActivity implements RepoLoader.RepoListener, ModuleUtil.ModuleListener { + + private RepoLoader mRepoLoader; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + mRepoLoader = RepoLoader.getInstance(); + ModuleUtil.getInstance().addListener(this); + mRepoLoader.addListener(this, false); + findViewById(R.id.activity_main_modules).setOnClickListener(v -> { + Intent intent = new Intent(); + intent.setClass(getApplicationContext(), ModulesActivity.class); + startActivity(intent); + }); + findViewById(R.id.activity_main_downloads).setOnClickListener(v -> { + Intent intent = new Intent(); + intent.setClass(getApplicationContext(), DownloadActivity.class); + startActivity(intent); + }); + findViewById(R.id.activity_main_apps).setOnClickListener(v -> { + Intent intent = new Intent(); + intent.setClass(getApplicationContext(), BlackListActivity.class); + startActivity(intent); + }); + findViewById(R.id.activity_main_status).setOnClickListener(v -> { + Intent intent = new Intent(); + intent.setClass(getApplicationContext(), EdDownloadActivity.class); + startActivity(intent); + }); + findViewById(R.id.activity_main_settings).setOnClickListener(v -> { + Intent intent = new Intent(); + intent.setClass(getApplicationContext(), SettingsActivity.class); + startActivity(intent); + }); + findViewById(R.id.activity_main_logs).setOnClickListener(v -> { + Intent intent = new Intent(); + intent.setClass(getApplicationContext(), LogsActivity.class); + startActivity(intent); + }); + findViewById(R.id.activity_main_about).setOnClickListener(v -> { + Intent intent = new Intent(); + intent.setClass(getApplicationContext(), AboutActivity.class); + startActivity(intent); + }); + String installedXposedVersion; + try { + installedXposedVersion = XposedApp.getXposedProp().getVersion(); + } catch (NullPointerException e) { + installedXposedVersion = null; + } + MaterialCardView cardView = findViewById(R.id.activity_main_status); + TextView title = findViewById(R.id.activity_main_status_title); + ImageView icon = findViewById(R.id.activity_main_status_icon); + TextView details = findViewById(R.id.activity_main_status_summary); + if (installedXposedVersion != null) { + int installedXposedVersionInt = extractIntPart(installedXposedVersion); + if (installedXposedVersionInt == XposedApp.getXposedVersion()) { + String installedXposedVersionStr = installedXposedVersionInt + ".0"; + title.setText(R.string.Activated); + details.setText(installedXposedVersion.replace(installedXposedVersionStr + "-", "")); + cardView.setCardBackgroundColor(getResources().getColor(R.color.download_status_update_available)); + icon.setImageDrawable(getDrawable(R.drawable.ic_check_circle)); + } else { + title.setText(R.string.Inactivate); + details.setText(R.string.installed_lollipop_inactive); + cardView.setCardBackgroundColor(getResources().getColor(R.color.amber_500)); + icon.setImageDrawable(getDrawable(R.drawable.ic_warning)); + } + } else { + title.setText(R.string.Install); + details.setText(R.string.InstallDetail); + cardView.setCardBackgroundColor(getResources().getColor(R.color.colorPrimary)); + icon.setImageDrawable(getDrawable(R.drawable.ic_error)); + } + notifyDataSetChanged(); + } + + private int extractIntPart(String str) { + int result = 0, length = str.length(); + for (int offset = 0; offset < length; offset++) { + char c = str.charAt(offset); + if ('0' <= c && c <= '9') + result = result * 10 + (c - '0'); + else + break; + } + return result; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.menu_main, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + int id = item.getItemId(); + + //noinspection SimplifiableIfStatement + if (id == R.id.action_settings) { + return true; + } + + return super.onOptionsItemSelected(item); + } + + @SuppressLint("SetTextI18n") + private void notifyDataSetChanged() { + runOnUiThread(new Runnable() { + @Override + public void run() { + String frameworkUpdateVersion = mRepoLoader.getFrameworkUpdateVersion(); + boolean moduleUpdateAvailable = mRepoLoader.hasModuleUpdates(); + ModuleUtil.getInstance().getEnabledModules().size(); + TextView description = findViewById(R.id.activity_main_modules_summary); + description.setText(String.format(getString(R.string.ModulesDetail), ModuleUtil.getInstance().getEnabledModules().size())); + if (frameworkUpdateVersion != null) { + description = findViewById(R.id.activity_main_status_summary); + description.setText(String.format(getString(R.string.welcome_framework_update_available), frameworkUpdateVersion)); + } + description = findViewById(R.id.activity_main_download_summary); + if (moduleUpdateAvailable) { + description.setText(R.string.modules_updates_available); + } else { + description.setText(R.string.ModuleUptodate); + } + } + }); + } + + + @Override + public void onInstalledModulesReloaded(ModuleUtil moduleUtil) { + notifyDataSetChanged(); + } + + @Override + public void onSingleInstalledModuleReloaded(ModuleUtil moduleUtil, String packageName, ModuleUtil.InstalledModule module) { + notifyDataSetChanged(); + } + + @Override + public void onRepoReloaded(RepoLoader loader) { + notifyDataSetChanged(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + ModuleUtil.getInstance().removeListener(this); + mRepoLoader.removeListener(this); + } + +} diff --git a/app/src/main/java/org/meowcat/edxposed/manager/ModulesActivity.java b/app/src/main/java/org/meowcat/edxposed/manager/ModulesActivity.java new file mode 100644 index 000000000..04b598807 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/ModulesActivity.java @@ -0,0 +1,654 @@ +package org.meowcat.edxposed.manager; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.graphics.Color; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Filter; +import android.widget.ImageView; +import android.widget.Switch; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.view.menu.MenuBuilder; +import androidx.appcompat.view.menu.MenuPopupHelper; +import androidx.appcompat.widget.PopupMenu; +import androidx.appcompat.widget.SearchView; +import androidx.core.app.ActivityCompat; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +import org.meowcat.edxposed.manager.repo.Module; +import org.meowcat.edxposed.manager.repo.ModuleVersion; +import org.meowcat.edxposed.manager.repo.ReleaseType; +import org.meowcat.edxposed.manager.repo.RepoDb; +import org.meowcat.edxposed.manager.util.DownloadsUtil; +import org.meowcat.edxposed.manager.util.InstallApkUtil; +import org.meowcat.edxposed.manager.util.ModuleUtil; +import org.meowcat.edxposed.manager.util.NavUtil; +import org.meowcat.edxposed.manager.util.RepoLoader; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import static android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS; + +public class ModulesActivity extends BaseActivity implements ModuleUtil.ModuleListener { + + public static final String SETTINGS_CATEGORY = "de.robv.android.xposed.category.MODULE_SETTINGS"; + private int installedXposedVersion; + private ApplicationFilter filter; + private SearchView mSearchView; + private SearchView.OnQueryTextListener mSearchListener; + private PackageManager mPm; + private DateFormat dateformat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); + private ModuleUtil mModuleUtil; + private ModuleAdapter mAdapter = null; + private MenuItem mClickedMenuItem = null; + private RecyclerView mListView; + private SwipeRefreshLayout mSwipeRefreshLayout; + private Runnable reloadModules = new Runnable() { + public void run() { + String queryStr = mSearchView != null ? mSearchView.getQuery().toString() : ""; + Collection showList; + Collection fullList = mModuleUtil.getModules().values(); + if (queryStr.length() == 0) { + showList = fullList; + } else { + showList = new ArrayList<>(); + String filter = queryStr.toLowerCase(); + for (ModuleUtil.InstalledModule info : fullList) { + if (lowercaseContains(InstallApkUtil.getAppLabel(info.app, mPm), filter) + || lowercaseContains(info.packageName, filter)) { + showList.add(info); + } + } + } + mAdapter.addAll(showList); + mAdapter.notifyDataSetChanged(); + mModuleUtil.updateModulesList(false); + mSwipeRefreshLayout.setRefreshing(false); + } + }; + + private void filter(String constraint) { + filter.filter(constraint); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_modules); + setSupportActionBar(findViewById(R.id.toolbar)); + ActionBar bar = getSupportActionBar(); + if (bar != null) { + bar.setDisplayHomeAsUpEnabled(true); + } + filter = new ApplicationFilter(); + mModuleUtil = ModuleUtil.getInstance(); + mPm = getPackageManager(); + installedXposedVersion = XposedApp.getXposedVersion(); + if (Build.VERSION.SDK_INT >= 21) { + if (installedXposedVersion <= 0) { + addHeader(); + } + } else { + //if (StatusInstallerFragment.DISABLE_FILE.exists()) installedXposedVersion = -1; + if (installedXposedVersion <= 0) { + addHeader(); + } + } + mAdapter = new ModuleAdapter(); + mModuleUtil.addListener(this); + mListView = findViewById(R.id.recyclerView); + mListView.setAdapter(mAdapter); + mListView.setLayoutManager(new LinearLayoutManager(this)); + DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(mListView.getContext(), + DividerItemDecoration.VERTICAL); + mListView.addItemDecoration(dividerItemDecoration); + mSwipeRefreshLayout = findViewById(R.id.swipeRefreshLayout); + mSwipeRefreshLayout.setOnRefreshListener(() -> reloadModules.run()); + reloadModules.run(); + mSearchListener = new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + filter(query); + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + filter(newText); + return false; + } + }; + + } + + private void addHeader() { + //View notActiveNote = getLayoutInflater().inflate(R.layout.xposed_not_active_note, mListView, false); + //notActiveNote.setTag(NOT_ACTIVE_NOTE_TAG); + //mListView.addHeaderView(notActiveNote); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_modules, menu); + mSearchView = (SearchView) menu.findItem(R.id.app_search).getActionView(); + mSearchView.setOnQueryTextListener(mSearchListener); + return super.onCreateOptionsMenu(menu); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, + grantResults); + if (requestCode == XposedApp.WRITE_EXTERNAL_PERMISSION) { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + if (mClickedMenuItem != null) { + new Handler().postDelayed(() -> onOptionsItemSelected(mClickedMenuItem), 500); + } + } else { + Toast.makeText(this, R.string.permissionNotGranted, Toast.LENGTH_LONG).show(); + } + } + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + File enabledModulesPath = new File(XposedApp.createFolder(), "enabled_modules.list"); + File installedModulesPath = new File(XposedApp.createFolder(), "installed_modules.list"); + File listModules = new File(XposedApp.ENABLED_MODULES_LIST_FILE); + + mClickedMenuItem = item; + + if (checkPermissions()) + return false; + + switch (item.getItemId()) { + case R.id.export_enabled_modules: + if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + return false; + } + + if (ModuleUtil.getInstance().getEnabledModules().isEmpty()) { + Toast.makeText(this, getString(R.string.no_enabled_modules), Toast.LENGTH_SHORT).show(); + return false; + } + + try { + XposedApp.createFolder(); + + FileInputStream in = new FileInputStream(listModules); + FileOutputStream out = new FileOutputStream(enabledModulesPath); + + byte[] buffer = new byte[1024]; + int len; + while ((len = in.read(buffer)) > 0) { + out.write(buffer, 0, len); + } + in.close(); + out.close(); + } catch (IOException e) { + Toast.makeText(this, getResources().getString(R.string.logs_save_failed) + "\n" + e.getMessage(), Toast.LENGTH_LONG).show(); + return false; + } + + Toast.makeText(this, enabledModulesPath.toString(), Toast.LENGTH_LONG).show(); + return true; + case R.id.export_installed_modules: + if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + Toast.makeText(this, R.string.sdcard_not_writable, Toast.LENGTH_LONG).show(); + return false; + } + Map installedModules = ModuleUtil.getInstance().getModules(); + + if (installedModules.isEmpty()) { + Toast.makeText(this, getString(R.string.no_installed_modules), Toast.LENGTH_SHORT).show(); + return false; + } + + try { + XposedApp.createFolder(); + + FileWriter fw = new FileWriter(installedModulesPath); + BufferedWriter bw = new BufferedWriter(fw); + PrintWriter fileOut = new PrintWriter(bw); + + Set keys = installedModules.keySet(); + for (String key1 : keys) { + fileOut.println(key1); + } + + fileOut.close(); + } catch (IOException e) { + Toast.makeText(this, getResources().getString(R.string.logs_save_failed) + "\n" + e.getMessage(), Toast.LENGTH_LONG).show(); + return false; + } + + Toast.makeText(this, installedModulesPath.toString(), Toast.LENGTH_LONG).show(); + return true; + case R.id.import_installed_modules: + return importModules(installedModulesPath); + case R.id.import_enabled_modules: + return importModules(enabledModulesPath); + } + return super.onOptionsItemSelected(item); + } + + private boolean checkPermissions() { + if (ActivityCompat.checkSelfPermission(Objects.requireNonNull(this), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, XposedApp.WRITE_EXTERNAL_PERMISSION); + } + return true; + } + return false; + } + + private boolean importModules(File path) { + if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + Toast.makeText(this, R.string.sdcard_not_writable, Toast.LENGTH_LONG).show(); + return false; + } + InputStream ips = null; + RepoLoader repoLoader = RepoLoader.getInstance(); + List list = new ArrayList<>(); + if (!path.exists()) { + Toast.makeText(this, getString(R.string.no_backup_found), + Toast.LENGTH_LONG).show(); + return false; + } + try { + ips = new FileInputStream(path); + } catch (FileNotFoundException e) { + Log.e(XposedApp.TAG, "ModulesFragment -> " + e.getMessage()); + } + + if (path.length() == 0) { + Toast.makeText(this, R.string.file_is_empty, + Toast.LENGTH_LONG).show(); + return false; + } + + try { + assert ips != null; + InputStreamReader ipsr = new InputStreamReader(ips); + BufferedReader br = new BufferedReader(ipsr); + String line; + while ((line = br.readLine()) != null) { + Module m = repoLoader.getModule(line); + + if (m == null) { + Toast.makeText(this, getString(R.string.download_details_not_found, + line), Toast.LENGTH_SHORT).show(); + } else { + list.add(m); + } + } + br.close(); + } catch (ActivityNotFoundException | IOException e) { + Toast.makeText(this, e.toString(), Toast.LENGTH_SHORT).show(); + } + + for (final Module m : list) { + ModuleVersion mv = null; + for (int i = 0; i < m.versions.size(); i++) { + ModuleVersion mvTemp = m.versions.get(i); + + if (mvTemp.relType == ReleaseType.STABLE) { + mv = mvTemp; + break; + } + } + + if (mv != null) { + DownloadsUtil.addModule(this, m.name, mv.downloadLink, false, (context, info) -> new InstallApkUtil(this, info).execute()); + } + } + + ModuleUtil.getInstance().reloadInstalledModules(); + + return true; + } + + @Override + public void onDestroy() { + super.onDestroy(); + mModuleUtil.removeListener(this); + mListView.setAdapter(null); + mAdapter = null; + } + + @Override + public void onSingleInstalledModuleReloaded(ModuleUtil moduleUtil, String packageName, ModuleUtil.InstalledModule module) { + mModuleUtil.updateModulesList(false); + runOnUiThread(reloadModules); + } + + @Override + public void onInstalledModulesReloaded(ModuleUtil moduleUtil) { + mModuleUtil.updateModulesList(false); + runOnUiThread(reloadModules); + } + + @SuppressLint("RestrictedApi") + private void showMenu(@NonNull Context context, + @NonNull View anchor, + @NonNull ApplicationInfo info) { + PopupMenu appMenu = new PopupMenu(context, anchor); + appMenu.inflate(R.menu.context_menu_modules); + ModuleUtil.InstalledModule installedModule = ModuleUtil.getInstance().getModule(info.packageName); + if (installedModule == null) { + return; + } + try { + String support = RepoDb + .getModuleSupport(installedModule.packageName); + if (NavUtil.parseURL(support) == null) + appMenu.getMenu().removeItem(R.id.menu_support); + } catch (RepoDb.RowNotFoundException e) { + appMenu.getMenu().removeItem(R.id.menu_download_updates); + appMenu.getMenu().removeItem(R.id.menu_support); + } + appMenu.setOnMenuItemClickListener(menuItem -> { + ModuleUtil.InstalledModule module = ModuleUtil.getInstance().getModule(info.packageName); + if (module == null) { + return false; + } + switch (menuItem.getItemId()) { + case R.id.menu_launch: + String packageName = module.packageName; + if (packageName == null) { + return false; + } + Intent launchIntent = getSettingsIntent(packageName); + if (launchIntent != null) { + startActivity(launchIntent); + } else { + Toast.makeText(this, getString(R.string.module_no_ui), Toast.LENGTH_LONG).show(); + } + return true; + + case R.id.menu_download_updates: + Intent detailsIntent = new Intent(this, DownloadDetailsActivity.class); + detailsIntent.setData(Uri.fromParts("package", module.packageName, null)); + startActivity(detailsIntent); + return true; + + case R.id.menu_support: + NavUtil.startURL(this, Uri.parse(RepoDb.getModuleSupport(module.packageName))); + return true; + + case R.id.menu_app_store: + Uri uri = Uri.parse("market://details?id=" + module.packageName); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + try { + startActivity(intent); + } catch (Exception ex) { + ex.printStackTrace(); + } + return true; + + case R.id.menu_app_info: + startActivity(new Intent(ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", module.packageName, null))); + return true; + + case R.id.menu_uninstall: + startActivity(new Intent(Intent.ACTION_UNINSTALL_PACKAGE, Uri.fromParts("package", module.packageName, null))); + return true; + } + return true; + }); + MenuPopupHelper menuHelper = new MenuPopupHelper(context, (MenuBuilder) appMenu.getMenu(), anchor); + menuHelper.setForceShowIcon(true); + menuHelper.show(); + } + + private Intent getSettingsIntent(String packageName) { + // taken from + // ApplicationPackageManager.getLaunchIntentForPackage(String) + // first looks for an Xposed-specific category, falls back to + // getLaunchIntentForPackage + PackageManager pm = getPackageManager(); + + Intent intentToResolve = new Intent(Intent.ACTION_MAIN); + intentToResolve.addCategory(SETTINGS_CATEGORY); + intentToResolve.setPackage(packageName); + List ris = pm.queryIntentActivities(intentToResolve, 0); + + if (ris.size() <= 0) { + return pm.getLaunchIntentForPackage(packageName); + } + + Intent intent = new Intent(intentToResolve); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setClassName(ris.get(0).activityInfo.packageName, ris.get(0).activityInfo.name); + return intent; + } + + public void onItemClick(View view) { + if (getFragmentManager() != null) { + try { + showMenu(this, view, Objects.requireNonNull(this).getPackageManager().getApplicationInfo((String) view.getTag(), 0)); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + String packageName = (String) view.getTag(); + if (packageName == null) + return; + + Intent launchIntent = getSettingsIntent(packageName); + if (launchIntent != null) { + startActivity(launchIntent); + } else { + Toast.makeText(this, getString(R.string.module_no_ui), Toast.LENGTH_LONG).show(); + } + } + } else { + String packageName = (String) view.getTag(); + if (packageName == null) { + return; + } + Intent launchIntent = getSettingsIntent(packageName); + if (launchIntent != null) { + startActivity(launchIntent); + } else { + Toast.makeText(this, getString(R.string.module_no_ui), Toast.LENGTH_LONG).show(); + } + } + } + + private boolean lowercaseContains(String s, CharSequence filter) { + return !TextUtils.isEmpty(s) && s.toLowerCase().contains(filter); + } + + private class ModuleAdapter extends RecyclerView.Adapter { + Collection items; + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_module, parent, false); + return new ViewHolder(v); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + //View view = holder.itemView; + ModuleUtil.InstalledModule item = (ModuleUtil.InstalledModule) items.toArray()[position]; + holder.itemView.setOnClickListener(v -> ModulesActivity.this.onItemClick(holder.itemView)); + holder.itemView.setTag(item.packageName); + + holder.appName.setText(item.getAppName()); + + TextView version = holder.appVersion; + version.setText(Objects.requireNonNull(item).versionName); + version.setSelected(true); + version.setTextColor(Color.parseColor("#808080")); + + TextView packageTv = holder.appPackage; + packageTv.setText(item.packageName); + packageTv.setSelected(true); + + TextView installTimeTv = holder.appInstallTime; + installTimeTv.setText(dateformat.format(new Date(item.installTime))); + installTimeTv.setSelected(true); + + TextView updateTv = holder.appUpdateTime; + updateTv.setText(dateformat.format(new Date(item.updateTime))); + updateTv.setSelected(true); + + holder.appIcon.setImageDrawable(item.getIcon()); + + TextView descriptionText = holder.appDescription; + if (!item.getDescription().isEmpty()) { + descriptionText.setText(item.getDescription()); + //descriptionText.setTextColor(ThemeUtil.getThemeColor(this, android.R.attr.textColorSecondary)); + } else { + descriptionText.setText(getString(R.string.module_empty_description)); + descriptionText.setTextColor(getResources().getColor(R.color.warning)); + } + + Switch mSwitch = holder.mSwitch; + mSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { + String packageName = item.packageName; + boolean changed = mModuleUtil.isModuleEnabled(packageName) ^ isChecked; + if (changed) { + mModuleUtil.setModuleEnabled(packageName, isChecked); + mModuleUtil.updateModulesList(true); + } + }); + mSwitch.setChecked(mModuleUtil.isModuleEnabled(item.packageName)); + TextView warningText = holder.warningText; + + if (item.minVersion == 0) { + if (!XposedApp.getPreferences().getBoolean("skip_xposedminversion_check", false)) { + mSwitch.setEnabled(false); + } + warningText.setText(getString(R.string.no_min_version_specified)); + warningText.setVisibility(View.VISIBLE); + } else if (installedXposedVersion > 0 && item.minVersion > installedXposedVersion) { + if (!XposedApp.getPreferences().getBoolean("skip_xposedminversion_check", false)) { + mSwitch.setEnabled(false); + } + warningText.setText(String.format(getString(R.string.warning_xposed_min_version), item.minVersion)); + warningText.setVisibility(View.VISIBLE); + } else if (item.minVersion < ModuleUtil.MIN_MODULE_VERSION) { + if (!XposedApp.getPreferences().getBoolean("skip_xposedminversion_check", false)) { + mSwitch.setEnabled(false); + } + warningText.setText(String.format(getString(R.string.warning_min_version_too_low), item.minVersion, ModuleUtil.MIN_MODULE_VERSION)); + warningText.setVisibility(View.VISIBLE); + } else if (item.isInstalledOnExternalStorage()) { + if (!XposedApp.getPreferences().getBoolean("skip_xposedminversion_check", false)) { + mSwitch.setEnabled(false); + } + warningText.setText(getString(R.string.warning_installed_on_external_storage)); + warningText.setVisibility(View.VISIBLE); + } else if (installedXposedVersion == 0 || (installedXposedVersion == -1)) { + if (!XposedApp.getPreferences().getBoolean("skip_xposedminversion_check", false)) { + mSwitch.setEnabled(false); + } + warningText.setText(getString(R.string.not_installed_no_lollipop)); + warningText.setVisibility(View.VISIBLE); + } else { + mSwitch.setEnabled(true); + warningText.setVisibility(View.GONE); + } + } + + void addAll(Collection items) { + this.items = items; + notifyDataSetChanged(); + } + + @Override + public int getItemCount() { + if (items != null) { + return items.size(); + } else { + return 0; + } + } + + class ViewHolder extends RecyclerView.ViewHolder { + ImageView appIcon; + TextView appName; + TextView appPackage; + TextView appDescription; + TextView appVersion; + TextView appInstallTime; + TextView appUpdateTime; + TextView warningText; + Switch mSwitch; + + ViewHolder(View itemView) { + super(itemView); + appIcon = itemView.findViewById(R.id.icon); + appName = itemView.findViewById(R.id.title); + appDescription = itemView.findViewById(R.id.description); + appPackage = itemView.findViewById(R.id.package_name); + appVersion = itemView.findViewById(R.id.version_name); + appInstallTime = itemView.findViewById(R.id.tvInstallTime); + appUpdateTime = itemView.findViewById(R.id.tvUpdateTime); + warningText = itemView.findViewById(R.id.warning); + mSwitch = itemView.findViewById(R.id.checkbox); + } + } + } + + class ApplicationFilter extends Filter { + + @Override + protected FilterResults performFiltering(CharSequence constraint) { + runOnUiThread(reloadModules); + return null; + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults results) { + runOnUiThread(reloadModules); + } + } +} diff --git a/app/src/main/java/org/meowcat/edxposed/manager/SettingsActivity.java b/app/src/main/java/org/meowcat/edxposed/manager/SettingsActivity.java new file mode 100644 index 000000000..cefb46c64 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/SettingsActivity.java @@ -0,0 +1,371 @@ +package org.meowcat.edxposed.manager; + +import android.annotation.SuppressLint; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.os.FileUtils; +import android.widget.Toast; + +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatDelegate; +import androidx.preference.Preference; +import androidx.preference.SwitchPreference; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.takisoft.preferencex.PreferenceFragmentCompat; +import com.topjohnwu.superuser.Shell; + +import org.meowcat.edxposed.manager.util.RepoLoader; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Objects; + +public class SettingsActivity extends BaseActivity { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_settings); + setSupportActionBar(findViewById(R.id.toolbar)); + ActionBar bar = getSupportActionBar(); + if (bar != null) { + bar.setDisplayHomeAsUpEnabled(true); + } + + if (savedInstanceState == null) { + getSupportFragmentManager().beginTransaction() + .add(R.id.container, new SettingsFragment()).commit(); + } + + } + + + @SuppressWarnings({"ResultOfMethodCallIgnored", "deprecation"}) + public static class SettingsFragment extends PreferenceFragmentCompat implements Preference.OnPreferenceClickListener, SharedPreferences.OnSharedPreferenceChangeListener { + static final File mDisableResourcesFlag = new File(XposedApp.BASE_DIR + "conf/disable_resources"); + static final File mDynamicModulesFlag = new File(XposedApp.BASE_DIR + "conf/dynamicmodules"); + static final File mWhiteListModeFlag = new File(XposedApp.BASE_DIR + "conf/usewhitelist"); + static final File mBlackWhiteListModeFlag = new File(XposedApp.BASE_DIR + "conf/blackwhitelist"); + static final File mDeoptBootFlag = new File(XposedApp.BASE_DIR + "conf/deoptbootimage"); + static final File mDisableVerboseLogsFlag = new File(XposedApp.BASE_DIR + "conf/disable_verbose_log"); + static final File mDisableModulesLogsFlag = new File(XposedApp.BASE_DIR + "conf/disable_modules_log"); + static final File mVerboseLogProcessID = new File(XposedApp.BASE_DIR + "log/all.pid"); + static final File mModulesLogProcessID = new File(XposedApp.BASE_DIR + "log/error.pid"); + + private Preference stopVerboseLog; + private Preference stopLog; + + public SettingsFragment() { + } + + @SuppressWarnings("SameParameterValue") + @SuppressLint({"WorldReadableFiles", "WorldWriteableFiles"}) + static void setFilePermissionsFromMode(String name, int mode) { + int perms = FileUtils.S_IRUSR | FileUtils.S_IWUSR + | FileUtils.S_IRGRP | FileUtils.S_IWGRP; + if ((mode & MODE_WORLD_READABLE) != 0) { + perms |= FileUtils.S_IROTH; + } + if ((mode & MODE_WORLD_WRITEABLE) != 0) { + perms |= FileUtils.S_IWOTH; + } + FileUtils.setPermissions(name, perms, -1, -1); + } + + @SuppressLint({"ObsoleteSdkInt", "WorldReadableFiles"}) + @Override + public void onCreatePreferencesFix(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.prefs); + + stopVerboseLog = findPreference("stop_verbose_log"); + stopLog = findPreference("stop_log"); + + //noinspection ConstantConditions + findPreference("release_type_global").setOnPreferenceChangeListener((preference, newValue) -> { + RepoLoader.getInstance().setReleaseTypeGlobal((String) newValue); + return true; + }); + + SwitchPreference prefWhiteListMode = findPreference("white_list_switch"); + Objects.requireNonNull(prefWhiteListMode).setChecked(mWhiteListModeFlag.exists()); + prefWhiteListMode.setOnPreferenceChangeListener((preference, newValue) -> { + boolean enabled = (Boolean) newValue; + if (enabled) { + FileOutputStream fos = null; + try { + fos = new FileOutputStream(mWhiteListModeFlag.getPath()); + setFilePermissionsFromMode(mWhiteListModeFlag.getPath(), MODE_WORLD_READABLE); + } catch (FileNotFoundException e) { + Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_SHORT).show(); + } finally { + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_SHORT).show(); + try { + mWhiteListModeFlag.createNewFile(); + } catch (IOException e1) { + Toast.makeText(getActivity(), e1.getMessage(), Toast.LENGTH_SHORT).show(); + } + } + } + } + } else { + mWhiteListModeFlag.delete(); + } + return (enabled == mWhiteListModeFlag.exists()); + }); + + SwitchPreference prefVerboseLogs = findPreference("disable_verbose_log"); + Objects.requireNonNull(prefVerboseLogs).setChecked(mDisableVerboseLogsFlag.exists()); + prefVerboseLogs.setOnPreferenceChangeListener((preference, newValue) -> { + boolean enabled = (Boolean) newValue; + if (enabled) { + FileOutputStream fos = null; + try { + fos = new FileOutputStream(mDisableVerboseLogsFlag.getPath()); + setFilePermissionsFromMode(mDisableVerboseLogsFlag.getPath(), MODE_WORLD_READABLE); + } catch (FileNotFoundException e) { + Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_SHORT).show(); + } finally { + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_SHORT).show(); + try { + mDisableVerboseLogsFlag.createNewFile(); + } catch (IOException e1) { + Toast.makeText(getActivity(), e1.getMessage(), Toast.LENGTH_SHORT).show(); + } + } + } + } + } else { + mDisableVerboseLogsFlag.delete(); + } + return (enabled == mDisableVerboseLogsFlag.exists()); + }); + + SwitchPreference prefModulesLogs = findPreference("disable_modules_log"); + Objects.requireNonNull(prefModulesLogs).setChecked(mDisableModulesLogsFlag.exists()); + prefModulesLogs.setOnPreferenceChangeListener((preference, newValue) -> { + boolean enabled = (Boolean) newValue; + if (enabled) { + FileOutputStream fos = null; + try { + fos = new FileOutputStream(mDisableModulesLogsFlag.getPath()); + setFilePermissionsFromMode(mDisableModulesLogsFlag.getPath(), MODE_WORLD_READABLE); + } catch (FileNotFoundException e) { + Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_SHORT).show(); + } finally { + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_SHORT).show(); + try { + mDisableModulesLogsFlag.createNewFile(); + } catch (IOException e1) { + Toast.makeText(getActivity(), e1.getMessage(), Toast.LENGTH_SHORT).show(); + } + } + } + } + } else { + mDisableModulesLogsFlag.delete(); + } + return (enabled == mDisableModulesLogsFlag.exists()); + }); + + SwitchPreference prefBlackWhiteListMode = findPreference("black_white_list_switch"); + Objects.requireNonNull(prefBlackWhiteListMode).setChecked(mBlackWhiteListModeFlag.exists()); + prefBlackWhiteListMode.setOnPreferenceChangeListener((preference, newValue) -> { + boolean enabled = (Boolean) newValue; + if (enabled) { + FileOutputStream fos = null; + try { + fos = new FileOutputStream(mBlackWhiteListModeFlag.getPath()); + setFilePermissionsFromMode(mBlackWhiteListModeFlag.getPath(), MODE_WORLD_READABLE); + } catch (FileNotFoundException e) { + Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_SHORT).show(); + } finally { + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_SHORT).show(); + try { + mBlackWhiteListModeFlag.createNewFile(); + } catch (IOException e1) { + Toast.makeText(getActivity(), e1.getMessage(), Toast.LENGTH_SHORT).show(); + } + } + } + } + } else { + mBlackWhiteListModeFlag.delete(); + } + return (enabled == mBlackWhiteListModeFlag.exists()); + }); + + SwitchPreference prefEnableDeopt = findPreference("enable_boot_image_deopt"); + Objects.requireNonNull(prefEnableDeopt).setChecked(mDeoptBootFlag.exists()); + prefEnableDeopt.setOnPreferenceChangeListener((preference, newValue) -> { + boolean enabled = (Boolean) newValue; + if (enabled) { + FileOutputStream fos = null; + try { + fos = new FileOutputStream(mDeoptBootFlag.getPath()); + setFilePermissionsFromMode(mDeoptBootFlag.getPath(), MODE_WORLD_READABLE); + } catch (FileNotFoundException e) { + Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_SHORT).show(); + } finally { + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_SHORT).show(); + try { + mDeoptBootFlag.createNewFile(); + } catch (IOException e1) { + Toast.makeText(getActivity(), e1.getMessage(), Toast.LENGTH_SHORT).show(); + } + } + } + } + } else { + mDeoptBootFlag.delete(); + } + return (enabled == mDeoptBootFlag.exists()); + }); + + SwitchPreference prefDynamicResources = findPreference("is_dynamic_modules"); + Objects.requireNonNull(prefDynamicResources).setChecked(mDynamicModulesFlag.exists()); + prefDynamicResources.setOnPreferenceChangeListener((preference, newValue) -> { + boolean enabled = (Boolean) newValue; + if (enabled) { + FileOutputStream fos = null; + try { + fos = new FileOutputStream(mDynamicModulesFlag.getPath()); + setFilePermissionsFromMode(mDynamicModulesFlag.getPath(), MODE_WORLD_READABLE); + } catch (FileNotFoundException e) { + Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_SHORT).show(); + } finally { + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_SHORT).show(); + try { + mDynamicModulesFlag.createNewFile(); + } catch (IOException e1) { + Toast.makeText(getActivity(), e1.getMessage(), Toast.LENGTH_SHORT).show(); + } + } + } + } + } else { + mDynamicModulesFlag.delete(); + } + return (enabled == mDynamicModulesFlag.exists()); + }); + + SwitchPreference prefDisableResources = findPreference("disable_resources"); + Objects.requireNonNull(prefDisableResources).setChecked(mDisableResourcesFlag.exists()); + prefDisableResources.setOnPreferenceChangeListener((preference, newValue) -> { + boolean enabled = (Boolean) newValue; + if (enabled) { + FileOutputStream fos = null; + try { + fos = new FileOutputStream(mDisableResourcesFlag.getPath()); + setFilePermissionsFromMode(mDisableResourcesFlag.getPath(), MODE_WORLD_READABLE); + } catch (FileNotFoundException e) { + Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_SHORT).show(); + } finally { + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_SHORT).show(); + try { + mDisableResourcesFlag.createNewFile(); + } catch (IOException e1) { + Toast.makeText(getActivity(), e1.getMessage(), Toast.LENGTH_SHORT).show(); + } + } + } + } + } else { + mDisableResourcesFlag.delete(); + } + return (enabled == mDisableResourcesFlag.exists()); + }); + + } + + @Override + public void onResume() { + super.onResume(); + + getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); + } + + @Override + public void onPause() { + super.onPause(); + + getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (key.contains("theme") || key.equals("ignore_chinese")) { + AppCompatDelegate.setDefaultNightMode(XposedApp.getPreferences().getInt("theme", 0)); + Objects.requireNonNull(getActivity()).recreate(); + } + } + + @Override + public boolean onPreferenceClick(Preference preference) { + SettingsActivity act = (SettingsActivity) getActivity(); + if (act == null) + return false; + + if (preference.getKey().equals(stopVerboseLog.getKey())) { + new Runnable() { + @Override + public void run() { + areYouSure(R.string.stop_verbose_log_summary, (dialog, which) -> { + + Shell.su("kill $(cat " + mVerboseLogProcessID.getAbsolutePath() + ")").exec(); + + }); + } + }; + } else if (preference.getKey().equals(stopLog.getKey())) { + new Runnable() { + @Override + public void run() { + areYouSure(R.string.stop_log_summary, (dialog, which) -> Shell.su("kill $(cat " + mModulesLogProcessID.getAbsolutePath() + ")").exec()); + } + }; + } + return true; + } + + private void areYouSure(int contentTextId, DialogInterface.OnClickListener listener) { + new MaterialAlertDialogBuilder(Objects.requireNonNull(getActivity())).setTitle(R.string.areyousure) + .setMessage(contentTextId) + .setPositiveButton(android.R.string.yes, listener) + .setNegativeButton(android.R.string.no, null) + .show(); + } + + } +} diff --git a/app/src/main/java/org/meowcat/edxposed/manager/StatusInstallerFragment.java b/app/src/main/java/org/meowcat/edxposed/manager/StatusInstallerFragment.java new file mode 100644 index 000000000..c7af62647 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/StatusInstallerFragment.java @@ -0,0 +1,327 @@ +package org.meowcat.edxposed.manager; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.FileUtils; +import android.text.Html; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Objects; + +@SuppressLint("StaticFieldLeak") +public class StatusInstallerFragment extends Fragment { + + public static final File DISABLE_FILE = new File(XposedApp.BASE_DIR + "conf/disabled"); + private static Activity sActivity; + private static String mUpdateLink; + private static View mUpdateView; + private static View mUpdateButton; + + static void setUpdate(final String link, final String changelog, Context mContext) { + mUpdateLink = link; + + mUpdateView.setVisibility(View.VISIBLE); + mUpdateButton.setVisibility(View.VISIBLE); + mUpdateButton.setOnClickListener(v -> new MaterialAlertDialogBuilder(sActivity) + .setTitle(R.string.changes) + .setMessage(Html.fromHtml(changelog)) + .setPositiveButton(R.string.update, (dialog, which) -> update(mContext)) + .setNegativeButton(R.string.later, null).show()); + } + + private static void update(Context mContext) { + Uri uri = Uri.parse(mUpdateLink); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + mContext.startActivity(intent); + } + + private static String getCompleteArch() { + String info = ""; + + try { + FileReader fr = new FileReader("/proc/cpuinfo"); + BufferedReader br = new BufferedReader(fr); + String text; + while ((text = br.readLine()) != null) { + if (!text.startsWith("processor")) break; + } + br.close(); + String[] array = text != null ? text.split(":\\s+", 2) : new String[0]; + if (array.length >= 2) { + info += array[1] + " "; + } + } catch (IOException ignored) { + } + + info += Build.SUPPORTED_ABIS[0]; + return info + " (" + getArch() + ")"; + } + + private static String getArch() { + if (Build.CPU_ABI.equals("arm64-v8a")) { + return "arm64"; + } else if (Build.CPU_ABI.equals("x86_64")) { + return "x86_64"; + } else if (Build.CPU_ABI.equals("mips64")) { + return "mips64"; + } else if (Build.CPU_ABI.startsWith("x86") || Build.CPU_ABI2.startsWith("x86")) { + return "x86"; + } else if (Build.CPU_ABI.startsWith("mips")) { + return "mips"; + } else if (Build.CPU_ABI.startsWith("armeabi-v5") || Build.CPU_ABI.startsWith("armeabi-v6")) { + return "armv5"; + } else { + return "arm"; + } + } + + @SuppressWarnings("SameParameterValue") + @SuppressLint({"WorldReadableFiles", "WorldWriteableFiles"}) + private static void setFilePermissionsFromMode(String name, int mode) { + int perms = FileUtils.S_IRUSR | FileUtils.S_IWUSR + | FileUtils.S_IRGRP | FileUtils.S_IWGRP; + if ((mode & Context.MODE_WORLD_READABLE) != 0) { + perms |= FileUtils.S_IROTH; + } + if ((mode & Context.MODE_WORLD_WRITEABLE) != 0) { + perms |= FileUtils.S_IWOTH; + } + FileUtils.setPermissions(name, perms, -1, -1); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + sActivity = getActivity(); + } + + @SuppressLint("WorldReadableFiles") + @SuppressWarnings("ResultOfMethodCallIgnored") + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.status_installer, container, false); + + mUpdateView = v.findViewById(R.id.updateView); + mUpdateButton = v.findViewById(R.id.click_to_update); + + + String installedXposedVersion; + try { + installedXposedVersion = XposedApp.getXposedProp().getVersion(); + } catch (NullPointerException e) { + installedXposedVersion = null; + } + + TextView api = v.findViewById(R.id.api); + TextView framework = v.findViewById(R.id.framework); + TextView manager = v.findViewById(R.id.manager); + TextView androidSdk = v.findViewById(R.id.android_version); + TextView manufacturer = v.findViewById(R.id.ic_manufacturer); + TextView cpu = v.findViewById(R.id.cpu); + + String mAppVer = "v" + BuildConfig.VERSION_NAME + " (" + BuildConfig.VERSION_CODE + ")"; + manager.setText(mAppVer); + if (installedXposedVersion != null) { + int installedXposedVersionInt = extractIntPart(installedXposedVersion); + String installedXposedVersionStr = installedXposedVersionInt + ".0"; + api.setText(installedXposedVersionStr); + framework.setText(installedXposedVersion.replace(installedXposedVersionStr + "-", "")); + } + + androidSdk.setText(getString(R.string.android_sdk, getAndroidVersion(), Build.VERSION.RELEASE, Build.VERSION.SDK_INT)); + manufacturer.setText(getUIFramework()); + cpu.setText(getCompleteArch()); + + determineVerifiedBootState(v); + + refreshKnownIssue(); + return v; + } + + private void determineVerifiedBootState(View v) { + try { + @SuppressLint("PrivateApi") Class c = Class.forName("android.os.SystemProperties"); + Method m = c.getDeclaredMethod("get", String.class, String.class); + m.setAccessible(true); + + String propSystemVerified = (String) m.invoke(null, "partition.system.verified", "0"); + String propState = (String) m.invoke(null, "ro.boot.verifiedbootstate", ""); + File fileDmVerityModule = new File("/sys/module/dm_verity"); + + boolean verified = !propSystemVerified.equals("0"); + boolean detected = !propState.isEmpty() || fileDmVerityModule.exists(); + + TextView tv = v.findViewById(R.id.dmverity); + if (verified) { + tv.setText(R.string.verified_boot_active); + tv.setTextColor(getResources().getColor(R.color.warning)); + } else if (detected) { + tv.setText(R.string.verified_boot_deactivated); + v.findViewById(R.id.dmverity_explanation).setVisibility(View.GONE); + } else { + tv.setText(R.string.verified_boot_none); + tv.setTextColor(getResources().getColor(R.color.warning)); + v.findViewById(R.id.dmverity_explanation).setVisibility(View.GONE); + } + } catch (Exception e) { + Log.e(XposedApp.TAG, "Could not detect Verified Boot state", e); + } + } + + @SuppressWarnings("SameParameterValue") + private boolean checkAppInstalled(Context context, String pkgName) { + if (pkgName == null || pkgName.isEmpty()) { + return false; + } + final PackageManager packageManager = context.getPackageManager(); + List info = packageManager.getInstalledPackages(0); + if (info == null || info.isEmpty()) { + return false; + } + for (int i = 0; i < info.size(); i++) { + if (pkgName.equals(info.get(i).packageName)) { + return true; + } + } + return false; + } + + @SuppressLint("StringFormatInvalid") + private void refreshKnownIssue() { + String issueName = null; + String issueLink = null; + final ApplicationInfo appInfo = Objects.requireNonNull(getActivity()).getApplicationInfo(); + final File baseDir = new File(XposedApp.BASE_DIR); + final File baseDirCanonical = getCanonicalFile(baseDir); + final File baseDirActual = new File(Build.VERSION.SDK_INT >= 24 ? appInfo.deviceProtectedDataDir : appInfo.dataDir); + final File baseDirActualCanonical = getCanonicalFile(baseDirActual); + + if (new File("/system/framework/core.jar.jex").exists()) { + issueName = "Aliyun OS"; + issueLink = "https://forum.xda-developers.com/showpost.php?p=52289793&postcount=5"; +// } else if (Build.VERSION.SDK_INT < 24 && (new File("/data/miui/DexspyInstaller.jar").exists() || checkClassExists("miui.dexspy.DexspyInstaller"))) { +// issueName = "MIUI/Dexspy"; +// issueLink = "https://forum.xda-developers.com/showpost.php?p=52291098&postcount=6"; +// } else if (Build.VERSION.SDK_INT < 24 && new File("/system/framework/twframework.jar").exists()) { +// issueName = "Samsung TouchWiz ROM"; +// issueLink = "https://forum.xda-developers.com/showthread.php?t=3034811"; + } else if (!baseDirCanonical.equals(baseDirActualCanonical)) { + Log.e(XposedApp.TAG, "Base directory: " + getPathWithCanonicalPath(baseDir, baseDirCanonical)); + Log.e(XposedApp.TAG, "Expected: " + getPathWithCanonicalPath(baseDirActual, baseDirActualCanonical)); + issueName = getString(R.string.known_issue_wrong_base_directory, getPathWithCanonicalPath(baseDirActual, baseDirActualCanonical)); + } else if (!baseDir.exists()) { + issueName = getString(R.string.known_issue_missing_base_directory); + issueLink = "https://github.com/rovo89/XposedInstaller/issues/393"; + } else if (checkAppInstalled(getContext(), "com.solohsu.android.edxp.manager")) { + issueName = getString(R.string.edxp_installer_installed); + issueLink = getString(R.string.about_support); + } + + } + + private String getAndroidVersion() { + switch (Build.VERSION.SDK_INT) { +// case 16: +// case 17: +// case 18: +// return "Jelly Bean"; +// case 19: +// return "KitKat"; + case 21: + case 22: + return "Lollipop"; + case 23: + return "Marshmallow"; + case 24: + case 25: + return "Nougat"; + case 26: + case 27: + return "Oreo"; + case 28: + return "Pie"; + case 29: + return "Q"; + case 30: + return "R"; + } + return "Unknown"; + } + + private String getUIFramework() { + String manufacturer = Character.toUpperCase(Build.MANUFACTURER.charAt(0)) + Build.MANUFACTURER.substring(1); + if (!Build.BRAND.equals(Build.MANUFACTURER)) { + manufacturer += " " + Character.toUpperCase(Build.BRAND.charAt(0)) + Build.BRAND.substring(1); + } + manufacturer += " " + Build.MODEL + " "; + if (new File("/system/framework/twframework.jar").exists() || new File("/system/framework/samsung-services.jar").exists()) { + manufacturer += "(TouchWiz)"; + } else if (new File("/system/framework/framework-miui-res.apk").exists() || new File("/system/app/miui/miui.apk").exists() || new File("/system/app/miuisystem/miuisystem.apk").exists()) { + manufacturer += "(Mi UI)"; + } else if (new File("/system/priv-app/oneplus-framework-res/oneplus-framework-res.apk").exists()) { + manufacturer += "(Oxygen/Hydrogen OS)"; + } else if (new File("/system/framework/com.samsung.device.jar").exists() || new File("/system/framework/sec_platform_library.jar").exists()) { + manufacturer += "(One UI)"; + } + /*if (manufacturer.contains("Samsung")) { + manufacturer += new File("/system/framework/twframework.jar").exists() || + new File("/system/framework/samsung-services.jar").exists() + ? "(TouchWiz)" : "(AOSP-based ROM)"; + } else if (manufacturer.contains("Xiaomi")) { + manufacturer += new File("/system/framework/framework-miui-res.apk").exists() ? "(MIUI)" : "(AOSP-based ROM)"; + }*/ + return manufacturer; + } + + private File getCanonicalFile(File file) { + try { + return file.getCanonicalFile(); + } catch (IOException e) { + Log.e(XposedApp.TAG, "Failed to get canonical file for " + file.getAbsolutePath(), e); + return file; + } + } + + private String getPathWithCanonicalPath(File file, File canonical) { + if (file.equals(canonical)) { + return file.getAbsolutePath(); + } else { + return file.getAbsolutePath() + " \u2192 " + canonical.getAbsolutePath(); + } + } + + private int extractIntPart(String str) { + int result = 0, length = str.length(); + for (int offset = 0; offset < length; offset++) { + char c = str.charAt(offset); + if ('0' <= c && c <= '9') + result = result * 10 + (c - '0'); + else + break; + } + return result; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/meowcat/edxposed/manager/XposedApp.java b/app/src/main/java/org/meowcat/edxposed/manager/XposedApp.java new file mode 100644 index 000000000..6c976a47a --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/XposedApp.java @@ -0,0 +1,244 @@ +package org.meowcat.edxposed.manager; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.Application; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.os.FileUtils; +import android.os.Handler; +import android.preference.PreferenceManager; +import android.util.DisplayMetrics; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +import org.meowcat.edxposed.manager.receivers.PackageChangeReceiver; +import org.meowcat.edxposed.manager.util.ModuleUtil; +import org.meowcat.edxposed.manager.util.NotificationUtil; +import org.meowcat.edxposed.manager.util.RepoLoader; + +import java.io.File; +import java.lang.reflect.Method; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.Objects; + +import de.robv.android.xposed.installer.util.InstallZipUtil; + +public class XposedApp extends de.robv.android.xposed.installer.XposedApp implements Application.ActivityLifecycleCallbacks { + public static final String TAG = "EdXposedManager"; + @SuppressLint("SdCardPath") + private static final String BASE_DIR_LEGACY = "/data/data/" + BuildConfig.APPLICATION_ID + "/"; + public static final String BASE_DIR = Build.VERSION.SDK_INT >= 24 + ? "/data/user_de/0/" + BuildConfig.APPLICATION_ID + "/" : BASE_DIR_LEGACY; + public static final String ENABLED_MODULES_LIST_FILE = (Build.VERSION.SDK_INT >= 24 + ? "/data/user_de/0/" + BuildConfig.APPLICATION_ID + "/" : BASE_DIR_LEGACY) + "conf/enabled_modules.list"; + public static int WRITE_EXTERNAL_PERMISSION = 69; + @SuppressLint("StaticFieldLeak") + private static XposedApp mInstance = null; + private static Thread mUiThread; + private static Handler mMainHandler; + private SharedPreferences mPref; + private Activity mCurrentActivity = null; + private boolean mIsUiLoaded = false; + + public static XposedApp getInstance() { + return mInstance; + } + + public static InstallZipUtil.XposedProp getXposedProp() { + return de.robv.android.xposed.installer.XposedApp.getInstance().mXposedProp; + } + + public static void runOnUiThread(Runnable action) { + if (Thread.currentThread() != mUiThread) { + mMainHandler.post(action); + } else { + action.run(); + } + } + + public static File createFolder() { + File dir = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Download/EdXposedManager/"); + + if (!dir.exists()) dir.mkdir(); + + return dir; + } + +// public static void postOnUiThread(Runnable action) { +// mMainHandler.post(action); +// } + + public static Integer getXposedVersion() { + return getActiveXposedVersion(); + } + + public static SharedPreferences getPreferences() { + return mInstance.mPref; + } + + public static int getColor(Context context) { + SharedPreferences prefs = context.getSharedPreferences(context.getPackageName() + "_preferences", MODE_PRIVATE); + int defaultColor = context.getResources().getColor(R.color.colorPrimary); + + return prefs.getInt("colors", defaultColor); + } + + public static String getDownloadPath() { + return getPreferences().getString("download_location", Environment.getExternalStorageDirectory() + "/Download/EdXposedManager/"); + } + + public static void mkdirAndChmod(String dir, int permissions) { + dir = BASE_DIR + dir; + //noinspection ResultOfMethodCallIgnored + new File(dir).mkdir(); + FileUtils.setPermissions(dir, permissions, -1, -1); + } + + public void onCreate() { + super.onCreate(); + mInstance = this; + mUiThread = Thread.currentThread(); + mMainHandler = new Handler(); + + mPref = PreferenceManager.getDefaultSharedPreferences(this); + + de.robv.android.xposed.installer.XposedApp.getInstance().reloadXposedProp(); + createDirectories(); + delete(new File(Environment.getExternalStorageDirectory() + "/Download/EdXposedManager/.temp")); + NotificationUtil.init(); + registerReceivers(); + + registerActivityLifecycleCallbacks(this); + + @SuppressLint("SimpleDateFormat") DateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy"); + Date date = new Date(); + + if (!Objects.requireNonNull(mPref.getString("date", "")).equals(dateFormat.format(date))) { + mPref.edit().putString("date", dateFormat.format(date)).apply(); + + try { + Log.i(TAG, String.format("EdXposedManager - %s - %s", BuildConfig.VERSION_CODE, getPackageManager().getPackageInfo(getPackageName(), 0).versionName)); + } catch (PackageManager.NameNotFoundException ignored) { + } + } + + if (mPref.getBoolean("force_english", false)) { + Resources res = getResources(); + DisplayMetrics dm = res.getDisplayMetrics(); + android.content.res.Configuration conf = res.getConfiguration(); + conf.locale = Locale.ENGLISH; + res.updateConfiguration(conf, dm); + } + + RepoLoader.getInstance().triggerFirstLoadIfNecessary(); + } + + private void registerReceivers() { + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_PACKAGE_ADDED); + filter.addAction(Intent.ACTION_PACKAGE_CHANGED); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addDataScheme("package"); + registerReceiver(new PackageChangeReceiver(), filter); + + PendingIntent.getBroadcast(this, 0, + new Intent(this, PackageChangeReceiver.class), 0); + } + + private void delete(File file) { + if (file != null) { + if (file.isDirectory()) { + File[] files = file.listFiles(); + if (files != null) for (File f : file.listFiles()) delete(f); + } + file.delete(); + } + } + + @SuppressWarnings("JavaReflectionMemberAccess") + @SuppressLint({"PrivateApi", "NewApi"}) + private void createDirectories() { + //FileUtils.setPermissions(BASE_DIR, 00777, -1, -1); + mkdirAndChmod("conf", 00777); + mkdirAndChmod("log", 00777); + + if (Build.VERSION.SDK_INT >= 24) { + try { + @SuppressLint("SoonBlockedPrivateApi") Method deleteDir = FileUtils.class.getDeclaredMethod("deleteContentsAndDir", File.class); + deleteDir.invoke(null, new File(BASE_DIR_LEGACY, "bin")); + deleteDir.invoke(null, new File(BASE_DIR_LEGACY, "conf")); + deleteDir.invoke(null, new File(BASE_DIR_LEGACY, "log")); + } catch (ReflectiveOperationException e) { + Log.w(de.robv.android.xposed.installer.XposedApp.TAG, "Failed to delete obsolete directories", e); + } + } + } + + public void updateProgressIndicator(final SwipeRefreshLayout refreshLayout) { + final boolean isLoading = RepoLoader.getInstance().isLoading() || ModuleUtil.getInstance().isLoading(); + runOnUiThread(() -> { + synchronized (XposedApp.this) { + if (mCurrentActivity != null) { + mCurrentActivity.setProgressBarIndeterminateVisibility(isLoading); + if (refreshLayout != null) + refreshLayout.setRefreshing(isLoading); + } + } + }); + } + + @Override + public synchronized void onActivityCreated(@NonNull Activity activity, Bundle savedInstanceState) { + if (mIsUiLoaded) + return; + + RepoLoader.getInstance().triggerFirstLoadIfNecessary(); + mIsUiLoaded = true; + } + + @Override + public void onActivityStarted(@NonNull Activity activity) { + + } + + @Override + public synchronized void onActivityResumed(@NonNull Activity activity) { + mCurrentActivity = activity; + updateProgressIndicator(null); + } + + @Override + public synchronized void onActivityPaused(Activity activity) { + activity.setProgressBarIndeterminateVisibility(false); + mCurrentActivity = null; + } + + @Override + public void onActivityStopped(@NonNull Activity activity) { + + } + + @Override + public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) { + + } + + @Override + public void onActivityDestroyed(@NonNull Activity activity) { + + } +} diff --git a/app/src/main/java/org/meowcat/edxposed/manager/adapters/AppAdapter.java b/app/src/main/java/org/meowcat/edxposed/manager/adapters/AppAdapter.java new file mode 100644 index 000000000..b71983d29 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/adapters/AppAdapter.java @@ -0,0 +1,271 @@ +package org.meowcat.edxposed.manager.adapters; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.AsyncTask; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CompoundButton; +import android.widget.Filter; +import android.widget.ImageView; +import android.widget.Switch; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import org.meowcat.edxposed.manager.R; +import org.meowcat.edxposed.manager.XposedApp; +import org.meowcat.edxposed.manager.util.InstallApkUtil; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +public class AppAdapter extends RecyclerView.Adapter { + + protected final Context context; + private final ApplicationInfo.DisplayNameComparator displayNameComparator; + public Callback callback; + private List fullList, showList; + private DateFormat dateformat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); + private List checkedList; + private PackageManager pm; + private ApplicationFilter filter; + private Comparator cmp; + + AppAdapter(Context context) { + this.context = context; + fullList = showList = Collections.emptyList(); + checkedList = Collections.emptyList(); + filter = new ApplicationFilter(); + pm = context.getPackageManager(); + displayNameComparator = new ApplicationInfo.DisplayNameComparator(pm); + cmp = displayNameComparator; + refresh(); + } + + public void setCallback(Callback callback) { + this.callback = callback; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = LayoutInflater.from(context).inflate(R.layout.item_app, parent, false); + return new ViewHolder(v); + } + + private void loadApps() { + fullList = pm.getInstalledApplications(PackageManager.GET_META_DATA); + if (!XposedApp.getPreferences().getBoolean("show_modules", true)) { + List rmList = new ArrayList<>(); + for (ApplicationInfo info : fullList) { + if (info.metaData != null && info.metaData.containsKey("xposedmodule") || AppHelper.FORCE_WHITE_LIST_MODULE.contains(info.packageName)) { + rmList.add(info); + } + } + if (rmList.size() > 0) { + fullList.removeAll(rmList); + } + } + AppHelper.makeSurePath(); + checkedList = generateCheckedList(); + sortApps(); + if (callback != null) { + callback.onDataReady(); + } + } + + /** + * Called during {@link #loadApps()} in non-UI thread. + * + * @return list of package names which should be checked when shown + */ + protected List generateCheckedList() { + return Collections.emptyList(); + } + + private void sortApps() { + switch (XposedApp.getPreferences().getInt("list_sort", 0)) { + case 7: + cmp = Collections.reverseOrder((ApplicationInfo a, ApplicationInfo b) -> { + try { + return Long.compare(pm.getPackageInfo(a.packageName, 0).lastUpdateTime, pm.getPackageInfo(b.packageName, 0).lastUpdateTime); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + return displayNameComparator.compare(a, b); + } + }); + break; + case 6: + cmp = (ApplicationInfo a, ApplicationInfo b) -> { + try { + return Long.compare(pm.getPackageInfo(a.packageName, 0).lastUpdateTime, pm.getPackageInfo(b.packageName, 0).lastUpdateTime); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + return displayNameComparator.compare(a, b); + } + }; + break; + case 5: + cmp = Collections.reverseOrder((ApplicationInfo a, ApplicationInfo b) -> { + try { + return Long.compare(pm.getPackageInfo(a.packageName, 0).firstInstallTime, pm.getPackageInfo(b.packageName, 0).firstInstallTime); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + return displayNameComparator.compare(a, b); + } + }); + break; + case 4: + cmp = (ApplicationInfo a, ApplicationInfo b) -> { + try { + return Long.compare(pm.getPackageInfo(a.packageName, 0).firstInstallTime, pm.getPackageInfo(b.packageName, 0).firstInstallTime); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + return displayNameComparator.compare(a, b); + } + }; + break; + case 3: + cmp = Collections.reverseOrder((a, b) -> a.packageName.compareTo(b.packageName)); + break; + case 2: + cmp = (a, b) -> a.packageName.compareTo(b.packageName); + break; + case 1: + cmp = Collections.reverseOrder(displayNameComparator); + break; + case 0: + default: + cmp = displayNameComparator; + break; + } + Collections.sort(fullList, (a, b) -> { + if (XposedApp.getPreferences().getBoolean("enabled_top", true)) { + boolean aChecked = checkedList.contains(a.packageName); + boolean bChecked = checkedList.contains(b.packageName); + if (aChecked == bChecked) { + return cmp.compare(a, b); + } else if (aChecked) { + return -1; + } else { + return 1; + } + } else { + return cmp.compare(a, b); + } + }); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + ApplicationInfo info = showList.get(position); + holder.appIcon.setImageDrawable(info.loadIcon(pm)); + holder.appName.setText(InstallApkUtil.getAppLabel(info, pm)); + try { + holder.appVersion.setText(pm.getPackageInfo(info.packageName, 0).versionName); + holder.appInstallTime.setText(dateformat.format(new Date(pm.getPackageInfo(info.packageName, 0).firstInstallTime))); + holder.appUpdateTime.setText(dateformat.format(new Date(pm.getPackageInfo(info.packageName, 0).lastUpdateTime))); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + holder.appPackage.setText(info.packageName); + //holder.appPackage.setTextColor(ThemeUtil.getThemeColor(context, android.R.attr.textColorSecondary)); + + holder.mSwitch.setOnCheckedChangeListener(null); + holder.mSwitch.setChecked(checkedList.contains(info.packageName)); + holder.mSwitch.setOnCheckedChangeListener((v, isChecked) -> + onCheckedChange(v, isChecked, info)); + holder.infoLayout.setOnClickListener(v -> { + if (callback != null) { + callback.onItemClick(v, info); + } + }); + } + + @Override + public int getItemCount() { + return showList.size(); + } + + public void filter(String constraint) { + filter.filter(constraint); + } + + public void refresh() { + AsyncTask.THREAD_POOL_EXECUTOR.execute(this::loadApps); + } + + protected void onCheckedChange(CompoundButton buttonView, boolean isChecked, ApplicationInfo info) { + // override this to implements your functions + } + + public interface Callback { + void onDataReady(); + + void onItemClick(View v, ApplicationInfo info); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + + View infoLayout; + ImageView appIcon; + TextView appName; + TextView appPackage; + TextView appVersion; + TextView appInstallTime; + TextView appUpdateTime; + Switch mSwitch; + + ViewHolder(View itemView) { + super(itemView); + infoLayout = itemView.findViewById(R.id.info_layout); + appIcon = itemView.findViewById(R.id.app_icon); + appName = itemView.findViewById(R.id.app_name); + appPackage = itemView.findViewById(R.id.package_name); + appVersion = itemView.findViewById(R.id.version_name); + appInstallTime = itemView.findViewById(R.id.tvInstallTime); + appUpdateTime = itemView.findViewById(R.id.tvUpdateTime); + mSwitch = itemView.findViewById(R.id.checkbox); + } + } + + class ApplicationFilter extends Filter { + + private boolean lowercaseContains(String s, CharSequence filter) { + return !TextUtils.isEmpty(s) && s.toLowerCase().contains(filter); + } + + @Override + protected FilterResults performFiltering(CharSequence constraint) { + if (constraint == null || constraint.length() == 0) { + showList = fullList; + } else { + showList = new ArrayList<>(); + String filter = constraint.toString().toLowerCase(); + for (ApplicationInfo info : fullList) { + if (lowercaseContains(InstallApkUtil.getAppLabel(info, pm), filter) + || lowercaseContains(info.packageName, filter)) { + showList.add(info); + } + } + } + return null; + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults results) { + notifyDataSetChanged(); + } + } +} diff --git a/app/src/main/java/org/meowcat/edxposed/manager/adapters/AppHelper.java b/app/src/main/java/org/meowcat/edxposed/manager/adapters/AppHelper.java new file mode 100644 index 000000000..a4c860fa2 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/adapters/AppHelper.java @@ -0,0 +1,362 @@ +package org.meowcat.edxposed.manager.adapters; + +import android.annotation.SuppressLint; +import android.app.ActivityManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.net.Uri; +import android.os.FileUtils; +import android.view.View; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.view.menu.MenuBuilder; +import androidx.appcompat.view.menu.MenuPopupHelper; +import androidx.appcompat.widget.PopupMenu; +import androidx.fragment.app.FragmentManager; + +import org.meowcat.edxposed.manager.BuildConfig; +import org.meowcat.edxposed.manager.R; +import org.meowcat.edxposed.manager.XposedApp; +import org.meowcat.edxposed.manager.util.CompileUtil; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import static android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS; + +@SuppressWarnings("deprecation") +public class AppHelper { + + public static final String TAG = XposedApp.TAG; + + private static final String BASE_PATH = XposedApp.BASE_DIR; + private static final String WHITE_LIST_PATH = "conf/whitelist/"; + private static final String BLACK_LIST_PATH = "conf/blacklist/"; + private static final String COMPAT_LIST_PATH = "conf/compatlist/"; + private static final String WHITE_LIST_MODE = "conf/usewhitelist"; + private static final String BLACK_LIST_MODE = "conf/blackwhitelist"; + + private static final List FORCE_WHITE_LIST = new ArrayList<>(Collections.singletonList(BuildConfig.APPLICATION_ID)); + private static final List SAFETYNET_BLACK_LIST = new ArrayList<>(Arrays.asList("com.google.android.gms", "com.google.android.gsf")); + static List FORCE_WHITE_LIST_MODULE = new ArrayList<>(FORCE_WHITE_LIST); + + @SuppressWarnings("OctalInteger") + static void makeSurePath() { + XposedApp.mkdirAndChmod(WHITE_LIST_PATH, 00777); + XposedApp.mkdirAndChmod(BLACK_LIST_PATH, 00777); + XposedApp.mkdirAndChmod(COMPAT_LIST_PATH, 00777); + } + + public static boolean isWhiteListMode() { + return new File(BASE_PATH + WHITE_LIST_MODE).exists(); + } + + public static boolean isBlackListMode() { + return new File(BASE_PATH + BLACK_LIST_MODE).exists(); + } + + private static boolean addWhiteList(String packageName) { + if (SAFETYNET_BLACK_LIST.contains(packageName)) { + if (XposedApp.getPreferences().getBoolean("pass_safetynet", false)) { + removeWhiteList(packageName); + return false; + } + } + return whiteListFileName(packageName, true); + } + + private static boolean addBlackList(String packageName) { + if (FORCE_WHITE_LIST_MODULE.contains(packageName)) { + removeBlackList(packageName); + return false; + } + return blackListFileName(packageName, true); + } + + private static boolean removeWhiteList(String packageName) { + if (FORCE_WHITE_LIST_MODULE.contains(packageName)) { + return false; + } + return whiteListFileName(packageName, false); + } + + private static boolean removeBlackList(String packageName) { + if (SAFETYNET_BLACK_LIST.contains(packageName)) { + if (XposedApp.getPreferences().getBoolean("pass_safetynet", false)) { + return false; + } + } + return blackListFileName(packageName, false); + } + + static List getBlackList() { + File file = new File(BASE_PATH + BLACK_LIST_PATH); + File[] files = file.listFiles(); + if (files == null) { + return new ArrayList<>(); + } + List s = new ArrayList<>(); + for (File file1 : files) { + if (!file1.isDirectory()) { + s.add(file1.getName()); + } + } + for (String pn : FORCE_WHITE_LIST_MODULE) { + if (s.contains(pn)) { + s.remove(pn); + removeBlackList(pn); + } + } + if (XposedApp.getPreferences().getBoolean("pass_safetynet", false)) { + for (String pn : SAFETYNET_BLACK_LIST) { + if (!s.contains(pn)) { + s.add(pn); + addBlackList(pn); + } + } + } + return s; + } + + static List getWhiteList() { + File file = new File(BASE_PATH + WHITE_LIST_PATH); + File[] files = file.listFiles(); + if (files == null) { + return FORCE_WHITE_LIST_MODULE; + } + List result = new ArrayList<>(); + for (File file1 : files) { + result.add(file1.getName()); + } + for (String pn : FORCE_WHITE_LIST_MODULE) { + if (!result.contains(pn)) { + result.add(pn); + addWhiteList(pn); + } + } + if (XposedApp.getPreferences().getBoolean("pass_safetynet", false)) { + for (String pn : SAFETYNET_BLACK_LIST) { + if (result.contains(pn)) { + result.remove(pn); + removeWhiteList(pn); + } + } + } + return result; + } + + @SuppressLint("WorldReadableFiles") + private static Boolean whiteListFileName(String packageName, boolean isAdd) { + boolean returns = true; + File file = new File(BASE_PATH + WHITE_LIST_PATH + packageName); + if (isAdd) { + if (!file.exists()) { + FileOutputStream fos = null; + try { + fos = new FileOutputStream(file.getPath()); + setFilePermissionsFromMode(file.getPath(), Context.MODE_WORLD_READABLE); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } finally { + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + e.printStackTrace(); + try { + returns = file.createNewFile(); + } catch (IOException e1) { + e.printStackTrace(); + } + } + } + } + } + } else { + if (file.exists()) { + returns = file.delete(); + } + } + return returns; + } + + @SuppressWarnings("SameParameterValue") + @SuppressLint({"WorldReadableFiles", "WorldWriteableFiles"}) + private static void setFilePermissionsFromMode(String name, int mode) { + int perms = FileUtils.S_IRUSR | FileUtils.S_IWUSR + | FileUtils.S_IRGRP | FileUtils.S_IWGRP; + if ((mode & Context.MODE_WORLD_READABLE) != 0) { + perms |= FileUtils.S_IROTH; + } + if ((mode & Context.MODE_WORLD_WRITEABLE) != 0) { + perms |= FileUtils.S_IWOTH; + } + FileUtils.setPermissions(name, perms, -1, -1); + } + + @SuppressLint("WorldReadableFiles") + private static Boolean blackListFileName(String packageName, boolean isAdd) { + boolean returns = true; + File file = new File(BASE_PATH + BLACK_LIST_PATH + packageName); + if (isAdd) { + if (!file.exists()) { + FileOutputStream fos = null; + try { + fos = new FileOutputStream(file.getPath()); + setFilePermissionsFromMode(file.getPath(), Context.MODE_WORLD_READABLE); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } finally { + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + e.printStackTrace(); + try { + returns = file.createNewFile(); + } catch (IOException e1) { + e.printStackTrace(); + } + } + } + } + } + } else { + if (file.exists()) { + returns = file.delete(); + } + } + return returns; + } + + @SuppressLint("WorldReadableFiles") + private static Boolean compatListFileName(String packageName, boolean isAdd) { + boolean returns = true; + File file = new File(BASE_PATH + COMPAT_LIST_PATH + packageName); + if (isAdd) { + if (!file.exists()) { + FileOutputStream fos = null; + try { + fos = new FileOutputStream(file.getPath()); + setFilePermissionsFromMode(file.getPath(), Context.MODE_WORLD_READABLE); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } finally { + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + e.printStackTrace(); + try { + returns = file.createNewFile(); + } catch (IOException e1) { + e.printStackTrace(); + } + } + } + } + } + } else { + if (file.exists()) { + returns = file.delete(); + } + } + return returns; + } + + static boolean addPackageName(boolean isWhiteListMode, String packageName) { + return isWhiteListMode ? addWhiteList(packageName) : addBlackList(packageName); + } + + static boolean removePackageName(boolean isWhiteListMode, String packageName) { + return isWhiteListMode ? removeWhiteList(packageName) : removeBlackList(packageName); + } + + @SuppressLint("RestrictedApi") + public static void showMenu(@NonNull Context context, + @NonNull FragmentManager fragmentManager, + @NonNull View anchor, + @NonNull ApplicationInfo info) { + PopupMenu appMenu = new PopupMenu(context, anchor); + appMenu.inflate(R.menu.menu_app_item); + appMenu.setOnMenuItemClickListener(menuItem -> { + switch (menuItem.getItemId()) { + case R.id.app_menu_launch: + Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(info.packageName); + if (launchIntent != null) { + context.startActivity(launchIntent); + } else { + Toast.makeText(context, context.getString(R.string.module_no_ui), Toast.LENGTH_LONG).show(); + } + break; + case R.id.app_menu_stop: + try { + ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + Objects.requireNonNull(manager).killBackgroundProcesses(info.packageName); + } catch (Exception ex) { + ex.printStackTrace(); + } + break; + case R.id.app_menu_compile_speed: + CompileUtil.compileSpeed(context, fragmentManager, info); + break; + case R.id.app_menu_compile_dexopt: + CompileUtil.compileDexopt(context, fragmentManager, info); + break; + case R.id.app_menu_compile_reset: + CompileUtil.reset(context, fragmentManager, info); + break; + case R.id.app_menu_store: + Uri uri = Uri.parse("market://details?id=" + info.packageName); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + try { + context.startActivity(intent); + } catch (Exception ex) { + ex.printStackTrace(); + } + break; + case R.id.app_menu_info: + context.startActivity(new Intent(ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", info.packageName, null))); + break; + case R.id.app_menu_uninstall: + context.startActivity(new Intent(Intent.ACTION_UNINSTALL_PACKAGE, Uri.fromParts("package", info.packageName, null))); + break; + } + return true; + }); + MenuPopupHelper menuHelper = new MenuPopupHelper(context, (MenuBuilder) appMenu.getMenu(), anchor); + menuHelper.setForceShowIcon(true); + menuHelper.show(); + } + + static List getCompatList() { + File file = new File(BASE_PATH + COMPAT_LIST_PATH); + File[] files = file.listFiles(); + if (files == null) { + return new ArrayList<>(); + } + List s = new ArrayList<>(); + for (File file1 : files) { + s.add(file1.getName()); + } + return s; + } + + static boolean addCompatList(String packageName) { + return compatListFileName(packageName, true); + } + + static boolean removeCompatList(String packageName) { + return compatListFileName(packageName, false); + } +} diff --git a/app/src/main/java/org/meowcat/edxposed/manager/adapters/BlackListAdapter.java b/app/src/main/java/org/meowcat/edxposed/manager/adapters/BlackListAdapter.java new file mode 100644 index 000000000..3b33a20b7 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/adapters/BlackListAdapter.java @@ -0,0 +1,63 @@ +package org.meowcat.edxposed.manager.adapters; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.widget.CompoundButton; + +import org.meowcat.edxposed.manager.R; +import org.meowcat.edxposed.manager.XposedApp; +import org.meowcat.edxposed.manager.util.ModuleUtil; +import org.meowcat.edxposed.manager.util.ToastUtil; + +import java.util.Collection; +import java.util.List; + + +public class BlackListAdapter extends AppAdapter { + + private volatile boolean isWhiteListMode; + private List checkedList; + + public BlackListAdapter(Context context, boolean isWhiteListMode) { + super(context); + this.isWhiteListMode = isWhiteListMode; + } + +// public void setWhiteListMode(boolean isWhiteListMode) { +// this.isWhiteListMode = isWhiteListMode; +// } + + @Override + protected List generateCheckedList() { + if (XposedApp.getPreferences().getBoolean("hook_modules", true)) { + Collection installedModules = ModuleUtil.getInstance().getModules().values(); + for (ModuleUtil.InstalledModule info : installedModules) { + AppHelper.FORCE_WHITE_LIST_MODULE.add(info.packageName); + } + } + AppHelper.makeSurePath(); + if (isWhiteListMode) { + checkedList = AppHelper.getWhiteList(); + } else { + checkedList = AppHelper.getBlackList(); + } + return checkedList; + } + + @Override + protected void onCheckedChange(CompoundButton view, boolean isChecked, ApplicationInfo info) { + boolean success = isChecked ? + AppHelper.addPackageName(isWhiteListMode, info.packageName) : + AppHelper.removePackageName(isWhiteListMode, info.packageName); + if (success) { + if (isChecked) { + checkedList.add(info.packageName); + } else { + checkedList.remove(info.packageName); + } + } else { + ToastUtil.showShortToast(context, R.string.add_package_failed); + view.setChecked(!isChecked); + } + } +} diff --git a/app/src/main/java/org/meowcat/edxposed/manager/adapters/CursorRecyclerViewAdapter.java b/app/src/main/java/org/meowcat/edxposed/manager/adapters/CursorRecyclerViewAdapter.java new file mode 100644 index 000000000..1b9499aa5 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/adapters/CursorRecyclerViewAdapter.java @@ -0,0 +1,127 @@ +package org.meowcat.edxposed.manager.adapters; + +import android.content.Context; +import android.database.Cursor; +import android.database.DataSetObserver; + +import androidx.recyclerview.widget.RecyclerView; + +public abstract class CursorRecyclerViewAdapter extends RecyclerView.Adapter { + + private Context mContext; + + private Cursor mCursor; + + private boolean mDataValid; + + private int mRowIdColumn; + + private DataSetObserver mDataSetObserver; + + public CursorRecyclerViewAdapter(Context context, Cursor cursor) { + mContext = context; + mCursor = cursor; + mDataValid = cursor != null; + mRowIdColumn = mDataValid ? mCursor.getColumnIndex("_id") : -1; + mDataSetObserver = new NotifyingDataSetObserver(); + if (mCursor != null) { + mCursor.registerDataSetObserver(mDataSetObserver); + } + } + + public Cursor getCursor() { + return mCursor; + } + + @Override + public int getItemCount() { + if (mDataValid && mCursor != null) { + return mCursor.getCount(); + } + return 0; + } + + @Override + public long getItemId(int position) { + if (mDataValid && mCursor != null && mCursor.moveToPosition(position)) { + return mCursor.getLong(mRowIdColumn); + } + return 0; + } + + @Override + public void setHasStableIds(boolean hasStableIds) { + super.setHasStableIds(true); + } + + public abstract void onBindViewHolder(VH viewHolder, Cursor cursor); + + @Override + public void onBindViewHolder(VH viewHolder, int position) { + if (!mDataValid) { + throw new IllegalStateException("this should only be called when the cursor is valid"); + } + if (!mCursor.moveToPosition(position)) { + throw new IllegalStateException("couldn't move cursor to position " + position); + } + onBindViewHolder(viewHolder, mCursor); + } + + /** + * Change the underlying cursor to a new cursor. If there is an existing cursor it will be + * closed. + */ + public void changeCursor(Cursor cursor) { + Cursor old = swapCursor(cursor); + if (old != null) { + old.close(); + } + } + + /** + * Swap in a new Cursor, returning the old Cursor. Unlike + * {@link #changeCursor(Cursor)}, the returned old Cursor is not + * closed. + */ + public Cursor swapCursor(Cursor newCursor) { + if (newCursor == mCursor) { + return null; + } + final Cursor oldCursor = mCursor; + if (oldCursor != null && mDataSetObserver != null) { + oldCursor.unregisterDataSetObserver(mDataSetObserver); + } + mCursor = newCursor; + if (mCursor != null) { + if (mDataSetObserver != null) { + mCursor.registerDataSetObserver(mDataSetObserver); + } + mRowIdColumn = newCursor.getColumnIndexOrThrow("_id"); + mDataValid = true; + notifyDataSetChanged(); + } else { + mRowIdColumn = -1; + mDataValid = false; + notifyDataSetChanged(); + //There is no notifyDataSetInvalidated() method in RecyclerView.Adapter + } + return oldCursor; + } + + private class NotifyingDataSetObserver extends DataSetObserver { + @Override + public void onChanged() { + super.onChanged(); + mDataValid = true; + notifyDataSetChanged(); + } + + @Override + public void onInvalidated() { + super.onInvalidated(); + mDataValid = false; + notifyDataSetChanged(); + //There is no notifyDataSetInvalidated() method in RecyclerView.Adapter + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/meowcat/edxposed/manager/receivers/BootReceiver.java b/app/src/main/java/org/meowcat/edxposed/manager/receivers/BootReceiver.java new file mode 100644 index 000000000..26e2d906b --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/receivers/BootReceiver.java @@ -0,0 +1,58 @@ +package org.meowcat.edxposed.manager.receivers; + +import android.annotation.SuppressLint; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.AsyncTask; +import android.util.Log; + +import org.json.JSONObject; +import org.meowcat.edxposed.manager.BuildConfig; +import org.meowcat.edxposed.manager.XposedApp; +import org.meowcat.edxposed.manager.util.NotificationUtil; +import org.meowcat.edxposed.manager.util.json.JSONUtils; + +public class BootReceiver extends BroadcastReceiver { + + @Override + public void onReceive(final Context context, Intent intent) { + new android.os.Handler().postDelayed(() -> { + if (!isOnline(context)) return; + + new CheckUpdates().execute(); + }, 60 * 60 * 1000 /*60 min*/); + } + + private boolean isOnline(Context context) { + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo netInfo = cm.getActiveNetworkInfo(); + return netInfo != null && netInfo.isConnectedOrConnecting(); + } + + @SuppressLint("StaticFieldLeak") + private class CheckUpdates extends AsyncTask { + + @Override + protected Void doInBackground(Void... params) { + try { + String jsonString = JSONUtils.getFileContent(JSONUtils.JSON_LINK).replace("%XPOSED_ZIP%", ""); + + String newApkVersion = new JSONObject(jsonString).getJSONObject("apk").getString("version"); + + Integer a = BuildConfig.VERSION_CODE; + Integer b = Integer.valueOf(newApkVersion); + + if (a.compareTo(b) < 0) { + NotificationUtil.showInstallerUpdateNotification(); + } + } catch (Exception e) { + Log.d(XposedApp.TAG, e.getMessage()); + } + return null; + } + + } +} diff --git a/app/src/main/java/org/meowcat/edxposed/manager/receivers/DownloadReceiver.java b/app/src/main/java/org/meowcat/edxposed/manager/receivers/DownloadReceiver.java new file mode 100644 index 000000000..3ad77e57f --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/receivers/DownloadReceiver.java @@ -0,0 +1,19 @@ +package org.meowcat.edxposed.manager.receivers; + +import android.app.DownloadManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import org.meowcat.edxposed.manager.util.DownloadsUtil; + +public class DownloadReceiver extends BroadcastReceiver { + @Override + public void onReceive(final Context context, final Intent intent) { + String action = intent.getAction(); + if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)) { + long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0); + DownloadsUtil.triggerDownloadFinishedCallback(context, downloadId); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/meowcat/edxposed/manager/receivers/PackageChangeReceiver.java b/app/src/main/java/org/meowcat/edxposed/manager/receivers/PackageChangeReceiver.java new file mode 100644 index 000000000..791ce376f --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/receivers/PackageChangeReceiver.java @@ -0,0 +1,80 @@ +package org.meowcat.edxposed.manager.receivers; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; + +import org.meowcat.edxposed.manager.util.ModuleUtil; +import org.meowcat.edxposed.manager.util.ModuleUtil.InstalledModule; +import org.meowcat.edxposed.manager.util.NotificationUtil; + +import java.util.Objects; + +public class PackageChangeReceiver extends BroadcastReceiver { + private static ModuleUtil mModuleUtil = null; + + private static String getPackageName(Intent intent) { + Uri uri = intent.getData(); + return (uri != null) ? uri.getSchemeSpecificPart() : null; + } + + @Override + public void onReceive(final Context context, final Intent intent) { + if (Objects.requireNonNull(intent.getAction()).equals(Intent.ACTION_PACKAGE_REMOVED) && intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) + // Ignore existing packages being removed in order to be updated + return; + + String packageName = getPackageName(intent); + if (packageName == null) + return; + + if (intent.getAction().equals(Intent.ACTION_PACKAGE_CHANGED)) { + // make sure that the change is for the complete package, not only a + // component + String[] components = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST); + if (components != null) { + boolean isForPackage = false; + for (String component : components) { + if (packageName.equals(component)) { + isForPackage = true; + break; + } + } + if (!isForPackage) + return; + } + } else if (intent.getAction().equals(Intent.ACTION_PACKAGE_REMOVED)) { + NotificationUtil.cancel(packageName, NotificationUtil.NOTIFICATION_MODULE_NOT_ACTIVATED_YET); + return; + } + + mModuleUtil = getModuleUtilInstance(); + + InstalledModule module = ModuleUtil.getInstance().reloadSingleModule(packageName); + if (module == null + || intent.getAction().equals(Intent.ACTION_PACKAGE_REMOVED)) { + // Package being removed, disable it if it was a previously active + // Xposed mod + if (mModuleUtil.isModuleEnabled(packageName)) { + mModuleUtil.setModuleEnabled(packageName, false); + mModuleUtil.updateModulesList(false); + } + return; + } + + if (mModuleUtil.isModuleEnabled(packageName)) { + mModuleUtil.updateModulesList(false); + NotificationUtil.showModulesUpdatedNotification(); + } else { + NotificationUtil.showNotActivatedNotification(packageName, module.getAppName()); + } + } + + private ModuleUtil getModuleUtilInstance() { + if (mModuleUtil == null) { + mModuleUtil = ModuleUtil.getInstance(); + } + return mModuleUtil; + } +} diff --git a/app/src/main/java/org/meowcat/edxposed/manager/repo/Module.java b/app/src/main/java/org/meowcat/edxposed/manager/repo/Module.java new file mode 100644 index 000000000..83e76989e --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/repo/Module.java @@ -0,0 +1,28 @@ +package org.meowcat.edxposed.manager.repo; + +import android.util.Pair; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +public class Module { + @SuppressWarnings("WeakerAccess") + public final Repository repository; + public final List> moreInfo = new LinkedList<>(); + public final List versions = new ArrayList<>(); + final List screenshots = new ArrayList<>(); + public String packageName; + public String name; + public String summary; + public String description; + public boolean descriptionIsHtml = false; + public String author; + public String support; + long created = -1; + long updated = -1; + + Module(Repository repository) { + this.repository = repository; + } +} diff --git a/app/src/main/java/org/meowcat/edxposed/manager/repo/ModuleVersion.java b/app/src/main/java/org/meowcat/edxposed/manager/repo/ModuleVersion.java new file mode 100644 index 000000000..299a0da0b --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/repo/ModuleVersion.java @@ -0,0 +1,17 @@ +package org.meowcat.edxposed.manager.repo; + +public class ModuleVersion { + public final Module module; + public String name; + public int code; + public String downloadLink; + public String md5sum; + public String changelog; + public boolean changelogIsHtml = false; + public ReleaseType relType = ReleaseType.STABLE; + public long uploaded = -1; + + /* package */ ModuleVersion(Module module) { + this.module = module; + } +} diff --git a/app/src/main/java/org/meowcat/edxposed/manager/repo/ReleaseType.java b/app/src/main/java/org/meowcat/edxposed/manager/repo/ReleaseType.java new file mode 100644 index 000000000..2e549a9a3 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/repo/ReleaseType.java @@ -0,0 +1,40 @@ +package org.meowcat.edxposed.manager.repo; + + +import org.meowcat.edxposed.manager.R; + +public enum ReleaseType { + STABLE(R.string.reltype_stable, R.string.reltype_stable_summary), BETA(R.string.reltype_beta, R.string.reltype_beta_summary), EXPERIMENTAL(R.string.reltype_experimental, R.string.reltype_experimental_summary); + + private static final ReleaseType[] sValuesCache = values(); + private final int mTitleId; + private final int mSummaryId; + + ReleaseType(int titleId, int summaryId) { + mTitleId = titleId; + mSummaryId = summaryId; + } + + public static ReleaseType fromString(String value) { + if (value == null || value.equals("stable")) + return STABLE; + else if (value.equals("beta")) + return BETA; + else if (value.equals("experimental")) + return EXPERIMENTAL; + else + return STABLE; + } + + public static ReleaseType fromOrdinal(int ordinal) { + return sValuesCache[ordinal]; + } + + public int getTitleId() { + return mTitleId; + } + + public int getSummaryId() { + return mSummaryId; + } +} diff --git a/app/src/main/java/org/meowcat/edxposed/manager/repo/RepoDb.java b/app/src/main/java/org/meowcat/edxposed/manager/repo/RepoDb.java new file mode 100644 index 000000000..427dba285 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/repo/RepoDb.java @@ -0,0 +1,492 @@ +package org.meowcat.edxposed.manager.repo; + +import android.annotation.SuppressLint; +import android.content.ContentValues; +import android.content.Context; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.text.TextUtils; +import android.util.Pair; + +import org.meowcat.edxposed.manager.BuildConfig; +import org.meowcat.edxposed.manager.XposedApp; +import org.meowcat.edxposed.manager.repo.RepoDbDefinitions.InstalledModulesColumns; +import org.meowcat.edxposed.manager.repo.RepoDbDefinitions.InstalledModulesUpdatesColumns; +import org.meowcat.edxposed.manager.repo.RepoDbDefinitions.ModuleVersionsColumns; +import org.meowcat.edxposed.manager.repo.RepoDbDefinitions.ModulesColumns; +import org.meowcat.edxposed.manager.repo.RepoDbDefinitions.MoreInfoColumns; +import org.meowcat.edxposed.manager.repo.RepoDbDefinitions.OverviewColumns; +import org.meowcat.edxposed.manager.repo.RepoDbDefinitions.OverviewColumnsIndexes; +import org.meowcat.edxposed.manager.repo.RepoDbDefinitions.RepositoriesColumns; +import org.meowcat.edxposed.manager.util.ModuleUtil; +import org.meowcat.edxposed.manager.util.ModuleUtil.InstalledModule; +import org.meowcat.edxposed.manager.util.RepoLoader; + +import java.io.File; +import java.util.LinkedHashMap; +import java.util.Map; + +import static android.content.Context.MODE_PRIVATE; + +public final class RepoDb extends SQLiteOpenHelper { + public static final int SORT_STATUS = 0; + public static final int SORT_UPDATED = 1; + private static final int SORT_CREATED = 2; + + @SuppressLint("StaticFieldLeak") + private static Context context; + private static SQLiteDatabase sDb; + + static { + RepoDb instance = new RepoDb(XposedApp.getInstance()); + sDb = instance.getWritableDatabase(); + sDb.execSQL("PRAGMA foreign_keys=ON"); + instance.createTempTables(sDb); + } + + private RepoDb(Context context) { + super(context, getDbPath(context), null, RepoDbDefinitions.DATABASE_VERSION); + RepoDb.context = context; + } + + private static String getDbPath(Context context) { + return new File(context.getNoBackupFilesDir(), RepoDbDefinitions.DATABASE_NAME).getPath(); + } + + public static void beginTransation() { + sDb.beginTransaction(); + } + + public static void setTransactionSuccessful() { + sDb.setTransactionSuccessful(); + } + + public static void endTransation() { + sDb.endTransaction(); + } + + private static String getString(@SuppressWarnings("SameParameterValue") String table, @SuppressWarnings("SameParameterValue") String searchColumn, String searchValue, @SuppressWarnings("SameParameterValue") String resultColumn) { + String[] projection = new String[]{resultColumn}; + String where = searchColumn + " = ?"; + String[] whereArgs = new String[]{searchValue}; + Cursor c = sDb.query(table, projection, where, whereArgs, null, null, null, "1"); + if (c.moveToFirst()) { + String result = c.getString(c.getColumnIndexOrThrow(resultColumn)); + c.close(); + return result; + } else { + c.close(); + throw new RowNotFoundException("Could not find " + table + "." + searchColumn + " with value '" + searchValue + "'"); + } + } + + @SuppressWarnings("UnusedReturnValue") + public static long insertRepository(String url) { + ContentValues values = new ContentValues(); + values.put(RepositoriesColumns.URL, url); + return sDb.insertOrThrow(RepositoriesColumns.TABLE_NAME, null, values); + } + + public static void deleteRepositories() { + if (sDb != null) + sDb.delete(RepositoriesColumns.TABLE_NAME, null, null); + } + + public static Map getRepositories() { + Map result = new LinkedHashMap<>(1); + + String[] projection = new String[]{ + RepositoriesColumns._ID, + RepositoriesColumns.URL, + RepositoriesColumns.TITLE, + RepositoriesColumns.PARTIAL_URL, + RepositoriesColumns.VERSION, + }; + + Cursor c = sDb.query(RepositoriesColumns.TABLE_NAME, projection, null, null, null, null, RepositoriesColumns._ID); + while (c.moveToNext()) { + Repository repo = new Repository(); + long id = c.getLong(c.getColumnIndexOrThrow(RepositoriesColumns._ID)); + repo.url = c.getString(c.getColumnIndexOrThrow(RepositoriesColumns.URL)); + repo.name = c.getString(c.getColumnIndexOrThrow(RepositoriesColumns.TITLE)); + repo.partialUrl = c.getString(c.getColumnIndexOrThrow(RepositoriesColumns.PARTIAL_URL)); + repo.version = c.getString(c.getColumnIndexOrThrow(RepositoriesColumns.VERSION)); + result.put(id, repo); + } + c.close(); + + return result; + } + + public static void updateRepository(long repoId, Repository repository) { + ContentValues values = new ContentValues(); + values.put(RepositoriesColumns.TITLE, repository.name); + values.put(RepositoriesColumns.PARTIAL_URL, repository.partialUrl); + values.put(RepositoriesColumns.VERSION, repository.version); + sDb.update(RepositoriesColumns.TABLE_NAME, values, RepositoriesColumns._ID + " = ?", new String[]{Long.toString(repoId)}); + } + + public static void updateRepositoryVersion(long repoId, String version) { + ContentValues values = new ContentValues(); + values.put(RepositoriesColumns.VERSION, version); + sDb.update(RepositoriesColumns.TABLE_NAME, values, RepositoriesColumns._ID + " = ?", new String[]{Long.toString(repoId)}); + } + + @SuppressWarnings("UnusedReturnValue") + public static long insertModule(long repoId, Module mod) { + ContentValues values = new ContentValues(); + values.put(ModulesColumns.REPO_ID, repoId); + values.put(ModulesColumns.PKGNAME, mod.packageName); + values.put(ModulesColumns.TITLE, mod.name); + values.put(ModulesColumns.SUMMARY, mod.summary); + values.put(ModulesColumns.DESCRIPTION, mod.description); + values.put(ModulesColumns.DESCRIPTION_IS_HTML, mod.descriptionIsHtml); + values.put(ModulesColumns.AUTHOR, mod.author); + values.put(ModulesColumns.SUPPORT, mod.support); + values.put(ModulesColumns.CREATED, mod.created); + values.put(ModulesColumns.UPDATED, mod.updated); + + ModuleVersion latestVersion = RepoLoader.getInstance().getLatestVersion(mod); + + sDb.beginTransaction(); + try { + long moduleId = sDb.insertOrThrow(ModulesColumns.TABLE_NAME, null, values); + + long latestVersionId = -1; + for (ModuleVersion version : mod.versions) { + long versionId = insertModuleVersion(moduleId, version); + if (latestVersion == version) + latestVersionId = versionId; + } + + if (latestVersionId > -1) { + values = new ContentValues(); + values.put(ModulesColumns.LATEST_VERSION, latestVersionId); + sDb.update(ModulesColumns.TABLE_NAME, values, ModulesColumns._ID + " = ?", new String[]{Long.toString(moduleId)}); + } + + for (Pair moreInfoEntry : mod.moreInfo) { + insertMoreInfo(moduleId, moreInfoEntry.first, moreInfoEntry.second); + } + + // TODO Add mod.screenshots + + sDb.setTransactionSuccessful(); + return moduleId; + + } finally { + sDb.endTransaction(); + } + } + + private static long insertModuleVersion(long moduleId, ModuleVersion version) { + ContentValues values = new ContentValues(); + values.put(ModuleVersionsColumns.MODULE_ID, moduleId); + values.put(ModuleVersionsColumns.NAME, version.name); + values.put(ModuleVersionsColumns.CODE, version.code); + values.put(ModuleVersionsColumns.DOWNLOAD_LINK, version.downloadLink); + values.put(ModuleVersionsColumns.MD5SUM, version.md5sum); + values.put(ModuleVersionsColumns.CHANGELOG, version.changelog); + values.put(ModuleVersionsColumns.CHANGELOG_IS_HTML, version.changelogIsHtml); + values.put(ModuleVersionsColumns.RELTYPE, version.relType.ordinal()); + values.put(ModuleVersionsColumns.UPLOADED, version.uploaded); + return sDb.insertOrThrow(ModuleVersionsColumns.TABLE_NAME, null, + values); + } + + @SuppressWarnings("UnusedReturnValue") + private static long insertMoreInfo(long moduleId, String title, String value) { + ContentValues values = new ContentValues(); + values.put(MoreInfoColumns.MODULE_ID, moduleId); + values.put(MoreInfoColumns.LABEL, title); + values.put(MoreInfoColumns.VALUE, value); + return sDb.insertOrThrow(MoreInfoColumns.TABLE_NAME, null, values); + } + + public static void deleteAllModules(long repoId) { + sDb.delete(ModulesColumns.TABLE_NAME, ModulesColumns.REPO_ID + " = ?", new String[]{Long.toString(repoId)}); + } + + public static void deleteModule(long repoId, String packageName) { + sDb.delete(ModulesColumns.TABLE_NAME, ModulesColumns.REPO_ID + " = ? AND " + ModulesColumns.PKGNAME + " = ?", new String[]{Long.toString(repoId), packageName}); + } + + public static Module getModuleByPackageName(String packageName) { + // The module itself + String[] projection = new String[]{ + ModulesColumns._ID, + ModulesColumns.REPO_ID, + ModulesColumns.PKGNAME, + ModulesColumns.TITLE, + ModulesColumns.SUMMARY, + ModulesColumns.DESCRIPTION, + ModulesColumns.DESCRIPTION_IS_HTML, + ModulesColumns.AUTHOR, + ModulesColumns.SUPPORT, + ModulesColumns.CREATED, + ModulesColumns.UPDATED, + }; + + String where = ModulesColumns.PREFERRED + " = 1 AND " + ModulesColumns.PKGNAME + " = ?"; + String[] whereArgs = new String[]{packageName}; + + Cursor c = sDb.query(ModulesColumns.TABLE_NAME, projection, where, whereArgs, null, null, null, "1"); + if (!c.moveToFirst()) { + c.close(); + return null; + } + + long moduleId = c.getLong(c.getColumnIndexOrThrow(ModulesColumns._ID)); + long repoId = c.getLong(c.getColumnIndexOrThrow(ModulesColumns.REPO_ID)); + + Module mod = new Module(RepoLoader.getInstance().getRepository(repoId)); + mod.packageName = c.getString(c.getColumnIndexOrThrow(ModulesColumns.PKGNAME)); + mod.name = c.getString(c.getColumnIndexOrThrow(ModulesColumns.TITLE)); + mod.summary = c.getString(c.getColumnIndexOrThrow(ModulesColumns.SUMMARY)); + mod.description = c.getString(c.getColumnIndexOrThrow(ModulesColumns.DESCRIPTION)); + mod.descriptionIsHtml = c.getInt(c.getColumnIndexOrThrow(ModulesColumns.DESCRIPTION_IS_HTML)) > 0; + mod.author = c.getString(c.getColumnIndexOrThrow(ModulesColumns.AUTHOR)); + mod.support = c.getString(c.getColumnIndexOrThrow(ModulesColumns.SUPPORT)); + mod.created = c.getLong(c.getColumnIndexOrThrow(ModulesColumns.CREATED)); + mod.updated = c.getLong(c.getColumnIndexOrThrow(ModulesColumns.UPDATED)); + + c.close(); + + // Versions + projection = new String[]{ + ModuleVersionsColumns.NAME, + ModuleVersionsColumns.CODE, ModuleVersionsColumns.DOWNLOAD_LINK, + ModuleVersionsColumns.MD5SUM, ModuleVersionsColumns.CHANGELOG, + ModuleVersionsColumns.CHANGELOG_IS_HTML, + ModuleVersionsColumns.RELTYPE, + ModuleVersionsColumns.UPLOADED, + }; + + where = ModuleVersionsColumns.MODULE_ID + " = ?"; + whereArgs = new String[]{Long.toString(moduleId)}; + + c = sDb.query(ModuleVersionsColumns.TABLE_NAME, projection, where, whereArgs, null, null, null); + while (c.moveToNext()) { + ModuleVersion version = new ModuleVersion(mod); + version.name = c.getString(c.getColumnIndexOrThrow(ModuleVersionsColumns.NAME)); + version.code = c.getInt(c.getColumnIndexOrThrow(ModuleVersionsColumns.CODE)); + version.downloadLink = c.getString(c.getColumnIndexOrThrow(ModuleVersionsColumns.DOWNLOAD_LINK)); + version.md5sum = c.getString(c.getColumnIndexOrThrow(ModuleVersionsColumns.MD5SUM)); + version.changelog = c.getString(c.getColumnIndexOrThrow(ModuleVersionsColumns.CHANGELOG)); + version.changelogIsHtml = c.getInt(c.getColumnIndexOrThrow(ModuleVersionsColumns.CHANGELOG_IS_HTML)) > 0; + version.relType = ReleaseType.fromOrdinal(c.getInt(c.getColumnIndexOrThrow(ModuleVersionsColumns.RELTYPE))); + version.uploaded = c.getLong(c.getColumnIndexOrThrow(ModuleVersionsColumns.UPLOADED)); + mod.versions.add(version); + } + c.close(); + + // MoreInfo + projection = new String[]{ + MoreInfoColumns.LABEL, + MoreInfoColumns.VALUE, + }; + + where = MoreInfoColumns.MODULE_ID + " = ?"; + whereArgs = new String[]{Long.toString(moduleId)}; + + c = sDb.query(MoreInfoColumns.TABLE_NAME, projection, where, whereArgs, null, null, MoreInfoColumns._ID); + while (c.moveToNext()) { + String label = c.getString(c.getColumnIndexOrThrow(MoreInfoColumns.LABEL)); + String value = c.getString(c.getColumnIndexOrThrow(MoreInfoColumns.VALUE)); + mod.moreInfo.add(new Pair<>(label, value)); + } + c.close(); + + return mod; + } + + public static String getModuleSupport(String packageName) { + return getString(ModulesColumns.TABLE_NAME, ModulesColumns.PKGNAME, packageName, ModulesColumns.SUPPORT); + } + + public static void updateModuleLatestVersion(String packageName) { + int maxShownReleaseType = RepoLoader.getInstance().getMaxShownReleaseType(packageName).ordinal(); + sDb.execSQL("UPDATE " + ModulesColumns.TABLE_NAME + + " SET " + ModulesColumns.LATEST_VERSION + + " = (SELECT " + ModuleVersionsColumns._ID + " FROM " + ModuleVersionsColumns.TABLE_NAME + " AS v" + + " WHERE v." + ModuleVersionsColumns.MODULE_ID + + " = " + ModulesColumns.TABLE_NAME + "." + ModulesColumns._ID + + " AND reltype <= ? LIMIT 1)" + + " WHERE " + ModulesColumns.PKGNAME + " = ?", + new Object[]{maxShownReleaseType, packageName}); + } + + public static void updateAllModulesLatestVersion() { + sDb.beginTransaction(); + try { + String[] projection = new String[]{ModulesColumns.PKGNAME}; + Cursor c = sDb.query(true, ModulesColumns.TABLE_NAME, projection, null, null, null, null, null, null); + while (c.moveToNext()) { + updateModuleLatestVersion(c.getString(0)); + } + c.close(); + sDb.setTransactionSuccessful(); + } finally { + sDb.endTransaction(); + } + } + + @SuppressWarnings("UnusedReturnValue") + public static long insertInstalledModule(InstalledModule installed) { + ContentValues values = new ContentValues(); + values.put(InstalledModulesColumns.PKGNAME, installed.packageName); + values.put(InstalledModulesColumns.VERSION_CODE, installed.versionCode); + values.put(InstalledModulesColumns.VERSION_NAME, installed.versionName); + return sDb.insertOrThrow(InstalledModulesColumns.TABLE_NAME, null, values); + } + + public static void deleteInstalledModule(String packageName) { + sDb.delete(InstalledModulesColumns.TABLE_NAME, InstalledModulesColumns.PKGNAME + " = ?", new String[]{packageName}); + } + + public static void deleteAllInstalledModules() { + sDb.delete(InstalledModulesColumns.TABLE_NAME, null, null); + } + + public static Cursor queryModuleOverview(int sortingOrder, + CharSequence filterText) { + // Columns + String[] projection = new String[]{ + "m." + ModulesColumns._ID, + "m." + ModulesColumns.PKGNAME, + "m." + ModulesColumns.TITLE, + "m." + ModulesColumns.SUMMARY, + "m." + ModulesColumns.CREATED, + "m." + ModulesColumns.UPDATED, + + "v." + ModuleVersionsColumns.NAME + " AS " + OverviewColumns.LATEST_VERSION, + "i." + InstalledModulesColumns.VERSION_NAME + " AS " + OverviewColumns.INSTALLED_VERSION, + + "(CASE WHEN m." + ModulesColumns.PKGNAME + " = '" + ModuleUtil.getInstance().getFrameworkPackageName() + + "' THEN 1 ELSE 0 END) AS " + OverviewColumns.IS_FRAMEWORK, + + "(CASE WHEN i." + InstalledModulesColumns.VERSION_NAME + " IS NOT NULL" + + " THEN 1 ELSE 0 END) AS " + OverviewColumns.IS_INSTALLED, + + "(CASE WHEN v." + ModuleVersionsColumns.CODE + " > " + InstalledModulesColumns.VERSION_CODE + + " THEN 1 ELSE 0 END) AS " + OverviewColumns.HAS_UPDATE, + }; + + // Conditions + StringBuilder where = new StringBuilder(ModulesColumns.PREFERRED + " = 1"); + String[] whereArgs = null; + if (!TextUtils.isEmpty(filterText)) { + where.append(" AND (m." + ModulesColumns.TITLE + " LIKE ?" + " OR m." + ModulesColumns.SUMMARY + " LIKE ?" + " OR m." + ModulesColumns.DESCRIPTION + " LIKE ?" + " OR m." + ModulesColumns.AUTHOR + " LIKE ?)"); + String filterTextArg = "%" + filterText + "%"; + whereArgs = new String[]{filterTextArg, filterTextArg, filterTextArg, filterTextArg}; + } else { + SharedPreferences prefs = context.getSharedPreferences(BuildConfig.APPLICATION_ID + "_preferences", MODE_PRIVATE); + + if (prefs.getBoolean("ignore_chinese", false)) { + for (char ch : "的一是不了人我在有他这为中设微模块淘".toCharArray()) { + where.append(" AND NOT (m." + ModulesColumns.TITLE + " LIKE '%").append(ch).append("%'").append(" OR m.").append(ModulesColumns.SUMMARY).append(" LIKE '%").append(ch).append("%'").append(" OR m.").append(ModulesColumns.DESCRIPTION).append(" LIKE '%").append(ch).append("%')"); + } + } + } + + // Sorting order + StringBuilder sbOrder = new StringBuilder(); + if (sortingOrder == SORT_CREATED) { + sbOrder.append(OverviewColumns.CREATED); + sbOrder.append(" DESC,"); + } else if (sortingOrder == SORT_UPDATED) { + sbOrder.append(OverviewColumns.UPDATED); + sbOrder.append(" DESC,"); + } + sbOrder.append(OverviewColumns.IS_FRAMEWORK); + sbOrder.append(" DESC, "); + sbOrder.append(OverviewColumns.HAS_UPDATE); + sbOrder.append(" DESC, "); + sbOrder.append(OverviewColumns.IS_INSTALLED); + sbOrder.append(" DESC, "); + sbOrder.append("m."); + sbOrder.append(OverviewColumns.TITLE); + sbOrder.append(" COLLATE NOCASE, "); + sbOrder.append("m."); + sbOrder.append(OverviewColumns.PKGNAME); + + // Query + Cursor c = sDb.query( + ModulesColumns.TABLE_NAME + " AS m" + + " LEFT JOIN " + ModuleVersionsColumns.TABLE_NAME + " AS v" + + " ON v." + ModuleVersionsColumns._ID + " = m." + ModulesColumns.LATEST_VERSION + + " LEFT JOIN " + InstalledModulesColumns.TABLE_NAME + " AS i" + + " ON i." + InstalledModulesColumns.PKGNAME + " = m." + ModulesColumns.PKGNAME, + projection, where.toString(), whereArgs, null, null, sbOrder.toString()); + + // Cache column indexes + OverviewColumnsIndexes.fillFromCursor(c); + + return c; + } + + public static String getFrameworkUpdateVersion() { + return getFirstUpdate(true); + } + + public static boolean hasModuleUpdates() { + return getFirstUpdate(false) != null; + } + + private static String getFirstUpdate(boolean framework) { + String[] projection = new String[]{InstalledModulesUpdatesColumns.LATEST_NAME}; + String where = ModulesColumns.PKGNAME + (framework ? " = ?" : " != ?"); + String[] whereArgs = new String[]{ModuleUtil.getInstance().getFrameworkPackageName()}; + Cursor c = sDb.query(InstalledModulesUpdatesColumns.VIEW_NAME, projection, where, whereArgs, null, null, null, "1"); + String latestVersion = null; + if (c.moveToFirst()) + latestVersion = c.getString(c.getColumnIndexOrThrow(InstalledModulesUpdatesColumns.LATEST_NAME)); + c.close(); + return latestVersion; + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL(RepoDbDefinitions.SQL_CREATE_TABLE_REPOSITORIES); + db.execSQL(RepoDbDefinitions.SQL_CREATE_TABLE_MODULES); + db.execSQL(RepoDbDefinitions.SQL_CREATE_TABLE_MODULE_VERSIONS); + db.execSQL(RepoDbDefinitions.SQL_CREATE_INDEX_MODULE_VERSIONS_MODULE_ID); + db.execSQL(RepoDbDefinitions.SQL_CREATE_TABLE_MORE_INFO); + + RepoLoader.getInstance().clear(false); + } + + private void createTempTables(SQLiteDatabase db) { + db.execSQL(RepoDbDefinitions.SQL_CREATE_TEMP_TABLE_INSTALLED_MODULES); + db.execSQL(RepoDbDefinitions.SQL_CREATE_TEMP_VIEW_INSTALLED_MODULES_UPDATES); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // This is only a cache, so simply drop & recreate the tables + db.execSQL("DROP TABLE IF EXISTS " + RepositoriesColumns.TABLE_NAME); + db.execSQL("DROP TABLE IF EXISTS " + ModulesColumns.TABLE_NAME); + db.execSQL("DROP TABLE IF EXISTS " + ModuleVersionsColumns.TABLE_NAME); + db.execSQL("DROP TABLE IF EXISTS " + MoreInfoColumns.TABLE_NAME); + + db.execSQL("DROP TABLE IF EXISTS " + InstalledModulesColumns.TABLE_NAME); + db.execSQL("DROP VIEW IF EXISTS " + InstalledModulesUpdatesColumns.VIEW_NAME); + + onCreate(db); + } + + @Override + public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + onUpgrade(db, oldVersion, newVersion); + } + + public static class RowNotFoundException extends RuntimeException { + private static final long serialVersionUID = -396324186622439535L; + + RowNotFoundException(String reason) { + super(reason); + } + } +} diff --git a/app/src/main/java/org/meowcat/edxposed/manager/repo/RepoDbDefinitions.java b/app/src/main/java/org/meowcat/edxposed/manager/repo/RepoDbDefinitions.java new file mode 100644 index 000000000..666b55130 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/repo/RepoDbDefinitions.java @@ -0,0 +1,216 @@ +package org.meowcat.edxposed.manager.repo; + +import android.database.Cursor; +import android.provider.BaseColumns; + +public class RepoDbDefinitions { + static final int DATABASE_VERSION = 4; + static final String DATABASE_NAME = "repo_cache.db"; + static final String SQL_CREATE_TABLE_REPOSITORIES = "CREATE TABLE " + + RepositoriesColumns.TABLE_NAME + " (" + RepositoriesColumns._ID + + " INTEGER PRIMARY KEY AUTOINCREMENT," + RepositoriesColumns.URL + + " TEXT NOT NULL, " + RepositoriesColumns.TITLE + " TEXT, " + + RepositoriesColumns.PARTIAL_URL + " TEXT, " + + RepositoriesColumns.VERSION + " TEXT, " + "UNIQUE (" + + RepositoriesColumns.URL + ") ON CONFLICT REPLACE)"; + static final String SQL_CREATE_TABLE_MODULES = "CREATE TABLE " + + ModulesColumns.TABLE_NAME + " (" + ModulesColumns._ID + + " INTEGER PRIMARY KEY AUTOINCREMENT," + + + ModulesColumns.REPO_ID + " INTEGER NOT NULL REFERENCES " + + RepositoriesColumns.TABLE_NAME + " ON DELETE CASCADE, " + + ModulesColumns.PKGNAME + " TEXT NOT NULL, " + ModulesColumns.TITLE + + " TEXT NOT NULL, " + ModulesColumns.SUMMARY + " TEXT, " + + ModulesColumns.DESCRIPTION + " TEXT, " + + ModulesColumns.DESCRIPTION_IS_HTML + " INTEGER DEFAULT 0, " + + ModulesColumns.AUTHOR + " TEXT, " + ModulesColumns.SUPPORT + + " TEXT, " + ModulesColumns.CREATED + " INTEGER DEFAULT -1, " + + ModulesColumns.UPDATED + " INTEGER DEFAULT -1, " + + ModulesColumns.PREFERRED + " INTEGER DEFAULT 1, " + + ModulesColumns.LATEST_VERSION + " INTEGER REFERENCES " + + ModuleVersionsColumns.TABLE_NAME + ", " + "UNIQUE (" + + ModulesColumns.PKGNAME + ", " + ModulesColumns.REPO_ID + + ") ON CONFLICT REPLACE)"; + static final String SQL_CREATE_TABLE_MODULE_VERSIONS = "CREATE TABLE " + + ModuleVersionsColumns.TABLE_NAME + " (" + + ModuleVersionsColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + ModuleVersionsColumns.MODULE_ID + " INTEGER NOT NULL REFERENCES " + + ModulesColumns.TABLE_NAME + " ON DELETE CASCADE, " + + ModuleVersionsColumns.NAME + " TEXT NOT NULL, " + + ModuleVersionsColumns.CODE + " INTEGER NOT NULL, " + + ModuleVersionsColumns.DOWNLOAD_LINK + " TEXT, " + + ModuleVersionsColumns.MD5SUM + " TEXT, " + + ModuleVersionsColumns.CHANGELOG + " TEXT, " + + ModuleVersionsColumns.CHANGELOG_IS_HTML + " INTEGER DEFAULT 0, " + + ModuleVersionsColumns.RELTYPE + " INTEGER DEFAULT 0, " + + ModuleVersionsColumns.UPLOADED + " INTEGER DEFAULT -1)"; + static final String SQL_CREATE_INDEX_MODULE_VERSIONS_MODULE_ID = "CREATE INDEX " + + ModuleVersionsColumns.IDX_MODULE_ID + " ON " + + ModuleVersionsColumns.TABLE_NAME + " (" + + ModuleVersionsColumns.MODULE_ID + ")"; + static final String SQL_CREATE_TABLE_MORE_INFO = "CREATE TABLE " + + MoreInfoColumns.TABLE_NAME + " (" + MoreInfoColumns._ID + + " INTEGER PRIMARY KEY AUTOINCREMENT," + MoreInfoColumns.MODULE_ID + + " INTEGER NOT NULL REFERENCES " + ModulesColumns.TABLE_NAME + + " ON DELETE CASCADE, " + MoreInfoColumns.LABEL + + " TEXT NOT NULL, " + MoreInfoColumns.VALUE + " TEXT)"; + static final String SQL_CREATE_TEMP_TABLE_INSTALLED_MODULES = "CREATE TEMP TABLE " + + InstalledModulesColumns.TABLE_NAME + " (" + + InstalledModulesColumns.PKGNAME + + " TEXT PRIMARY KEY ON CONFLICT REPLACE, " + + InstalledModulesColumns.VERSION_CODE + " INTEGER NOT NULL, " + + InstalledModulesColumns.VERSION_NAME + " TEXT)"; + static final String SQL_CREATE_TEMP_VIEW_INSTALLED_MODULES_UPDATES = "CREATE TEMP VIEW " + + InstalledModulesUpdatesColumns.VIEW_NAME + " AS SELECT " + "m." + + ModulesColumns._ID + " AS " + + InstalledModulesUpdatesColumns.MODULE_ID + ", " + "i." + + InstalledModulesColumns.PKGNAME + " AS " + + InstalledModulesUpdatesColumns.PKGNAME + ", " + "i." + + InstalledModulesColumns.VERSION_CODE + " AS " + + InstalledModulesUpdatesColumns.INSTALLED_CODE + ", " + "i." + + InstalledModulesColumns.VERSION_NAME + " AS " + + InstalledModulesUpdatesColumns.INSTALLED_NAME + ", " + "v." + + ModuleVersionsColumns._ID + " AS " + + InstalledModulesUpdatesColumns.LATEST_ID + ", " + "v." + + ModuleVersionsColumns.CODE + " AS " + + InstalledModulesUpdatesColumns.LATEST_CODE + ", " + "v." + + ModuleVersionsColumns.NAME + " AS " + + InstalledModulesUpdatesColumns.LATEST_NAME + " FROM " + + InstalledModulesColumns.TABLE_NAME + " AS i" + " INNER JOIN " + + ModulesColumns.TABLE_NAME + " AS m" + " ON m." + + ModulesColumns.PKGNAME + " = i." + InstalledModulesColumns.PKGNAME + + " INNER JOIN " + ModuleVersionsColumns.TABLE_NAME + " AS v" + + " ON v." + ModuleVersionsColumns._ID + " = m." + + ModulesColumns.LATEST_VERSION + " WHERE " + + InstalledModulesUpdatesColumns.LATEST_CODE + " > " + + InstalledModulesUpdatesColumns.INSTALLED_CODE + " AND " + + ModulesColumns.PREFERRED + " = 1"; + + ////////////////////////////////////////////////////////////////////////// + public interface RepositoriesColumns extends BaseColumns { + String TABLE_NAME = "repositories"; + + String URL = "url"; + String TITLE = "title"; + String PARTIAL_URL = "partial_url"; + String VERSION = "version"; + } + + ////////////////////////////////////////////////////////////////////////// + public interface ModulesColumns extends BaseColumns { + String TABLE_NAME = "modules"; + + String REPO_ID = "repo_id"; + String PKGNAME = "pkgname"; + String TITLE = "title"; + String SUMMARY = "summary"; + String DESCRIPTION = "description"; + String DESCRIPTION_IS_HTML = "description_is_html"; + String AUTHOR = "author"; + String SUPPORT = "support"; + String CREATED = "created"; + String UPDATED = "updated"; + + String PREFERRED = "preferred"; + String LATEST_VERSION = "latest_version_id"; + } + + ////////////////////////////////////////////////////////////////////////// + public interface ModuleVersionsColumns extends BaseColumns { + String TABLE_NAME = "module_versions"; + String IDX_MODULE_ID = "module_versions_module_id_idx"; + + String MODULE_ID = "module_id"; + String NAME = "name"; + String CODE = "code"; + String DOWNLOAD_LINK = "download_link"; + String MD5SUM = "md5sum"; + String CHANGELOG = "changelog"; + String CHANGELOG_IS_HTML = "changelog_is_html"; + String RELTYPE = "reltype"; + String UPLOADED = "uploaded"; + } + + ////////////////////////////////////////////////////////////////////////// + public interface MoreInfoColumns extends BaseColumns { + String TABLE_NAME = "more_info"; + + String MODULE_ID = "module_id"; + String LABEL = "label"; + String VALUE = "value"; + } + + ////////////////////////////////////////////////////////////////////////// + public interface InstalledModulesColumns { + String TABLE_NAME = "installed_modules"; + + String PKGNAME = "pkgname"; + String VERSION_CODE = "version_code"; + String VERSION_NAME = "version_name"; + } + + ////////////////////////////////////////////////////////////////////////// + public interface InstalledModulesUpdatesColumns { + String VIEW_NAME = InstalledModulesColumns.TABLE_NAME + "_updates"; + + String MODULE_ID = "module_id"; + String PKGNAME = "pkgname"; + String INSTALLED_CODE = "installed_code"; + String INSTALLED_NAME = "installed_name"; + String LATEST_ID = "latest_id"; + String LATEST_CODE = "latest_code"; + String LATEST_NAME = "latest_name"; + } + + ////////////////////////////////////////////////////////////////////////// + public interface OverviewColumns extends BaseColumns { + String PKGNAME = ModulesColumns.PKGNAME; + String TITLE = ModulesColumns.TITLE; + String SUMMARY = ModulesColumns.SUMMARY; + String CREATED = ModulesColumns.CREATED; + String UPDATED = ModulesColumns.UPDATED; + + String INSTALLED_VERSION = "installed_version"; + String LATEST_VERSION = "latest_version"; + + String IS_FRAMEWORK = "is_framework"; + String IS_INSTALLED = "is_installed"; + String HAS_UPDATE = "has_update"; + } + + public static class OverviewColumnsIndexes { + public static int PKGNAME = -1; + public static int TITLE = -1; + public static int SUMMARY = -1; + public static int CREATED = -1; + public static int UPDATED = -1; + public static int INSTALLED_VERSION = -1; + public static int LATEST_VERSION = -1; + public static int IS_FRAMEWORK = -1; + public static int IS_INSTALLED = -1; + public static int HAS_UPDATE = -1; + private static boolean isFilled = false; + + private OverviewColumnsIndexes() { + } + + static void fillFromCursor(Cursor cursor) { + if (isFilled || cursor == null) + return; + + PKGNAME = cursor.getColumnIndexOrThrow(OverviewColumns.PKGNAME); + TITLE = cursor.getColumnIndexOrThrow(OverviewColumns.TITLE); + SUMMARY = cursor.getColumnIndexOrThrow(OverviewColumns.SUMMARY); + CREATED = cursor.getColumnIndexOrThrow(OverviewColumns.CREATED); + UPDATED = cursor.getColumnIndexOrThrow(OverviewColumns.UPDATED); + INSTALLED_VERSION = cursor.getColumnIndexOrThrow(OverviewColumns.INSTALLED_VERSION); + LATEST_VERSION = cursor.getColumnIndexOrThrow(OverviewColumns.LATEST_VERSION); + INSTALLED_VERSION = cursor.getColumnIndexOrThrow(OverviewColumns.INSTALLED_VERSION); + IS_FRAMEWORK = cursor.getColumnIndexOrThrow(OverviewColumns.IS_FRAMEWORK); + IS_INSTALLED = cursor.getColumnIndexOrThrow(OverviewColumns.IS_INSTALLED); + HAS_UPDATE = cursor.getColumnIndexOrThrow(OverviewColumns.HAS_UPDATE); + + isFilled = true; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/meowcat/edxposed/manager/repo/RepoParser.java b/app/src/main/java/org/meowcat/edxposed/manager/repo/RepoParser.java new file mode 100644 index 000000000..911fee904 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/repo/RepoParser.java @@ -0,0 +1,322 @@ +package org.meowcat.edxposed.manager.repo; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Point; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LevelListDrawable; +import android.text.Html; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.util.Log; +import android.util.Pair; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.target.CustomTarget; +import com.bumptech.glide.request.transition.Transition; + +import org.meowcat.edxposed.manager.XposedApp; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + +import java.io.IOException; +import java.io.InputStream; + +public class RepoParser { + public final static String TAG = XposedApp.TAG; + private final static String NS = null; + private final XmlPullParser parser; + private RepoParserCallback mCallback; + private boolean mRepoEventTriggered = false; + + private RepoParser(InputStream is, RepoParserCallback callback) throws XmlPullParserException, IOException { + XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); + parser = factory.newPullParser(); + parser.setInput(is, null); + parser.nextTag(); + mCallback = callback; + } + + public static void parse(InputStream is, RepoParserCallback callback) throws XmlPullParserException, IOException { + new RepoParser(is, callback).readRepo(); + } + + public static Spanned parseSimpleHtml(final Context context, String source, final TextView textView) { + source = source.replaceAll("
  • ", "\t\u0095 "); + source = source.replaceAll("
  • ", "
    "); + Spanned html = Html.fromHtml(source, new Html.ImageGetter() { + @Override + public Drawable getDrawable(String source) { + LevelListDrawable levelListDrawable = new LevelListDrawable(); + final Drawable[] drawable = new Drawable[1]; + Glide.with(context).asBitmap().load(source).into(new CustomTarget() { + @Override + public void onResourceReady(@NonNull Bitmap bitmap, @Nullable Transition transition) { + try { + drawable[0] = new BitmapDrawable(context.getResources(), bitmap); + Point size = new Point(); + ((Activity) context).getWindowManager().getDefaultDisplay().getSize(size); + int multiplier = size.x / bitmap.getWidth(); + if (multiplier <= 0) multiplier = 1; + levelListDrawable.addLevel(1, 1, drawable[0]); + levelListDrawable.setBounds(0, 0, bitmap.getWidth() * multiplier, bitmap.getHeight() * multiplier); + levelListDrawable.setLevel(1); + textView.setText(textView.getText()); + } catch (Exception ignored) { /* Like a null bitmap, etc. */ + } + } + + @Override + public void onLoadCleared(@Nullable Drawable placeholder) { + } + }); + return drawable[0]; + } + }, null); + + // trim trailing newlines + int len = html.length(); + int end = len; + for (int i = len - 1; i >= 0; i--) { + if (html.charAt(i) != '\n') + break; + end = i; + } + + if (end == len) + return html; + else + return new SpannableStringBuilder(html, 0, end); + } + + private void readRepo() throws XmlPullParserException, IOException { + parser.require(XmlPullParser.START_TAG, NS, "repository"); + Repository repository = new Repository(); + repository.isPartial = "true".equals(parser.getAttributeValue(NS, "partial")); + repository.partialUrl = parser.getAttributeValue(NS, "partial-url"); + repository.version = parser.getAttributeValue(NS, "version"); + + while (parser.nextTag() == XmlPullParser.START_TAG) { + String tagName = parser.getName(); + switch (tagName) { + case "name": + repository.name = parser.nextText(); + break; + case "module": + triggerRepoEvent(repository); + Module module = readModule(repository); + if (module != null) + mCallback.onNewModule(module); + break; + case "remove-module": + triggerRepoEvent(repository); + String packageName = readRemoveModule(); + if (packageName != null) + mCallback.onRemoveModule(packageName); + break; + default: + //skip(true); + skip(false); + break; + } + } + + mCallback.onCompleted(repository); + } + + private void triggerRepoEvent(Repository repository) { + if (mRepoEventTriggered) + return; + + mCallback.onRepositoryMetadata(repository); + mRepoEventTriggered = true; + } + + private Module readModule(Repository repository) throws XmlPullParserException, IOException { + parser.require(XmlPullParser.START_TAG, NS, "module"); + final int startDepth = parser.getDepth(); + + Module module = new Module(repository); + module.packageName = parser.getAttributeValue(NS, "package"); + if (module.packageName == null) { + logError("no package name defined"); + leave(startDepth); + return null; + } + + module.created = parseTimestamp("created"); + module.updated = parseTimestamp("updated"); + + while (parser.nextTag() == XmlPullParser.START_TAG) { + String tagName = parser.getName(); + switch (tagName) { + case "name": + module.name = parser.nextText(); + break; + case "author": + module.author = parser.nextText(); + break; + case "summary": + module.summary = parser.nextText(); + break; + case "description": + String isHtml = parser.getAttributeValue(NS, "html"); + if (isHtml != null && isHtml.equals("true")) + module.descriptionIsHtml = true; + module.description = parser.nextText(); + break; + case "screenshot": + module.screenshots.add(parser.nextText()); + break; + case "moreinfo": + String label = parser.getAttributeValue(NS, "label"); + String role = parser.getAttributeValue(NS, "role"); + String value = parser.nextText(); + module.moreInfo.add(new Pair<>(label, value)); + + if (role != null && role.contains("support")) + module.support = value; + break; + case "version": + ModuleVersion version = readModuleVersion(module); + if (version != null) + module.versions.add(version); + break; + default: + //skip(true); + skip(false); + break; + } + } + + if (module.name == null) { + logError("packages need at least a name"); + return null; + } + + return module; + } + + private long parseTimestamp(String attName) { + String value = parser.getAttributeValue(NS, attName); + if (value == null) + return -1; + try { + return Long.parseLong(value) * 1000L; + } catch (NumberFormatException ex) { + return -1; + } + } + + private ModuleVersion readModuleVersion(Module module) throws XmlPullParserException, IOException { + parser.require(XmlPullParser.START_TAG, NS, "version"); + final int startDepth = parser.getDepth(); + ModuleVersion version = new ModuleVersion(module); + + version.uploaded = parseTimestamp("uploaded"); + + while (parser.nextTag() == XmlPullParser.START_TAG) { + String tagName = parser.getName(); + switch (tagName) { + case "name": + version.name = parser.nextText(); + break; + case "code": + try { + version.code = Integer.parseInt(parser.nextText()); + } catch (NumberFormatException nfe) { + logError(nfe.getMessage()); + leave(startDepth); + return null; + } + break; + case "reltype": + version.relType = ReleaseType.fromString(parser.nextText()); + break; + case "download": + version.downloadLink = parser.nextText(); + break; + case "md5sum": + version.md5sum = parser.nextText(); + break; + case "changelog": + String isHtml = parser.getAttributeValue(NS, "html"); + if (isHtml != null && isHtml.equals("true")) + version.changelogIsHtml = true; + version.changelog = parser.nextText(); + break; + case "branch": + // obsolete +// skip(false); +// break; + default: + skip(false); + //skip(true); + break; + } + } + + return version; + } + + private String readRemoveModule() throws XmlPullParserException, IOException { + parser.require(XmlPullParser.START_TAG, NS, "remove-module"); + final int startDepth = parser.getDepth(); + + String packageName = parser.getAttributeValue(NS, "package"); + if (packageName == null) { + logError("no package name defined"); + leave(startDepth); + return null; + } + + return packageName; + } + + private void skip(@SuppressWarnings("SameParameterValue") boolean showWarning) throws XmlPullParserException, IOException { + parser.require(XmlPullParser.START_TAG, null, null); + if (showWarning) + Log.w(TAG, "skipping unknown/erronous tag: " + parser.getPositionDescription()); + int level = 1; + while (level > 0) { + int eventType = parser.next(); + if (eventType == XmlPullParser.END_TAG) { + level--; + } else if (eventType == XmlPullParser.START_TAG) { + level++; + } + } + } + + private void leave(int targetDepth) throws XmlPullParserException, IOException { + Log.w(TAG, "leaving up to level " + targetDepth + ": " + parser.getPositionDescription()); + while (parser.getDepth() > targetDepth) { + //noinspection StatementWithEmptyBody + while (parser.next() != XmlPullParser.END_TAG) { + // do nothing + } + } + } + + private void logError(String error) { + Log.e(TAG, parser.getPositionDescription() + ": " + error); + } + + public interface RepoParserCallback { + void onRepositoryMetadata(Repository repository); + + void onNewModule(Module module); + + void onRemoveModule(String packageName); + + void onCompleted(Repository repository); + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/meowcat/edxposed/manager/repo/Repository.java b/app/src/main/java/org/meowcat/edxposed/manager/repo/Repository.java new file mode 100644 index 000000000..f4d851ff3 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/repo/Repository.java @@ -0,0 +1,12 @@ +package org.meowcat.edxposed.manager.repo; + +public class Repository { + public String name; + public String url; + public boolean isPartial = false; + public String partialUrl; + public String version; + + Repository() { + } +} diff --git a/app/src/main/java/org/meowcat/edxposed/manager/util/CompileUtil.java b/app/src/main/java/org/meowcat/edxposed/manager/util/CompileUtil.java new file mode 100644 index 000000000..bbf3e413c --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/util/CompileUtil.java @@ -0,0 +1,43 @@ +package org.meowcat.edxposed.manager.util; + +import android.content.Context; +import android.content.pm.ApplicationInfo; + +import androidx.fragment.app.FragmentManager; + +import org.meowcat.edxposed.manager.CompileDialogFragment; +import org.meowcat.edxposed.manager.R; + +public class CompileUtil { + + private static final String COMPILE_COMMAND_PREFIX = "cmd package "; + private static final String COMPILE_RESET_COMMAND = COMPILE_COMMAND_PREFIX + "compile --reset "; + private static final String COMPILE_SPEED_COMMAND = COMPILE_COMMAND_PREFIX + "compile -f -m speed "; + private static final String COMPILE_DEXOPT_COMMAND = COMPILE_COMMAND_PREFIX + "force-dex-opt "; + private static final String TAG_COMPILE_DIALOG = "compile_dialog"; + + public static void reset(Context context, FragmentManager fragmentManager, + ApplicationInfo info) { + compilePackageInBg(fragmentManager, info, + context.getString(R.string.compile_reset_msg), COMPILE_RESET_COMMAND); + } + + public static void compileSpeed(Context context, FragmentManager fragmentManager, + ApplicationInfo info) { + compilePackageInBg(fragmentManager, info, + context.getString(R.string.compile_speed_msg), COMPILE_SPEED_COMMAND); + } + + public static void compileDexopt(Context context, FragmentManager fragmentManager, + ApplicationInfo info) { + compilePackageInBg(fragmentManager, info, + context.getString(R.string.compile_speed_msg), COMPILE_DEXOPT_COMMAND); + } + + private static void compilePackageInBg(FragmentManager fragmentManager, + ApplicationInfo info, String msg, String... commands) { + CompileDialogFragment fragment = CompileDialogFragment.newInstance(info, msg, commands); + fragment.show(fragmentManager, TAG_COMPILE_DIALOG); + } + +} diff --git a/app/src/main/java/org/meowcat/edxposed/manager/util/DownloadsUtil.java b/app/src/main/java/org/meowcat/edxposed/manager/util/DownloadsUtil.java new file mode 100644 index 000000000..12a83ee05 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/util/DownloadsUtil.java @@ -0,0 +1,672 @@ +package org.meowcat.edxposed.manager.util; + +import android.annotation.SuppressLint; +import android.app.DownloadManager; +import android.app.DownloadManager.Query; +import android.app.DownloadManager.Request; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.net.Uri; +import android.os.Environment; +import android.provider.MediaStore; +import android.util.Log; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.core.os.EnvironmentCompat; + +import org.meowcat.edxposed.manager.R; +import org.meowcat.edxposed.manager.XposedApp; +import org.meowcat.edxposed.manager.repo.Module; +import org.meowcat.edxposed.manager.repo.ModuleVersion; +import org.meowcat.edxposed.manager.repo.ReleaseType; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class DownloadsUtil { + static final String MIME_TYPE_APK = "application/vnd.android.package-archive"; + //private static final String MIME_TYPE_ZIP = "application/zip"; + private static final Map mCallbacks = new HashMap<>(); + @SuppressLint("StaticFieldLeak") + private static final XposedApp mApp = XposedApp.getInstance(); + private static final SharedPreferences mPref = mApp + .getSharedPreferences("download_cache", Context.MODE_PRIVATE); + + private static String DOWNLOAD_MODULES = "modules"; + + private static DownloadInfo add(Builder b) { + Context context = b.mContext; + removeAllForUrl(context, b.mUrl); + + if (!b.mDialog) { + synchronized (mCallbacks) { + mCallbacks.put(b.mUrl, b.mCallback); + } + } + + String savePath = "Download/EdXposedManager"; + if (b.mModule) { + savePath += "/modules"; + } + + Request request = new Request(Uri.parse(b.mUrl)); + request.setTitle(b.mTitle); + request.setMimeType(b.mMimeType.toString()); + if (b.mSave) { + try { + request.setDestinationInExternalPublicDir(savePath, b.mTitle + b.mMimeType.getExtension()); + } catch (IllegalStateException e) { + Toast.makeText(context, e.getMessage(), Toast.LENGTH_SHORT).show(); + } + } else if (b.mDestination != null) { + //noinspection ResultOfMethodCallIgnored + b.mDestination.getParentFile().mkdirs(); + removeAllForLocalFile(context, b.mDestination); + request.setDestinationUri(Uri.fromFile(b.mDestination)); + } + + request.setNotificationVisibility(Request.VISIBILITY_VISIBLE); + + DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + long id = dm.enqueue(request); + + if (b.mDialog) { + showDownloadDialog(b, id); + } + + return getById(context, id); + } + + private static File[] getDownloadDirs(String subDir) { + Context context = XposedApp.getInstance(); + ArrayList dirs = new ArrayList<>(2); + for (File dir : ContextCompat.getExternalCacheDirs(context)) { + if (dir != null && EnvironmentCompat.getStorageState(dir).equals(Environment.MEDIA_MOUNTED)) { + dirs.add(new File(new File(dir, "downloads"), subDir)); + } + } + dirs.add(new File(new File(context.getCacheDir(), "downloads"), subDir)); + return dirs.toArray(new File[0]); + } + + private static File getDownloadTarget(String subDir, String filename) { + return new File(getDownloadDirs(subDir)[0], filename); + } + + private static File getDownloadTargetForUrl(String subDir, String url) { + return getDownloadTarget(subDir, Uri.parse(url).getLastPathSegment()); + } + + public static DownloadInfo addModule(Context context, String title, String url, boolean save, DownloadFinishedCallback callback) { + return new Builder(context) + .setTitle(title) + .setUrl(url) + .setDestinationFromUrl(DownloadsUtil.DOWNLOAD_MODULES) + .setCallback(callback) + .setSave(save) + .setModule(true) + .setMimeType(MIME_TYPES.APK) + .download(); + } + + private static void showDownloadDialog(final Builder b, final long id) { + final Context context = b.mContext; + final ProgressDialog dialog = new ProgressDialog(context); + dialog.setTitle(b.mTitle); + dialog.setMessage(context.getString(R.string.download_view_waiting)); + dialog.setButton(DialogInterface.BUTTON_NEGATIVE, context.getString(R.string.download_view_cancel), (dialog1, which) -> dialog1.cancel()); + dialog.setOnCancelListener(dialog12 -> removeById(context, id)); + + dialog.setProgress(0); + dialog.setCanceledOnTouchOutside(false); + dialog.setProgressNumberFormat(context.getString(R.string.download_progress)); + dialog.show(); + + new Thread("DownloadDialog") { + @Override + public void run() { + while (true) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + return; + } + + final DownloadInfo info = getById(context, id); + if (info == null) { + dialog.cancel(); + return; + } else if (info.status == DownloadManager.STATUS_FAILED) { + dialog.cancel(); + XposedApp.runOnUiThread(() -> Toast.makeText(context, + context.getString(R.string.download_view_failed, info.reason), + Toast.LENGTH_LONG).show()); + return; + } else if (info.status == DownloadManager.STATUS_SUCCESSFUL) { + dialog.dismiss(); + // Hack to reset stat information. + //noinspection ResultOfMethodCallIgnored + new File(info.localFilename).setExecutable(false); + if (b.mCallback != null) { + b.mCallback.onDownloadFinished(context, info); + } + return; + } + + XposedApp.runOnUiThread(() -> { + if (info.totalSize <= 0 || info.status != DownloadManager.STATUS_RUNNING) { + dialog.setMessage(context.getString(R.string.download_view_waiting)); + } else { + dialog.setMessage(context.getString(R.string.download_running)); + dialog.setProgress(info.bytesDownloaded / 1024); + dialog.setMax(info.totalSize / 1024); + } + }); + } + } + }.start(); + } + + public static ModuleVersion getStableVersion(Module m) { + for (int i = 0; i < m.versions.size(); i++) { + ModuleVersion mvTemp = m.versions.get(i); + + if (mvTemp.relType == ReleaseType.STABLE) { + return mvTemp; + } + } + return null; + } + + public static DownloadInfo getById(Context context, long id) { + DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + Cursor c = dm.query(new Query().setFilterById(id)); + if (!c.moveToFirst()) { + c.close(); + return null; + } + + int columnUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_URI); + int columnTitle = c.getColumnIndexOrThrow(DownloadManager.COLUMN_TITLE); + int columnLastMod = c.getColumnIndexOrThrow( + DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP); + int columnLocalUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI); + int columnStatus = c.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS); + int columnTotalSize = c.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES); + int columnBytesDownloaded = c.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR); + int columnReason = c.getColumnIndexOrThrow(DownloadManager.COLUMN_REASON); + + int status = c.getInt(columnStatus); + String localFilename; + try { + localFilename = getFilenameFromUri(c.getString(columnLocalUri)); + } catch (UnsupportedOperationException e) { + Toast.makeText(context, "An error occurred. Restart app and try again.\n" + e.getMessage(), Toast.LENGTH_SHORT).show(); + return null; + } + if (status == DownloadManager.STATUS_SUCCESSFUL && !new File(localFilename).isFile()) { + dm.remove(id); + c.close(); + return null; + } + + DownloadInfo info = new DownloadInfo(id, c.getString(columnUri), + c.getString(columnTitle), c.getLong(columnLastMod), + localFilename, status, + c.getInt(columnTotalSize), c.getInt(columnBytesDownloaded), + c.getInt(columnReason)); + c.close(); + return info; + } + + public static DownloadInfo getLatestForUrl(Context context, String url) { + List all; + try { + all = getAllForUrl(context, url); + } catch (Throwable throwable) { + return null; + } + return Objects.requireNonNull(all).isEmpty() ? null : all.get(0); + } + + private static List getAllForUrl(Context context, String url) { + DownloadManager dm = (DownloadManager) context + .getSystemService(Context.DOWNLOAD_SERVICE); + Cursor c = dm.query(new Query()); + int columnId = c.getColumnIndexOrThrow(DownloadManager.COLUMN_ID); + int columnUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_URI); + int columnTitle = c.getColumnIndexOrThrow(DownloadManager.COLUMN_TITLE); + int columnLastMod = c.getColumnIndexOrThrow( + DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP); + int columnLocalUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI); + int columnStatus = c.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS); + int columnTotalSize = c.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES); + int columnBytesDownloaded = c.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR); + int columnReason = c.getColumnIndexOrThrow(DownloadManager.COLUMN_REASON); + + List downloads = new ArrayList<>(); + while (c.moveToNext()) { + if (!url.equals(c.getString(columnUri))) + continue; + + int status = c.getInt(columnStatus); + String localFilename; + try { + localFilename = getFilenameFromUri(c.getString(columnLocalUri)); + } catch (UnsupportedOperationException e) { + Toast.makeText(context, "An error occurred. Restart app and try again.\n" + e.getMessage(), Toast.LENGTH_SHORT).show(); + return null; + } + if (status == DownloadManager.STATUS_SUCCESSFUL && !new File(localFilename).isFile()) { + dm.remove(c.getLong(columnId)); + continue; + } + + downloads.add(new DownloadInfo(c.getLong(columnId), + c.getString(columnUri), c.getString(columnTitle), + c.getLong(columnLastMod), localFilename, + status, c.getInt(columnTotalSize), + c.getInt(columnBytesDownloaded), c.getInt(columnReason))); + } + c.close(); + + Collections.sort(downloads); + return downloads; + } + + public static void removeById(Context context, long id) { + DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + dm.remove(id); + } + + private static void removeAllForUrl(Context context, String url) { + DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + Cursor c = dm.query(new Query()); + int columnId = c.getColumnIndexOrThrow(DownloadManager.COLUMN_ID); + int columnUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_URI); + + List idsList = new ArrayList<>(1); + while (c.moveToNext()) { + if (url.equals(c.getString(columnUri))) + idsList.add(c.getLong(columnId)); + } + c.close(); + + if (idsList.isEmpty()) + return; + + long[] ids = new long[idsList.size()]; + for (int i = 0; i < ids.length; i++) + ids[i] = idsList.get(i); + + dm.remove(ids); + } + + private static void removeAllForLocalFile(Context context, File file) { + //noinspection ResultOfMethodCallIgnored + file.delete(); + + String filename; + try { + filename = file.getCanonicalPath(); + } catch (IOException e) { + Log.w(XposedApp.TAG, "Could not resolve path for " + file.getAbsolutePath(), e); + return; + } + + DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + Cursor c = dm.query(new Query()); + int columnId = c.getColumnIndexOrThrow(DownloadManager.COLUMN_ID); + int columnLocalUri = c.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI); + + List idsList = new ArrayList<>(1); + while (c.moveToNext()) { + String itemFilename; + try { + itemFilename = getFilenameFromUri(c.getString(columnLocalUri)); + } catch (UnsupportedOperationException e) { + Toast.makeText(context, "An error occurred. Restart app and try again.\n" + e.getMessage(), Toast.LENGTH_SHORT).show(); + itemFilename = null; + } + if (itemFilename != null) { + if (filename.equals(itemFilename)) { + idsList.add(c.getLong(columnId)); + } else { + try { + if (filename.equals(new File(itemFilename).getCanonicalPath())) { + idsList.add(c.getLong(columnId)); + } + } catch (IOException ignored) { + } + } + } + } + c.close(); + + if (idsList.isEmpty()) + return; + + long[] ids = new long[idsList.size()]; + for (int i = 0; i < ids.length; i++) + ids[i] = idsList.get(i); + + dm.remove(ids); + } + +// public static void removeOutdated(Context context, long cutoff) { +// DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); +// Cursor c = dm.query(new Query()); +// int columnId = c.getColumnIndexOrThrow(DownloadManager.COLUMN_ID); +// int columnLastMod = c.getColumnIndexOrThrow( +// DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP); +// +// List idsList = new ArrayList<>(); +// while (c.moveToNext()) { +// if (c.getLong(columnLastMod) < cutoff) +// idsList.add(c.getLong(columnId)); +// } +// c.close(); +// +// if (idsList.isEmpty()) +// return; +// +// long[] ids = new long[idsList.size()]; +// for (int i = 0; i < ids.length; i++) +// ids[i] = idsList.get(0); +// +// dm.remove(ids); +// } + + public static void triggerDownloadFinishedCallback(Context context, long id) { + DownloadInfo info = getById(context, id); + if (info == null || info.status != DownloadManager.STATUS_SUCCESSFUL) + return; + + DownloadFinishedCallback callback; + synchronized (mCallbacks) { + callback = mCallbacks.get(info.url); + } + + if (callback == null) + return; + + // Hack to reset stat information. + //noinspection ResultOfMethodCallIgnored + new File(info.localFilename).setExecutable(false); + callback.onDownloadFinished(context, info); + } + + private static String getFilenameFromUri(String uriString) { + if (uriString == null) { + return null; + } + Uri uri = Uri.parse(uriString); + if (Objects.requireNonNull(uri.getScheme()).equals("file")) { + return uri.getPath(); + } else if (uri.getScheme().equals("content")) { + Context context = XposedApp.getInstance(); + try (Cursor c = context.getContentResolver().query(uri, new String[]{MediaStore.Files.FileColumns.DATA}, null, null, null)) { + Objects.requireNonNull(c).moveToFirst(); + return c.getString(c.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA)); + } + } else { + throw new UnsupportedOperationException("Unexpected URI: " + uriString); + } + } + + static SyncDownloadInfo downloadSynchronously(String url, File target) { + final boolean useNotModifiedTags = target.exists(); + + URLConnection connection = null; + InputStream in = null; + FileOutputStream out = null; + try { + connection = new URL(url).openConnection(); + connection.setDoOutput(false); + connection.setConnectTimeout(30000); + connection.setReadTimeout(30000); + + if (connection instanceof HttpURLConnection) { + // Disable transparent gzip encoding for gzipped files + if (url.endsWith(".gz")) { + connection.addRequestProperty("Accept-Encoding", "identity"); + } + + if (useNotModifiedTags) { + String modified = mPref.getString("download_" + url + "_modified", null); + String etag = mPref.getString("download_" + url + "_etag", null); + + if (modified != null) { + connection.addRequestProperty("If-Modified-Since", modified); + } + if (etag != null) { + connection.addRequestProperty("If-None-Match", etag); + } + } + } + + connection.connect(); + + if (connection instanceof HttpURLConnection) { + HttpURLConnection httpConnection = (HttpURLConnection) connection; + int responseCode = httpConnection.getResponseCode(); + if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) { + return new SyncDownloadInfo(SyncDownloadInfo.STATUS_NOT_MODIFIED, null); + } else if (responseCode < 200 || responseCode >= 300) { + return new SyncDownloadInfo(SyncDownloadInfo.STATUS_FAILED, + mApp.getString(R.string.repo_download_failed_http, + url, responseCode, + httpConnection.getResponseMessage())); + } + } + + in = connection.getInputStream(); + out = new FileOutputStream(target); + byte[] buf = new byte[1024]; + int read; + while ((read = in.read(buf)) != -1) { + out.write(buf, 0, read); + } + + if (connection instanceof HttpURLConnection) { + HttpURLConnection httpConnection = (HttpURLConnection) connection; + String modified = httpConnection.getHeaderField("Last-Modified"); + String etag = httpConnection.getHeaderField("ETag"); + + mPref.edit() + .putString("download_" + url + "_modified", modified) + .putString("download_" + url + "_etag", etag).apply(); + } + + return new SyncDownloadInfo(SyncDownloadInfo.STATUS_SUCCESS, null); + + } catch (Throwable t) { + return new SyncDownloadInfo(SyncDownloadInfo.STATUS_FAILED, + mApp.getString(R.string.repo_download_failed, url, + t.getMessage())); + + } finally { + if (connection instanceof HttpURLConnection) + ((HttpURLConnection) connection).disconnect(); + if (in != null) + try { + in.close(); + } catch (IOException ignored) { + } + if (out != null) + try { + out.close(); + } catch (IOException ignored) { + } + } + } + + static void clearCache(String url) { + if (url != null) { + mPref.edit().remove("download_" + url + "_modified") + .remove("download_" + url + "_etag").apply(); + } else { + mPref.edit().clear().apply(); + } + } + + public enum MIME_TYPES { + APK { + @NonNull + public String toString() { + return MIME_TYPE_APK; + } + + public String getExtension() { + return ".apk"; + } + }; +// ZIP { +// public String toString() { +// return MIME_TYPE_ZIP; +// } +// +// public String getExtension() { +// return ".zip"; +// } +// }; + + public String getExtension() { + return null; + } + } + + public interface DownloadFinishedCallback { + void onDownloadFinished(Context context, DownloadInfo info); + } + + public static class Builder { + private final Context mContext; + boolean mModule = false; + private String mTitle = null; + private String mUrl = null; + private DownloadFinishedCallback mCallback = null; + private MIME_TYPES mMimeType = MIME_TYPES.APK; + private File mDestination = null; + private boolean mDialog = false; + private boolean mSave = false; + + public Builder(Context context) { + mContext = context; + } + + public Builder setTitle(String title) { + mTitle = title; + return this; + } + + public Builder setUrl(String url) { + mUrl = url; + return this; + } + + public Builder setCallback(DownloadFinishedCallback callback) { + mCallback = callback; + return this; + } + + Builder setMimeType(@SuppressWarnings("SameParameterValue") MIME_TYPES mimeType) { + mMimeType = mimeType; + return this; + } + + Builder setDestination(File file) { + mDestination = file; + return this; + } + + Builder setDestinationFromUrl(String subDir) { + if (mUrl == null) { + throw new IllegalStateException("URL must be set first"); + } + return setDestination(getDownloadTargetForUrl(subDir, mUrl)); + } + + public Builder setSave(boolean save) { + this.mSave = save; + return this; + } + + public Builder setModule(boolean module) { + this.mModule = module; + return this; + } + + public Builder setDialog(boolean dialog) { + mDialog = dialog; + return this; + } + + public DownloadInfo download() { + return add(this); + } + } + + public static class DownloadInfo implements Comparable { + public final long id; + public final String url; + public final String title; + public final String localFilename; + public final int status; + public final int totalSize; + public final int bytesDownloaded; + public final int reason; + final long lastModification; + + private DownloadInfo(long id, String url, String title, long lastModification, String localFilename, int status, int totalSize, int bytesDownloaded, int reason) { + this.id = id; + this.url = url; + this.title = title; + this.lastModification = lastModification; + this.localFilename = localFilename; + this.status = status; + this.totalSize = totalSize; + this.bytesDownloaded = bytesDownloaded; + this.reason = reason; + } + + @Override + public int compareTo(@NonNull DownloadInfo another) { + int compare = (int) (another.lastModification + - this.lastModification); + if (compare != 0) + return compare; + return this.url.compareTo(another.url); + } + } + + public static class SyncDownloadInfo { + static final int STATUS_SUCCESS = 0; + static final int STATUS_NOT_MODIFIED = 1; + static final int STATUS_FAILED = 2; + + public final int status; + final String errorMessage; + + private SyncDownloadInfo(int status, String errorMessage) { + this.status = status; + this.errorMessage = errorMessage; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/meowcat/edxposed/manager/util/HashUtil.java b/app/src/main/java/org/meowcat/edxposed/manager/util/HashUtil.java new file mode 100644 index 000000000..0322988be --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/util/HashUtil.java @@ -0,0 +1,64 @@ +package org.meowcat.edxposed.manager.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class HashUtil { + private static String hash(String input, @SuppressWarnings("SameParameterValue") String algorithm) { + try { + MessageDigest md = MessageDigest.getInstance(algorithm); + byte[] messageDigest = md.digest(input.getBytes()); + return toHexString(messageDigest); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException(e); + } + } + + static String md5(String input) { + return hash(input, "MD5"); + } + +// public static String sha1(String input) { +// return hash(input, "SHA-1"); +// } + + private static String hash(File file, @SuppressWarnings("SameParameterValue") String algorithm) throws IOException { + try { + MessageDigest md = MessageDigest.getInstance(algorithm); + InputStream is = new FileInputStream(file); + byte[] buffer = new byte[8192]; + int read; + while ((read = is.read(buffer)) > 0) { + md.update(buffer, 0, read); + } + is.close(); + byte[] messageDigest = md.digest(); + return toHexString(messageDigest); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException(e); + } + } + + public static String md5(File input) throws IOException { + return hash(input, "MD5"); + } + +// public static String sha1(File input) throws IOException { +// return hash(input, "SHA-1"); +// } + + private static String toHexString(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + int unsignedB = b & 0xff; + if (unsignedB < 0x10) + sb.append("0"); + sb.append(Integer.toHexString(unsignedB)); + } + return sb.toString(); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/meowcat/edxposed/manager/util/InstallApkUtil.java b/app/src/main/java/org/meowcat/edxposed/manager/util/InstallApkUtil.java new file mode 100644 index 000000000..c47bbf132 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/util/InstallApkUtil.java @@ -0,0 +1,131 @@ +package org.meowcat.edxposed.manager.util; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; + +import androidx.core.content.FileProvider; + +import com.topjohnwu.superuser.Shell; + +import org.meowcat.edxposed.manager.R; +import org.meowcat.edxposed.manager.XposedApp; + +import java.io.File; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; + +public class InstallApkUtil extends AsyncTask { + + private static final int ERROR_ROOT_NOT_GRANTED = -99; + + private final DownloadsUtil.DownloadInfo info; + @SuppressLint("StaticFieldLeak") + private final Context context; + private boolean isApkRootInstallOn; + private List output = new LinkedList<>(); + + public InstallApkUtil(Context context, DownloadsUtil.DownloadInfo info) { + this.context = context; + this.info = info; + } + + public static String getAppLabel(ApplicationInfo info, PackageManager pm) { + try { + if (info.labelRes > 0) { + Resources res = pm.getResourcesForApplication(info); + Configuration config = new Configuration(); + config.setLocale(Locale.getDefault()); + res.updateConfiguration(config, res.getDisplayMetrics()); + return res.getString(info.labelRes); + } + } catch (Exception ignored) { + } + return info.loadLabel(pm).toString(); + } + + static void installApkNormally(Context context, String localFilename) { + Intent installIntent = new Intent(Intent.ACTION_INSTALL_PACKAGE); + installIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + Uri uri; + if (Build.VERSION.SDK_INT >= 24) { + uri = FileProvider.getUriForFile(context, "moe.guo.edxpmanager.fileprovider", new File(localFilename)); + installIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + } else { + uri = Uri.fromFile(new File(localFilename)); + } + installIntent.setDataAndType(uri, DownloadsUtil.MIME_TYPE_APK); + installIntent.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME, context.getApplicationInfo().packageName); + context.startActivity(installIntent); + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + + SharedPreferences prefs = XposedApp.getPreferences(); + isApkRootInstallOn = prefs.getBoolean("install_with_su", false); + + if (isApkRootInstallOn) { + NotificationUtil.showModuleInstallingNotification(info.title); + } + } + + @Override + protected Integer doInBackground(Void... params) { + int returnCode = 0; + if (isApkRootInstallOn) { + try { + String path = "/data/local/tmp/"; + String fileName = new File(info.localFilename).getName(); + Shell.su("cat \"" + info.localFilename + "\">" + path + fileName).exec(); + returnCode = Shell.su("pm install -r -f \"" + path + fileName + "\"").exec().getCode(); + //noinspection ResultOfMethodCallIgnored + new File(path + fileName).delete(); + } catch (IllegalStateException e) { + returnCode = ERROR_ROOT_NOT_GRANTED; + } + } + return returnCode; + } + + @Override + protected void onPostExecute(Integer result) { + super.onPostExecute(result); + + if (isApkRootInstallOn) { + NotificationUtil.cancel(NotificationUtil.NOTIFICATION_MODULE_INSTALLING); + + if (result.equals(ERROR_ROOT_NOT_GRANTED)) { + NotificationUtil.showModuleInstallNotification(R.string.installation_error, R.string.root_failed, info.localFilename); + return; + } + + StringBuilder out = new StringBuilder(); + for (String o : output) { + out.append(o); + out.append("\n"); + } +// Pattern failurePattern = Pattern.compile("(?m)^Failure\\s+\\[(.*?)]$"); +// Matcher failureMatcher = failurePattern.matcher(out); + + if (result.equals(0)) { + NotificationUtil.showModuleInstallNotification(R.string.installation_successful, R.string.installation_successful_message, info.localFilename, info.title); + } else { + NotificationUtil.showModuleInstallNotification(R.string.installation_error, R.string.installation_error_message, info.localFilename, info.title, out); + installApkNormally(context, info.localFilename); + } + } else { + installApkNormally(context, info.localFilename); + } + } +} diff --git a/app/src/main/java/org/meowcat/edxposed/manager/util/LocaleUtil.java b/app/src/main/java/org/meowcat/edxposed/manager/util/LocaleUtil.java new file mode 100644 index 000000000..95997b836 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/util/LocaleUtil.java @@ -0,0 +1,16 @@ +package org.meowcat.edxposed.manager.util; + +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; + +import java.util.Locale; + +public class LocaleUtil { + public static void setLocale(Context context, Locale locale) { + Resources resources = context.getResources(); + Configuration configuration = resources.getConfiguration(); + configuration.setLocale(locale); + resources.updateConfiguration(configuration, resources.getDisplayMetrics()); + } +} diff --git a/app/src/main/java/org/meowcat/edxposed/manager/util/ModuleUtil.java b/app/src/main/java/org/meowcat/edxposed/manager/util/ModuleUtil.java new file mode 100644 index 000000000..5b03b0525 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/util/ModuleUtil.java @@ -0,0 +1,400 @@ +package org.meowcat.edxposed.manager.util; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.FileUtils; +import android.util.Log; +import android.widget.Toast; + +import androidx.annotation.NonNull; + +import org.meowcat.edxposed.manager.R; +import org.meowcat.edxposed.manager.XposedApp; +import org.meowcat.edxposed.manager.repo.ModuleVersion; +import org.meowcat.edxposed.manager.repo.RepoDb; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; + +@SuppressWarnings("OctalInteger") +public final class ModuleUtil { + // xposedminversion below this + private static final String MODULES_LIST_FILE = XposedApp.BASE_DIR + "conf/modules.list"; + private static final String PLAY_STORE_PACKAGE = "com.android.vending"; + public static int MIN_MODULE_VERSION = 2; // reject modules with + private static ModuleUtil mInstance = null; + private final XposedApp mApp; + private final PackageManager mPm; + private final String mFrameworkPackageName; + private final List mListeners = new CopyOnWriteArrayList<>(); + private SharedPreferences mPref; + private InstalledModule mFramework = null; + private Map mInstalledModules; + private boolean mIsReloading = false; + private Toast mToast; + + private ModuleUtil() { + mApp = XposedApp.getInstance(); + mPref = mApp.getSharedPreferences("enabled_modules", Context.MODE_PRIVATE); + mPm = mApp.getPackageManager(); + mFrameworkPackageName = mApp.getPackageName(); + } + + public static synchronized ModuleUtil getInstance() { + if (mInstance == null) { + mInstance = new ModuleUtil(); + mInstance.reloadInstalledModules(); + } + return mInstance; + } + + public static int extractIntPart(String str) { + int result = 0, length = str.length(); + for (int offset = 0; offset < length; offset++) { + char c = str.charAt(offset); + if ('0' <= c && c <= '9') + result = result * 10 + (c - '0'); + else + break; + } + return result; + } + + @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") + public void reloadInstalledModules() { + synchronized (this) { + if (mIsReloading) + return; + mIsReloading = true; + } + + Map modules = new HashMap<>(); + RepoDb.beginTransation(); + try { + RepoDb.deleteAllInstalledModules(); + + for (PackageInfo pkg : mPm.getInstalledPackages(PackageManager.GET_META_DATA)) { + ApplicationInfo app = pkg.applicationInfo; + if (!app.enabled) + continue; + + InstalledModule installed = null; + if (app.metaData != null && app.metaData.containsKey("xposedmodule")) { + installed = new InstalledModule(pkg, false); + modules.put(pkg.packageName, installed); + } else if (isFramework(pkg.packageName)) { + mFramework = installed = new InstalledModule(pkg, true); + } + + if (installed != null) + RepoDb.insertInstalledModule(installed); + } + + RepoDb.setTransactionSuccessful(); + } finally { + RepoDb.endTransation(); + } + + mInstalledModules = modules; + synchronized (this) { + mIsReloading = false; + } + for (ModuleListener listener : mListeners) { + listener.onInstalledModulesReloaded(mInstance); + } + } + + public InstalledModule reloadSingleModule(String packageName) { + PackageInfo pkg; + try { + pkg = mPm.getPackageInfo(packageName, PackageManager.GET_META_DATA); + } catch (NameNotFoundException e) { + RepoDb.deleteInstalledModule(packageName); + InstalledModule old = mInstalledModules.remove(packageName); + if (old != null) { + for (ModuleListener listener : mListeners) { + listener.onSingleInstalledModuleReloaded(mInstance, packageName, null); + } + } + return null; + } + + ApplicationInfo app = pkg.applicationInfo; + if (app.enabled && app.metaData != null && app.metaData.containsKey("xposedmodule")) { + InstalledModule module = new InstalledModule(pkg, false); + RepoDb.insertInstalledModule(module); + mInstalledModules.put(packageName, module); + for (ModuleListener listener : mListeners) { + listener.onSingleInstalledModuleReloaded(mInstance, packageName, + module); + } + return module; + } else { + RepoDb.deleteInstalledModule(packageName); + InstalledModule old = mInstalledModules.remove(packageName); + if (old != null) { + for (ModuleListener listener : mListeners) { + listener.onSingleInstalledModuleReloaded(mInstance, packageName, null); + } + } + return null; + } + } + + public synchronized boolean isLoading() { + return mIsReloading; + } + + public InstalledModule getFramework() { + return mFramework; + } + + public String getFrameworkPackageName() { + return mFrameworkPackageName; + } + + private boolean isFramework(String packageName) { + return mFrameworkPackageName.equals(packageName); + } + +// public boolean isInstalled(String packageName) { +// return mInstalledModules.containsKey(packageName) || isFramework(packageName); +// } + + public InstalledModule getModule(String packageName) { + return mInstalledModules.get(packageName); + } + + public Map getModules() { + return mInstalledModules; + } + + public void setModuleEnabled(String packageName, boolean enabled) { + if (enabled) { + mPref.edit().putInt(packageName, 1).apply(); + } else { + mPref.edit().remove(packageName).apply(); + } + } + + public boolean isModuleEnabled(String packageName) { + return mPref.contains(packageName); + } + + public List getEnabledModules() { + LinkedList result = new LinkedList<>(); + + for (String packageName : mPref.getAll().keySet()) { + InstalledModule module = getModule(packageName); + if (module != null) + result.add(module); + else + setModuleEnabled(packageName, false); + } + + return result; + } + + public synchronized void updateModulesList(boolean showToast) { + try { + Log.i(XposedApp.TAG, "ModuleUtil -> updating modules.list"); + int installedXposedVersion = XposedApp.getXposedVersion(); + boolean disabled = false;//StatusInstallerFragment.DISABLE_FILE.exists(); + if (!XposedApp.getPreferences().getBoolean("skip_xposedminversion_check", false) && !disabled && installedXposedVersion <= 0 && showToast) { + Toast.makeText(mApp, R.string.notinstalled, Toast.LENGTH_SHORT).show(); + return; + } + + PrintWriter modulesList = new PrintWriter(MODULES_LIST_FILE); + PrintWriter enabledModulesList = new PrintWriter(XposedApp.ENABLED_MODULES_LIST_FILE); + List enabledModules = getEnabledModules(); + for (InstalledModule module : enabledModules) { + + if (!XposedApp.getPreferences().getBoolean("skip_xposedminversion_check", false) && (!disabled && (module.minVersion > installedXposedVersion || module.minVersion < MIN_MODULE_VERSION)) && showToast) { + Toast.makeText(mApp, R.string.notinstalled, Toast.LENGTH_SHORT).show(); + continue; + } + + modulesList.println(module.app.sourceDir); + + try { + String installer = mPm.getInstallerPackageName(module.app.packageName); + if (!PLAY_STORE_PACKAGE.equals(installer)) + enabledModulesList.println(module.app.packageName); + } catch (Exception ignored) { + } + } + modulesList.close(); + enabledModulesList.close(); + + FileUtils.setPermissions(MODULES_LIST_FILE, 00664, -1, -1); + FileUtils.setPermissions(XposedApp.ENABLED_MODULES_LIST_FILE, 00664, -1, -1); + + if (showToast) { + showToast(R.string.xposed_module_list_updated); + } + } catch (IOException e) { + Log.e(XposedApp.TAG, "ModuleUtil -> cannot write " + MODULES_LIST_FILE, e); + Toast.makeText(mApp, "cannot write " + MODULES_LIST_FILE + e, Toast.LENGTH_SHORT).show(); + } + } + + @SuppressWarnings("SameParameterValue") + private void showToast(int message) { + if (mToast != null) { + mToast.cancel(); + mToast = null; + } + mToast = Toast.makeText(mApp, mApp.getString(message), Toast.LENGTH_SHORT); + mToast.show(); + } + + public void addListener(ModuleListener listener) { + if (!mListeners.contains(listener)) + mListeners.add(listener); + } + + public void removeListener(ModuleListener listener) { + mListeners.remove(listener); + } + + public interface ModuleListener { + /** + * Called whenever one (previously or now) installed module has been + * reloaded + */ + void onSingleInstalledModuleReloaded(ModuleUtil moduleUtil, String packageName, InstalledModule module); + + /** + * Called whenever all installed modules have been reloaded + */ + void onInstalledModulesReloaded(ModuleUtil moduleUtil); + } + + public class InstalledModule { + //private static final int FLAG_FORWARD_LOCK = 1 << 29; + public final String packageName; + public final String versionName; + public final long versionCode; + public final int minVersion; + public final long installTime; + public final long updateTime; + final boolean isFramework; + public ApplicationInfo app; + private String appName; // loaded lazyily + private String description; // loaded lazyily + + private Drawable.ConstantState iconCache = null; + + private InstalledModule(PackageInfo pkg, boolean isFramework) { + this.app = pkg.applicationInfo; + this.packageName = pkg.packageName; + this.isFramework = isFramework; + this.versionName = pkg.versionName; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + //noinspection deprecation + this.versionCode = pkg.versionCode; + } else { + this.versionCode = pkg.getLongVersionCode(); + } + this.installTime = pkg.firstInstallTime; + this.updateTime = pkg.lastUpdateTime; + + if (isFramework) { + this.minVersion = 0; + this.description = ""; + } else { + int version = XposedApp.getXposedVersion(); + if (version > 0 && XposedApp.getPreferences().getBoolean("skip_xposedminversion_check", false)) { + this.minVersion = version; + } else { + Object minVersionRaw = app.metaData.get("xposedminversion"); + if (minVersionRaw instanceof Integer) { + this.minVersion = (Integer) minVersionRaw; + } else if (minVersionRaw instanceof String) { + this.minVersion = extractIntPart((String) minVersionRaw); + } else { + this.minVersion = 0; + } + } + } + } + + public boolean isInstalledOnExternalStorage() { + return (app.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0; + } + + /** + * @hide + */ +// public boolean isForwardLocked() { +// return (app.flags & FLAG_FORWARD_LOCK) != 0; +// } + public String getAppName() { + if (appName == null) + appName = app.loadLabel(mPm).toString(); + return appName; + } + + public String getDescription() { + if (this.description == null) { + Object descriptionRaw = app.metaData.get("xposeddescription"); + String descriptionTmp = null; + if (descriptionRaw instanceof String) { + descriptionTmp = ((String) descriptionRaw).trim(); + } else if (descriptionRaw instanceof Integer) { + try { + int resId = (Integer) descriptionRaw; + if (resId != 0) + descriptionTmp = mPm.getResourcesForApplication(app).getString(resId).trim(); + } catch (Exception ignored) { + } + } + this.description = (descriptionTmp != null) ? descriptionTmp : ""; + } + return this.description; + } + + public boolean isUpdate(ModuleVersion version) { + return (version != null) && version.code > versionCode; + } + + public Drawable getIcon() { + if (iconCache != null) + return iconCache.newDrawable(); + + Intent mIntent = new Intent(Intent.ACTION_MAIN); + //mIntent.addCategory(ModulesFragment.SETTINGS_CATEGORY); + mIntent.setPackage(app.packageName); + List ris = mPm.queryIntentActivities(mIntent, 0); + + Drawable result; + if (ris == null || ris.size() <= 0) + result = app.loadIcon(mPm); + else + result = ris.get(0).activityInfo.loadIcon(mPm); + iconCache = result.getConstantState(); + + return result; + } + + @NonNull + @Override + public String toString() { + return getAppName(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/meowcat/edxposed/manager/util/NavUtil.java b/app/src/main/java/org/meowcat/edxposed/manager/util/NavUtil.java new file mode 100644 index 000000000..c2b87e09a --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/util/NavUtil.java @@ -0,0 +1,59 @@ +package org.meowcat.edxposed.manager.util; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.provider.Browser; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.style.URLSpan; +import android.text.util.Linkify; + +import androidx.annotation.AnyThread; +import androidx.annotation.NonNull; +import androidx.browser.customtabs.CustomTabsIntent; + +import org.meowcat.edxposed.manager.XposedApp; + +public final class NavUtil { + + public static Uri parseURL(String str) { + if (str == null || str.isEmpty()) + return null; + + Spannable spannable = new SpannableString(str); + Linkify.addLinks(spannable, Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES); + + URLSpan[] spans = spannable.getSpans(0, spannable.length(), URLSpan.class); + return (spans.length > 0) ? Uri.parse(spans[0].getURL()) : null; + } + + public static void startURL(Activity activity, Uri uri) { + if (!XposedApp.getPreferences().getBoolean("chrome_tabs", true)) { + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + intent.putExtra(Browser.EXTRA_APPLICATION_ID, activity.getPackageName()); + activity.startActivity(intent); + return; + } + + CustomTabsIntent.Builder customTabsIntent = new CustomTabsIntent.Builder(); + customTabsIntent.setShowTitle(true); + customTabsIntent.setToolbarColor(XposedApp.getColor(activity)); + customTabsIntent.build().launchUrl(activity, uri); + } + + public static void startURL(Activity activity, String url) { + startURL(activity, parseURL(url)); + } + + @AnyThread + public static void showMessage(final @NonNull Context context, final CharSequence message) { + XposedApp.runOnUiThread(() -> new AlertDialog.Builder(context) + .setMessage(message) + .setPositiveButton(android.R.string.ok, null) + .show()); + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/meowcat/edxposed/manager/util/NotificationUtil.java b/app/src/main/java/org/meowcat/edxposed/manager/util/NotificationUtil.java new file mode 100644 index 000000000..9a0dbca97 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/util/NotificationUtil.java @@ -0,0 +1,313 @@ +package org.meowcat.edxposed.manager.util; + +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Build; +import android.util.Log; +import android.widget.Toast; + +import androidx.annotation.StringRes; +import androidx.core.app.NotificationCompat; + +import com.topjohnwu.superuser.Shell; + +import org.meowcat.edxposed.manager.MainActivity; +import org.meowcat.edxposed.manager.R; +import org.meowcat.edxposed.manager.XposedApp; + +import java.util.LinkedList; +import java.util.List; + +public final class NotificationUtil { + + public static final int NOTIFICATION_MODULE_NOT_ACTIVATED_YET = 0; + public static final int NOTIFICATION_MODULE_INSTALLING = 4; + private static final int NOTIFICATION_MODULES_UPDATED = 1; + private static final int NOTIFICATION_INSTALLER_UPDATE = 2; + private static final int NOTIFICATION_MODULE_INSTALLATION = 3; + private static final int PENDING_INTENT_OPEN_MODULES = 0; + private static final int PENDING_INTENT_OPEN_INSTALL = 1; + private static final int PENDING_INTENT_SOFT_REBOOT = 2; + private static final int PENDING_INTENT_REBOOT = 3; + private static final int PENDING_INTENT_ACTIVATE_MODULE_AND_REBOOT = 4; + private static final int PENDING_INTENT_ACTIVATE_MODULE = 5; + private static final int PENDING_INTENT_INSTALL_APK = 6; + + private static final String COLORED_NOTIFICATION = "colored_notification"; + private static final String HEADS_UP = "heads_up"; + private static final String FRAGMENT_ID = "fragment"; + + private static final String NOTIFICATION_UPDATE_CHANNEL = "app_update_channel"; + private static final String NOTIFICATION_MODULES_CHANNEL = "modules_channel"; + + private static Context sContext = null; + private static NotificationManager sNotificationManager; + private static SharedPreferences prefs; + + public static void init() { + if (sContext != null) return; + + sContext = XposedApp.getInstance(); + prefs = XposedApp.getPreferences(); + sNotificationManager = (NotificationManager) sContext.getSystemService(Context.NOTIFICATION_SERVICE); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel(NOTIFICATION_UPDATE_CHANNEL, sContext.getString(R.string.download_section_update_available), NotificationManager.IMPORTANCE_DEFAULT); + NotificationChannel channel1 = new NotificationChannel(NOTIFICATION_MODULES_CHANNEL, sContext.getString(R.string.nav_item_modules), NotificationManager.IMPORTANCE_DEFAULT); + sNotificationManager.createNotificationChannel(channel); + sNotificationManager.createNotificationChannel(channel1); + } + } + + public static void cancel(int id) { + sNotificationManager.cancel(id); + } + + public static void cancel(String tag, int id) { + sNotificationManager.cancel(tag, id); + } + + public static void cancelAll() { + sNotificationManager.cancelAll(); + } + + public static void showNotActivatedNotification(String packageName, String appName) { + Intent intent = new Intent(sContext, MainActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK).putExtra(FRAGMENT_ID, 1); + PendingIntent pModulesTab = PendingIntent.getActivity(sContext, PENDING_INTENT_OPEN_MODULES, intent, PendingIntent.FLAG_UPDATE_CURRENT); + + String title = sContext.getString(R.string.module_is_not_activated_yet); + NotificationCompat.Builder builder = new NotificationCompat.Builder(sContext).setContentTitle(title).setContentText(appName) + .setTicker(title).setContentIntent(pModulesTab) + .setVibrate(new long[]{0}).setAutoCancel(true) + .setSmallIcon(R.drawable.ic_notification); + + if (prefs.getBoolean(HEADS_UP, true) && Build.VERSION.SDK_INT >= 21) + builder.setPriority(2); + + if (prefs.getBoolean(COLORED_NOTIFICATION, false)) + builder.setColor(XposedApp.getColor(sContext)); + + Intent iActivateAndReboot = new Intent(sContext, RebootReceiver.class); + iActivateAndReboot.putExtra(RebootReceiver.EXTRA_ACTIVATE_MODULE, packageName); + PendingIntent pActivateAndReboot = PendingIntent.getBroadcast(sContext, PENDING_INTENT_ACTIVATE_MODULE_AND_REBOOT, + iActivateAndReboot, PendingIntent.FLAG_UPDATE_CURRENT); + + Intent iActivate = new Intent(sContext, RebootReceiver.class); + iActivate.putExtra(RebootReceiver.EXTRA_ACTIVATE_MODULE, packageName); + iActivate.putExtra(RebootReceiver.EXTRA_ACTIVATE_MODULE_AND_RETURN, true); + PendingIntent pActivate = PendingIntent.getBroadcast(sContext, PENDING_INTENT_ACTIVATE_MODULE, + iActivate, PendingIntent.FLAG_UPDATE_CURRENT); + + NotificationCompat.BigTextStyle notiStyle = new NotificationCompat.BigTextStyle(); + notiStyle.setBigContentTitle(title); + notiStyle.bigText(sContext.getString(R.string.module_is_not_activated_yet_detailed, appName)); + builder.setStyle(notiStyle).setChannelId(NOTIFICATION_MODULES_CHANNEL); + + // Only show the quick activation button if any module has been + // enabled before, + // to ensure that the user know the way to disable the module later. + if (!ModuleUtil.getInstance().getEnabledModules().isEmpty()) { + builder.addAction(new NotificationCompat.Action.Builder(R.drawable.ic_apps, sContext.getString(R.string.activate_and_reboot), pActivateAndReboot).build()); + builder.addAction(new NotificationCompat.Action.Builder(R.drawable.ic_apps, sContext.getString(R.string.activate_only), pActivate).build()); + } + + sNotificationManager.notify(packageName, NOTIFICATION_MODULE_NOT_ACTIVATED_YET, builder.build()); + } + + public static void showModulesUpdatedNotification() { + Intent intent = new Intent(sContext, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(FRAGMENT_ID, 0); + + PendingIntent pInstallTab = PendingIntent.getActivity(sContext, PENDING_INTENT_OPEN_INSTALL, + intent, PendingIntent.FLAG_UPDATE_CURRENT); + + String title = sContext + .getString(R.string.xposed_module_updated_notification_title); + String message = sContext + .getString(R.string.xposed_module_updated_notification); + NotificationCompat.Builder builder = new NotificationCompat.Builder(sContext).setContentTitle(title).setContentText(message) + .setTicker(title).setContentIntent(pInstallTab) + .setVibrate(new long[]{0}).setAutoCancel(true) + .setSmallIcon(R.drawable.ic_notification); + + if (prefs.getBoolean(HEADS_UP, true) && Build.VERSION.SDK_INT >= 21) + builder.setPriority(2); + + if (prefs.getBoolean(COLORED_NOTIFICATION, false)) + builder.setColor(XposedApp.getColor(sContext)); + + Intent iSoftReboot = new Intent(sContext, RebootReceiver.class); + iSoftReboot.putExtra(RebootReceiver.EXTRA_SOFT_REBOOT, true); + PendingIntent pSoftReboot = PendingIntent.getBroadcast(sContext, PENDING_INTENT_SOFT_REBOOT, + iSoftReboot, PendingIntent.FLAG_UPDATE_CURRENT); + + Intent iReboot = new Intent(sContext, RebootReceiver.class); + PendingIntent pReboot = PendingIntent.getBroadcast(sContext, PENDING_INTENT_REBOOT, + iReboot, PendingIntent.FLAG_UPDATE_CURRENT); + + builder.addAction(new NotificationCompat.Action.Builder(0, sContext.getString(R.string.reboot), pReboot).build()); + builder.addAction(new NotificationCompat.Action.Builder(0, sContext.getString(R.string.soft_reboot), pSoftReboot).build()); + builder.setChannelId(NOTIFICATION_MODULES_CHANNEL); + + sNotificationManager.notify(null, NOTIFICATION_MODULES_UPDATED, builder.build()); + } + + static void showModuleInstallNotification(@StringRes int title, @StringRes int message, String path, Object... args) { + showModuleInstallNotification(sContext.getString(title), sContext.getString(message, args), path, title == R.string.installation_error); + } + + private static void showModuleInstallNotification(String title, String message, String path, boolean error) { + NotificationCompat.Builder builder = new NotificationCompat.Builder( + sContext).setContentTitle(title).setContentText(message) + .setVibrate(new long[]{0}).setAutoCancel(true) + .setSmallIcon(R.drawable.ic_notification); + + if (error) { + Intent iInstallApk = new Intent(sContext, ApkReceiver.class); + iInstallApk.putExtra(ApkReceiver.EXTRA_APK_PATH, path); + PendingIntent pInstallApk = PendingIntent.getBroadcast(sContext, PENDING_INTENT_INSTALL_APK, iInstallApk, PendingIntent.FLAG_UPDATE_CURRENT); + + builder.addAction(new NotificationCompat.Action.Builder(0, sContext.getString(R.string.installation_apk_normal), pInstallApk).build()); + } + + if (prefs.getBoolean(HEADS_UP, true) && Build.VERSION.SDK_INT >= 21) + builder.setPriority(2); + + if (prefs.getBoolean(COLORED_NOTIFICATION, false)) + builder.setColor(XposedApp.getColor(sContext)); + + NotificationCompat.BigTextStyle notiStyle = new NotificationCompat.BigTextStyle(); + notiStyle.setBigContentTitle(title); + notiStyle.bigText(message); + builder.setStyle(notiStyle).setChannelId(NOTIFICATION_MODULES_CHANNEL); + + sNotificationManager.notify(null, NOTIFICATION_MODULE_INSTALLATION, builder.build()); + + new android.os.Handler().postDelayed(new Runnable() { + @Override + public void run() { + cancel(NOTIFICATION_MODULE_INSTALLATION); + } + }, 10 * 1000); + } + + public static void showModuleInstallingNotification(String appName) { + String title = sContext.getString(R.string.install_load); + String message = sContext.getString(R.string.install_load_apk, appName); + NotificationCompat.Builder builder = new NotificationCompat.Builder(sContext).setContentTitle(title).setContentText(message) + .setVibrate(new long[]{0}).setProgress(0, 0, true) + .setSmallIcon(R.drawable.ic_notification).setOngoing(true); + + if (prefs.getBoolean(COLORED_NOTIFICATION, false)) + builder.setColor(XposedApp.getColor(sContext)); + + NotificationCompat.BigTextStyle notiStyle = new NotificationCompat.BigTextStyle(); + notiStyle.setBigContentTitle(title); + notiStyle.bigText(message); + builder.setStyle(notiStyle).setChannelId(NOTIFICATION_MODULES_CHANNEL); + + sNotificationManager.notify(null, NOTIFICATION_MODULE_INSTALLING, builder.build()); + } + + public static void showInstallerUpdateNotification() { + Intent intent = new Intent(sContext, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(FRAGMENT_ID, 0); + + PendingIntent pInstallTab = PendingIntent.getActivity(sContext, PENDING_INTENT_OPEN_INSTALL, + intent, PendingIntent.FLAG_UPDATE_CURRENT); + + String title = sContext.getString(R.string.app_name); + String message = sContext.getString(R.string.newVersion); + NotificationCompat.Builder builder = new NotificationCompat.Builder(sContext).setContentTitle(title).setContentText(message) + .setTicker(title).setContentIntent(pInstallTab) + .setVibrate(new long[]{0}).setAutoCancel(true) + .setSmallIcon(R.drawable.ic_notification); + + if (prefs.getBoolean(HEADS_UP, true) && Build.VERSION.SDK_INT >= 21) + builder.setPriority(2); + + if (prefs.getBoolean(COLORED_NOTIFICATION, false)) + builder.setColor(XposedApp.getColor(sContext)); + + NotificationCompat.BigTextStyle notiStyle = new NotificationCompat.BigTextStyle(); + notiStyle.setBigContentTitle(title); + notiStyle.bigText(message); + builder.setStyle(notiStyle).setChannelId(NOTIFICATION_UPDATE_CHANNEL); + + sNotificationManager.notify(null, NOTIFICATION_INSTALLER_UPDATE, builder.build()); + } + + public static class RebootReceiver extends BroadcastReceiver { + public static String EXTRA_SOFT_REBOOT = "soft"; + public static String EXTRA_ACTIVATE_MODULE = "activate_module"; + public static String EXTRA_ACTIVATE_MODULE_AND_RETURN = "activate_module_and_return"; + + @Override + public void onReceive(Context context, Intent intent) { + /* + * Close the notification bar in order to see the toast that module + * was enabled successfully. Furthermore, if SU permissions haven't + * been granted yet, the SU dialog will be prompted behind the + * expanded notification panel and is therefore not visible to the + * user. + */ + sContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); + cancelAll(); + + if (intent.hasExtra(EXTRA_ACTIVATE_MODULE)) { + String packageName = intent.getStringExtra(EXTRA_ACTIVATE_MODULE); + ModuleUtil moduleUtil = ModuleUtil.getInstance(); + moduleUtil.setModuleEnabled(packageName, true); + moduleUtil.updateModulesList(false); + Toast.makeText(sContext, R.string.module_activated, Toast.LENGTH_SHORT).show(); + + if (intent.hasExtra(EXTRA_ACTIVATE_MODULE_AND_RETURN)) return; + } + + if (!Shell.rootAccess()) { + Log.e(XposedApp.TAG, "NotificationUtil -> Could not start root shell"); + return; + } + + List messages = new LinkedList<>(); + boolean isSoftReboot = intent.getBooleanExtra(EXTRA_SOFT_REBOOT, + false); + int returnCode = isSoftReboot + ? Shell.su("setprop ctl.restart surfaceflinger; setprop ctl.restart zygote").exec().getCode() + : Shell.su("reboot").exec().getCode(); + + if (returnCode != 0) { + Log.e(XposedApp.TAG, "NotificationUtil -> Could not reboot"); + } + } + } + + public static class ApkReceiver extends BroadcastReceiver { + public static final String EXTRA_APK_PATH = "path"; + + @Override + public void onReceive(Context context, Intent intent) { + /* + * Close the notification bar in order to see the toast that module + * was enabled successfully. Furthermore, if SU permissions haven't + * been granted yet, the SU dialog will be prompted behind the + * expanded notification panel and is therefore not visible to the + * user. + */ + sContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); + + if (intent.hasExtra(EXTRA_APK_PATH)) { + String path = intent.getStringExtra(EXTRA_APK_PATH); + InstallApkUtil.installApkNormally(context, path); + } + NotificationUtil.cancel(NotificationUtil.NOTIFICATION_MODULE_INSTALLATION); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/meowcat/edxposed/manager/util/PrefixedSharedPreferences.java b/app/src/main/java/org/meowcat/edxposed/manager/util/PrefixedSharedPreferences.java new file mode 100644 index 000000000..4dd2d9026 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/util/PrefixedSharedPreferences.java @@ -0,0 +1,161 @@ +package org.meowcat.edxposed.manager.util; + +import android.annotation.SuppressLint; +import android.content.SharedPreferences; + +import androidx.preference.PreferenceManager; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +public class PrefixedSharedPreferences implements SharedPreferences { + private final SharedPreferences mBase; + private final String mPrefix; + + public PrefixedSharedPreferences(SharedPreferences base, String prefix) { + mBase = base; + mPrefix = prefix + "_"; + } + + public static void injectToPreferenceManager(PreferenceManager manager, String prefix) { + SharedPreferences prefixedPrefs = new PrefixedSharedPreferences(manager.getSharedPreferences(), prefix); + + try { + Field fieldSharedPref = PreferenceManager.class.getDeclaredField("mSharedPreferences"); + fieldSharedPref.setAccessible(true); + fieldSharedPref.set(manager, prefixedPrefs); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + @Override + public Map getAll() { + Map baseResult = mBase.getAll(); + Map prefixedResult = new HashMap(baseResult); + for (Entry entry : baseResult.entrySet()) { + prefixedResult.put(mPrefix + entry.getKey(), entry.getValue()); + } + return prefixedResult; + } + + @Override + public String getString(String key, String defValue) { + return mBase.getString(mPrefix + key, defValue); + } + + @Override + public Set getStringSet(String key, Set defValues) { + return mBase.getStringSet(mPrefix + key, defValues); + } + + @Override + public int getInt(String key, int defValue) { + return mBase.getInt(mPrefix + key, defValue); + } + + @Override + public long getLong(String key, long defValue) { + return mBase.getLong(mPrefix + key, defValue); + } + + @Override + public float getFloat(String key, float defValue) { + return mBase.getFloat(mPrefix + key, defValue); + } + + @Override + public boolean getBoolean(String key, boolean defValue) { + return mBase.getBoolean(mPrefix + key, defValue); + } + + @Override + public boolean contains(String key) { + return mBase.contains(mPrefix + key); + } + + @SuppressLint("CommitPrefEdits") + @Override + public Editor edit() { + return new EditorImpl(mBase.edit()); + } + + @Override + public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { + throw new UnsupportedOperationException("listeners are not supported in this implementation"); + } + + @Override + public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { + throw new UnsupportedOperationException("listeners are not supported in this implementation"); + } + + private class EditorImpl implements Editor { + private final Editor mEditorBase; + + public EditorImpl(Editor base) { + mEditorBase = base; + } + + @Override + public Editor putString(String key, String value) { + mEditorBase.putString(mPrefix + key, value); + return this; + } + + @Override + public Editor putStringSet(String key, Set values) { + mEditorBase.putStringSet(mPrefix + key, values); + return this; + } + + @Override + public Editor putInt(String key, int value) { + mEditorBase.putInt(mPrefix + key, value); + return this; + } + + @Override + public Editor putLong(String key, long value) { + mEditorBase.putLong(mPrefix + key, value); + return this; + } + + @Override + public Editor putFloat(String key, float value) { + mEditorBase.putFloat(mPrefix + key, value); + return this; + } + + @Override + public Editor putBoolean(String key, boolean value) { + mEditorBase.putBoolean(mPrefix + key, value); + return this; + } + + @Override + public Editor remove(String key) { + mEditorBase.remove(mPrefix + key); + return this; + } + + @Override + public Editor clear() { + mEditorBase.clear(); + return this; + } + + @Override + public boolean commit() { + return mEditorBase.commit(); + } + + @Override + public void apply() { + mEditorBase.apply(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/meowcat/edxposed/manager/util/RepoLoader.java b/app/src/main/java/org/meowcat/edxposed/manager/util/RepoLoader.java new file mode 100644 index 000000000..d98dc5b48 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/util/RepoLoader.java @@ -0,0 +1,437 @@ +package org.meowcat.edxposed.manager.util; + +import android.content.Context; +import android.content.SharedPreferences; +import android.database.sqlite.SQLiteException; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.text.TextUtils; +import android.util.Log; +import android.widget.Toast; + +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +import org.meowcat.edxposed.manager.R; +import org.meowcat.edxposed.manager.XposedApp; +import org.meowcat.edxposed.manager.repo.Module; +import org.meowcat.edxposed.manager.repo.ModuleVersion; +import org.meowcat.edxposed.manager.repo.ReleaseType; +import org.meowcat.edxposed.manager.repo.RepoDb; +import org.meowcat.edxposed.manager.repo.RepoParser; +import org.meowcat.edxposed.manager.repo.RepoParser.RepoParserCallback; +import org.meowcat.edxposed.manager.repo.Repository; +import org.meowcat.edxposed.manager.util.DownloadsUtil.SyncDownloadInfo; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.zip.GZIPInputStream; + +public class RepoLoader { + private static final int UPDATE_FREQUENCY = 24 * 60 * 60 * 1000; + private static String DEFAULT_REPOSITORIES; + private static RepoLoader mInstance = null; + private final List mListeners = new CopyOnWriteArrayList<>(); + private final Map mLocalReleaseTypesCache = new HashMap<>(); + private XposedApp mApp = null; + private SharedPreferences mPref; + private SharedPreferences mModulePref; + private ConnectivityManager mConMgr; + private boolean mIsLoading = false; + private boolean mReloadTriggeredOnce = false; + private Map mRepositories = null; + private ReleaseType mGlobalReleaseType; + private SwipeRefreshLayout mSwipeRefreshLayout; + + private RepoLoader() { + mInstance = this; + mApp = XposedApp.getInstance(); + mPref = mApp.getSharedPreferences("repo", Context.MODE_PRIVATE); + DEFAULT_REPOSITORIES = XposedApp.getPreferences().getBoolean("custom_list", false) ? "https://cdn.jsdelivr.net/gh/ElderDrivers/Repository-Website@gh-pages/assets/full.xml.gz" : "https://dl-xda.xposed.info/repo/full.xml.gz"; + mModulePref = mApp.getSharedPreferences("module_settings", Context.MODE_PRIVATE); + mConMgr = (ConnectivityManager) mApp.getSystemService(Context.CONNECTIVITY_SERVICE); + mGlobalReleaseType = ReleaseType.fromString(XposedApp.getPreferences().getString("release_type_global", "stable")); + refreshRepositories(); + } + + public static synchronized RepoLoader getInstance() { + if (mInstance == null) + new RepoLoader(); + return mInstance; + } + + private boolean refreshRepositories() { + mRepositories = RepoDb.getRepositories(); + + // Unlikely case (usually only during initial load): DB state doesn't + // fit to configuration + boolean needReload = false; + String[] config = (mPref.getString("repositories", DEFAULT_REPOSITORIES) + "").split("\\|"); + if (mRepositories.size() != config.length) { + needReload = true; + } else { + int i = 0; + for (Repository repo : mRepositories.values()) { + if (!repo.url.equals(config[i++])) { + needReload = true; + break; + } + } + } + + if (!needReload) + return false; + + clear(false); + for (String url : config) { + RepoDb.insertRepository(url); + } + mRepositories = RepoDb.getRepositories(); + return true; + } + + public void setReleaseTypeGlobal(String relTypeString) { + ReleaseType relType = ReleaseType.fromString(relTypeString); + if (mGlobalReleaseType == relType) + return; + + mGlobalReleaseType = relType; + + // Updating the latest version for all modules takes a moment + new Thread("DBUpdate") { + @Override + public void run() { + RepoDb.updateAllModulesLatestVersion(); + notifyListeners(); + } + }.start(); + } + + public void setReleaseTypeLocal(String packageName, String relTypeString) { + ReleaseType relType = (!TextUtils.isEmpty(relTypeString)) ? ReleaseType.fromString(relTypeString) : null; + + if (getReleaseTypeLocal(packageName) == relType) + return; + + synchronized (mLocalReleaseTypesCache) { + mLocalReleaseTypesCache.put(packageName, relType); + } + + RepoDb.updateModuleLatestVersion(packageName); + notifyListeners(); + } + + private ReleaseType getReleaseTypeLocal(String packageName) { + synchronized (mLocalReleaseTypesCache) { + if (mLocalReleaseTypesCache.containsKey(packageName)) + return mLocalReleaseTypesCache.get(packageName); + + String value = mModulePref.getString(packageName + "_release_type", + null); + ReleaseType result = (!TextUtils.isEmpty(value)) ? ReleaseType.fromString(value) : null; + mLocalReleaseTypesCache.put(packageName, result); + return result; + } + } + + public Repository getRepository(long repoId) { + return mRepositories.get(repoId); + } + + public Module getModule(String packageName) { + return RepoDb.getModuleByPackageName(packageName); + } + + public ModuleVersion getLatestVersion(Module module) { + if (module == null || module.versions.isEmpty()) + return null; + + for (ModuleVersion version : module.versions) { + if (version.downloadLink != null && isVersionShown(version)) + return version; + } + return null; + } + + public boolean isVersionShown(ModuleVersion version) { + return version.relType + .ordinal() <= getMaxShownReleaseType(version.module.packageName).ordinal(); + } + + public ReleaseType getMaxShownReleaseType(String packageName) { + ReleaseType localSetting = getReleaseTypeLocal(packageName); + if (localSetting != null) + return localSetting; + else + return mGlobalReleaseType; + } + + public void triggerReload(final boolean force) { + mReloadTriggeredOnce = true; + + if (force) { + resetLastUpdateCheck(); + } else { + long lastUpdateCheck = mPref.getLong("last_update_check", 0); + if (System.currentTimeMillis() < lastUpdateCheck + UPDATE_FREQUENCY) + return; + } + + NetworkInfo netInfo = mConMgr.getActiveNetworkInfo(); + if (netInfo == null || !netInfo.isConnected()) + return; + + synchronized (this) { + if (mIsLoading) + return; + mIsLoading = true; + } + mApp.updateProgressIndicator(mSwipeRefreshLayout); + + new Thread("RepositoryReload") { + public void run() { + final List messages = new LinkedList<>(); + boolean hasChanged = downloadAndParseFiles(messages); + + mPref.edit().putLong("last_update_check", System.currentTimeMillis()).apply(); + + if (!messages.isEmpty()) { + XposedApp.runOnUiThread(new Runnable() { + public void run() { + for (String message : messages) { + Toast.makeText(mApp, message, Toast.LENGTH_LONG).show(); + } + } + }); + } + + if (hasChanged) + notifyListeners(); + + synchronized (this) { + mIsLoading = false; + } + mApp.updateProgressIndicator(mSwipeRefreshLayout); + } + }.start(); + } + + public void setSwipeRefreshLayout(SwipeRefreshLayout mSwipeRefreshLayout) { + this.mSwipeRefreshLayout = mSwipeRefreshLayout; + } + + public void triggerFirstLoadIfNecessary() { + if (!mReloadTriggeredOnce) + triggerReload(false); + } + + public void resetLastUpdateCheck() { + mPref.edit().remove("last_update_check").apply(); + } + + public synchronized boolean isLoading() { + return mIsLoading; + } + + public void clear(boolean notify) { + synchronized (this) { + // TODO Stop reloading repository when it should be cleared + if (mIsLoading) + return; + + RepoDb.deleteRepositories(); + mRepositories = new LinkedHashMap(0); + DownloadsUtil.clearCache(null); + resetLastUpdateCheck(); + } + + if (notify) + notifyListeners(); + } + + public void setRepositories(String... repos) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < repos.length; i++) { + if (i > 0) + sb.append("|"); + sb.append(repos[i]); + } + mPref.edit().putString("repositories", sb.toString()).apply(); + if (refreshRepositories()) + triggerReload(true); + } + + public boolean hasModuleUpdates() { + return RepoDb.hasModuleUpdates(); + } + + public String getFrameworkUpdateVersion() { + return RepoDb.getFrameworkUpdateVersion(); + } + + private File getRepoCacheFile(String repo) { + String filename = "repo_" + HashUtil.md5(repo) + ".xml"; + if (repo.endsWith(".gz")) + filename += ".gz"; + return new File(mApp.getCacheDir(), filename); + } + + private boolean downloadAndParseFiles(List messages) { + // These variables don't need to be atomic, just mutable + final AtomicBoolean hasChanged = new AtomicBoolean(false); + final AtomicInteger insertCounter = new AtomicInteger(); + final AtomicInteger deleteCounter = new AtomicInteger(); + + for (Entry repoEntry : mRepositories.entrySet()) { + final long repoId = repoEntry.getKey(); + final Repository repo = repoEntry.getValue(); + + String url = (repo.partialUrl != null && repo.version != null) ? String.format(repo.partialUrl, repo.version) : repo.url; + + File cacheFile = getRepoCacheFile(url); + SyncDownloadInfo info = DownloadsUtil.downloadSynchronously(url, + cacheFile); + + Log.i(XposedApp.TAG, String.format( + "RepoLoader -> Downloaded %s with status %d (error: %s), size %d bytes", + url, info.status, info.errorMessage, cacheFile.length())); + + if (info.status != SyncDownloadInfo.STATUS_SUCCESS) { + if (info.errorMessage != null) + messages.add(info.errorMessage); + continue; + } + + InputStream in = null; + RepoDb.beginTransation(); + try { + in = new FileInputStream(cacheFile); + if (url.endsWith(".gz")) + in = new GZIPInputStream(in); + + RepoParser.parse(in, new RepoParserCallback() { + @Override + public void onRepositoryMetadata(Repository repository) { + if (!repository.isPartial) { + RepoDb.deleteAllModules(repoId); + hasChanged.set(true); + } + } + + @Override + public void onNewModule(Module module) { + RepoDb.insertModule(repoId, module); + hasChanged.set(true); + insertCounter.incrementAndGet(); + } + + @Override + public void onRemoveModule(String packageName) { + RepoDb.deleteModule(repoId, packageName); + hasChanged.set(true); + deleteCounter.decrementAndGet(); + } + + @Override + public void onCompleted(Repository repository) { + if (!repository.isPartial) { + RepoDb.updateRepository(repoId, repository); + repo.name = repository.name; + repo.partialUrl = repository.partialUrl; + repo.version = repository.version; + } else { + RepoDb.updateRepositoryVersion(repoId, repository.version); + repo.version = repository.version; + } + + Log.i(XposedApp.TAG, String.format( + "RepoLoader -> Updated repository %s to version %s (%d new / %d removed modules)", + repo.url, repo.version, insertCounter.get(), + deleteCounter.get())); + } + }); + + RepoDb.setTransactionSuccessful(); + } catch (SQLiteException e) { + XposedApp.runOnUiThread(new Runnable() { + @Override + public void run() { + /*new MaterialDialog.Builder(DownloadFragment.sActivity) + .title(R.string.restart_needed) + .content(R.string.cache_cleaned) + .onPositive(new MaterialDialog.SingleButtonCallback() { + @Override + public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { + Intent i = new Intent(DownloadFragment.sActivity, WelcomeActivity.class); + i.putExtra("fragment", 2); + + PendingIntent pi = PendingIntent.getActivity(DownloadFragment.sActivity, 0, i, PendingIntent.FLAG_CANCEL_CURRENT); + + AlarmManager mgr = (AlarmManager) mApp.getSystemService(Context.ALARM_SERVICE); + mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, pi); + System.exit(0); + } + }) + .positiveText(R.string.ok) + .canceledOnTouchOutside(false) + .show();*/ + } + }); + + DownloadsUtil.clearCache(url); + } catch (Throwable t) { + Log.e(XposedApp.TAG, "RepoLoader -> Cannot load repository from " + url, t); + messages.add(mApp.getString(R.string.repo_load_failed, url, t.getMessage())); + DownloadsUtil.clearCache(url); + } finally { + if (in != null) + try { + in.close(); + } catch (IOException ignored) { + } + cacheFile.delete(); + RepoDb.endTransation(); + } + } + + // TODO Set ModuleColumns.PREFERRED for modules which appear in multiple + // repositories + return hasChanged.get(); + } + + public void addListener(RepoListener listener, boolean triggerImmediately) { + if (!mListeners.contains(listener)) + mListeners.add(listener); + + if (triggerImmediately) + listener.onRepoReloaded(this); + } + + public void removeListener(RepoListener listener) { + mListeners.remove(listener); + } + + private void notifyListeners() { + for (RepoListener listener : mListeners) { + listener.onRepoReloaded(mInstance); + } + } + + public interface RepoListener { + /** + * Called whenever the list of modules from repositories has been + * successfully reloaded + */ + void onRepoReloaded(RepoLoader loader); + } +} diff --git a/app/src/main/java/org/meowcat/edxposed/manager/util/ToastUtil.java b/app/src/main/java/org/meowcat/edxposed/manager/util/ToastUtil.java new file mode 100644 index 000000000..fa8d602ae --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/util/ToastUtil.java @@ -0,0 +1,18 @@ +package org.meowcat.edxposed.manager.util; + +import android.content.Context; +import android.widget.Toast; + +import androidx.annotation.StringRes; + +public class ToastUtil { + + public static void showShortToast(Context context, @StringRes int resId) { + Toast.makeText(context, resId, Toast.LENGTH_SHORT).show(); + } + + public static void showLongToast(Context context, @StringRes int resId) { + Toast.makeText(context, resId, Toast.LENGTH_LONG).show(); + } + +} diff --git a/app/src/main/java/org/meowcat/edxposed/manager/util/chrome/CustomTabsURLSpan.java b/app/src/main/java/org/meowcat/edxposed/manager/util/chrome/CustomTabsURLSpan.java new file mode 100644 index 000000000..cd75146c4 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/util/chrome/CustomTabsURLSpan.java @@ -0,0 +1,26 @@ +package org.meowcat.edxposed.manager.util.chrome; + +import android.app.Activity; +import android.text.style.URLSpan; +import android.view.View; + +import org.meowcat.edxposed.manager.util.NavUtil; + +/** + * Created by Nikola D. on 12/23/2015. + */ +public class CustomTabsURLSpan extends URLSpan { + + private Activity activity; + + CustomTabsURLSpan(Activity activity, String url) { + super(url); + this.activity = activity; + } + + @Override + public void onClick(View widget) { + String url = getURL(); + NavUtil.startURL(activity, url); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/meowcat/edxposed/manager/util/chrome/LinkTransformationMethod.java b/app/src/main/java/org/meowcat/edxposed/manager/util/chrome/LinkTransformationMethod.java new file mode 100644 index 000000000..40eea65ce --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/util/chrome/LinkTransformationMethod.java @@ -0,0 +1,50 @@ +package org.meowcat.edxposed.manager.util.chrome; + +import android.app.Activity; +import android.graphics.Rect; +import android.text.Spannable; +import android.text.Spanned; +import android.text.method.TransformationMethod; +import android.text.style.URLSpan; +import android.text.util.Linkify; +import android.view.View; +import android.widget.TextView; + +/** + * Created by Nikola D. on 12/23/2015. + */ +public class LinkTransformationMethod implements TransformationMethod { + + private Activity activity; + + public LinkTransformationMethod(Activity activity) { + this.activity = activity; + } + + @Override + public CharSequence getTransformation(CharSequence source, View view) { + if (view instanceof TextView) { + TextView textView = (TextView) view; + Linkify.addLinks(textView, Linkify.WEB_URLS); + if (textView.getText() == null || !(textView.getText() instanceof Spannable)) { + return source; + } + Spannable text = (Spannable) textView.getText(); + URLSpan[] spans = text.getSpans(0, textView.length(), URLSpan.class); + for (int i = spans.length - 1; i >= 0; i--) { + URLSpan oldSpan = spans[i]; + int start = text.getSpanStart(oldSpan); + int end = text.getSpanEnd(oldSpan); + String url = oldSpan.getURL(); + text.removeSpan(oldSpan); + text.setSpan(new CustomTabsURLSpan(activity, url), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + return text; + } + return source; + } + + @Override + public void onFocusChanged(View view, CharSequence sourceText, boolean focused, int direction, Rect previouslyFocusedRect) { + } +} \ No newline at end of file diff --git a/app/src/main/java/org/meowcat/edxposed/manager/util/json/JSONUtils.java b/app/src/main/java/org/meowcat/edxposed/manager/util/json/JSONUtils.java new file mode 100644 index 000000000..5e7710039 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/util/json/JSONUtils.java @@ -0,0 +1,97 @@ +package org.meowcat.edxposed.manager.util.json; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.List; + +public class JSONUtils { + + public static final String JSON_LINK = "http://edxp.meowcat.org/assets/version.json"; + + public static String getFileContent(String url) throws IOException { + HttpURLConnection c = (HttpURLConnection) new URL(url).openConnection(); + c.setRequestMethod("GET"); + c.setInstanceFollowRedirects(false); + c.setDoOutput(false); + c.connect(); + + BufferedReader br = new BufferedReader(new InputStreamReader(c.getInputStream())); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) { + sb.append(line); + } + br.close(); + + return sb.toString(); + } + +// private static String getLatestVersion() throws IOException { +// String site = getFileContent("http://dl-xda.xposed.info/framework/sdk" + Build.VERSION.SDK_INT + "/arm/"); +// +// Pattern pattern = Pattern.compile("(href=\")([^?\"]*)\\.zip"); +// Matcher matcher = pattern.matcher(site); +// String last = ""; +// while (matcher.find()) { +// if (matcher.group().contains("test")) continue; +// last = matcher.group(); +// } +// last = last.replace("href=\"", ""); +// String[] file = last.split("-"); +// +// return file[1].replace("v", ""); +// } +// +// public static String listZip() { +// String latest; +// try { +// latest = getLatestVersion(); +// } catch (IOException e) { +// // Got 404 response; no official Xposed zips available +// return ""; +// } +// +// StringBuilder newJson = new StringBuilder(",\"" + Build.VERSION.SDK_INT + "\": ["); +// String[] arch = new String[]{ +// "arm", +// "arm64", +// "x86" +// }; +// +// for (String a : arch) { +// newJson.append(installerToString(latest, a)).append(","); +// } +// +// newJson = new StringBuilder(newJson.substring(0, newJson.length() - 1)); +// newJson.append("]"); +// +// return newJson.toString(); +// } +// +// private static String installerToString(String latest, String architecture) { +// String filename = "xposed-v" + latest + "-sdk" + Build.VERSION.SDK_INT + "-" + architecture; +// +// XposedZip installer = new XposedZip(); +// installer.name = filename; +// installer.version = latest; +// installer.architecture = architecture; +// installer.link = "http://dl-xda.xposed.info/framework/sdk" + Build.VERSION.SDK_INT + "/" + architecture + "/" + filename + ".zip"; +// +// return new Gson().toJson(installer); +// } + + public class XposedJson { + public List tabs; + public ApkRelease apk; + } + + public class ApkRelease { + public String version; + public String changelog; + public String link; + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/meowcat/edxposed/manager/util/json/XposedTab.java b/app/src/main/java/org/meowcat/edxposed/manager/util/json/XposedTab.java new file mode 100644 index 000000000..3c8b17a98 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/util/json/XposedTab.java @@ -0,0 +1,91 @@ +package org.meowcat.edxposed.manager.util.json; + + +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.ArrayList; +import java.util.List; + +@SuppressWarnings("MismatchedQueryAndUpdateOfCollection") +public class XposedTab implements Parcelable { + + public static final Creator CREATOR = new Creator() { + @Override + public XposedTab createFromParcel(Parcel in) { + return new XposedTab(in); + } + + @Override + public XposedTab[] newArray(int size) { + return new XposedTab[size]; + } + }; + + public List sdks = new ArrayList<>(); + public String name; + public String author; + public String description; + public String support; + public String notice; + public boolean stable; + public boolean official; + public List installers = new ArrayList<>(); + public List uninstallers = new ArrayList<>(); +// private HashMap compatibility = new HashMap<>(); +// private HashMap incompatibility = new HashMap<>(); + +// public XposedTab() { +// } + + private XposedTab(Parcel in) { + name = in.readString(); + author = in.readString(); + description = in.readString(); + support = in.readString(); + notice = in.readString(); + stable = in.readByte() != 0; + official = in.readByte() != 0; + } + +// public String getNotice() { +// if (notice == null) return ""; +// return notice.get(Integer.toString(Build.VERSION.SDK_INT)); +// } + +// public String getCompatibility() { +// if (compatibility == null) return ""; +// return compatibility.get(Integer.toString(Build.VERSION.SDK_INT)); +// } +// +// public String getIncompatibility() { +// if (incompatibility == null) return ""; +// return incompatibility.get(Integer.toString(Build.VERSION.SDK_INT)); +// } + +// public String getSupport() { +// if (support == null) return ""; +// return support.get(Integer.toString(Build.VERSION.SDK_INT)); +// } +// +// public List getInstallers() { +// if (support == null) return new ArrayList<>(); +// return installers.get(Integer.toString(Build.VERSION.SDK_INT)); +// } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(name); + dest.writeString(author); + dest.writeString(description); + dest.writeString(support); + dest.writeString(notice); + dest.writeByte((byte) (stable ? 1 : 0)); + dest.writeByte((byte) (official ? 1 : 0)); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/meowcat/edxposed/manager/util/json/XposedZip.java b/app/src/main/java/org/meowcat/edxposed/manager/util/json/XposedZip.java new file mode 100644 index 000000000..b9f689679 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/util/json/XposedZip.java @@ -0,0 +1,66 @@ +package org.meowcat.edxposed.manager.util.json; + +import android.app.Activity; +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.List; + +public class XposedZip { + + public String name; + public String link; + public String version; + public String description; + + public static class MyAdapter extends ArrayAdapter { + + private final Context context; + List list; + + public MyAdapter(Context context, List objects) { + super(context, android.R.layout.simple_dropdown_item_1line, objects); + this.context = context; + this.list = objects; + } + + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + return getMyView(parent, position); + } + + @Override + public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + return getMyView(parent, position); + } + + private View getMyView(ViewGroup parent, int position) { + View row; + ItemHolder holder = new ItemHolder(); + + LayoutInflater inflater = ((Activity) context).getLayoutInflater(); + row = inflater.inflate(android.R.layout.simple_dropdown_item_1line, parent, false); + + holder.name = row.findViewById(android.R.id.text1); + + row.setTag(holder); + + holder.name.setText(list.get(position).name); + return row; + } + + private class ItemHolder { + TextView name; + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/meowcat/edxposed/manager/widget/DownloadView.java b/app/src/main/java/org/meowcat/edxposed/manager/widget/DownloadView.java new file mode 100644 index 000000000..142a10e42 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/widget/DownloadView.java @@ -0,0 +1,257 @@ +package org.meowcat.edxposed.manager.widget; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.app.DownloadManager; +import android.content.Context; +import android.content.pm.PackageManager; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.core.app.ActivityCompat; +import androidx.fragment.app.Fragment; + +import org.meowcat.edxposed.manager.R; +import org.meowcat.edxposed.manager.util.DownloadsUtil; +import org.meowcat.edxposed.manager.util.DownloadsUtil.DownloadFinishedCallback; + +import java.util.Objects; + +import static org.meowcat.edxposed.manager.XposedApp.WRITE_EXTERNAL_PERMISSION; + +public class DownloadView extends LinearLayout { + @SuppressLint("StaticFieldLeak") + public static Button mClickedButton; + private final Button btnDownload; + private final Button btnDownloadCancel; + private final Button btnInstall; + private final Button btnRemove; + private final Button btnSave; + private final ProgressBar progressBar; + private final TextView txtInfo; + public Fragment fragment; + private DownloadsUtil.DownloadInfo mInfo = null; + private String mUrl = null; + private final Runnable refreshViewRunnable = new Runnable() { + @Override + public void run() { + if (mUrl == null) { + btnDownload.setVisibility(View.GONE); + btnSave.setVisibility(View.GONE); + btnDownloadCancel.setVisibility(View.GONE); + btnRemove.setVisibility(View.GONE); + btnInstall.setVisibility(View.GONE); + progressBar.setVisibility(View.GONE); + txtInfo.setVisibility(View.VISIBLE); + txtInfo.setText(R.string.download_view_no_url); + } else if (mInfo == null) { + btnDownload.setVisibility(View.VISIBLE); + btnSave.setVisibility(View.VISIBLE); + btnDownloadCancel.setVisibility(View.GONE); + btnRemove.setVisibility(View.GONE); + btnInstall.setVisibility(View.GONE); + progressBar.setVisibility(View.GONE); + txtInfo.setVisibility(View.GONE); + } else { + switch (mInfo.status) { + case DownloadManager.STATUS_PENDING: + case DownloadManager.STATUS_PAUSED: + case DownloadManager.STATUS_RUNNING: + btnDownload.setVisibility(View.GONE); + btnSave.setVisibility(View.GONE); + btnDownloadCancel.setVisibility(View.VISIBLE); + btnRemove.setVisibility(View.GONE); + btnInstall.setVisibility(View.GONE); + progressBar.setVisibility(View.VISIBLE); + txtInfo.setVisibility(View.VISIBLE); + if (mInfo.totalSize <= 0 || mInfo.status != DownloadManager.STATUS_RUNNING) { + progressBar.setIndeterminate(true); + txtInfo.setText(R.string.download_view_waiting); + } else { + progressBar.setIndeterminate(false); + progressBar.setMax(mInfo.totalSize); + progressBar.setProgress(mInfo.bytesDownloaded); + txtInfo.setText(getContext().getString( + R.string.download_view_running, + mInfo.bytesDownloaded / 1024, + mInfo.totalSize / 1024)); + } + break; + + case DownloadManager.STATUS_FAILED: + btnDownload.setVisibility(View.VISIBLE); + btnSave.setVisibility(View.VISIBLE); + btnDownloadCancel.setVisibility(View.GONE); + btnRemove.setVisibility(View.GONE); + btnInstall.setVisibility(View.GONE); + progressBar.setVisibility(View.GONE); + txtInfo.setVisibility(View.VISIBLE); + txtInfo.setText(getContext().getString( + R.string.download_view_failed, mInfo.reason)); + break; + + case DownloadManager.STATUS_SUCCESSFUL: + btnDownload.setVisibility(View.GONE); + btnSave.setVisibility(View.GONE); + btnDownloadCancel.setVisibility(View.GONE); + btnRemove.setVisibility(View.VISIBLE); + btnInstall.setVisibility(View.VISIBLE); + progressBar.setVisibility(View.GONE); + txtInfo.setVisibility(View.VISIBLE); + txtInfo.setText(R.string.download_view_successful); + break; + } + } + } + }; + private String mTitle = null; + private DownloadFinishedCallback mCallback = null; + + public DownloadView(Context context, final AttributeSet attrs) { + super(context, attrs); + setFocusable(false); + setOrientation(LinearLayout.VERTICAL); + + LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + Objects.requireNonNull(inflater).inflate(R.layout.download_view, this, true); + + btnDownload = findViewById(R.id.btnDownload); + btnDownloadCancel = findViewById(R.id.btnDownloadCancel); + btnRemove = findViewById(R.id.btnRemove); + btnInstall = findViewById(R.id.btnInstall); + btnSave = findViewById(R.id.save); + + btnDownload.setOnClickListener(v -> { + mClickedButton = btnDownload; + + mInfo = DownloadsUtil.addModule(getContext(), mTitle, mUrl, false, mCallback); + refreshViewFromUiThread(); + + if (mInfo != null) + new DownloadMonitor().start(); + }); + + btnSave.setOnClickListener(v -> { + mClickedButton = btnSave; + + if (checkPermissions()) + return; + + mInfo = DownloadsUtil.addModule(getContext(), mTitle, mUrl, true, (context1, info) -> Toast.makeText(context1, context1.getString(R.string.module_saved, info.localFilename), Toast.LENGTH_SHORT).show()); + }); + + btnDownloadCancel.setOnClickListener(v -> { + if (mInfo == null) + return; + + DownloadsUtil.removeById(getContext(), mInfo.id); + // UI update will happen automatically by the DownloadMonitor + }); + + btnRemove.setOnClickListener(v -> { + if (mInfo == null) + return; + + DownloadsUtil.removeById(getContext(), mInfo.id); + // UI update will happen automatically by the DownloadMonitor + }); + + btnInstall.setOnClickListener(v -> { + if (mCallback == null) + return; + + mCallback.onDownloadFinished(getContext(), mInfo); + }); + + progressBar = findViewById(R.id.progress); + txtInfo = findViewById(R.id.txtInfo); + + refreshViewFromUiThread(); + } + + private boolean checkPermissions() { + if (ActivityCompat.checkSelfPermission(this.getContext(), + Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + fragment.requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, WRITE_EXTERNAL_PERMISSION); + return true; + } + return false; + } + + private void refreshViewFromUiThread() { + refreshViewRunnable.run(); + } + + private void refreshView() { + post(refreshViewRunnable); + } + + public String getUrl() { + return mUrl; + } + + public void setUrl(String url) { + mUrl = url; + + if (mUrl != null) + mInfo = DownloadsUtil.getLatestForUrl(getContext(), mUrl); + else + mInfo = null; + + refreshView(); + } + + public String getTitle() { + return mTitle; + } + + public void setTitle(String title) { + this.mTitle = title; + } + + @SuppressWarnings("unused") + public DownloadFinishedCallback getDownloadFinishedCallback() { + return mCallback; + } + + public void setDownloadFinishedCallback(DownloadFinishedCallback downloadFinishedCallback) { + this.mCallback = downloadFinishedCallback; + } + + private class DownloadMonitor extends Thread { + DownloadMonitor() { + super("DownloadMonitor"); + } + + @Override + public void run() { + while (true) { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + return; + } + + try { + mInfo = DownloadsUtil.getById(getContext(), mInfo.id); + } catch (NullPointerException ignored) { + } + + refreshView(); + if (mInfo == null) + return; + + if (mInfo.status != DownloadManager.STATUS_PENDING + && mInfo.status != DownloadManager.STATUS_PAUSED + && mInfo.status != DownloadManager.STATUS_RUNNING) + return; + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/meowcat/edxposed/manager/widget/IntegerListPreference.java b/app/src/main/java/org/meowcat/edxposed/manager/widget/IntegerListPreference.java new file mode 100644 index 000000000..ac5c98b85 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/widget/IntegerListPreference.java @@ -0,0 +1,60 @@ +package org.meowcat.edxposed.manager.widget; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.AttributeSet; + +public class IntegerListPreference extends com.takisoft.preferencex.SimpleMenuPreference { + public IntegerListPreference(Context context) { + super(context); + } + + public IntegerListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public static int getIntValue(String value) { + if (value == null) + return 0; + + return (int) ((value.startsWith("0x")) + ? Long.parseLong(value.substring(2), 16) + : Long.parseLong(value)); + } + + @Override + public void setValue(String value) { + super.setValue(value); + notifyChanged(); + } + + @Override + protected boolean persistString(String value) { + return value != null && persistInt(getIntValue(value)); + + } + + @Override + protected String getPersistedString(String defaultReturnValue) { + SharedPreferences pref = getPreferenceManager().getSharedPreferences(); + String key = getKey(); + if (!shouldPersist() || !pref.contains(key)) + return defaultReturnValue; + + return String.valueOf(pref.getInt(key, 0)); + } + + @Override + public int findIndexOfValue(String value) { + CharSequence[] entryValues = getEntryValues(); + int intValue = getIntValue(value); + if (value != null && entryValues != null) { + for (int i = entryValues.length - 1; i >= 0; i--) { + if (getIntValue(entryValues[i].toString()) == intValue) { + return i; + } + } + } + return -1; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/meowcat/edxposed/manager/widget/ListPreferenceSummaryFix.java b/app/src/main/java/org/meowcat/edxposed/manager/widget/ListPreferenceSummaryFix.java new file mode 100644 index 000000000..ca183c1d4 --- /dev/null +++ b/app/src/main/java/org/meowcat/edxposed/manager/widget/ListPreferenceSummaryFix.java @@ -0,0 +1,22 @@ +package org.meowcat.edxposed.manager.widget; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.preference.ListPreference; + +public class ListPreferenceSummaryFix extends ListPreference { + public ListPreferenceSummaryFix(Context context) { + super(context); + } + + public ListPreferenceSummaryFix(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void setValue(String value) { + super.setValue(value); + notifyChanged(); + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..1f6bb2906 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_android.xml b/app/src/main/res/drawable/ic_android.xml new file mode 100644 index 000000000..37cdbbf14 --- /dev/null +++ b/app/src/main/res/drawable/ic_android.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_apps.xml b/app/src/main/res/drawable/ic_apps.xml new file mode 100644 index 000000000..2d903a92e --- /dev/null +++ b/app/src/main/res/drawable/ic_apps.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_assignment.xml b/app/src/main/res/drawable/ic_assignment.xml new file mode 100644 index 000000000..6fd06e9df --- /dev/null +++ b/app/src/main/res/drawable/ic_assignment.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_bug.xml b/app/src/main/res/drawable/ic_bug.xml new file mode 100644 index 000000000..7b4e21c1b --- /dev/null +++ b/app/src/main/res/drawable/ic_bug.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_check_circle.xml b/app/src/main/res/drawable/ic_check_circle.xml new file mode 100644 index 000000000..7d778e529 --- /dev/null +++ b/app/src/main/res/drawable/ic_check_circle.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_chip.xml b/app/src/main/res/drawable/ic_chip.xml new file mode 100644 index 000000000..67847c5f6 --- /dev/null +++ b/app/src/main/res/drawable/ic_chip.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_description.xml b/app/src/main/res/drawable/ic_description.xml new file mode 100644 index 000000000..d87d4019f --- /dev/null +++ b/app/src/main/res/drawable/ic_description.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_donate.xml b/app/src/main/res/drawable/ic_donate.xml new file mode 100644 index 000000000..4cc67358a --- /dev/null +++ b/app/src/main/res/drawable/ic_donate.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_error.xml b/app/src/main/res/drawable/ic_error.xml new file mode 100644 index 000000000..cce259fa8 --- /dev/null +++ b/app/src/main/res/drawable/ic_error.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_framework.xml b/app/src/main/res/drawable/ic_framework.xml new file mode 100644 index 000000000..76deadaa7 --- /dev/null +++ b/app/src/main/res/drawable/ic_framework.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_get_app.xml b/app/src/main/res/drawable/ic_get_app.xml new file mode 100644 index 000000000..09cd9664c --- /dev/null +++ b/app/src/main/res/drawable/ic_get_app.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_github.xml b/app/src/main/res/drawable/ic_github.xml new file mode 100644 index 000000000..47f21458e --- /dev/null +++ b/app/src/main/res/drawable/ic_github.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_help.xml b/app/src/main/res/drawable/ic_help.xml new file mode 100644 index 000000000..a3f80358e --- /dev/null +++ b/app/src/main/res/drawable/ic_help.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_history.xml b/app/src/main/res/drawable/ic_history.xml new file mode 100644 index 000000000..3daf095bc --- /dev/null +++ b/app/src/main/res/drawable/ic_history.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_info.xml b/app/src/main/res/drawable/ic_info.xml new file mode 100644 index 000000000..1e1faf7eb --- /dev/null +++ b/app/src/main/res/drawable/ic_info.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_language.xml b/app/src/main/res/drawable/ic_language.xml new file mode 100644 index 000000000..f96822236 --- /dev/null +++ b/app/src/main/res/drawable/ic_language.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..0d025f9bf --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_manager.xml b/app/src/main/res/drawable/ic_manager.xml new file mode 100644 index 000000000..92356905c --- /dev/null +++ b/app/src/main/res/drawable/ic_manager.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_modules.xml b/app/src/main/res/drawable/ic_modules.xml new file mode 100644 index 000000000..5b9d32ac4 --- /dev/null +++ b/app/src/main/res/drawable/ic_modules.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_notification.xml b/app/src/main/res/drawable/ic_notification.xml new file mode 100644 index 000000000..bcd9d769c --- /dev/null +++ b/app/src/main/res/drawable/ic_notification.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_person.xml b/app/src/main/res/drawable/ic_person.xml new file mode 100644 index 000000000..d41fc0128 --- /dev/null +++ b/app/src/main/res/drawable/ic_person.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_phone.xml b/app/src/main/res/drawable/ic_phone.xml new file mode 100644 index 000000000..9ab32d03d --- /dev/null +++ b/app/src/main/res/drawable/ic_phone.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_refresh.xml b/app/src/main/res/drawable/ic_refresh.xml new file mode 100644 index 000000000..0d768f68d --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_save.xml b/app/src/main/res/drawable/ic_save.xml new file mode 100644 index 000000000..2cc51d6fe --- /dev/null +++ b/app/src/main/res/drawable/ic_save.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_send.xml b/app/src/main/res/drawable/ic_send.xml new file mode 100644 index 000000000..0e8967ef0 --- /dev/null +++ b/app/src/main/res/drawable/ic_send.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml new file mode 100644 index 000000000..61d75531e --- /dev/null +++ b/app/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_share.xml b/app/src/main/res/drawable/ic_share.xml new file mode 100644 index 000000000..59b0e84dd --- /dev/null +++ b/app/src/main/res/drawable/ic_share.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_sort.xml b/app/src/main/res/drawable/ic_sort.xml new file mode 100644 index 000000000..707e960db --- /dev/null +++ b/app/src/main/res/drawable/ic_sort.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_update.xml b/app/src/main/res/drawable/ic_update.xml new file mode 100644 index 000000000..3bc2ab5eb --- /dev/null +++ b/app/src/main/res/drawable/ic_update.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_verified.xml b/app/src/main/res/drawable/ic_verified.xml new file mode 100644 index 000000000..349944e4c --- /dev/null +++ b/app/src/main/res/drawable/ic_verified.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_warning.xml b/app/src/main/res/drawable/ic_warning.xml new file mode 100644 index 000000000..2eccab985 --- /dev/null +++ b/app/src/main/res/drawable/ic_warning.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/outline_list_24.xml b/app/src/main/res/drawable/outline_list_24.xml new file mode 100644 index 000000000..a862eb688 --- /dev/null +++ b/app/src/main/res/drawable/outline_list_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/shortcut_ic_downloads.xml b/app/src/main/res/drawable/shortcut_ic_downloads.xml new file mode 100644 index 000000000..9d5bf9dea --- /dev/null +++ b/app/src/main/res/drawable/shortcut_ic_downloads.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/shortcut_ic_modules.xml b/app/src/main/res/drawable/shortcut_ic_modules.xml new file mode 100644 index 000000000..fe33d6312 --- /dev/null +++ b/app/src/main/res/drawable/shortcut_ic_modules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml new file mode 100644 index 000000000..19c694637 --- /dev/null +++ b/app/src/main/res/layout/activity_about.xml @@ -0,0 +1,796 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_black_list.xml b/app/src/main/res/layout/activity_black_list.xml new file mode 100644 index 000000000..4fedbb6aa --- /dev/null +++ b/app/src/main/res/layout/activity_black_list.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_download.xml b/app/src/main/res/layout/activity_download.xml new file mode 100644 index 000000000..92d0831ce --- /dev/null +++ b/app/src/main/res/layout/activity_download.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_download_details.xml b/app/src/main/res/layout/activity_download_details.xml new file mode 100644 index 000000000..ded5bcce1 --- /dev/null +++ b/app/src/main/res/layout/activity_download_details.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_download_details_not_found.xml b/app/src/main/res/layout/activity_download_details_not_found.xml new file mode 100644 index 000000000..a317c5d6c --- /dev/null +++ b/app/src/main/res/layout/activity_download_details_not_found.xml @@ -0,0 +1,30 @@ + + + + + + + +