├── 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 | 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 | 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 | 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 | 3 | 6 | 7 | 10 | 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 | 3 | 7 | 8 | 12 | 13 | 17 | 18 | -------------------------------------------------------------------------------- /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 | 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 | 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 | 4 | 8 | 12 | 17 | 21 | 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 | 8 | 9 | 15 | 17 | 18 | 19 | 21 | 22 | 23 | 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 | 4 | 9 | 10 | 15 | 16 | 21 | 22 | 27 | 28 | 33 | 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 | 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 | --------------------------------------------------------------------------------