├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── values
│ │ │ │ ├── attrs.xml
│ │ │ │ ├── ic_launcher_background.xml
│ │ │ │ ├── dimens.xml
│ │ │ │ ├── arrays.xml
│ │ │ │ ├── styles.xml
│ │ │ │ ├── colors.xml
│ │ │ │ └── themes.xml
│ │ │ ├── drawable-nodpi
│ │ │ │ ├── ic_fb_ads.png
│ │ │ │ ├── ic_fb_lite.png
│ │ │ │ ├── ic_fb_orca.png
│ │ │ │ ├── ic_fb_page.png
│ │ │ │ ├── ic_fb_katana.png
│ │ │ │ └── ic_fb_mlite.png
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_round.png
│ │ │ │ └── ic_launcher_foreground.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_round.png
│ │ │ │ └── ic_launcher_foreground.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_round.png
│ │ │ │ └── ic_launcher_foreground.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_round.png
│ │ │ │ └── ic_launcher_foreground.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_round.png
│ │ │ │ └── ic_launcher_foreground.png
│ │ │ ├── xml
│ │ │ │ ├── file_provider_paths.xml
│ │ │ │ ├── backup_rules.xml
│ │ │ │ ├── data_extraction_rules.xml
│ │ │ │ ├── preference_advanced.xml
│ │ │ │ └── preference.xml
│ │ │ ├── color
│ │ │ │ ├── drop_down_text_color.xml
│ │ │ │ └── drop_down_box_stroke.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── drawable
│ │ │ │ ├── tab_bg_selected.xml
│ │ │ │ ├── ic_home_black_24dp.xml
│ │ │ │ ├── ic_patched_black_24dp.xml
│ │ │ │ ├── home_top_icon.xml
│ │ │ │ ├── ic_pref_config.xml
│ │ │ │ ├── ic_pref_back.xml
│ │ │ │ ├── splash_screen_icon.xml
│ │ │ │ ├── selectable_background.xml
│ │ │ │ ├── ic_pref_other.xml
│ │ │ │ ├── generic_placeholder.xml
│ │ │ │ ├── ic_patch_version.xml
│ │ │ │ ├── ic_pref_action.xml
│ │ │ │ ├── ic_setting_black_24dp.xml
│ │ │ │ └── ic_pref_advanced.xml
│ │ │ ├── menu
│ │ │ │ ├── manual_patch_menu.xml
│ │ │ │ ├── bottom_nav_menu.xml
│ │ │ │ ├── app_bar_menu.xml
│ │ │ │ └── app_bar_context_menu.xml
│ │ │ ├── layout
│ │ │ │ ├── fragment_settings.xml
│ │ │ │ ├── simple_progress_view.xml
│ │ │ │ ├── dropdown_item.xml
│ │ │ │ ├── installed_apps_dialog.xml
│ │ │ │ ├── apk_simple_item_view.xml
│ │ │ │ ├── fragment_patched.xml
│ │ │ │ ├── apk_item_view.xml
│ │ │ │ ├── about_dialog.xml
│ │ │ │ ├── version_input_dialog.xml
│ │ │ │ ├── pref_extra_modules_dialog.xml
│ │ │ │ ├── activity_main.xml
│ │ │ │ └── keystore_dialog.xml
│ │ │ ├── animator
│ │ │ │ ├── nav_default_enter_anim.xml
│ │ │ │ ├── nav_default_exit_anim.xml
│ │ │ │ ├── nav_default_pop_enter_anim.xml
│ │ │ │ └── nav_default_pop_exit_anim.xml
│ │ │ ├── navigation
│ │ │ │ └── main_navigation.xml
│ │ │ └── values-night
│ │ │ │ └── colors.xml
│ │ ├── assets
│ │ │ └── module.pkg
│ │ ├── java
│ │ │ └── app
│ │ │ │ └── neonorbit
│ │ │ │ └── mrvpatchmanager
│ │ │ │ ├── network
│ │ │ │ ├── marker
│ │ │ │ │ ├── XmlMarker.kt
│ │ │ │ │ ├── HtmlMarker.kt
│ │ │ │ │ └── JsonMarker.kt
│ │ │ │ ├── HttpSpec.kt
│ │ │ │ ├── ConverterFactory.kt
│ │ │ │ ├── RetrofitClient.kt
│ │ │ │ └── ApiService.kt
│ │ │ │ ├── data
│ │ │ │ ├── AppFileData.kt
│ │ │ │ ├── AppItemData.kt
│ │ │ │ ├── UpdateEventData.kt
│ │ │ │ └── AppType.kt
│ │ │ │ ├── remote
│ │ │ │ ├── data
│ │ │ │ │ ├── RemoteApkInfo.kt
│ │ │ │ │ ├── GithubReleaseData.kt
│ │ │ │ │ ├── ApkMirrorIFormData.kt
│ │ │ │ │ ├── ApkMirrorReleaseData.kt
│ │ │ │ │ ├── ApkComboReleaseData.kt
│ │ │ │ │ ├── ApkMirrorRssFeedData.kt
│ │ │ │ │ ├── ApkFlashReleaseData.kt
│ │ │ │ │ ├── ApkPureReleaseData.kt
│ │ │ │ │ ├── ApkMirrorItemData.kt
│ │ │ │ │ ├── ApkMirrorVariantData.kt
│ │ │ │ │ ├── ApkComboVariantData.kt
│ │ │ │ │ ├── ApkFlashVariantData.kt
│ │ │ │ │ └── ApkPureVariantData.kt
│ │ │ │ ├── ApkRemoteService.kt
│ │ │ │ ├── GithubService.kt
│ │ │ │ ├── ApkComboService.kt
│ │ │ │ ├── ApkFlashService.kt
│ │ │ │ ├── ApkMirrorService.kt
│ │ │ │ ├── ApkPureService.kt
│ │ │ │ └── ApkRemoteFileProvider.kt
│ │ │ │ ├── keystore
│ │ │ │ ├── KeystoreData.kt
│ │ │ │ ├── KeystoreInputData.kt
│ │ │ │ └── KeystoreManager.kt
│ │ │ │ ├── repository
│ │ │ │ ├── data
│ │ │ │ │ └── ApkFileData.kt
│ │ │ │ └── ApkRepository.kt
│ │ │ │ ├── util
│ │ │ │ ├── NullableElementConverter.java
│ │ │ │ ├── RelaxedEmitter.kt
│ │ │ │ ├── Utils.kt
│ │ │ │ └── AppUtil.kt
│ │ │ │ ├── event
│ │ │ │ ├── ChannelEvent.kt
│ │ │ │ ├── SingleEvent.kt
│ │ │ │ └── ConfirmationEvent.kt
│ │ │ │ ├── download
│ │ │ │ ├── DownloadStatus.kt
│ │ │ │ └── DownloadCache.kt
│ │ │ │ ├── MRVPatchManager.kt
│ │ │ │ ├── ui
│ │ │ │ ├── settings
│ │ │ │ │ ├── SettingsFragment.kt
│ │ │ │ │ ├── SettingsData.kt
│ │ │ │ │ └── SettingsViewModel.kt
│ │ │ │ ├── patched
│ │ │ │ │ ├── ApkItemHolder.kt
│ │ │ │ │ └── ApkListAdapter.kt
│ │ │ │ ├── home
│ │ │ │ │ ├── AppsAdapter.kt
│ │ │ │ │ └── InstalledAppAdapter.kt
│ │ │ │ ├── SelectionTrackerFactory.kt
│ │ │ │ ├── ConfirmationDialog.kt
│ │ │ │ └── AutoProgressDialog.kt
│ │ │ │ ├── glide
│ │ │ │ ├── GlideAppManager.kt
│ │ │ │ ├── RecyclerPreloadProvider.kt
│ │ │ │ └── ApkIconLoaderFactory.kt
│ │ │ │ ├── local
│ │ │ │ └── ApkLocalFileProvider.kt
│ │ │ │ ├── CacheManager.kt
│ │ │ │ ├── SystemServices.kt
│ │ │ │ ├── apk
│ │ │ │ ├── ApkParser.kt
│ │ │ │ └── ApkConfigs.kt
│ │ │ │ ├── ExLifeCycle.kt
│ │ │ │ ├── AppInstaller.kt
│ │ │ │ ├── DefaultPreference.kt
│ │ │ │ ├── AppServices.kt
│ │ │ │ ├── DefaultPatcher.kt
│ │ │ │ └── MainActivity.kt
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── app
│ │ │ └── neonorbit
│ │ │ └── mrvpatchmanager
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── java
│ │ └── app
│ │ └── neonorbit
│ │ └── mrvpatchmanager
│ │ └── ExampleInstrumentedTest.kt
├── proguard-rules.pro
└── dependency.gradle
├── .github
└── FUNDING.yml
├── resource
└── screenshot.png
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .idea
├── .gitignore
├── codeStyles
│ ├── codeStyleConfig.xml
│ └── Project.xml
├── compiler.xml
├── kotlinc.xml
├── vcs.xml
├── deploymentTargetDropDown.xml
├── migrations.xml
├── deploymentTargetSelector.xml
├── gradle.xml
├── misc.xml
└── runConfigurations
│ └── MRVPatchManager.xml
├── gradle.properties
├── .gitattributes
├── .gitignore
├── settings.gradle
├── README.md
└── gradlew.bat
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | custom: ['neonorbit.github.io/support']
2 | buy_me_a_coffee: neonorbit
--------------------------------------------------------------------------------
/resource/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NeonOrbit/MRVPatchManager/HEAD/resource/screenshot.png
--------------------------------------------------------------------------------
/app/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/assets/module.pkg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NeonOrbit/MRVPatchManager/HEAD/app/src/main/assets/module.pkg
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NeonOrbit/MRVPatchManager/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/drawable-nodpi/ic_fb_ads.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NeonOrbit/MRVPatchManager/HEAD/app/src/main/res/drawable-nodpi/ic_fb_ads.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-nodpi/ic_fb_lite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NeonOrbit/MRVPatchManager/HEAD/app/src/main/res/drawable-nodpi/ic_fb_lite.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-nodpi/ic_fb_orca.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NeonOrbit/MRVPatchManager/HEAD/app/src/main/res/drawable-nodpi/ic_fb_orca.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-nodpi/ic_fb_page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NeonOrbit/MRVPatchManager/HEAD/app/src/main/res/drawable-nodpi/ic_fb_page.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NeonOrbit/MRVPatchManager/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NeonOrbit/MRVPatchManager/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NeonOrbit/MRVPatchManager/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NeonOrbit/MRVPatchManager/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | /appInsightsSettings.xml
5 | /material_theme_project_new.xml
6 | /studiobot.xml
--------------------------------------------------------------------------------
/app/src/main/res/drawable-nodpi/ic_fb_katana.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NeonOrbit/MRVPatchManager/HEAD/app/src/main/res/drawable-nodpi/ic_fb_katana.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-nodpi/ic_fb_mlite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NeonOrbit/MRVPatchManager/HEAD/app/src/main/res/drawable-nodpi/ic_fb_mlite.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NeonOrbit/MRVPatchManager/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NeonOrbit/MRVPatchManager/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NeonOrbit/MRVPatchManager/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NeonOrbit/MRVPatchManager/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/network/marker/XmlMarker.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.network.marker
2 |
3 | annotation class XmlMarker
4 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NeonOrbit/MRVPatchManager/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NeonOrbit/MRVPatchManager/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/network/marker/HtmlMarker.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.network.marker
2 |
3 | annotation class HtmlMarker
4 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/network/marker/JsonMarker.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.network.marker
2 |
3 | annotation class JsonMarker
4 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NeonOrbit/MRVPatchManager/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NeonOrbit/MRVPatchManager/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NeonOrbit/MRVPatchManager/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NeonOrbit/MRVPatchManager/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NeonOrbit/MRVPatchManager/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #05C6B7
4 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/file_provider_paths.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/data/AppFileData.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.data
2 |
3 | import java.io.File
4 |
5 | data class AppFileData(val name: String, val file: File)
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/remote/data/RemoteApkInfo.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.remote.data
2 |
3 | data class RemoteApkInfo(val link: String, val version: String? = null)
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
2 | android.useAndroidX=true
3 | kotlin.code.style=official
4 | android.nonTransitiveRClass=true
5 | android.nonFinalResIds=true
6 | android.enableR8.fullMode=false
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
3 | distributionPath=wrapper/dists
4 | zipStorePath=wrapper/dists
5 | zipStoreBase=GRADLE_USER_HOME
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # All files are checked into the repo with LF
2 | * text=auto
3 |
4 | # These files are checked out using CRLF locally
5 | *.cmd text eol=crlf
6 | *.bat text eol=crlf
7 |
8 | # These files are binary and should not be modified
9 | *.pkg binary
--------------------------------------------------------------------------------
/.idea/deploymentTargetDropDown.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/color/drop_down_text_color.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 20dp
5 | 14dp
6 | 20dp
7 |
--------------------------------------------------------------------------------
/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/data/AppItemData.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.data
2 |
3 | import androidx.annotation.DrawableRes
4 |
5 | data class AppItemData(val name: String, val type: AppType, @DrawableRes val icon: Int = -1) {
6 | override fun toString(): String {
7 | return name
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/data/UpdateEventData.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.data
2 |
3 | sealed class UpdateEventData {
4 | data class Manager(val current: String, val latest: String, val link: String) : UpdateEventData()
5 | data class Module(val current: String, val latest: String, val link: String) : UpdateEventData()
6 | }
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/tab_bg_selected.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/deploymentTargetSelector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_home_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/manual_patch_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/keystore/KeystoreData.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.keystore
2 |
3 | import com.google.gson.Gson
4 |
5 | data class KeystoreData(
6 | val path: String,
7 | val password: String,
8 | val aliasName: String,
9 | val aliasPassword: String,
10 | val keySignature: String
11 | ) {
12 | fun toJson(): String = Gson().toJson(this)
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/repository/data/ApkFileData.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.repository.data
2 |
3 | data class ApkFileData(val name: String, val path: String) {
4 | val version: String by lazy {
5 | "Version: " + name.substringAfterLast("-v").removeSuffix(".apk")
6 | }
7 |
8 | override fun toString(): String {
9 | return name
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | *.log
3 | .gradle
4 | /local.properties
5 | /.idea/caches
6 | /.idea/libraries
7 | /.idea/modules.xml
8 | /.idea/workspace.xml
9 | /.idea/navEditor.xml
10 | /.idea/runConfigurations.xml
11 | /.idea/assetWizardSettings.xml
12 | /.idea/AndroidProjectSystem.xml
13 | .DS_Store
14 | /build
15 | /app/release
16 | /app/debug
17 | /captures
18 | /out
19 | .externalNativeBuild
20 | .cxx
21 | local.properties
22 |
--------------------------------------------------------------------------------
/app/src/main/res/color/drop_down_box_stroke.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_patched_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 | include ':app'
16 | rootProject.name = "MRVPatchManager"
17 | project(':app').name = 'MRVPatchManager'
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/test/java/app/neonorbit/mrvpatchmanager/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/home_top_icon.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_pref_config.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/values/arrays.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | "auto"
4 |
5 | - "Auto"
6 | - "Arm64 v8a"
7 | - "Armeabi v7a"
8 |
9 |
10 | - @string/apk_abi_auto
11 | - "arm64-v8a"
12 | - "armeabi-v7a"
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/data/AppType.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.data
2 |
3 | import app.neonorbit.mrvpatchmanager.AppConfigs
4 |
5 | enum class AppType {
6 | FACEBOOK,
7 | MESSENGER,
8 | FACEBOOK_LITE,
9 | MESSENGER_LITE,
10 | BUSINESS_SUITE,
11 | FB_ADS_MANAGER;
12 |
13 | fun getName(): String {
14 | return AppConfigs.getFbAppName(this)
15 | }
16 |
17 | fun getPackage(): String {
18 | return AppConfigs.getFbAppPkg(this)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_pref_back.xml:
--------------------------------------------------------------------------------
1 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/util/NullableElementConverter.java:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.util;
2 |
3 | import androidx.annotation.NonNull;
4 | import androidx.annotation.Nullable;
5 |
6 | import org.jsoup.nodes.Element;
7 |
8 | import pl.droidsonroids.jspoon.ElementConverter;
9 | import pl.droidsonroids.jspoon.annotation.Selector;
10 |
11 | public interface NullableElementConverter extends ElementConverter {
12 | @Override
13 | T convert(@Nullable Element node, @NonNull Selector selector);
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/event/ChannelEvent.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.event
2 |
3 | import androidx.lifecycle.LifecycleOwner
4 | import app.neonorbit.mrvpatchmanager.repeatOnUI
5 | import kotlinx.coroutines.Job
6 | import kotlinx.coroutines.flow.FlowCollector
7 |
8 | interface ChannelEvent {
9 | suspend fun observe(observer: FlowCollector)
10 |
11 | fun observeOnUI(owner: LifecycleOwner, observer: FlowCollector): Job {
12 | return owner.repeatOnUI { observe(observer) }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/remote/data/GithubReleaseData.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.remote.data
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class GithubReleaseData(
6 | @field:SerializedName("tag_name")
7 | val version: String,
8 | @field:SerializedName("assets")
9 | val assets: List
10 | ) {
11 | data class Asset(
12 | @field:SerializedName("name")
13 | val name: String,
14 | @field:SerializedName("browser_download_url")
15 | val link: String,
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/splash_screen_icon.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/download/DownloadStatus.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.download
2 |
3 | import java.io.File
4 |
5 | sealed class DownloadStatus {
6 | data object DOWNLOADING : DownloadStatus()
7 | data class FETCHING(val server: String) : DownloadStatus()
8 | data class FETCHED(val version: String) : DownloadStatus()
9 | data class PROGRESS(val current: Long, val total: Long) : DownloadStatus()
10 | data class FAILED(val error: String) : DownloadStatus()
11 | data class FINISHED(val file: File) : DownloadStatus()
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/selectable_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
-
6 |
7 |
8 | -
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/simple_progress_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/MRVPatchManager.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager
2 |
3 | import android.app.Application
4 | import app.neonorbit.mrvpatchmanager.remote.GithubService
5 |
6 | class MRVPatchManager : Application() {
7 | companion object {
8 | lateinit var instance: MRVPatchManager
9 | }
10 |
11 | override fun onCreate() {
12 | super.onCreate()
13 | instance = this
14 | onAppInitialized()
15 | }
16 |
17 | private fun onAppInitialized() {
18 | AppInstaller.register(this)
19 | GithubService.checkForUpdate()
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/ui/settings/SettingsFragment.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.ui.settings
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.ViewGroup
6 | import androidx.fragment.app.Fragment
7 | import app.neonorbit.mrvpatchmanager.databinding.FragmentSettingsBinding
8 |
9 | class SettingsFragment : Fragment() {
10 | override fun onCreateView(
11 | inflater: LayoutInflater,
12 | container: ViewGroup?,
13 | savedInstanceState: Bundle?
14 | ) = FragmentSettingsBinding.inflate(inflater, container, false).root
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dropdown_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/ui/settings/SettingsData.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.ui.settings
2 |
3 | import app.neonorbit.mrvpatchmanager.AppConfigs
4 | import app.neonorbit.mrvpatchmanager.remote.ApkRemoteFileProvider
5 | import java.io.File
6 |
7 | object SettingsData {
8 | const val DEFAULT_SERVER = "All servers"
9 |
10 | val SERVERS by lazy {
11 | ApkRemoteFileProvider.services.map { it.server() }.apply {
12 | (this as MutableList).add(0, DEFAULT_SERVER)
13 | }.toTypedArray()
14 | }
15 |
16 | val CUSTOM_KEY_FILE: File get() = AppConfigs.CUSTOM_KEYSTORE_FILE
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/glide/GlideAppManager.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.glide
2 |
3 | import android.content.Context
4 | import android.graphics.drawable.Drawable
5 | import com.bumptech.glide.Glide
6 | import com.bumptech.glide.Registry
7 | import com.bumptech.glide.annotation.GlideModule
8 | import com.bumptech.glide.module.AppGlideModule
9 |
10 | @GlideModule
11 | class GlideAppManager : AppGlideModule() {
12 | override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
13 | registry.prepend(String::class.java, Drawable::class.java, ApkIconLoaderFactory(context))
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/bottom_nav_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_pref_other.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/animator/nav_default_enter_anim.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/animator/nav_default_exit_anim.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/animator/nav_default_pop_enter_anim.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/animator/nav_default_pop_exit_anim.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
17 |
18 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/generic_placeholder.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/event/SingleEvent.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.event
2 |
3 | import kotlinx.coroutines.CoroutineScope
4 | import kotlinx.coroutines.channels.Channel
5 | import kotlinx.coroutines.flow.FlowCollector
6 | import kotlinx.coroutines.flow.receiveAsFlow
7 | import kotlinx.coroutines.launch
8 |
9 | class SingleEvent : ChannelEvent {
10 | private val channel = Channel(Channel.CONFLATED)
11 |
12 | suspend fun post(event: T) {
13 | channel.send(event)
14 | }
15 |
16 | fun post(scope: CoroutineScope, event: T) {
17 | scope.launch {
18 | channel.send(event)
19 | }
20 | }
21 |
22 | override suspend fun observe(observer: FlowCollector) {
23 | channel.receiveAsFlow().collect(observer)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/app/neonorbit/mrvpatchmanager/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("app.neonorbit.mrvpatchmanager", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/res/menu/app_bar_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
22 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/util/RelaxedEmitter.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.util
2 |
3 | import kotlinx.coroutines.flow.FlowCollector
4 |
5 | class RelaxedEmitter(
6 | private val collector: FlowCollector,
7 | private val interval: Long
8 | ) {
9 | private var skipped: T? = null
10 | private var emittedAt: Long = 0L
11 |
12 | suspend fun emit(value: T) {
13 | if (skip()) {
14 | skipped = value
15 | } else {
16 | skipped = null
17 | collector.emit(value)
18 | emittedAt = System.currentTimeMillis()
19 | }
20 | }
21 |
22 | suspend fun finish() {
23 | skipped?.let { collector.emit(it) }
24 | }
25 |
26 | private fun skip(): Boolean {
27 | return (interval - (System.currentTimeMillis() - emittedAt)) > 0
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | -dontoptimize
2 | -dontobfuscate
3 | -keepattributes Signature, *Annotation*
4 | -keepattributes SourceFile, LineNumberTable
5 |
6 | -keep class app.neonorbit.mrvpatchmanager.** {*;}
7 | -keep class org.simpleframework.xml.** {*;}
8 | -keep class com.google.gson.annotations.** {*;}
9 | -keep class pl.droidsonroids.jspoon.annotation.** {*;}
10 |
11 | -keep class org.lsposed.** {*;}
12 | -keep class com.android.apksig.** {*;}
13 | -keep class com.beust.jcommander.** {*;}
14 | -keep class com.android.tools.build.apkzlib.** {*;}
15 |
16 | -keepclassmembers enum * {
17 | public static **[] values();
18 | public static ** valueOf(java.lang.String);
19 | }
20 |
21 | -dontwarn android.content.res.**
22 | -dontwarn org.xmlpull.v1.**
23 | -dontwarn org.openjsse.**
24 | -dontwarn org.conscrypt.**
25 | -dontwarn org.bouncycastle.**
26 | -dontwarn com.google.auto.value.**
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/network/HttpSpec.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.network
2 |
3 | object HttpSpec {
4 | object Header {
5 | const val E_TAG = "ETag"
6 | const val RANGE = "Range"
7 | const val IF_RANGE = "If-Range"
8 | const val USER_AGENT = "User-Agent"
9 | const val CONTENT_TYPE = "Content-Type"
10 | const val CACHE_CONTROL = "Cache-Control"
11 | const val CONTENT_RANGE = "Content-Range"
12 | const val IF_NONE_MATCH = "If-None-Match"
13 | const val LAST_MODIFIED = "Last-Modified"
14 | const val IF_MODIFIED_SINCE = "If-Modified-Since"
15 | }
16 |
17 | object Code {
18 | const val NOT_MODIFIED = 304
19 | const val PARTIAL_CONTENT = 206
20 | const val TOO_MANY_REQUESTS = 429
21 | const val RANGE_NOT_SATISFIABLE = 416
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_patch_version.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/glide/RecyclerPreloadProvider.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.glide
2 |
3 | import android.graphics.drawable.Drawable
4 | import androidx.fragment.app.Fragment
5 | import app.neonorbit.mrvpatchmanager.repository.data.ApkFileData
6 | import com.bumptech.glide.Glide
7 | import com.bumptech.glide.ListPreloader
8 | import com.bumptech.glide.RequestBuilder
9 |
10 | class RecyclerPreloadProvider(
11 | private val fragment: Fragment,
12 | private val paths: List
13 | ) : ListPreloader.PreloadModelProvider {
14 | override fun getPreloadItems(position: Int): List {
15 | return if (paths.isEmpty()) listOf() else listOf(paths[position])
16 | }
17 |
18 | override fun getPreloadRequestBuilder(item: ApkFileData): RequestBuilder {
19 | return Glide.with(fragment).load(item)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_pref_action.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/remote/data/ApkMirrorIFormData.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.remote.data
2 |
3 | import app.neonorbit.mrvpatchmanager.remote.ApkMirrorService
4 | import app.neonorbit.mrvpatchmanager.util.Utils
5 | import pl.droidsonroids.jspoon.annotation.Selector
6 |
7 | class ApkMirrorIFormData {
8 | @Selector(value = "#download-link", attr = "href")
9 | private var href: String? = null
10 |
11 | @Selector(value = "#filedownload", attr = "action")
12 | private var action: String? = null
13 |
14 | @Selector(value = "#filedownload > input[name=id]", attr = "value")
15 | private var id: String? = null
16 |
17 | @Selector(value = "#filedownload > input[name=key]", attr = "value")
18 | private var key: String? = null
19 |
20 | val link: String get() = Utils.absoluteUrl(ApkMirrorService.BASE_URL,
21 | href ?: "$action?id=$id&key=$key&forcebaseapk=true"
22 | )
23 |
24 | override fun toString(): String = "link: $link"
25 | }
26 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/installed_apps_dialog.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
17 |
18 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/apk_simple_item_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
17 |
18 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_setting_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
13 |
16 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/remote/data/ApkMirrorReleaseData.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.remote.data
2 |
3 | import app.neonorbit.mrvpatchmanager.apk.ApkConfigs
4 | import app.neonorbit.mrvpatchmanager.remote.ApkMirrorService
5 | import app.neonorbit.mrvpatchmanager.util.Utils
6 | import pl.droidsonroids.jspoon.annotation.Selector
7 |
8 | class ApkMirrorReleaseData {
9 | @Selector("#primary .listWidget:contains(Uploads) .appRow .appRowTitle")
10 | var releases: List = listOf()
11 |
12 | override fun toString(): String {
13 | return "releases: $releases"
14 | }
15 |
16 | class Release {
17 | @Selector("a", defValue = "")
18 | lateinit var name: String
19 |
20 | @Selector("a", attr = "href")
21 | private lateinit var href: String
22 |
23 | val version: String? get() = ApkConfigs.extractVersionName(name)
24 |
25 | val link: String get() = Utils.absoluteUrl(ApkMirrorService.BASE_URL, href)
26 |
27 | override fun toString(): String {
28 | return "name: $name, link: $link"
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/res/navigation/main_navigation.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
14 |
19 |
20 |
25 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/ui/patched/ApkItemHolder.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.ui.patched
2 |
3 | import android.view.View
4 | import android.widget.ImageView
5 | import android.widget.TextView
6 | import androidx.recyclerview.selection.ItemDetailsLookup
7 | import androidx.recyclerview.widget.RecyclerView
8 | import app.neonorbit.mrvpatchmanager.R
9 | import app.neonorbit.mrvpatchmanager.ui.SelectionTrackerFactory
10 |
11 | class ApkItemHolder(itemView: View) :
12 | RecyclerView.ViewHolder(itemView),
13 | SelectionTrackerFactory.TrackerItemDetails
14 | {
15 | val apkIcon: ImageView = itemView.findViewById(R.id.apk_icon)
16 | val apkTitle: TextView = itemView.findViewById(R.id.apk_title)
17 | val apkInfo: TextView = itemView.findViewById(R.id.apk_info)
18 |
19 | override fun getItemDetails(): ItemDetailsLookup.ItemDetails {
20 | return object: ItemDetailsLookup.ItemDetails() {
21 | override fun getPosition(): Int {
22 | return bindingAdapterPosition
23 | }
24 | override fun getSelectionKey(): Long {
25 | return itemId
26 | }
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/dependency.gradle:
--------------------------------------------------------------------------------
1 | import org.codehaus.groovy.runtime.MethodClosure
2 |
3 | import java.security.MessageDigest
4 |
5 | ext {
6 | mrvpatcher = this.&getMRVPatcher as MethodClosure
7 | }
8 |
9 | def getMRVPatcher(String version, String sha1sum) {
10 | if (gradle.gradleUserHomeDir == null) throw AssertionError()
11 | String name = "MRVPatcher-${version}.jar"
12 | File file = file("${gradle.gradleUserHomeDir}/download/MRVPatcher/$name")
13 | String url = "https://github.com/NeonOrbit/MRVPatcher/releases/download/$version/$name"
14 | if (!file.exists()) {
15 | file.parentFile.mkdirs()
16 | new URL(url).withInputStream { input ->
17 | file.withOutputStream { output ->
18 | output << input
19 | }
20 | }
21 | }
22 | verifyChecksum(file, sha1sum)
23 | files(file.absolutePath)
24 | }
25 |
26 | static def verifyChecksum(File input, String sha1sum) {
27 | def digest = MessageDigest.getInstance("SHA-1")
28 | try (def is = input.newInputStream()) {
29 | def hash = digest.digest(is.bytes).encodeHex().toString()
30 | if (!hash.equalsIgnoreCase(sha1sum)) {
31 | throw new GradleException("MRVPatcher jar verification failed")
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/local/ApkLocalFileProvider.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.local
2 |
3 | import app.neonorbit.mrvpatchmanager.AppConfigs
4 | import app.neonorbit.mrvpatchmanager.AppServices
5 | import app.neonorbit.mrvpatchmanager.repository.data.ApkFileData
6 | import java.io.File
7 | import java.util.stream.Collectors
8 |
9 | class ApkLocalFileProvider {
10 | fun getModuleApk(): File {
11 | return File(AppConfigs.DOWNLOAD_DIR, AppConfigs.MODULE_APK_NAME).also { file ->
12 | AppServices.assetManager.open(AppConfigs.MODULE_ASSET_NAME).use { input ->
13 | file.outputStream().use { output ->
14 | input.copyTo(output)
15 | }
16 | }
17 | }
18 | }
19 |
20 | fun loadPatchedApks(): List {
21 | return AppConfigs.PATCHED_APK_DIR.listFiles()?.associateBy(
22 | {it.toApkData()}, {it.lastModified()}
23 | )?.entries?.stream()?.sorted(
24 | Comparator.comparing, Long> { it.value }.reversed()
25 | )?.map { it.key }?.collect(Collectors.toList()) ?: listOf()
26 | }
27 |
28 | private fun File.toApkData(): ApkFileData = ApkFileData(name, absolutePath)
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/app_bar_context_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
34 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/download/DownloadCache.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.download
2 |
3 | import app.neonorbit.mrvpatchmanager.DefaultPreference
4 | import app.neonorbit.mrvpatchmanager.network.HttpSpec
5 | import app.neonorbit.mrvpatchmanager.parseJson
6 | import com.google.gson.Gson
7 | import okhttp3.ResponseBody
8 | import retrofit2.Response
9 | import java.io.File
10 |
11 | object DownloadCache {
12 | data class Cache(val eTag: String?, val mDate: String?, val length: Long)
13 |
14 | fun get(file: File): Cache? {
15 | return DefaultPreference.getCache(file.asCacheKey())?.let {
16 | try { it.parseJson() } catch (_: Exception) { null }
17 | }
18 | }
19 |
20 | fun save(file: File, response: Response) {
21 | if (response.code() == HttpSpec.Code.PARTIAL_CONTENT) return
22 | Cache(
23 | response.headers()[HttpSpec.Header.E_TAG],
24 | response.headers()[HttpSpec.Header.LAST_MODIFIED],
25 | response.body()!!.contentLength()
26 | ).let {
27 | DefaultPreference.putCache(file.asCacheKey(), Gson().toJson(it))
28 | }
29 | }
30 |
31 | private fun File.asCacheKey() = "download_cache_key_${name.lowercase()}"
32 | }
33 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/keystore/KeystoreInputData.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.keystore
2 |
3 | import android.net.Uri
4 | import android.os.Parcel
5 | import android.os.Parcelable
6 |
7 | data class KeystoreInputData(
8 | val uri: Uri,
9 | val password: String,
10 | val aliasName: String?,
11 | val aliasPassword: String?
12 | ) : Parcelable {
13 | override fun writeToParcel(parcel: Parcel, flags: Int) {
14 | uri.writeToParcel(parcel, flags)
15 | parcel.writeString(password)
16 | parcel.writeString(aliasName)
17 | parcel.writeString(aliasPassword)
18 | }
19 |
20 | override fun describeContents(): Int {
21 | return 0
22 | }
23 |
24 | companion object CREATOR : Parcelable.Creator {
25 | override fun createFromParcel(parcel: Parcel): KeystoreInputData {
26 | return KeystoreInputData(
27 | Uri.CREATOR.createFromParcel(parcel),
28 | parcel.readString()!!,
29 | parcel.readString(),
30 | parcel.readString()
31 | )
32 | }
33 |
34 | override fun newArray(size: Int): Array {
35 | return arrayOfNulls(size)
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/CacheManager.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager
2 |
3 | import com.google.gson.Gson
4 | import com.google.gson.reflect.TypeToken
5 | import java.util.concurrent.TimeUnit.HOURS
6 |
7 | object CacheManager {
8 | fun put(key: String, value: Any, hours: Int) {
9 | DefaultPreference.putCache(key, serialize(value, hours.toLong()))
10 | }
11 |
12 | inline fun get(key: String, force: Boolean = false): T? {
13 | return DefaultPreference.getCache(key)?.let {
14 | try {
15 | val token = object : TypeToken>() {}
16 | (Gson().fromJson(it, token.type) as CachedData?)?.get(force)
17 | } catch (_: Exception) { null }
18 | }
19 | }
20 |
21 | private fun serialize(value: Any, hours: Long): String {
22 | return Gson().toJson(CachedData(value, hours))
23 | }
24 |
25 | class CachedData(private val data: T, private val hours: Long) {
26 | private val created = System.currentTimeMillis()
27 |
28 | fun get(force: Boolean = false): T? = if (isValid() || force) data else null
29 |
30 | private fun isValid(): Boolean {
31 | return (System.currentTimeMillis() - created) < HOURS.toMillis(hours)
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
13 |
14 |
18 |
19 |
23 |
24 |
27 |
28 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/remote/data/ApkComboReleaseData.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.remote.data
2 |
3 | import app.neonorbit.mrvpatchmanager.apk.ApkConfigs
4 | import app.neonorbit.mrvpatchmanager.remote.ApkComboService
5 | import app.neonorbit.mrvpatchmanager.util.Utils
6 | import pl.droidsonroids.jspoon.annotation.Selector
7 |
8 | class ApkComboReleaseData {
9 | @Selector(".list-versions > li")
10 | var releases: List = listOf()
11 |
12 | override fun toString(): String {
13 | return "releases: $releases"
14 | }
15 |
16 | class Release {
17 | @Selector(".vtype", defValue = "")
18 | lateinit var type: String
19 |
20 | @Selector("a.ver-item", attr = "href")
21 | private lateinit var href: String
22 |
23 | @Selector(".vername", defValue = "")
24 | lateinit var name: String
25 |
26 | val version: String? get() = ApkConfigs.extractVersionName(name)
27 |
28 | val link: String get() = Utils.absoluteUrl(ApkComboService.BASE_URL, href)
29 |
30 | val isValidType: Boolean get() = type.lowercase().let {
31 | "xapk" !in it || "apk" in it.replace("xapk", "")
32 | }
33 |
34 | override fun toString(): String {
35 | return "type: $type, name: $name, link: $link"
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/ui/home/AppsAdapter.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.ui.home
2 |
3 | import android.content.Context
4 | import android.view.View
5 | import android.view.ViewGroup
6 | import android.widget.ArrayAdapter
7 | import android.widget.Filter
8 | import android.widget.TextView
9 | import androidx.core.content.ContextCompat
10 | import app.neonorbit.mrvpatchmanager.R
11 | import app.neonorbit.mrvpatchmanager.data.AppItemData
12 |
13 | class AppsAdapter(context: Context, items: List)
14 | : ArrayAdapter(context, R.layout.dropdown_item, items) {
15 |
16 | override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
17 | return super.getView(position, convertView, parent).also {
18 | (it as TextView).setCompoundDrawablesRelativeWithIntrinsicBounds(
19 | ContextCompat.getDrawable(context, getItem(position)!!.icon), null, null, null
20 | )
21 | }
22 | }
23 |
24 | private val noOpFilter = object : Filter() {
25 | private val noOpResult = FilterResults()
26 | override fun performFiltering(constraint: CharSequence?) = noOpResult
27 | override fun publishResults(constraint: CharSequence?, results: FilterResults?) {}
28 | }
29 |
30 | override fun getFilter() = noOpFilter
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/remote/data/ApkMirrorRssFeedData.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.remote.data
2 |
3 | import app.neonorbit.mrvpatchmanager.apk.ApkConfigs
4 | import org.simpleframework.xml.Element
5 | import org.simpleframework.xml.ElementList
6 | import org.simpleframework.xml.Root
7 |
8 | @Root(name = "rss", strict = false)
9 | data class ApkMirrorRssFeedData (
10 | @field:Element(name = "channel")
11 | @param:Element(name = "channel")
12 | val channel: RssChannel
13 | ) {
14 | @Root(name = "channel", strict = false)
15 | data class RssChannel (
16 | @field:ElementList(name = "item", inline = true)
17 | @param:ElementList(name = "item", inline = true)
18 | val items: List
19 | ) {
20 | @Root(name = "item", strict = false)
21 | data class RssItem (
22 | @field:Element(name = "title")
23 | @param:Element(name = "title")
24 | val title: String,
25 |
26 | @field:Element(name = "link")
27 | @param:Element(name = "link")
28 | val link: String
29 | ) {
30 | val dpi: String? get() = title.takeIf { "dpi" in it }
31 | val minSDk: Int? get() = ApkConfigs.extractMinSdk(title)
32 | val version: String? get() = ApkConfigs.extractVersionName(title)
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/remote/data/ApkFlashReleaseData.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.remote.data
2 |
3 | import app.neonorbit.mrvpatchmanager.apk.ApkConfigs
4 | import app.neonorbit.mrvpatchmanager.remote.ApkFlashService
5 | import app.neonorbit.mrvpatchmanager.util.Utils
6 | import pl.droidsonroids.jspoon.annotation.Selector
7 |
8 | class ApkFlashReleaseData {
9 | @Selector(value = "ul.list-versions > li > a.version")
10 | var releases: List = listOf()
11 |
12 | override fun toString(): String {
13 | return "releases: $releases"
14 | }
15 |
16 | class Release {
17 | @Selector(".vtype", defValue = "")
18 | private lateinit var type: String
19 |
20 | @Selector("a.version", attr = "href")
21 | private lateinit var href: String
22 |
23 | @Selector(".vername", defValue = "")
24 | lateinit var name: String
25 |
26 | val version: String? get() = ApkConfigs.extractVersionName(name)
27 |
28 | val link: String get() = Utils.absoluteUrl(ApkFlashService.BASE_URL, href)
29 |
30 | val isValidType: Boolean get() = type.lowercase().let {
31 | "xapk" !in it || "apk" in it.replace("xapk", "")
32 | }
33 |
34 | override fun toString(): String {
35 | return "type: $type, name: $name, link: $link"
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/remote/data/ApkPureReleaseData.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.remote.data
2 |
3 | import app.neonorbit.mrvpatchmanager.apk.ApkConfigs
4 | import app.neonorbit.mrvpatchmanager.remote.ApkPureService
5 | import app.neonorbit.mrvpatchmanager.util.Utils
6 | import pl.droidsonroids.jspoon.annotation.Selector
7 |
8 | class ApkPureReleaseData {
9 | @Selector(value = "a.ver_download_link")
10 | var releases: List = listOf()
11 |
12 | override fun toString(): String {
13 | return "releases: $releases"
14 | }
15 |
16 | class Release {
17 | @Selector(".ver-item-type", defValue = "")
18 | private lateinit var type: String
19 |
20 | @Selector("a.ver_download_link", attr = "href")
21 | private lateinit var href: String
22 |
23 | @Selector(".ver-item-n", defValue = "")
24 | lateinit var name: String
25 |
26 | val version: String? get() = ApkConfigs.extractVersionName(name)
27 |
28 | val link: String get() = Utils.absoluteUrl(ApkPureService.BASE_URL, href)
29 |
30 | val isValidType: Boolean get() = type.lowercase().let {
31 | "xapk" !in it || "apk" in it.replace("xapk", "")
32 | }
33 |
34 | override fun toString(): String {
35 | return "type: $type, name: $name, link: $link"
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/util/Utils.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.util
2 |
3 | import android.util.Log
4 | import app.neonorbit.mrvpatchmanager.AppConfigs
5 | import app.neonorbit.mrvpatchmanager.BuildConfig
6 |
7 | @Suppress("FunctionName", "MemberVisibilityCanBePrivate", "Unused")
8 | object Utils {
9 | fun log(msg: String) = Log.d(AppConfigs.APP_TAG, msg)
10 | fun warn(msg: String) = Log.w(AppConfigs.APP_TAG, msg)
11 | fun error(msg: String) = Log.e(AppConfigs.APP_TAG, msg)
12 | fun warn(msg: String, t: Throwable) = Log.w(AppConfigs.APP_TAG, msg, t)
13 | fun error(msg: String, t: Throwable) = Log.e(AppConfigs.APP_TAG, msg, t)
14 |
15 | fun T.LOG(msg: String): T {
16 | if (BuildConfig.DEBUG) log("$msg: $this")
17 | return this
18 | }
19 |
20 | fun absoluteUrl(host: String, url: String) = when {
21 | url.isEmpty() || url.startsWith("http") -> url
22 | url[0] == '/' -> "$host$url"
23 | else -> "$host/$url"
24 | }
25 |
26 | fun sdkToVersion(sdk: Int) = when (
27 | if (sdk < 14) 0 else if (sdk in 14..20) 20 else sdk
28 | ) {
29 | 0 -> 0
30 | 20 -> 4
31 | 21, 22 -> 5
32 | 23 -> 6
33 | 24, 25 -> 7
34 | 26, 27 -> 8
35 | 28 -> 9
36 | 29 -> 10
37 | 30 -> 11
38 | 31, 32 -> 12
39 | else -> sdk - 20
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/SystemServices.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager
2 |
3 | import android.content.Context
4 | import android.net.ConnectivityManager
5 | import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
6 | import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
7 | import androidx.annotation.WorkerThread
8 | import java.net.InetSocketAddress
9 | import java.net.Socket
10 |
11 | object SystemServices {
12 | fun getNetworkService(context: Context): ConnectivityManager? {
13 | return context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
14 | }
15 |
16 | object Network {
17 | private fun isConnected(context: Context): Boolean {
18 | return getNetworkService(context)?.let {
19 | it.getNetworkCapabilities(it.activeNetwork)
20 | }?.let {
21 | it.hasCapability(NET_CAPABILITY_INTERNET) &&
22 | it.hasCapability(NET_CAPABILITY_VALIDATED)
23 | } == true
24 | }
25 |
26 | @WorkerThread
27 | fun isOnline(context: Context): Boolean {
28 | if (!isConnected(context)) return false
29 | return try {
30 | Socket().connect(InetSocketAddress("8.8.8.8", 53), 5000)
31 | true
32 | } catch (e: Exception) {
33 | false
34 | }
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/remote/ApkRemoteService.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.remote
2 |
3 | import app.neonorbit.mrvpatchmanager.data.AppType
4 | import app.neonorbit.mrvpatchmanager.isConnectError
5 | import app.neonorbit.mrvpatchmanager.remote.data.RemoteApkInfo
6 | import app.neonorbit.mrvpatchmanager.util.Utils
7 | import kotlin.coroutines.cancellation.CancellationException
8 |
9 | interface ApkRemoteService {
10 | fun server(): String
11 | suspend fun fetch(type: AppType, abi: String, ver: String?): RemoteApkInfo
12 |
13 | fun Exception.handleApkServiceException(type: AppType, ver: String?): Nothing {
14 | handleUnrelated()
15 | throwServiceException(type, ver)
16 | }
17 |
18 | fun Exception.handleApkServiceException(type: AppType, ver: String?, strict: Boolean) {
19 | this.handleUnrelated()
20 | if (strict) throwServiceException(type, ver)
21 | }
22 |
23 | private fun Exception.handleUnrelated() {
24 | if (this is CancellationException || this.isConnectError) throw this
25 | }
26 |
27 | private fun Exception.throwServiceException(type: AppType, ver: String?): Nothing {
28 | this.message?.let { Utils.warn(it, this) }
29 | throw Exception(
30 | ver?.let { "${type.getName()} version '$it' is not available"} ?:
31 | "Couldn't fetch apk info from the server"
32 | )
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_patched.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
13 |
14 |
20 |
21 |
29 |
30 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/event/ConfirmationEvent.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.event
2 |
3 | import kotlinx.coroutines.channels.Channel
4 | import kotlinx.coroutines.flow.FlowCollector
5 | import kotlinx.coroutines.flow.receiveAsFlow
6 | import kotlinx.coroutines.suspendCancellableCoroutine
7 | import kotlinx.coroutines.sync.Mutex
8 | import kotlinx.coroutines.sync.withLock
9 | import kotlin.coroutines.resume
10 |
11 | class ConfirmationEvent : ChannelEvent {
12 | private var pending: Event? = null
13 | private val mutex: Mutex = Mutex()
14 | private val channel = Channel(Channel.CONFLATED)
15 |
16 | suspend fun ask(msg: String): Boolean {
17 | return ask(null, msg)
18 | }
19 |
20 | suspend fun ask(title: String?, msg: String): Boolean {
21 | return ask(title, msg, null)
22 | }
23 |
24 | suspend fun ask(title: String? = null, msg: String, action: String? = null): Boolean {
25 | return mutex.withLock {
26 | suspendCancellableCoroutine { continuation ->
27 | channel.trySend(Event(title, msg, action) {
28 | pending = null
29 | continuation.resume(it)
30 | }.also { pending = it })
31 | }
32 | }
33 | }
34 |
35 | fun sendResponse(result: Boolean) {
36 | pending?.response?.invoke(result)
37 | }
38 |
39 | override suspend fun observe(observer: FlowCollector) {
40 | channel.receiveAsFlow().collect(observer)
41 | }
42 |
43 | data class Event(val title: String?, val message: String, val action: String?, val response: (Boolean) -> Unit)
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/preference_advanced.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
13 |
18 |
26 |
27 |
31 |
32 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/remote/data/ApkMirrorItemData.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.remote.data
2 |
3 | import app.neonorbit.mrvpatchmanager.apk.ApkConfigs
4 | import app.neonorbit.mrvpatchmanager.remote.ApkMirrorService
5 | import app.neonorbit.mrvpatchmanager.util.Utils
6 | import pl.droidsonroids.jspoon.annotation.Selector
7 |
8 | class ApkMirrorItemData {
9 | @Selector(value = ".downloadButton")
10 | private var links: List = listOf()
11 |
12 | @Selector(value = ".app-title", defValue = "")
13 | private lateinit var title: String
14 |
15 | @Selector(value = ".appspec-value:matches(\\b(?
15 | val libs = mutableListOf()
16 | for (entry in zip.entries()) {
17 | if (entry.name.startsWith("lib/")) {
18 | libs.add(entry.name)
19 | } else if (libs.isNotEmpty()) break
20 | }
21 | when {
22 | libs.any { ApkConfigs.ARM_64 in it } -> ApkConfigs.ARM_64
23 | libs.any { ApkConfigs.ARM_32 in it } -> ApkConfigs.ARM_32
24 | libs.any { ApkConfigs.X86_64 in it } -> ApkConfigs.X86_64
25 | libs.any { ApkConfigs.X86 in it } -> ApkConfigs.X86
26 | else -> null
27 | }
28 | }
29 | } catch (e: Exception) {
30 | Utils.warn(e.error, e)
31 | null
32 | }
33 |
34 | fun getPatchedConfig(file: File): PatchConfig? = try {
35 | ZipFile(file).use { zip ->
36 | zip.getInputStream(zip.getEntry(AppConfigs.PATCHED_APK_CONFIG_PATH)).use { input ->
37 | Gson().fromJson(input.bufferedReader(StandardCharsets.UTF_8), PatchConfig::class.java)
38 | }
39 | }
40 | } catch (e: Exception) {
41 | Utils.warn(e.error, e)
42 | null
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MRVPatch Manager
2 |
3 | MRVPatch Manager is an APK patching app that enables you to use [ChatHeadEnabler](https://github.com/NeonOrbit/ChatHeadEnabler)
4 | (along with other Xposed modules) on non-rooted devices.
5 |
6 |
7 |
8 | ## Features
9 | - Direct download from multiple APK servers.
10 | - Manual patching option for offline APKs.
11 | - Ensures safety by checking APK signature.
12 | - Download specific versions and ABI types.
13 | - Selection from a variety of APK servers.
14 | - Support for allowing third-party modules.
15 | - Support for signing with a custom keystore.
16 |
17 | ## Support
18 | Watch the [video tutorial](https://www.youtube.com/watch?v=UxHSTHam42w) for instructions.
19 | Refer to the [xda thread](https://forum.xda-developers.com/t/4331215) for more details.
20 |
21 | ------------
22 | Certificate:
23 | ```
24 | SHA-1: FE20183C7D2F5C5D9FE1BCE6B7AB31A35FF4C8D0
25 | SHA-256: 91870331A45A1C1E8F34BB27D6973CD72FD8AD98CBF4B306276BFEC06D61EBE8
26 | ```
27 | License:
28 | ```
29 | Copyright (C) 2022 NeonOrbit
30 |
31 | This program is free software: you can redistribute it and/or modify
32 | it under the terms of the GNU General Public License as published by
33 | the Free Software Foundation, either version 3 of the License, or
34 | (at your option) any later version.
35 |
36 | This program is distributed in the hope that it will be useful,
37 | but WITHOUT ANY WARRANTY; without even the implied warranty of
38 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
39 | GNU General Public License for more details.
40 |
41 | You should have received a copy of the GNU General Public License
42 | along with this program. If not, see .
43 | ```
44 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/apk_item_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
17 |
18 |
24 |
25 |
32 |
33 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/repository/ApkRepository.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.repository
2 |
3 | import app.neonorbit.mrvpatchmanager.data.AppType
4 | import app.neonorbit.mrvpatchmanager.repository.data.ApkFileData
5 | import app.neonorbit.mrvpatchmanager.download.DownloadStatus
6 | import app.neonorbit.mrvpatchmanager.error
7 | import app.neonorbit.mrvpatchmanager.local.ApkLocalFileProvider
8 | import app.neonorbit.mrvpatchmanager.remote.ApkRemoteFileProvider
9 | import kotlinx.coroutines.flow.Flow
10 | import kotlinx.coroutines.flow.FlowCollector
11 | import kotlinx.coroutines.flow.catch
12 | import kotlinx.coroutines.flow.flow
13 |
14 | class ApkRepository {
15 | private val local = ApkLocalFileProvider()
16 | private val remote = ApkRemoteFileProvider()
17 |
18 | fun getFbApk(type: AppType, abi: String, version: String?) = remote.getFbApk(type, abi, version)
19 |
20 | fun getPatchedApks(): List = local.loadPatchedApks()
21 |
22 | fun getManagerApk(): Flow = remote.getManagerApk()
23 |
24 | fun getModuleApk(force: Boolean = false): Flow = flow {
25 | remote.getModuleApk().catch {
26 | tryLocalModule(force, it.error)
27 | }.collect {
28 | if (it is DownloadStatus.FAILED) {
29 | tryLocalModule(force, it.error)
30 | } else emit(it)
31 | }
32 | }
33 |
34 | private suspend fun FlowCollector.tryLocalModule(force: Boolean, err: String) {
35 | try {
36 | if (!force) throw Exception()
37 | emit(DownloadStatus.FINISHED(local.getModuleApk()))
38 | } catch (_: Exception) {
39 | emit(DownloadStatus.FAILED(err))
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/ExLifeCycle.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager
2 |
3 | import androidx.lifecycle.Lifecycle
4 | import androidx.lifecycle.LifecycleOwner
5 | import androidx.lifecycle.ViewModel
6 | import androidx.lifecycle.lifecycleScope
7 | import androidx.lifecycle.repeatOnLifecycle
8 | import androidx.lifecycle.viewModelScope
9 | import kotlinx.coroutines.CoroutineScope
10 | import kotlinx.coroutines.Dispatchers
11 | import kotlinx.coroutines.Job
12 | import kotlinx.coroutines.flow.Flow
13 | import kotlinx.coroutines.flow.FlowCollector
14 | import kotlinx.coroutines.flow.MutableStateFlow
15 | import kotlinx.coroutines.launch
16 | import kotlinx.coroutines.sync.Mutex
17 | import kotlinx.coroutines.sync.withLock
18 | import kotlin.coroutines.CoroutineContext
19 | import kotlin.coroutines.EmptyCoroutineContext
20 |
21 | fun LifecycleOwner.repeatOnUI(
22 | block: suspend CoroutineScope.() -> Unit
23 | ) = lifecycleScope.launch(Dispatchers.Main.immediate) {
24 | repeatOnLifecycle(Lifecycle.State.STARTED, block)
25 | }
26 |
27 | fun Flow.observeOnUI(
28 | owner: LifecycleOwner,
29 | collector: FlowCollector
30 | ) = owner.repeatOnUI { this@observeOnUI.collect(collector) }
31 |
32 | fun MutableStateFlow.post(
33 | with: ViewModel, value: T
34 | ) = with.viewModelScope.launch { emit(value) }
35 |
36 | /**
37 | * Launches a coroutine and immediately returns the Job,
38 | * then executes the given action under the mutex's lock.
39 | */
40 | fun CoroutineScope.launchSyncedBlock(
41 | mutex: Mutex,
42 | context: CoroutineContext = EmptyCoroutineContext,
43 | block: suspend CoroutineScope.() -> Unit
44 | ): Job {
45 | return this.launch(context) {
46 | mutex.withLock {
47 | block()
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FF000000
4 | #FFFFFFFF
5 |
6 | #89B602
7 |
8 | #FAFDF9
9 | #A8DCD6
10 | #B7E8E3
11 |
12 | #29B3A5
13 | #EDF8F7
14 | #AED5D5
15 |
16 | #2E9187
17 | #576363
18 |
19 | #71A19C
20 | #EDFAF5
21 | #D9EDE5
22 |
23 | #315752
24 | #3F6863
25 | #DCEBE6
26 |
27 | #B4E4E0
28 | #598781
29 | #8ECFCF
30 | #315752
31 |
32 | #FFFFFFFF
33 | #EEFDF8
34 | #E8F7F2
35 | #E2F1EC
36 | #DCEBE6
37 |
38 | #A4CDCD
39 |
40 | #C9EFEB
41 | #FB7C54
42 | #A8DCD6
43 | #A6D5CF
44 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FF000000
4 | #FFFFFFFF
5 |
6 | #99CC00
7 |
8 | #171D1A
9 | #567571
10 | #223533
11 |
12 | #32C5B6
13 | #00382B
14 | #0D7E7D
15 |
16 | #2E9187
17 | #93A5A5
18 |
19 | #637C78
20 | #374A45
21 | #4D5E57
22 |
23 | #C7EDE9
24 | #C1DFDC
25 | #2E3E3B
26 |
27 | #024C3B
28 | #B0D8D3
29 | #476B6B
30 | #C4E6E2
31 |
32 | #011109
33 | #152522
34 | #192926
35 | #233330
36 | #2E3E3B
37 |
38 | #0D7E7D
39 |
40 | #2D4E49
41 | #DE643D
42 | #455754
43 | #5E7872
44 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/network/ConverterFactory.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.network
2 |
3 | import app.neonorbit.mrvpatchmanager.network.marker.HtmlMarker
4 | import app.neonorbit.mrvpatchmanager.network.marker.JsonMarker
5 | import app.neonorbit.mrvpatchmanager.network.marker.XmlMarker
6 | import okhttp3.ResponseBody
7 | import pl.droidsonroids.retrofit2.JspoonConverterFactory
8 | import retrofit2.Converter
9 | import retrofit2.Retrofit
10 | import retrofit2.converter.gson.GsonConverterFactory
11 | import java.lang.reflect.Type
12 |
13 | class ConverterFactory : Converter.Factory() {
14 | companion object {
15 | val PARSERS = listOf(
16 | XmlMarker::class, HtmlMarker::class, JsonMarker::class
17 | )
18 | }
19 |
20 | private val gson: GsonConverterFactory by lazy {
21 | GsonConverterFactory.create()
22 | }
23 |
24 | private val html: JspoonConverterFactory by lazy {
25 | JspoonConverterFactory.create()
26 | }
27 |
28 | @Suppress("deprecation")
29 | private val xml: retrofit2.converter.simplexml.SimpleXmlConverterFactory by lazy {
30 | retrofit2.converter.simplexml.SimpleXmlConverterFactory.create()
31 | }
32 |
33 | override fun responseBodyConverter(
34 | type: Type,
35 | annotations: Array,
36 | retrofit: Retrofit
37 | ): Converter? {
38 | return annotations.firstOrNull {
39 | it.annotationClass in PARSERS
40 | }?.annotationClass?.let { parser ->
41 | return when (parser) {
42 | XmlMarker::class -> xml.responseBodyConverter(type, annotations, retrofit)
43 | HtmlMarker::class -> html.responseBodyConverter(type, annotations, retrofit)
44 | JsonMarker::class -> gson.responseBodyConverter(type, annotations, retrofit)
45 | else -> null
46 | }
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/ui/home/InstalledAppAdapter.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.ui.home
2 |
3 | import android.view.LayoutInflater
4 | import android.view.View
5 | import android.view.ViewGroup
6 | import android.widget.ImageView
7 | import android.widget.TextView
8 | import androidx.recyclerview.widget.RecyclerView
9 | import app.neonorbit.mrvpatchmanager.R
10 | import app.neonorbit.mrvpatchmanager.data.AppFileData
11 | import app.neonorbit.mrvpatchmanager.ui.home.InstalledAppAdapter.ItemHolder
12 | import com.bumptech.glide.Glide
13 | import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
14 |
15 | class InstalledAppAdapter(private val list: List) : RecyclerView.Adapter() {
16 | private lateinit var callback: (AppFileData) -> Unit
17 |
18 | fun setItemClickListener(callback: (AppFileData) -> Unit) {
19 | this.callback = callback
20 | }
21 |
22 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemHolder {
23 | val inflater = LayoutInflater.from(parent.context)
24 | val view = inflater.inflate(R.layout.apk_simple_item_view, parent, false)
25 | return ItemHolder(view)
26 | }
27 |
28 | override fun onBindViewHolder(holder: ItemHolder, position: Int) {
29 | val item = list[position]
30 | holder.apkTitle.text = item.name
31 | Glide.with(holder.itemView)
32 | .load(item.file.absolutePath)
33 | .placeholder(R.drawable.generic_placeholder)
34 | .transition(DrawableTransitionOptions.withCrossFade())
35 | .into(holder.apkIcon)
36 | holder.itemView.setOnClickListener {
37 | callback(item)
38 | }
39 | }
40 |
41 | override fun getItemCount() = list.size
42 |
43 | class ItemHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
44 | val apkIcon: ImageView = itemView.findViewById(R.id.apk_icon)
45 | val apkTitle: TextView = itemView.findViewById(R.id.apk_title)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/ui/SelectionTrackerFactory.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.ui
2 |
3 | import android.view.MotionEvent
4 | import androidx.recyclerview.selection.ItemDetailsLookup
5 | import androidx.recyclerview.selection.OperationMonitor
6 | import androidx.recyclerview.selection.SelectionPredicates
7 | import androidx.recyclerview.selection.SelectionTracker
8 | import androidx.recyclerview.selection.StableIdKeyProvider
9 | import androidx.recyclerview.selection.StorageStrategy
10 | import androidx.recyclerview.widget.RecyclerView
11 |
12 | object SelectionTrackerFactory {
13 | fun buildFor(recyclerView: RecyclerView): SelectionTracker {
14 | val monitor = OperationMonitor()
15 | return SelectionTracker.Builder(
16 | recyclerView.javaClass.name,
17 | recyclerView,
18 | StableIdKeyProvider(recyclerView),
19 | SelectionItemLookup(recyclerView),
20 | StorageStrategy.createLongStorage()
21 | ).withSelectionPredicate(
22 | SelectionPredicates.createSelectAnything()
23 | ).withOperationMonitor(
24 | monitor
25 | ).withSelectionPredicate(object : SelectionTracker.SelectionPredicate() {
26 | override fun canSelectMultiple(): Boolean = true
27 | override fun canSetStateForKey(k: Long, n:Boolean): Boolean = !monitor.isStarted
28 | override fun canSetStateAtPosition(p: Int, n: Boolean): Boolean = !monitor.isStarted
29 | }).build()
30 | }
31 |
32 | class SelectionItemLookup(private val recyclerView: RecyclerView) : ItemDetailsLookup() {
33 | override fun getItemDetails(event: MotionEvent): ItemDetails? {
34 | return recyclerView.findChildViewUnder(event.x, event.y)?.let {
35 | (recyclerView.getChildViewHolder(it) as TrackerItemDetails).getItemDetails()
36 | }
37 | }
38 | }
39 |
40 | interface TrackerItemDetails {
41 | fun getItemDetails(): ItemDetailsLookup.ItemDetails
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/glide/ApkIconLoaderFactory.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.glide
2 |
3 | import android.content.Context
4 | import android.graphics.drawable.Drawable
5 | import app.neonorbit.mrvpatchmanager.apk.ApkUtil
6 | import com.bumptech.glide.Priority
7 | import com.bumptech.glide.load.DataSource
8 | import com.bumptech.glide.load.Options
9 | import com.bumptech.glide.load.data.DataFetcher
10 | import com.bumptech.glide.load.model.ModelLoader
11 | import com.bumptech.glide.load.model.ModelLoaderFactory
12 | import com.bumptech.glide.load.model.MultiModelLoaderFactory
13 | import com.bumptech.glide.signature.ObjectKey
14 | import java.io.File
15 |
16 | class ApkIconLoaderFactory(val context: Context) : ModelLoaderFactory {
17 | override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader {
18 | return ApkIconModelLoader()
19 | }
20 |
21 | override fun teardown() {}
22 |
23 | class ApkIconModelLoader : ModelLoader {
24 | override fun buildLoadData(
25 | model: String,
26 | width: Int,
27 | height: Int,
28 | options: Options
29 | ): ModelLoader.LoadData {
30 | return ModelLoader.LoadData(ObjectKey(model), ApkIconFetcher(model))
31 | }
32 |
33 | override fun handles(model: String): Boolean {
34 | return model.substringAfterLast(".").equals("apk", true)
35 | }
36 | }
37 |
38 | class ApkIconFetcher(private val path: String) : DataFetcher {
39 | override fun loadData(priority: Priority, callback: DataFetcher.DataCallback) {
40 | ApkUtil.getApkIcon(File(path)).let { icon ->
41 | callback.onDataReady(icon)
42 | }
43 | }
44 |
45 | override fun getDataSource(): DataSource = DataSource.LOCAL
46 |
47 | override fun getDataClass(): Class = Drawable::class.java
48 |
49 | override fun cancel() {}
50 |
51 | override fun cleanup() {}
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/util/AppUtil.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.util
2 |
3 | import android.content.Context
4 | import androidx.annotation.StringRes
5 | import com.google.android.material.dialog.MaterialAlertDialogBuilder
6 |
7 | object AppUtil {
8 | fun prompt(
9 | context: Context,
10 | @StringRes title: Int? = null,
11 | @StringRes message: Int? = null,
12 | @StringRes positive: Int? = null,
13 | block: ((Boolean) -> Unit)
14 | ) {
15 | prompt(context,
16 | title?.let { context.getString(it) },
17 | message?.let { context.getString(it) },
18 | positive?.let { context.getString(it) },
19 | block
20 | )
21 | }
22 |
23 | fun prompt(
24 | context: Context,
25 | title: String? = null,
26 | message: String? = null,
27 | positive: String? = null,
28 | block: ((Boolean) -> Unit)
29 | ) {
30 | var result = false
31 | MaterialAlertDialogBuilder(context)
32 | .setTitle(title)
33 | .setMessage(message)
34 | .setPositiveButton(
35 | positive ?: context.getString(android.R.string.ok)
36 | ) { _,_-> result = true }
37 | .setNegativeButton(android.R.string.cancel, null)
38 | .setOnDismissListener { block.invoke(result) }
39 | .show()
40 | }
41 |
42 | fun prompt(context: Context, @StringRes message: Int, vararg formatArgs: Any) {
43 | prompt(context, context.getString(message, *formatArgs))
44 | }
45 |
46 | fun prompt(context: Context, message: String) {
47 | MaterialAlertDialogBuilder(context).setMessage(message)
48 | .setNegativeButton(android.R.string.ok, null)
49 | .show()
50 | }
51 |
52 | fun show(context: Context, @StringRes message: Int) {
53 | show(context, context.getString(message))
54 | }
55 |
56 | fun show(context: Context, message: String) {
57 | MaterialAlertDialogBuilder(context).setMessage(message).show()
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/remote/data/ApkMirrorVariantData.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.remote.data
2 |
3 | import app.neonorbit.mrvpatchmanager.apk.ApkConfigs
4 | import app.neonorbit.mrvpatchmanager.remote.ApkMirrorService
5 | import app.neonorbit.mrvpatchmanager.util.Utils
6 | import pl.droidsonroids.jspoon.annotation.Selector
7 |
8 | class ApkMirrorVariantData {
9 | @Selector(".variants-table .table-row:has(a):matches(\\barm(?:eabi|64)-(?:v7a|v8a)\\b):contains(dpi)")
10 | var variants: List = listOf()
11 |
12 | override fun toString(): String {
13 | return "variants: $variants"
14 | }
15 |
16 | class Variant {
17 | @Selector(value = ".apkm-badge", defValue = "")
18 | private lateinit var type: String
19 |
20 | @Selector("a[href*=download]:matches(\\b(? get() = _variants.ifEmpty { _fallback }
10 |
11 | @Selector("#variants-tab ul:not(ul.file-list) > li")
12 | private var _variants: List = listOf()
13 |
14 | @Selector("#best-variant-tab ul:not(ul.file-list) > li")
15 | private var _fallback: List = listOf()
16 |
17 | override fun toString(): String {
18 | return "variants: $_variants, fallback: $_fallback"
19 | }
20 |
21 | class Variant {
22 | @Selector("span:matches(\\barm(?:eabi|64)-(?:v7a|v8a)\\b), code:contains(arm)", defValue = "")
23 | lateinit var arch: String
24 |
25 | @Selector(".file-list > li")
26 | var apks: List = listOf()
27 |
28 | override fun toString(): String {
29 | return "arch: $arch, apks: $apks"
30 | }
31 | }
32 |
33 | class Apk {
34 | @Selector(".vtype", defValue = "")
35 | private lateinit var type: String
36 |
37 | @Selector(".vername", defValue = "")
38 | private lateinit var name: String
39 |
40 | @Selector(".description", defValue = "")
41 | private lateinit var info: String
42 |
43 | @Selector("a.variant", attr = "href")
44 | private lateinit var href: String
45 |
46 | val dpi: String? get() = info.takeIf { "dpi" in it }
47 |
48 | val minSDk: Int? get() = ApkConfigs.extractMinSdk(info)
49 |
50 | val version: String? get() = ApkConfigs.extractVersionName(name)
51 |
52 | val link: String get() = Utils.absoluteUrl(ApkComboService.BASE_URL, href)
53 |
54 | val isValidType: Boolean get() = type.trim().lowercase().let { it == "apk" || "xapk" !in it }
55 |
56 | override fun toString(): String {
57 | return "type: $type, version: $version, dpi: $dpi, minSDk: $minSDk, link: $link"
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/keystore/KeystoreManager.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.keystore
2 |
3 | import android.content.pm.Signature
4 | import app.neonorbit.mrvpatchmanager.error
5 | import java.io.File
6 | import java.io.IOException
7 | import java.security.KeyStore
8 | import java.security.UnrecoverableKeyException
9 |
10 | object KeystoreManager {
11 | fun readKeyData(input: File, path: String, password: String, inAlias: String?, inAliasPass: String?): KeystoreData {
12 | try {
13 | var sig: String? = null
14 | var alias: String? = inAlias
15 | var aliasPass: String? = inAliasPass
16 | val keystore = KeyStore.getInstance(KeyStore.getDefaultType())
17 | input.inputStream().use { stream ->
18 | keystore.load(stream, password.toCharArray())
19 | if (alias?.isNotBlank() != true) {
20 | alias = keystore.aliases().nextElement()
21 | }
22 | if (aliasPass?.isNotEmpty() != true) {
23 | aliasPass = password
24 | }
25 | if (!keystore.containsAlias(alias!!)) {
26 | throw Exception("Wrong key alias: $alias")
27 | }
28 | try {
29 | keystore.getKey(alias!!, aliasPass!!.toCharArray()) ?: throw Exception(
30 | "Failed to retrieve key entry!"
31 | )
32 | keystore.getCertificate(alias!!)?.encoded?.let {
33 | sig = Signature(it).toCharsString()
34 | } ?: throw Exception("Failed to retrieve key signature!")
35 | } catch (e: UnrecoverableKeyException) {
36 | throw Exception("Wrong alias password!")
37 | }
38 | }
39 | return KeystoreData(path, password, alias!!, aliasPass!!, sig!!)
40 | } catch (e: IOException) {
41 | throw when {
42 | e.message?.contains("KeyStore integrity check failed") == true -> "Wrong keystore password!"
43 | e.message?.contains("Wrong version") == true -> "Unsupported keystore file!"
44 | else -> e.error
45 | }.let { Exception(it) }
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/ui/ConfirmationDialog.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.ui
2 |
3 | import android.app.Dialog
4 | import android.content.DialogInterface
5 | import android.os.Bundle
6 | import androidx.fragment.app.DialogFragment
7 | import androidx.fragment.app.Fragment
8 | import com.google.android.material.dialog.MaterialAlertDialogBuilder
9 |
10 | class ConfirmationDialog : DialogFragment() {
11 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
12 | return MaterialAlertDialogBuilder(requireContext())
13 | .setTitle(arguments?.getString(TITLE))
14 | .setMessage(arguments?.getString(MESSAGE))
15 | .setPositiveButton(
16 | arguments?.getString(POSITIVE) ?:
17 | requireContext().getString(android.R.string.ok)
18 | ) { _,_->
19 | listener?.onResponse(true)
20 | }
21 | .setNegativeButton(
22 | getString(android.R.string.cancel)
23 | ) { _,_->
24 | listener?.onResponse(false)
25 | }
26 | .create()
27 | }
28 |
29 | override fun onCancel(dialog: DialogInterface) {
30 | super.onCancel(dialog)
31 | listener?.onResponse(false)
32 | }
33 |
34 | interface ResponseListener {
35 | fun onResponse(response: Boolean)
36 | }
37 |
38 | private val listener: ResponseListener? get() = parentFragment?.let {
39 | if (it is ResponseListener) it else null
40 | }
41 |
42 | companion object {
43 | private const val TAG = "Confirmation"
44 | private const val TITLE = "TitleArgKey"
45 | private const val MESSAGE = "MessageArgKey"
46 | private const val POSITIVE = "PositiveArgKey"
47 |
48 | fun show(
49 | parent: Fragment,
50 | title: String? = null,
51 | message: String? = null,
52 | positive: String? = null
53 | ) {
54 | ConfirmationDialog().apply {
55 | arguments = Bundle().apply {
56 | putString(TITLE, title)
57 | putString(MESSAGE, message)
58 | putString(POSITIVE, positive)
59 | }
60 | }.show(parent.childFragmentManager, TAG)
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/AppInstaller.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager
2 |
3 | import android.app.Activity
4 | import android.app.Application
5 | import android.content.BroadcastReceiver
6 | import android.content.Context
7 | import android.content.Intent
8 | import android.content.IntentFilter
9 | import android.net.Uri
10 | import app.neonorbit.mrvpatchmanager.apk.ApkConfigs
11 | import org.greenrobot.eventbus.EventBus
12 | import java.io.File
13 |
14 | object AppInstaller {
15 | data class Event(val pkg: String)
16 |
17 | private const val VALID_PKG = "package:app.neonorbit."
18 |
19 | private val receiver by lazy {
20 | object: BroadcastReceiver() {
21 | override fun onReceive(context: Context?, intent: Intent?) {
22 | intent?.dataString?.takeIf { it.startsWith(VALID_PKG) }?.let {
23 | EventBus.getDefault().postSticky(Event(it.substringAfter(":")))
24 | }
25 | }
26 | }
27 | }
28 |
29 | fun register(application: Application) {
30 | application.registerReceiver(receiver, IntentFilter().apply {
31 | addDataScheme("package")
32 | addAction(Intent.ACTION_PACKAGE_ADDED)
33 | addAction(Intent.ACTION_PACKAGE_REMOVED)
34 | addAction(Intent.ACTION_PACKAGE_CHANGED)
35 | addAction(Intent.ACTION_PACKAGE_REPLACED)
36 | })
37 | }
38 |
39 | @Suppress("Deprecation")
40 | fun install(context: Context, file: File) {
41 | val intent = Intent(Intent.ACTION_INSTALL_PACKAGE).apply {
42 | addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
43 | setDataAndType(AppServices.resolveContentUri(file), ApkConfigs.APK_MIME_TYPE)
44 | }
45 | context.launch(intent)
46 | }
47 |
48 | @Suppress("Deprecation")
49 | fun uninstall(context: Context, pkg: String) {
50 | val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE).apply {
51 | data = Uri.fromParts("package", pkg, null)
52 | }
53 | context.launch(intent)
54 | }
55 |
56 | private fun Context.launch(intent: Intent) {
57 | if (this !is Activity)
58 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
59 | startActivity(intent)
60 | }
61 |
62 | /** TO-DO: Migrate to PackageInstaller **/
63 | }
64 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
33 |
34 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
50 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/about_dialog.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
20 |
21 |
27 |
28 |
34 |
35 |
42 |
43 |
50 |
51 |
58 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/network/RetrofitClient.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.network
2 |
3 | import app.neonorbit.mrvpatchmanager.AppServices
4 | import app.neonorbit.mrvpatchmanager.BuildConfig
5 | import okhttp3.Cache
6 | import okhttp3.CacheControl
7 | import okhttp3.OkHttpClient
8 | import okhttp3.Request
9 | import okhttp3.logging.HttpLoggingInterceptor
10 | import retrofit2.Retrofit
11 | import java.io.File
12 | import java.util.concurrent.TimeUnit
13 |
14 | object RetrofitClient {
15 | private const val BASE_URL = "https://place.holder/"
16 | private const val USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64)"
17 | private const val CACHE_PATH = "http-cache"
18 | private const val MAX_CACHE_HOUR = 1
19 | private const val MAX_CACHE_SIZE = 1024 * 1024 * 10L
20 |
21 | val SERVICE: ApiService by lazy {
22 | CLIENT.create(ApiService::class.java)
23 | }
24 |
25 | private val CLIENT: Retrofit by lazy {
26 | OkHttpClient.Builder()
27 | .addInterceptor { chain ->
28 | chain.request().newBuilder()
29 | .header(HttpSpec.Header.USER_AGENT, USER_AGENT)
30 | .build().let { request ->
31 | chain.proceed(request)
32 | }
33 | }.addNetworkInterceptor { chain ->
34 | chain.request().let { request ->
35 | chain.proceed(request).newBuilder()
36 | .header(HttpSpec.Header.CACHE_CONTROL, cacheHeader(request))
37 | .build()
38 | }
39 | }.also {
40 | if (BuildConfig.DEBUG) it.addNetworkInterceptor(
41 | HttpLoggingInterceptor().apply {
42 | level = HttpLoggingInterceptor.Level.HEADERS
43 | }
44 | )
45 | }
46 | .cache(httpCache)
47 | .readTimeout(50, TimeUnit.SECONDS)
48 | .connectTimeout(40, TimeUnit.SECONDS)
49 | .build().let { okHttp ->
50 | Retrofit.Builder()
51 | .addConverterFactory(ConverterFactory())
52 | .baseUrl(BASE_URL)
53 | .client(okHttp)
54 | .build()
55 | }
56 | }
57 |
58 | private val httpCache: Cache by lazy {
59 | Cache(File(AppServices.getCacheDir(), CACHE_PATH), MAX_CACHE_SIZE)
60 | }
61 |
62 | private fun cacheHeader(request: Request): String {
63 | return if (request.cacheControl.noStore) request.cacheControl.toString()
64 | else CacheControl.Builder().maxAge(MAX_CACHE_HOUR, TimeUnit.HOURS).build().toString()
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/version_input_dialog.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
15 |
16 |
23 |
28 |
29 |
30 |
33 |
34 |
46 |
47 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/apk/ApkConfigs.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.apk
2 |
3 | import android.os.Build
4 | import app.neonorbit.mrvpatchmanager.compareVersion
5 | import app.neonorbit.mrvpatchmanager.util.Utils
6 |
7 | object ApkConfigs {
8 | const val ARM_64 = "arm64-v8a"
9 | const val ARM_32 = "armeabi-v7a"
10 | const val X86_64 = "x86_64"
11 | const val X86 = "x86"
12 |
13 | private const val PREFERRED_DPI = "nodpi"
14 |
15 | private val SUPPORTED_DPIs = arrayOf(
16 | "nodpi", "360dpi", "400dpi", "420dpi", "480dpi", "560dpi", "640dpi"
17 | )
18 |
19 | const val APK_MIME_TYPE = "application/vnd.android.package-archive"
20 |
21 | private val ANDROID_VERSION by lazy { Utils.sdkToVersion(Build.VERSION.SDK_INT) }
22 |
23 | private val TEST_BUILDS = arrayOf("alpha", "beta", ".0.0.0.")
24 |
25 | fun isValidRelease(name: String) = name.isNotBlank() && name.lowercase().let { lower ->
26 | TEST_BUILDS.none { lower.contains(it) }
27 | }
28 |
29 | fun isSupportedMinSdk(sdk: Int?) = sdk?.let { it <= ANDROID_VERSION } != false
30 | fun isPreferredDPI(dpi: String?) = dpi?.lowercase()?.contains(PREFERRED_DPI) != false
31 | fun isSupportedDPI(dpi: String?) = dpi?.lowercase()?.let { SUPPORTED_DPIs.any { it in dpi } } != false
32 |
33 | private val MIN_SDK_REGEX: Regex by lazy { Regex("\\bAndroid\\s*\\W+(\\d+)(?:\\.\\d+)*\\+") }
34 | private val VERSION_REGEX: Regex by lazy { Regex("\\b(? apk.size) return false
50 | for (i in ver.indices) {
51 | if (ver[i] != apk[i]) return false
52 | }
53 | return true
54 | }
55 |
56 | fun compareLatest(selector: (T) -> String?, then: ((T) -> Boolean)? = null): Comparator {
57 | return Comparator { o1, o2 ->
58 | selector(o1)?.compareVersion(selector(o2)) ?: 0
59 | }.let {
60 | if (then == null) it else it.thenComparing { o1, o2 ->
61 | then(o1).compareTo(then(o2))
62 | }
63 | }.reversed()
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/remote/data/ApkFlashVariantData.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.remote.data
2 |
3 | import app.neonorbit.mrvpatchmanager.apk.ApkConfigs
4 | import app.neonorbit.mrvpatchmanager.remote.ApkFlashService
5 | import app.neonorbit.mrvpatchmanager.util.NullableElementConverter
6 | import app.neonorbit.mrvpatchmanager.util.Utils
7 | import org.jsoup.nodes.Element
8 | import pl.droidsonroids.jspoon.Jspoon
9 | import pl.droidsonroids.jspoon.annotation.Selector
10 |
11 | class ApkFlashVariantData {
12 | val variants: List get() = _variants.ifEmpty { _fallback }
13 |
14 | @Selector("#variants", attr = "html", converter = VariantsExtractor::class)
15 | private var _variants: List = listOf()
16 |
17 | @Selector("#download", attr = "html", converter = VariantsExtractor::class)
18 | private var _fallback: List = listOf()
19 |
20 | override fun toString(): String {
21 | return "variants: $_variants, fallback: $_fallback"
22 | }
23 |
24 | data class Variant(val arch: String, val apks: List) {
25 | override fun toString(): String {
26 | return "arch: $arch, apks: $apks"
27 | }
28 | }
29 |
30 | class Apk {
31 | @Selector(".vtype", defValue = "")
32 | private lateinit var type: String
33 |
34 | @Selector(".vername", defValue = "")
35 | private lateinit var name: String
36 |
37 | @Selector(".description", defValue = "")
38 | private lateinit var info: String
39 |
40 | @Selector("a.variant", attr = "href")
41 | private lateinit var href: String
42 |
43 | val dpi: String? get() = info.takeIf { "dpi" in it }
44 |
45 | val minSDk: Int? get() = ApkConfigs.extractMinSdk(info)
46 |
47 | val version: String? get() = ApkConfigs.extractVersionName(name)
48 |
49 | val link: String get() = Utils.absoluteUrl(ApkFlashService.BASE_URL, href)
50 |
51 | val isValidType: Boolean get() = type.trim().lowercase().let { it == "apk" || "xapk" !in it }
52 |
53 | override fun toString(): String {
54 | return "type: $type, version: $version, dpi: $dpi, minSDk: $minSDk, link: $link"
55 | }
56 | }
57 |
58 | object VariantsExtractor : NullableElementConverter> {
59 | private val apkParser = Jspoon.create().adapter(Apk::class.java)
60 |
61 | override fun convert(node: Element?, selector: Selector): List {
62 | return node?.select(".files-header:matches(\\barm(?:eabi|64)-(?:v7a|v8a)\\b)")?.map { arch ->
63 | Variant(
64 | arch.text(),
65 | arch.nextElementSibling().select(".variant").map {
66 | apkParser.fromHtml(it.outerHtml())
67 | }
68 | )
69 | } ?: listOf()
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
46 |
47 |
51 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_pref_advanced.xml:
--------------------------------------------------------------------------------
1 |
8 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/DefaultPreference.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager
2 |
3 | import android.content.SharedPreferences
4 | import app.neonorbit.mrvpatchmanager.keystore.KeystoreData
5 | import app.neonorbit.mrvpatchmanager.ui.settings.PreferenceAdvancedFragment
6 | import app.neonorbit.mrvpatchmanager.ui.settings.PreferenceFragment
7 |
8 | @Suppress("Unused", "SameParameterValue")
9 | object DefaultPreference {
10 | private val cache: SharedPreferences get() = AppServices.cachePreferences
11 | private val preferences: SharedPreferences get() = AppServices.preferences
12 |
13 | private const val KEY_PREF_APK_SERVER = PreferenceFragment.KEY_PREF_APK_SERVER
14 | private const val KEY_PREF_FIX_CONFLICT = PreferenceFragment.KEY_PREF_FIX_CONFLICT
15 | private const val KEY_PREF_MASK_PACKAGE = PreferenceFragment.KEY_PREF_MASK_PACKAGE
16 | private const val KEY_PREF_FALLBACK_MODE = PreferenceFragment.KEY_PREF_FALLBACK_MODE
17 | private const val KEY_PREF_APK_ABI_TYPE = PreferenceAdvancedFragment.KEY_PREF_APK_ABI_TYPE
18 | private const val KEY_PREF_EXTRA_MODULES = PreferenceAdvancedFragment.KEY_PREF_EXTRA_MODULES
19 | private const val KEY_PREF_CUSTOM_KEYSTORE = PreferenceAdvancedFragment.KEY_PREF_CUSTOM_KEYSTORE
20 |
21 | fun getApkServer(): String? = getString(KEY_PREF_APK_SERVER)
22 |
23 | fun isFixConflictEnabled(): Boolean = getBoolean(KEY_PREF_FIX_CONFLICT)
24 |
25 | fun isPackageMaskEnabled(): Boolean = getBoolean(KEY_PREF_MASK_PACKAGE)
26 |
27 | fun isFallbackModeEnabled(): Boolean = getBoolean(KEY_PREF_FALLBACK_MODE)
28 |
29 | fun getExtraModules(): List? = getStringList(KEY_PREF_EXTRA_MODULES, ',', true)
30 |
31 | fun getCustomKeystore(): KeystoreData? = getString(KEY_PREF_CUSTOM_KEYSTORE)?.parseJson()
32 |
33 | fun getPreferredABI(): String = getString(KEY_PREF_APK_ABI_TYPE).let {
34 | if (it != null && it != PreferenceAdvancedFragment.APK_ABI_AUTO) it else AppConfigs.DEVICE_ABI
35 | }
36 |
37 | fun getString(key: String): String? {
38 | return preferences.getString(key, null)
39 | }
40 |
41 | fun putString(key: String, value: String?) {
42 | if (value == null) remove(key)
43 | else preferences.edit().putString(key, value).apply()
44 | }
45 |
46 | private fun getStringList(key: String, del: Char, trim: Boolean): List? {
47 | return getString(key)?.takeIf { it.isNotEmpty() }?.split(del)?.let { list ->
48 | if (trim) list.map { it.trim() } else list
49 | }
50 | }
51 |
52 | private fun getBoolean(key: String): Boolean {
53 | return preferences.getBoolean(key, false)
54 | }
55 |
56 | private fun putBoolean(key: String, value: Boolean) {
57 | preferences.edit().putBoolean(key, value).apply()
58 | }
59 |
60 | private fun remove(key: String) {
61 | preferences.edit().remove(key).apply()
62 | }
63 |
64 | fun getCache(key: String): String? = cache.getString(key, null)
65 |
66 | fun putCache(key: String, value: String?) = value?.let {
67 | cache.edit().putString(key, it).apply()
68 | } ?: remove(key)
69 |
70 | fun clearCache() = cache.edit().clear().apply()
71 | }
72 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/AppServices.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager
2 |
3 | import android.content.ContentResolver
4 | import android.content.Context
5 | import android.content.SharedPreferences
6 | import android.content.pm.PackageManager
7 | import android.content.res.AssetManager
8 | import android.net.Uri
9 | import android.widget.Toast
10 | import androidx.annotation.StringRes
11 | import androidx.annotation.WorkerThread
12 | import androidx.core.content.FileProvider
13 | import androidx.documentfile.provider.DocumentFile
14 | import androidx.preference.PreferenceManager
15 | import app.neonorbit.mrvpatchmanager.util.Utils
16 | import kotlinx.coroutines.CoroutineExceptionHandler
17 | import kotlinx.coroutines.CoroutineScope
18 | import kotlinx.coroutines.Dispatchers
19 | import kotlinx.coroutines.SupervisorJob
20 | import kotlinx.coroutines.launch
21 | import java.io.File
22 |
23 | object AppServices {
24 | val application: MRVPatchManager get() = MRVPatchManager.instance
25 |
26 | val preferences: SharedPreferences by lazy {
27 | PreferenceManager.getDefaultSharedPreferences(application)
28 | }
29 |
30 | val cachePreferences: SharedPreferences by lazy {
31 | application.getSharedPreferences("${application.packageName}_cache", Context.MODE_PRIVATE)
32 | }
33 |
34 | val assetManager: AssetManager get() = application.assets
35 |
36 | val packageManager: PackageManager by lazy { application.packageManager }
37 |
38 | val contentResolver: ContentResolver by lazy { application.contentResolver }
39 |
40 | val globalScope: CoroutineScope by lazy {
41 | CoroutineScope(SupervisorJob() + CoroutineExceptionHandler { _, throwable ->
42 | Utils.error("Global Coroutine Exception: ${throwable.message}", throwable)
43 | })
44 | }
45 |
46 | @WorkerThread
47 | fun isNetworkOnline() = SystemServices.Network.isOnline(application)
48 |
49 | fun getAppCacheSize() = application.cacheDir.totalSize()
50 |
51 | fun clearAppCache() = application.cacheDir.deleteRecursively()
52 |
53 | fun getCacheDir(): File = application.cacheDir
54 |
55 | val appFilesDir: File get() = application.filesDir.init()
56 |
57 | fun getFilesDir(sub: String): File {
58 | return File(application.filesDir, sub).init()
59 | }
60 |
61 | fun getCacheDir(sub: String): File {
62 | return File(application.cacheDir, sub).init()
63 | }
64 |
65 | private fun File.init() = apply { if (!exists()) mkdirs() }
66 |
67 | fun showToast(@StringRes resId: Int, long: Boolean = false) {
68 | showToast(application.getString(resId), long)
69 | }
70 |
71 | fun showToast(message: String, long: Boolean = false) {
72 | globalScope.launch(Dispatchers.Main) {
73 | val duration = if (long) Toast.LENGTH_LONG else Toast.LENGTH_SHORT
74 | try {
75 | Toast.makeText(application, message, duration).show()
76 | } catch (_: Exception) {}
77 | }
78 | }
79 |
80 | fun resolveDocumentTree(uri: Uri) = DocumentFile.fromTreeUri(application, uri)
81 |
82 | fun resolveContentUri(file: File): Uri? {
83 | return FileProvider.getUriForFile(application, AppConfigs.FILE_PROVIDER_AUTH, file)
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/pref_extra_modules_dialog.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
17 |
18 |
25 |
31 |
32 |
33 |
36 |
37 |
51 |
52 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/MRVPatchManager.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/network/ApiService.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.network
2 |
3 | import app.neonorbit.mrvpatchmanager.network.marker.HtmlMarker
4 | import app.neonorbit.mrvpatchmanager.network.marker.JsonMarker
5 | import app.neonorbit.mrvpatchmanager.network.marker.XmlMarker
6 | import app.neonorbit.mrvpatchmanager.remote.data.ApkComboReleaseData
7 | import app.neonorbit.mrvpatchmanager.remote.data.ApkComboVariantData
8 | import app.neonorbit.mrvpatchmanager.remote.data.ApkFlashReleaseData
9 | import app.neonorbit.mrvpatchmanager.remote.data.ApkFlashVariantData
10 | import app.neonorbit.mrvpatchmanager.remote.data.ApkMirrorIFormData
11 | import app.neonorbit.mrvpatchmanager.remote.data.ApkMirrorItemData
12 | import app.neonorbit.mrvpatchmanager.remote.data.ApkMirrorReleaseData
13 | import app.neonorbit.mrvpatchmanager.remote.data.ApkMirrorVariantData
14 | import app.neonorbit.mrvpatchmanager.remote.data.ApkPureReleaseData
15 | import app.neonorbit.mrvpatchmanager.remote.data.ApkPureVariantData
16 | import app.neonorbit.mrvpatchmanager.remote.data.GithubReleaseData
17 | import app.neonorbit.mrvpatchmanager.remote.data.ApkMirrorRssFeedData
18 | import okhttp3.ResponseBody
19 | import retrofit2.Response
20 | import retrofit2.http.GET
21 | import retrofit2.http.HEAD
22 | import retrofit2.http.HeaderMap
23 | import retrofit2.http.Headers
24 | import retrofit2.http.Streaming
25 | import retrofit2.http.Url
26 |
27 | interface ApiService {
28 | @HEAD
29 | suspend fun head(@Url url: String): Response
30 |
31 | @GET
32 | suspend fun get(@Url url: String): Response
33 |
34 | @GET
35 | @Streaming
36 | @Headers("Cache-Control: no-store")
37 | suspend fun download(
38 | @Url directDownloadUrl: String,
39 | @HeaderMap headers: Map = mapOf()
40 | ): Response
41 |
42 | @GET
43 | @JsonMarker
44 | suspend fun getGithubRelease(@Url url: String): Response
45 |
46 | @GET
47 | @XmlMarker
48 | suspend fun getApkMirrorFeed(@Url url: String): Response
49 |
50 | @GET
51 | @HtmlMarker
52 | suspend fun getApkMirrorRelease(@Url url: String): Response
53 |
54 | @GET
55 | @HtmlMarker
56 | suspend fun getApkMirrorVariant(@Url url: String): Response
57 |
58 | @GET
59 | @HtmlMarker
60 | suspend fun getApkMirrorItem(@Url url: String): Response
61 |
62 | @GET
63 | @HtmlMarker
64 | suspend fun getApkMirrorInputForm(@Url url: String): Response
65 |
66 | @GET
67 | @HtmlMarker
68 | suspend fun getApkComboRelease(@Url url: String): Response
69 |
70 | @GET
71 | @HtmlMarker
72 | suspend fun getApkComboVariant(@Url url: String): Response
73 |
74 | @GET
75 | @HtmlMarker
76 | suspend fun getApkFlashRelease(@Url url: String): Response
77 |
78 | @GET
79 | @HtmlMarker
80 | suspend fun getApkFlashVariant(@Url url: String): Response
81 |
82 | @GET
83 | @HtmlMarker
84 | suspend fun getApkPureRelease(@Url url: String): Response
85 |
86 | @GET
87 | @HtmlMarker
88 | suspend fun getApkPureVariant(@Url url: String): Response
89 | }
90 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/DefaultPatcher.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager
2 |
3 | import app.neonorbit.mrvpatchmanager.apk.ApkUtil
4 | import app.neonorbit.mrvpatchmanager.keystore.KeystoreData
5 | import kotlinx.coroutines.channels.ProducerScope
6 | import kotlinx.coroutines.channels.awaitClose
7 | import kotlinx.coroutines.ensureActive
8 | import kotlinx.coroutines.flow.callbackFlow
9 | import kotlinx.coroutines.flow.onCompletion
10 | import org.lsposed.patch.MRVPatcher
11 | import org.lsposed.patch.OutputLogger
12 | import java.io.File
13 |
14 | class DefaultPatcher(private val input: File, private val options: Options) {
15 | private val output: File by lazy {
16 | File(AppConfigs.PATCHED_OUT_DIR, input.name)
17 | }
18 |
19 | fun patch() = callbackFlow {
20 | checkPreconditions()
21 | initStatusProducer(this)
22 | MRVPatcher.patch(*buildOptions())
23 | if (output.verify()) {
24 | send(PatchStatus.FINISHED(output))
25 | } else {
26 | output.delete()
27 | send(PatchStatus.FAILED("Something went wrong"))
28 | }
29 | channel.close()
30 | awaitClose()
31 | }.onCompletion { error ->
32 | if (error != null) output.delete()
33 | }
34 |
35 | private fun initStatusProducer(scope: ProducerScope) {
36 | MRVPatcher.setLogger(object : OutputLogger {
37 | override fun d(message: String) {
38 | scope.ensureActive()
39 | scope.trySend(PatchStatus.PATCHING(message))
40 | }
41 | override fun e(error: String) {
42 | throw Exception(error)
43 | }
44 | })
45 | }
46 |
47 | private fun checkPreconditions() {
48 | output.delete()
49 | if (output.exists()) throw Exception(
50 | "Failed to delete output file"
51 | )
52 | }
53 |
54 | private fun File.verify() = output.exists() && ApkUtil.verifySignature(
55 | this, options.customKeystore?.keySignature ?: AppConfigs.MRV_PUBLIC_SIGNATURE
56 | )
57 |
58 | private fun buildOptions() = ArrayList(17).apply {
59 | add(input.absolutePath)
60 | add("--temp-dir")
61 | add(AppConfigs.TEMP_DIR.absolutePath)
62 | add("--out-file")
63 | add(output.absolutePath)
64 | add("--force")
65 | if (options.fixConflict) add("--fix-conf")
66 | if (options.maskPackage) add("--mask-pkg")
67 | if (options.fallbackMode) add("--fallback")
68 | options.customKeystore?.let { data ->
69 | add("--key-args")
70 | add(data.path)
71 | add(data.password)
72 | add(data.aliasName)
73 | add(data.aliasPassword)
74 | }
75 | options.extraModules?.let { mods ->
76 | add("--modules")
77 | mods.forEach { add(it) }
78 | }
79 | }.toTypedArray()
80 |
81 | sealed class PatchStatus {
82 | data class PATCHING(val msg: String) : PatchStatus()
83 | data class FINISHED(val file: File) : PatchStatus()
84 | data class FAILED(val msg: String) : PatchStatus()
85 | }
86 |
87 | data class Options(
88 | val fixConflict: Boolean, val maskPackage: Boolean, val fallbackMode: Boolean,
89 | val customKeystore: KeystoreData?, val extraModules: List?
90 | )
91 | }
92 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/ui/AutoProgressDialog.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.ui
2 |
3 | import android.app.Dialog
4 | import android.content.DialogInterface
5 | import android.os.Bundle
6 | import androidx.annotation.UiThread
7 | import androidx.fragment.app.DialogFragment
8 | import androidx.fragment.app.Fragment
9 | import androidx.lifecycle.MutableLiveData
10 | import androidx.lifecycle.ViewModel
11 | import androidx.lifecycle.ViewModelProvider
12 | import app.neonorbit.mrvpatchmanager.databinding.SimpleProgressViewBinding
13 | import com.google.android.material.dialog.MaterialAlertDialogBuilder
14 |
15 | class AutoProgressDialog : DialogFragment() {
16 | private var viewModel: AutoProgressViewModel? = null
17 |
18 | @UiThread
19 | fun post(parent: Fragment, title: String? = null, progress: Int?) {
20 | progress?.let {
21 | if (!isAdded(parent)) {
22 | if (title != null) setTitle(title)
23 | showNow(parent.childFragmentManager, TAG)
24 | }
25 | viewModel?.liveProgress?.value = it
26 | } ?: finish(parent)
27 | }
28 |
29 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
30 | val binding = SimpleProgressViewBinding.inflate(layoutInflater)
31 | viewModel = ViewModelProvider(this)[AutoProgressViewModel::class.java]
32 | viewModel?.liveProgress?.observe(this) {
33 | binding.progressBar.progress = it
34 | binding.progressBar.isIndeterminate = it < 0
35 | }
36 | return MaterialAlertDialogBuilder(requireContext())
37 | .setTitle(arguments?.getString(TITLE))
38 | .setView(binding.root)
39 | .setNegativeButton(getString(android.R.string.cancel)) { _,_->
40 | listener?.onProgressCancelled()
41 | }.create()
42 | }
43 |
44 | override fun dismiss() {
45 | super.dismissNow()
46 | }
47 |
48 | override fun onCancel(dialog: DialogInterface) {
49 | super.onCancel(dialog)
50 | listener?.onProgressCancelled()
51 | }
52 |
53 | private fun setTitle(title: String) {
54 | arguments?.putString(TITLE, title)
55 | }
56 |
57 | private fun finish(parent: Fragment) {
58 | getFragment(parent)?.dismissNow()
59 | }
60 |
61 | interface OnCancelListener {
62 | fun onProgressCancelled()
63 | }
64 |
65 | private val listener: OnCancelListener? get() = parentFragment?.let {
66 | if (it is OnCancelListener) it else null
67 | }
68 |
69 | private fun isAdded(parent: Fragment): Boolean {
70 | return getFragment(parent)?.isAdded == true
71 | }
72 |
73 | private fun getFragment(parent: Fragment): AutoProgressDialog? {
74 | return parent.childFragmentManager.findFragmentByTag(TAG)?.let {
75 | it as AutoProgressDialog
76 | }
77 | }
78 |
79 | companion object {
80 | private const val TAG = "QuickProgress"
81 | private const val TITLE = "TitleArgKey"
82 |
83 | fun newInstance(title: String = "Progress"): AutoProgressDialog {
84 | return AutoProgressDialog().apply {
85 | isCancelable = false
86 | arguments = Bundle()
87 | setTitle(title)
88 | }
89 | }
90 | }
91 |
92 | class AutoProgressViewModel : ViewModel() {
93 | val liveProgress: MutableLiveData = MutableLiveData()
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/remote/GithubService.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.remote
2 |
3 | import app.neonorbit.mrvpatchmanager.AppConfigs
4 | import app.neonorbit.mrvpatchmanager.AppServices
5 | import app.neonorbit.mrvpatchmanager.CacheManager
6 | import app.neonorbit.mrvpatchmanager.apk.ApkUtil
7 | import app.neonorbit.mrvpatchmanager.compareVersion
8 | import app.neonorbit.mrvpatchmanager.data.UpdateEventData
9 | import app.neonorbit.mrvpatchmanager.network.RetrofitClient
10 | import app.neonorbit.mrvpatchmanager.remote.data.GithubReleaseData
11 | import app.neonorbit.mrvpatchmanager.result
12 | import kotlinx.coroutines.Dispatchers
13 | import kotlinx.coroutines.launch
14 | import org.greenrobot.eventbus.EventBus
15 |
16 | object GithubService {
17 | private const val CACHE_TIMEOUT_HOUR = 2
18 | private const val KEY_UPDATE_CACHE = "update_check_cache"
19 | private const val BASE_URL = "https://api.github.com/repos"
20 | private const val MANAGER_URL = "$BASE_URL/NeonOrbit/MRVPatchManager/releases/latest"
21 | private const val MODULE_URL = "$BASE_URL/NeonOrbit/ChatHeadEnabler/releases/latest"
22 |
23 | suspend fun getManagerLink(): String {
24 | return fetchDirectLink(AppConfigs.MANAGER_PACKAGE, MANAGER_URL)
25 | }
26 |
27 | suspend fun getModuleLink(): String {
28 | return fetchDirectLink(AppConfigs.MODULE_PACKAGE, MODULE_URL)
29 | }
30 |
31 | fun checkForUpdate(force: Boolean = false) {
32 | AppServices.globalScope.launch(Dispatchers.IO) {
33 | EventBus.getDefault().removeStickyEvent(UpdateEventData.Manager::class.java)
34 | fetchUpdate(AppConfigs.MANAGER_PACKAGE, MANAGER_URL, force)?.let {
35 | EventBus.getDefault().postSticky(it)
36 | }
37 | }
38 | AppServices.globalScope.launch(Dispatchers.IO) {
39 | EventBus.getDefault().removeStickyEvent(UpdateEventData.Module::class.java)
40 | fetchUpdate(AppConfigs.MODULE_PACKAGE, MODULE_URL, force)?.let {
41 | EventBus.getDefault().postSticky(it)
42 | }
43 | }
44 | }
45 |
46 | private suspend fun fetchDirectLink(pkg: String, from: String): String {
47 | return fetchData(pkg, from, false)!!.assets[0].link
48 | }
49 |
50 | private suspend fun fetchUpdate(pkg: String, url: String, force: Boolean): UpdateEventData? {
51 | return ApkUtil.getPrefixedVersionName(pkg)?.let { current ->
52 | fetchData(pkg, url, force)?.takeIf {
53 | it.version.compareVersion(current) > 0
54 | }?.let {
55 | if (pkg == AppConfigs.MANAGER_PACKAGE)
56 | UpdateEventData.Manager(current, it.version, it.assets[0].link)
57 | else UpdateEventData.Module(current, it.version, it.assets[0].link)
58 | }
59 | }
60 | }
61 |
62 | private suspend fun fetchData(pkg: String, url: String, force: Boolean): GithubReleaseData? {
63 | val cacheKey: String = getCacheKey(pkg)
64 | return (if (force) null else CacheManager.get(cacheKey)) ?: try {
65 | RetrofitClient.SERVICE.getGithubRelease(url).result().also {
66 | CacheManager.put(cacheKey, it, CACHE_TIMEOUT_HOUR)
67 | }
68 | } catch (_: Exception) {
69 | CacheManager.get(cacheKey, true)
70 | }
71 | }
72 |
73 | private fun getCacheKey(pkg: String): String {
74 | return "${KEY_UPDATE_CACHE}_${pkg.substringAfterLast('.')}"
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
20 |
21 |
24 |
25 |
32 |
33 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
51 |
52 |
64 |
65 |
73 |
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/ui/settings/SettingsViewModel.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.ui.settings
2 |
3 | import android.net.Uri
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import app.neonorbit.mrvpatchmanager.AppConfigs
7 | import app.neonorbit.mrvpatchmanager.AppServices
8 | import app.neonorbit.mrvpatchmanager.DefaultPreference
9 | import app.neonorbit.mrvpatchmanager.error
10 | import app.neonorbit.mrvpatchmanager.event.SingleEvent
11 | import app.neonorbit.mrvpatchmanager.keystore.KeystoreData
12 | import app.neonorbit.mrvpatchmanager.keystore.KeystoreInputData
13 | import app.neonorbit.mrvpatchmanager.keystore.KeystoreManager
14 | import app.neonorbit.mrvpatchmanager.post
15 | import app.neonorbit.mrvpatchmanager.toTempFile
16 | import kotlinx.coroutines.Dispatchers
17 | import kotlinx.coroutines.Job
18 | import kotlinx.coroutines.flow.MutableStateFlow
19 | import kotlinx.coroutines.launch
20 | import kotlinx.coroutines.withContext
21 | import java.io.File
22 |
23 | class SettingsViewModel : ViewModel() {
24 | val uriEvent = SingleEvent()
25 | val cacheSize = MutableStateFlow(null)
26 | val keystoreName = MutableStateFlow(null)
27 |
28 | val ksSaveFailed = SingleEvent()
29 | val keystoreSaved = SingleEvent()
30 |
31 | private var cacheJob: Job? = null
32 |
33 | fun loadCacheSize() {
34 | viewModelScope.launch(Dispatchers.IO) {
35 | cacheSize.emit(AppServices.getAppCacheSize())
36 | }
37 | }
38 |
39 | fun clearCache() {
40 | if (cacheJob != null) return
41 | viewModelScope.launch(Dispatchers.IO) {
42 | AppServices.clearAppCache()
43 | DefaultPreference.clearCache()
44 | withContext(Dispatchers.Main) {
45 | AppServices.showToast("Cache cleared")
46 | }
47 | }.also { cacheJob = it }.invokeOnCompletion {
48 | loadCacheSize()
49 | cacheJob = null
50 | }
51 | }
52 |
53 | fun loadKeystoreName() {
54 | viewModelScope.launch(Dispatchers.IO) {
55 | DefaultPreference.getCustomKeystore()?.let {
56 | keystoreName.emit(it.aliasName)
57 | }
58 | }
59 | }
60 |
61 | fun saveKeystore(input: KeystoreInputData?) {
62 | if (input == null) {
63 | SettingsData.CUSTOM_KEY_FILE.delete()
64 | keystoreName.post(with = this, null)
65 | keystoreSaved.post(viewModelScope, null)
66 | return
67 | }
68 | viewModelScope.launch(Dispatchers.IO) {
69 | var keyfile: File? = null
70 | try {
71 | keyfile = input.uri.toTempFile(AppServices.contentResolver)
72 | KeystoreManager.readKeyData(
73 | keyfile, SettingsData.CUSTOM_KEY_FILE.absolutePath,
74 | input.password, input.aliasName, input.aliasPassword
75 | ).let { data ->
76 | keyfile.copyTo(File(data.path), true)
77 | keystoreName.emit(data.aliasName)
78 | keystoreSaved.post(data)
79 | }
80 | } catch (e: Exception) {
81 | ksSaveFailed.post(viewModelScope, e.error)
82 | } finally {
83 | keyfile?.delete()
84 | }
85 | }
86 | }
87 |
88 | fun visitHelp() {
89 | uriEvent.post(viewModelScope, Uri.parse(AppConfigs.HELP_FORUM_URL))
90 | }
91 |
92 | fun visitGithub() {
93 | uriEvent.post(viewModelScope, Uri.parse(AppConfigs.GITHUB_REPO_URL))
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/remote/ApkComboService.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.remote
2 |
3 | import app.neonorbit.mrvpatchmanager.apk.ApkConfigs
4 | import app.neonorbit.mrvpatchmanager.data.AppType
5 | import app.neonorbit.mrvpatchmanager.network.RetrofitClient
6 | import app.neonorbit.mrvpatchmanager.remote.data.ApkComboReleaseData
7 | import app.neonorbit.mrvpatchmanager.remote.data.RemoteApkInfo
8 | import app.neonorbit.mrvpatchmanager.result
9 | import app.neonorbit.mrvpatchmanager.util.Utils.LOG
10 |
11 | object ApkComboService : ApkRemoteService {
12 | const val BASE_URL = "https://apkcombo.app"
13 | private const val RELEASE_URL = "old-versions"
14 | private const val TOKEN_URL = "$BASE_URL/checkin"
15 | private const val FB_APP_URL = "$BASE_URL/facebook/com.facebook.katana/$RELEASE_URL"
16 | private const val FB_LITE_URL = "$BASE_URL/facebook-lite/com.facebook.lite/$RELEASE_URL"
17 | private const val MSG_APP_URL = "$BASE_URL/facebook-messenger/com.facebook.orca/$RELEASE_URL"
18 | private const val MSG_LITE_URL = "$BASE_URL/messenger-lite/com.facebook.mlite/$RELEASE_URL"
19 | private const val BSN_SUITE_URL = "$BASE_URL/meta-business-suite/com.facebook.pages.app/$RELEASE_URL"
20 | private const val AD_MANAGER_URL = "$BASE_URL/meta-ads-manager/com.facebook.adsmanager/$RELEASE_URL"
21 |
22 | override fun server(): String {
23 | return "apkcombo.com"
24 | }
25 |
26 | override suspend fun fetch(type: AppType, abi: String, ver: String?): RemoteApkInfo {
27 | return try {
28 | when (type) {
29 | AppType.FACEBOOK -> fetchInfo(FB_APP_URL, abi, ver)
30 | AppType.MESSENGER -> fetchInfo(MSG_APP_URL, abi, ver)
31 | AppType.FACEBOOK_LITE -> fetchInfo(FB_LITE_URL, abi, ver)
32 | AppType.MESSENGER_LITE -> fetchInfo(MSG_LITE_URL, abi, ver)
33 | AppType.BUSINESS_SUITE -> fetchInfo(BSN_SUITE_URL, abi, ver)
34 | AppType.FB_ADS_MANAGER -> fetchInfo(AD_MANAGER_URL, abi, ver)
35 | }
36 | } catch (exception: Exception) {
37 | exception.handleApkServiceException(type, ver)
38 | }
39 | }
40 |
41 | private suspend fun fetchInfo(from: String, abi: String, ver: String?): RemoteApkInfo {
42 | return RetrofitClient.SERVICE.get(TOKEN_URL).result().string().LOG("Token").let { token ->
43 | RetrofitClient.SERVICE.getApkComboRelease(from).result().LOG("Response").releases.LOG("Releases").filter {
44 | it.isValidType && ApkConfigs.isValidRelease(it.name) && ApkConfigs.matchApkVersion(it.version, ver)
45 | }.LOG("Filtered").sortedWith(
46 | ApkConfigs.compareLatest({ it.version })
47 | ).LOG("Sorted").take(5).selectApk(abi).LOG("Selected")?.let { apk ->
48 | RemoteApkInfo("${apk.link}&$token", apk.version)
49 | }
50 | } ?: throw Exception()
51 | }
52 |
53 | private suspend fun List.selectApk(abi: String) = firstNotNullOfOrNull { release ->
54 | RetrofitClient.SERVICE.getApkComboVariant(release.link).result().LOG("Response").let { data ->
55 | data.variants.LOG("Variants").firstOrNull {
56 | it.arch.lowercase().contains(abi)
57 | }.LOG("Filtered")?.apks.LOG("APKs")?.filter {
58 | it.isValidType && ApkConfigs.isSupportedDPI(it.dpi) && ApkConfigs.isSupportedMinSdk(it.minSDk)
59 | }.LOG("Filtered")?.let { filtered ->
60 | filtered.firstOrNull { ApkConfigs.isPreferredDPI(it.dpi) } ?: filtered.firstOrNull()
61 | }
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/remote/ApkFlashService.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.remote
2 |
3 | import app.neonorbit.mrvpatchmanager.apk.ApkConfigs
4 | import app.neonorbit.mrvpatchmanager.data.AppType
5 | import app.neonorbit.mrvpatchmanager.network.RetrofitClient
6 | import app.neonorbit.mrvpatchmanager.remote.data.ApkFlashReleaseData
7 | import app.neonorbit.mrvpatchmanager.remote.data.RemoteApkInfo
8 | import app.neonorbit.mrvpatchmanager.result
9 | import app.neonorbit.mrvpatchmanager.util.Utils.LOG
10 |
11 | object ApkFlashService : ApkRemoteService {
12 | const val BASE_URL = "https://apkflash.com"
13 | private const val APK_URL = "$BASE_URL/apk/app"
14 | private const val RELEASE_URL = "old-versions"
15 | private const val TOKEN_URL = "$BASE_URL/checkin/"
16 | private const val FB_APP_URL = "$APK_URL/com.facebook.katana/facebook/$RELEASE_URL"
17 | private const val FB_LITE_URL = "$APK_URL/com.facebook.lite/facebook-lite/$RELEASE_URL"
18 | private const val MSG_APP_URL = "$APK_URL/com.facebook.orca/facebook-messenger/$RELEASE_URL"
19 | private const val MSG_LITE_URL = "$APK_URL/com.facebook.mlite/messenger-lite/$RELEASE_URL"
20 | private const val BSN_SUITE_URL = "$APK_URL/com.facebook.pages.app/meta-business-suite/$RELEASE_URL"
21 | private const val AD_MANAGER_URL = "$APK_URL/com.facebook.adsmanager/meta-ads-manager/$RELEASE_URL"
22 |
23 | override fun server(): String {
24 | return "apkflash.com"
25 | }
26 |
27 | override suspend fun fetch(type: AppType, abi: String, ver: String?): RemoteApkInfo {
28 | return try {
29 | when (type) {
30 | AppType.FACEBOOK -> fetchInfo(FB_APP_URL, abi, ver)
31 | AppType.MESSENGER -> fetchInfo(MSG_APP_URL, abi, ver)
32 | AppType.FACEBOOK_LITE -> fetchInfo(FB_LITE_URL, abi, ver)
33 | AppType.MESSENGER_LITE -> fetchInfo(MSG_LITE_URL, abi, ver)
34 | AppType.BUSINESS_SUITE -> fetchInfo(BSN_SUITE_URL, abi, ver)
35 | AppType.FB_ADS_MANAGER -> fetchInfo(AD_MANAGER_URL, abi, ver)
36 | }
37 | } catch (exception: Exception) {
38 | exception.handleApkServiceException(type, ver)
39 | }
40 | }
41 |
42 | private suspend fun fetchInfo(from: String, abi: String, ver: String?): RemoteApkInfo {
43 | return RetrofitClient.SERVICE.get(TOKEN_URL).result().string().LOG("Token").let { token ->
44 | RetrofitClient.SERVICE.getApkFlashRelease(from).result().LOG("Response").releases.LOG("Releases").filter {
45 | it.isValidType && ApkConfigs.isValidRelease(it.name) && ApkConfigs.matchApkVersion(it.version, ver)
46 | }.LOG("Filtered").sortedWith(
47 | ApkConfigs.compareLatest({ it.version })
48 | ).LOG("Sorted").take(5).selectApk(abi).LOG("Selected")?.let { apk ->
49 | RemoteApkInfo("${apk.link}&$token", apk.version)
50 | }
51 | } ?: throw Exception()
52 | }
53 |
54 | private suspend fun List.selectApk(abi: String) = firstNotNullOfOrNull { release ->
55 | RetrofitClient.SERVICE.getApkFlashVariant(release.link).result().LOG("Response").let { data ->
56 | data.variants.LOG("Variants").firstOrNull {
57 | it.arch.lowercase().contains(abi)
58 | }.LOG("Filtered")?.apks.LOG("APKs")?.filter {
59 | it.isValidType && ApkConfigs.isSupportedDPI(it.dpi) && ApkConfigs.isSupportedMinSdk(it.minSDk)
60 | }.LOG("Filtered")?.let { filtered ->
61 | filtered.firstOrNull { ApkConfigs.isPreferredDPI(it.dpi) } ?: filtered.firstOrNull()
62 | }
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/ui/patched/ApkListAdapter.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.ui.patched
2 |
3 | import android.annotation.SuppressLint
4 | import android.view.LayoutInflater
5 | import android.view.ViewGroup
6 | import androidx.recyclerview.selection.SelectionTracker
7 | import androidx.recyclerview.widget.RecyclerView
8 | import app.neonorbit.mrvpatchmanager.R
9 | import app.neonorbit.mrvpatchmanager.repository.data.ApkFileData
10 | import app.neonorbit.mrvpatchmanager.ui.SelectionTrackerFactory
11 | import com.bumptech.glide.Glide
12 | import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
13 | import java.util.Collections
14 |
15 | class ApkListAdapter : RecyclerView.Adapter() {
16 | private var callback: Callback? = null
17 | private val list = ArrayList()
18 | private val viewHolders = HashSet()
19 | private lateinit var infoPreloader: ApkInfoPreloader
20 | private lateinit var tracker: SelectionTracker
21 |
22 | val items: List get() = Collections.unmodifiableList(list)
23 |
24 | init {
25 | setHasStableIds(true)
26 | }
27 |
28 | fun setItemClickListener(callback: Callback) {
29 | this.callback = callback
30 | }
31 |
32 | fun setApkInfoPreloader(preloader: ApkInfoPreloader) {
33 | this.infoPreloader = preloader
34 | }
35 |
36 | fun initTracker(recyclerView: RecyclerView): SelectionTracker {
37 | return SelectionTrackerFactory.buildFor(recyclerView).also {
38 | this.tracker = it
39 | }
40 | }
41 |
42 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ApkItemHolder {
43 | val inflater = LayoutInflater.from(parent.context)
44 | val view = inflater.inflate(R.layout.apk_item_view, parent, false)
45 | return ApkItemHolder(view)
46 | }
47 |
48 | override fun onBindViewHolder(holder: ApkItemHolder, position: Int) {
49 | viewHolders.add(holder)
50 | val item = list[position]
51 | holder.apkTitle.text = item.name
52 | infoPreloader.load(holder.apkInfo, position)
53 | Glide.with(holder.itemView)
54 | .load(item.path)
55 | .placeholder(R.drawable.generic_placeholder)
56 | .transition(DrawableTransitionOptions.withCrossFade())
57 | .into(holder.apkIcon)
58 | callback?.let { call ->
59 | holder.itemView.setOnClickListener {
60 | call.onItemClicked(item)
61 | }
62 | }
63 | holder.itemView.isSelected = tracker.isSelected(holder.itemId)
64 | }
65 |
66 | override fun onViewRecycled(holder: ApkItemHolder) {
67 | super.onViewRecycled(holder)
68 | viewHolders.remove(holder)
69 | }
70 |
71 | override fun getItemCount(): Int {
72 | return list.size
73 | }
74 |
75 | override fun getItemId(position: Int): Long {
76 | return position.toLong()
77 | }
78 |
79 | fun getItemIds(): List {
80 | return (0L until list.size).toList()
81 | }
82 |
83 | fun refresh() {
84 | viewHolders.forEach {
85 | it.itemView.isSelected = tracker.isSelected(it.itemId)
86 | }
87 | }
88 |
89 | @SuppressLint("NotifyDataSetChanged")
90 | fun reloadItems(new: List) {
91 | if (new == list) return
92 | list.clear()
93 | list.addAll(new)
94 | infoPreloader.reload()
95 | notifyDataSetChanged()
96 | }
97 |
98 | interface Callback {
99 | fun onItemClicked(item: ApkFileData)
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/preference.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
10 |
15 |
20 |
25 |
31 |
32 |
33 |
35 |
40 |
41 |
42 |
44 |
49 |
54 |
59 |
64 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/remote/data/ApkPureVariantData.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.remote.data
2 |
3 | import app.neonorbit.mrvpatchmanager.apk.ApkConfigs
4 | import app.neonorbit.mrvpatchmanager.remote.ApkPureService
5 | import app.neonorbit.mrvpatchmanager.util.NullableElementConverter
6 | import app.neonorbit.mrvpatchmanager.util.Utils
7 | import org.jsoup.nodes.Element
8 | import pl.droidsonroids.jspoon.Jspoon
9 | import pl.droidsonroids.jspoon.annotation.Selector
10 |
11 | class ApkPureVariantData {
12 | val variants: List get() = _variants.ifEmpty { _fallback }
13 |
14 | @Selector("main", attr = "html", converter = PrimaryExtractor::class)
15 | private var _fallback: List = listOf()
16 |
17 | @Selector("#version-list", attr = "html", converter = VariantsExtractor::class)
18 | private var _variants: List = listOf()
19 |
20 | override fun toString(): String {
21 | return "variants: $_variants, fallback: $_fallback"
22 | }
23 |
24 | data class Variant(val arch: String, val apks: List) {
25 | override fun toString(): String {
26 | return "arch: $arch, apks: $apks"
27 | }
28 | }
29 |
30 | class Apk {
31 | @Selector(".name", defValue = "")
32 | private lateinit var name: String
33 |
34 | @Selector(".tag:contains(apk)", defValue = "")
35 | private lateinit var type: String
36 |
37 | @Selector(".sdk:contains(android)", defValue = "")
38 | private lateinit var sdk: String
39 |
40 | @Selector("a.download-btn", attr = "href")
41 | private lateinit var href: String
42 |
43 | val minSDk: Int? get() = ApkConfigs.extractMinSdk(sdk)
44 |
45 | val version: String? get() = ApkConfigs.extractVersionName(name)
46 |
47 | val link: String get() = Utils.absoluteUrl(ApkPureService.BASE_URL, href)
48 |
49 | val isValidType: Boolean get() = type.trim().lowercase().let { it == "apk" || "xapk" !in it }
50 |
51 | override fun toString(): String {
52 | return "type: $type, version: $version, minSDk: $minSDk, link: $link"
53 | }
54 |
55 | companion object {
56 | fun build(name: String, type: String, sdk: String, href: String) = Apk().apply {
57 | this.name = name; this.type = type; this.sdk = sdk; this.href = href
58 | }
59 | }
60 | }
61 |
62 | object VariantsExtractor : NullableElementConverter> {
63 | private val apkParser = Jspoon.create().adapter(Apk::class.java)
64 |
65 | override fun convert(node: Element?, selector: Selector): List {
66 | return node?.select(".group-title:matches(\\barm(?:eabi|64)-(?:v7a|v8a)\\b)")?.map { arch ->
67 | val items = mutableListOf()
68 | var current = arch.nextElementSibling()
69 | while (current?.hasClass("apk") == true) {
70 | items.add(apkParser.fromHtml(current.select(".apk").html()))
71 | current = current.nextElementSibling()
72 | }
73 | Variant(arch.text(), items)
74 | } ?: listOf()
75 | }
76 | }
77 |
78 | object PrimaryExtractor : NullableElementConverter> {
79 | override fun convert(node: Element?, selector: Selector): List = node?.let { main ->
80 | val name = main.select(".info-content .info-sdk").firstOrNull()?.text()
81 | val type = main.select(".info-content .info-tag").firstOrNull()?.text()
82 | val arch = main.select(".more-info .info:contains(Architecture)").select(".value").firstOrNull()?.text()
83 | val sdk = main.select(".more-info .info:contains(Requires Android)").select(".value").firstOrNull()?.text()
84 | val href = main.select("a#download_link").ifEmpty { main.select("a.download-start-btn") }.firstOrNull()?.attr("href")
85 | if (name != null && type != null && arch != null && href != null) {
86 | listOf(Variant(arch, listOf(Apk.build(name, type, sdk ?: "", href))))
87 | } else null
88 | } ?: listOf()
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/remote/ApkMirrorService.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.remote
2 |
3 | import app.neonorbit.mrvpatchmanager.apk.ApkConfigs
4 | import app.neonorbit.mrvpatchmanager.data.AppType
5 | import app.neonorbit.mrvpatchmanager.network.RetrofitClient
6 | import app.neonorbit.mrvpatchmanager.remote.data.ApkMirrorReleaseData
7 | import app.neonorbit.mrvpatchmanager.remote.data.RemoteApkInfo
8 | import app.neonorbit.mrvpatchmanager.result
9 | import app.neonorbit.mrvpatchmanager.util.Utils.LOG
10 |
11 | object ApkMirrorService : ApkRemoteService {
12 | const val BASE_URL = "https://www.apkmirror.com"
13 | private const val META_URL = "$BASE_URL/apk/facebook-2"
14 | private const val FALLBACK_URL = "$BASE_URL/uploads/?appcategory="
15 | private const val VARIANT_FEED_BEG = "variant-{\"arches_slug\":[\""
16 | private const val VARIANT_FEED_END = "\"]}/feed/"
17 |
18 | private val ids = mapOf(
19 | AppType.FACEBOOK to "facebook",
20 | AppType.MESSENGER to "messenger",
21 | AppType.FACEBOOK_LITE to "lite",
22 | AppType.MESSENGER_LITE to "messenger-lite",
23 | AppType.BUSINESS_SUITE to "pages-manager",
24 | AppType.FB_ADS_MANAGER to "facebook-ads-manager",
25 | )
26 |
27 | private fun buildFallbackUrl(type: AppType) = "$FALLBACK_URL/${ids[type]}"
28 |
29 | private fun buildFeedUrl(type: AppType, abi: String): String {
30 | return "$META_URL/${ids[type]}/$VARIANT_FEED_BEG$abi$VARIANT_FEED_END"
31 | }
32 |
33 | override fun server(): String {
34 | return "apkmirror.com"
35 | }
36 |
37 | override suspend fun fetch(type: AppType, abi: String, ver: String?): RemoteApkInfo {
38 | try {
39 | return fetchInfo(buildFeedUrl(type, abi), ver) ?:
40 | fallback(buildFallbackUrl(type), abi, ver) ?: throw Exception()
41 | } catch (exception: Exception) {
42 | exception.handleApkServiceException(type, ver)
43 | }
44 | }
45 |
46 | private suspend fun fetchInfo(from: String, ver: String?): RemoteApkInfo? {
47 | return RetrofitClient.SERVICE.getApkMirrorFeed(from).result().channel.items.LOG("Items").filter { item ->
48 | ApkConfigs.isValidRelease(item.title) && ApkConfigs.matchApkVersion(item.version, ver)
49 | && ApkConfigs.isSupportedDPI(item.dpi) && ApkConfigs.isSupportedMinSdk(item.minSDk)
50 | }.LOG("Filtered").sortedWith(
51 | ApkConfigs.compareLatest({ it.version }, { ApkConfigs.isPreferredDPI(it.dpi) })
52 | ).LOG("Sorted").take(3).firstNotNullOfOrNull { item ->
53 | fetchDownloadLink(item.link, item.version)
54 | }.LOG("Selected")
55 | }
56 |
57 | private suspend fun fetchDownloadLink(from: String, version: String?): RemoteApkInfo? {
58 | return RetrofitClient.SERVICE.getApkMirrorItem(from).result().LOG("ItemData").takeIf { it.isValidType }?.let {
59 | RemoteApkInfo(it.link, it.version?: version)
60 | }?.let { info ->
61 | RetrofitClient.SERVICE.getApkMirrorInputForm(info.link).result().LOG("InputForm").let {
62 | RemoteApkInfo(it.link, info.version)
63 | }
64 | }
65 | }
66 |
67 | private suspend fun fallback(from: String, abi: String, ver: String?): RemoteApkInfo? {
68 | return RetrofitClient.SERVICE.getApkMirrorRelease(from).result().releases.LOG("Fallback").filter { release ->
69 | ApkConfigs.isValidRelease(release.name) && ApkConfigs.matchApkVersion(release.version, ver)
70 | }.LOG("Filtered").sortedWith(
71 | ApkConfigs.compareLatest({ it.version })
72 | ).LOG("Sorted").take(3).selectApk(abi)?.let {
73 | fetchDownloadLink(it.link, it.version)
74 | }.LOG("Selected")
75 | }
76 |
77 | private suspend fun List.selectApk(abi: String) = firstNotNullOfOrNull { release ->
78 | RetrofitClient.SERVICE.getApkMirrorVariant(release.link).result().variants.LOG("Variants").filter {
79 | it.isValidType && it.arch.lowercase().contains(abi) &&
80 | ApkConfigs.isSupportedDPI(it.dpi) && ApkConfigs.isSupportedMinSdk(it.minSDk)
81 | }.LOG("Filtered").let { filtered ->
82 | filtered.firstOrNull { ApkConfigs.isPreferredDPI(it.dpi) } ?: filtered.firstOrNull()
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/remote/ApkPureService.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.remote
2 |
3 | import app.neonorbit.mrvpatchmanager.apk.ApkConfigs
4 | import app.neonorbit.mrvpatchmanager.data.AppType
5 | import app.neonorbit.mrvpatchmanager.isConnectError
6 | import app.neonorbit.mrvpatchmanager.network.HttpSpec
7 | import app.neonorbit.mrvpatchmanager.network.RetrofitClient
8 | import app.neonorbit.mrvpatchmanager.remote.data.ApkPureReleaseData
9 | import app.neonorbit.mrvpatchmanager.remote.data.RemoteApkInfo
10 | import app.neonorbit.mrvpatchmanager.result
11 | import app.neonorbit.mrvpatchmanager.util.Utils
12 | import app.neonorbit.mrvpatchmanager.util.Utils.LOG
13 | import kotlin.coroutines.cancellation.CancellationException
14 |
15 | object ApkPureService : ApkRemoteService {
16 | const val BASE_URL = "https://apkpure.com"
17 | private const val RELEASE_URL = "versions"
18 | private const val FB_APP_URL = "$BASE_URL/facebook/com.facebook.katana/$RELEASE_URL"
19 | private const val FB_LITE_URL = "$BASE_URL/facebook-lite/com.facebook.lite/$RELEASE_URL"
20 | private const val MSG_APP_URL = "$BASE_URL/facebook-messenger/com.facebook.orca/$RELEASE_URL"
21 | private const val MSG_LITE_URL = "$BASE_URL/messenger-lite/com.facebook.mlite/$RELEASE_URL"
22 | private const val BSN_SUITE_URL = "$BASE_URL/meta-business-suite/com.facebook.pages.app/$RELEASE_URL"
23 | private const val AD_MANAGER_URL = "$BASE_URL/meta-ads-manager/com.facebook.adsmanager/$RELEASE_URL"
24 |
25 | override fun server(): String {
26 | return "apkpure.com"
27 | }
28 |
29 | override suspend fun fetch(type: AppType, abi: String, ver: String?): RemoteApkInfo {
30 | return when (type) {
31 | AppType.FACEBOOK -> fetchInfo(type, FB_APP_URL, abi, ver) ?: hardcodedInfo("katana")
32 | AppType.MESSENGER -> fetchInfo(type, MSG_APP_URL, abi, ver) ?: hardcodedInfo("orca")
33 | AppType.FACEBOOK_LITE -> fetchInfo(type, FB_LITE_URL, abi, ver) ?: hardcodedInfo("lite")
34 | AppType.MESSENGER_LITE -> fetchInfo(type, MSG_LITE_URL, abi, ver) ?: hardcodedInfo("mlite")
35 | AppType.BUSINESS_SUITE -> fetchInfo(type, BSN_SUITE_URL, abi, ver) ?: hardcodedInfo("pages.app")
36 | AppType.FB_ADS_MANAGER -> fetchInfo(type, AD_MANAGER_URL, abi, ver) ?: hardcodedInfo("adsmanager")
37 | }
38 | }
39 |
40 | private suspend fun fetchInfo(type: AppType, from: String, abi: String, ver: String?): RemoteApkInfo? {
41 | return try {
42 | RetrofitClient.SERVICE.getApkPureRelease(from).result().LOG("Response").releases.LOG("Releases").filter {
43 | it.isValidType && ApkConfigs.isValidRelease(it.name) && ApkConfigs.matchApkVersion(it.version, ver)
44 | }.LOG("Filtered").sortedWith(
45 | ApkConfigs.compareLatest({ it.version })
46 | ).LOG("Sorted").take(5).selectApk(abi).LOG("Selected")?.let {
47 | RemoteApkInfo(it.link, it.version)
48 | } ?: throw Exception()
49 | } catch (exception: Exception) {
50 | exception.handleApkServiceException(type, ver, ver != null || abi != ApkConfigs.ARM_64)
51 | Utils.warn("Falling back: ${server()}", exception)
52 | null
53 | }
54 | }
55 |
56 | private suspend fun List.selectApk(abi: String) = firstNotNullOfOrNull { release ->
57 | RetrofitClient.SERVICE.getApkPureVariant(release.link).result().LOG("Response").let { data ->
58 | data.variants.LOG("Variants").firstOrNull {
59 | it.arch.lowercase().contains(abi)
60 | }.LOG("Filtered")?.apks.LOG("APKs")?.firstOrNull {
61 | it.isValidType && ApkConfigs.isSupportedMinSdk(it.minSDk)
62 | }
63 | }
64 | }
65 |
66 | private suspend fun hardcodedInfo(id: String): RemoteApkInfo {
67 | val link = "https://d.apkpure.com/b/APK/com.facebook.$id?version=latest"
68 | return try {
69 | RetrofitClient.SERVICE.head(link).let {
70 | if (ApkConfigs.APK_MIME_TYPE != it.headers()[HttpSpec.Header.CONTENT_TYPE]) {
71 | throw Exception()
72 | }
73 | }
74 | RemoteApkInfo(link)
75 | } catch (e: Exception) {
76 | throw if (e is CancellationException || e.isConnectError) e else Exception(
77 | "Failed to fetch apk info from the server ${server()}"
78 | )
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/remote/ApkRemoteFileProvider.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager.remote
2 |
3 | import android.util.Log
4 | import app.neonorbit.mrvpatchmanager.AppConfigs
5 | import app.neonorbit.mrvpatchmanager.AppServices
6 | import app.neonorbit.mrvpatchmanager.DefaultPreference
7 | import app.neonorbit.mrvpatchmanager.apk.ApkUtil
8 | import app.neonorbit.mrvpatchmanager.data.AppType
9 | import app.neonorbit.mrvpatchmanager.download.ApkDownloader
10 | import app.neonorbit.mrvpatchmanager.download.DownloadStatus
11 | import app.neonorbit.mrvpatchmanager.toNetworkError
12 | import kotlinx.coroutines.delay
13 | import kotlinx.coroutines.flow.Flow
14 | import kotlinx.coroutines.flow.catch
15 | import kotlinx.coroutines.flow.emitAll
16 | import kotlinx.coroutines.flow.flow
17 | import kotlinx.coroutines.flow.flowOf
18 | import kotlinx.coroutines.flow.onEach
19 | import kotlinx.coroutines.flow.retryWhen
20 | import java.io.File
21 | import java.security.SignatureException
22 |
23 | class ApkRemoteFileProvider {
24 | companion object {
25 | val services = listOf(
26 | ApkMirrorService,
27 | ApkComboService,
28 | ApkPureService,
29 | // ApkFlashService // Unavailable
30 | )
31 | private const val CACHED_THRESHOLD = 20L * 60 * 60 * 1000
32 | private val TAG = ApkRemoteFileProvider::class.simpleName
33 | }
34 |
35 | private fun getServices(): Iterator {
36 | return DefaultPreference.getApkServer()?.let { server ->
37 | services.firstOrNull { server == it.server() }?.let {
38 | listOf(it).iterator()
39 | }
40 | } ?: services.iterator()
41 | }
42 |
43 | fun getManagerApk(): Flow = flow {
44 | ApkDownloader.download(
45 | GithubService.getManagerLink(),
46 | File(AppConfigs.DOWNLOAD_DIR, AppConfigs.MANAGER_APK_NAME)
47 | ).catch {
48 | emit(DownloadStatus.FAILED(it.toNetworkError()))
49 | }.let { emitAll(it) }
50 | }
51 |
52 | fun getModuleApk(): Flow = flow {
53 | ApkDownloader.download(
54 | GithubService.getModuleLink(),
55 | File(AppConfigs.DOWNLOAD_DIR, AppConfigs.MODULE_APK_NAME)
56 | ).catch {
57 | emit(DownloadStatus.FAILED(it.toNetworkError()))
58 | }.let { emitAll(it) }
59 | }
60 |
61 | fun getFbApk(type: AppType, abi: String, version: String?): Flow {
62 | val file = AppConfigs.getDownloadApkFile(type, version)
63 | if (hasValidFile(file, version)) {
64 | return flowOf(DownloadStatus.FINISHED(file))
65 | }
66 | val services = getServices()
67 | var service: ApkRemoteService = services.next()
68 | return flow {
69 | emit(DownloadStatus.FETCHING(service.server()))
70 | val fetched = service.fetch(type, abi, version)
71 | fetched.version?.let {
72 | emit(DownloadStatus.FETCHED(it))
73 | }
74 | ApkDownloader.download(fetched.link, file).onEach {
75 | if (it is DownloadStatus.FINISHED) {
76 | if (!ApkUtil.verifyFbSignature(it.file)) {
77 | it.file.delete()
78 | throw SignatureException("Signature failed")
79 | }
80 | }
81 | }.let { emitAll(it) }
82 | }.retryWhen { e, _ ->
83 | Log.w(TAG, "getFbApk[${service.server()}]", e)
84 | AppServices.isNetworkOnline() && services.hasNext().also {
85 | if (it) {
86 | service = services.next()
87 | val err = if (e is SignatureException) e.message else "Failed"
88 | emit(DownloadStatus.FETCHING("$err, trying the next server..."))
89 | delay(3000)
90 | }
91 | }
92 | }.catch { exception ->
93 | val isOnline = AppServices.isNetworkOnline()
94 | emit(DownloadStatus.FAILED(exception.toNetworkError(isOnline)))
95 | }
96 | }
97 |
98 | private fun hasValidFile(file: File, version: String?): Boolean {
99 | val last = file.lastModified()
100 | val current = System.currentTimeMillis()
101 | return file.exists() && (current - last < CACHED_THRESHOLD) && try {
102 | ApkUtil.verifyFbSignatureWithVersion(file, version)
103 | } catch (_: Exception) { false }
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/keystore_dialog.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
12 |
13 |
18 |
19 |
27 |
28 |
34 |
41 |
42 |
43 |
50 |
55 |
56 |
57 |
64 |
69 |
70 |
71 |
78 |
83 |
84 |
85 |
95 |
96 |
97 |
--------------------------------------------------------------------------------
/app/src/main/java/app/neonorbit/mrvpatchmanager/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package app.neonorbit.mrvpatchmanager
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import android.os.Bundle
6 | import android.view.LayoutInflater
7 | import android.view.Menu
8 | import android.view.MenuItem
9 | import androidx.appcompat.app.AppCompatActivity
10 | import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
11 | import androidx.core.view.isVisible
12 | import androidx.navigation.NavController
13 | import androidx.navigation.fragment.NavHostFragment
14 | import androidx.navigation.ui.NavigationUI
15 | import app.neonorbit.mrvpatchmanager.databinding.AboutDialogBinding
16 | import app.neonorbit.mrvpatchmanager.databinding.ActivityMainBinding
17 | import app.neonorbit.mrvpatchmanager.remote.GithubService
18 | import com.bumptech.glide.Glide
19 | import com.bumptech.glide.load.resource.bitmap.RoundedCorners
20 | import com.google.android.material.dialog.MaterialAlertDialogBuilder
21 |
22 | class MainActivity : AppCompatActivity() {
23 | private lateinit var binding: ActivityMainBinding
24 |
25 | override fun onCreate(savedInstanceState: Bundle?) {
26 | this.installSplashScreen()
27 | super.onCreate(savedInstanceState)
28 | binding = ActivityMainBinding.inflate(layoutInflater).also {
29 | setContentView(it.root)
30 | setSupportActionBar(it.toolbar)
31 | }
32 | val navController = supportFragmentManager.findFragmentById(R.id.nav_host_fragment).let {
33 | (it as NavHostFragment).navController
34 | }
35 | NavigationUI.setupWithNavController(binding.navView, navController)
36 | setupActionBarWithNavController(navController)
37 | theme.applyStyle(
38 | rikka.material.preference.R.style.ThemeOverlay_Rikka_Material3_Preference, true
39 | )
40 | }
41 |
42 | private fun setupActionBarWithNavController(navController: NavController) {
43 | binding.icon.setOnClickListener { showAboutDialog() }
44 | navController.addOnDestinationChangedListener { _, destination, _ ->
45 | if (destination.id == R.id.navigation_home) {
46 | binding.icon.isClickable = true
47 | binding.title.animate().setDuration(300).alpha(0.0f)
48 | binding.icon.animate().setDuration(300).alpha(1.0f)
49 | } else {
50 | binding.icon.isClickable = false
51 | binding.title.text = destination.label
52 | binding.icon.animate().setDuration(300).alpha(0.0f)
53 | binding.title.animate().setDuration(300).alpha(1.0f)
54 | }
55 | if (!binding.title.isVisible) binding.title.isVisible = true
56 | }
57 | }
58 |
59 | override fun onCreateOptionsMenu(menu: Menu?): Boolean {
60 | menuInflater.inflate(R.menu.app_bar_menu, menu)
61 | return true
62 | }
63 |
64 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
65 | return when (item.itemId) {
66 | R.id.instruction -> {
67 | showInstructionDialog()
68 | true
69 | }
70 | R.id.tutorial -> {
71 | startActivity(Intent(
72 | Intent.ACTION_VIEW, Uri.parse(AppConfigs.TUTORIAL_URL)
73 | ))
74 | true
75 | }
76 | R.id.update -> {
77 | GithubService.checkForUpdate(force = true)
78 | AppServices.showToast(R.string.checking_for_update, true)
79 | true
80 | }
81 | R.id.about -> {
82 | showAboutDialog()
83 | true
84 | }
85 | else -> super.onOptionsItemSelected(item)
86 | }
87 | }
88 |
89 | private fun showInstructionDialog() {
90 | MaterialAlertDialogBuilder(this).setMessage(getString(R.string.instructions)).show()
91 | }
92 |
93 | private fun showAboutDialog() {
94 | AboutDialogBinding.inflate(LayoutInflater.from(this), null, false).apply {
95 | appTitle.text = getString(R.string.app_name)
96 | appVersion.text = getString(R.string.version_text, BuildConfig.VERSION_NAME)
97 | developerInfo.setLinkedText(
98 | R.string.developer_info_text, AppConfigs.DEVELOPER, AppConfigs.DEVELOPER_URL
99 | )
100 | helpForumInfo.setLinkedText(
101 | R.string.help_forum_info_text, AppConfigs.HELP_FORUM, AppConfigs.HELP_FORUM_URL
102 | )
103 | sourceCodeInfo.setLinkedText(
104 | R.string.source_code_info_text, AppConfigs.GITHUB_REPO, AppConfigs.GITHUB_REPO_URL
105 | )
106 | }.let {
107 | MaterialAlertDialogBuilder(this).setView(it.root).show()
108 | Glide.with(this).load(R.mipmap.ic_launcher).transform(RoundedCorners(50)).into(it.aboutIcon)
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------