From 8bfd3f00b6240d9949bcec32404f9670c3eab35f Mon Sep 17 00:00:00 2001 From: rumboalla Date: Sun, 24 Mar 2024 10:33:21 +0100 Subject: [PATCH] Play Source * Play Search: Search only works for package names. Install will work for some apps. Very alpha. --- app/build.gradle.kts | 7 +- .../kotlin/com/apkupdater/data/ui/Source.kt | 1 + .../kotlin/com/apkupdater/di/MainModule.kt | 5 +- .../main/kotlin/com/apkupdater/prefs/Prefs.kt | 3 + .../apkupdater/repository/PlayRepository.kt | 70 +++++++ .../apkupdater/repository/SearchRepository.kt | 2 + .../apkupdater/ui/screen/SettingsScreen.kt | 6 + .../com/apkupdater/util/SessionInstaller.kt | 3 + .../util/play/EglExtensionProvider.kt | 80 ++++++++ .../apkupdater/util/play/IProxyHttpClient.kt | 9 + .../util/play/NativeDeviceInfoProvider.kt | 152 ++++++++++++++ .../apkupdater/util/play/PlayHttpClient.kt | 187 ++++++++++++++++++ .../com/apkupdater/util/play/ProxyInfo.kt | 10 + .../apkupdater/viewmodel/InstallViewModel.kt | 12 ++ .../apkupdater/viewmodel/SettingsViewModel.kt | 2 + app/src/main/res/drawable/ic_play.xml | 22 +++ app/src/main/res/values/strings.xml | 1 + 17 files changed, 568 insertions(+), 4 deletions(-) create mode 100644 app/src/main/kotlin/com/apkupdater/repository/PlayRepository.kt create mode 100644 app/src/main/kotlin/com/apkupdater/util/play/EglExtensionProvider.kt create mode 100644 app/src/main/kotlin/com/apkupdater/util/play/IProxyHttpClient.kt create mode 100644 app/src/main/kotlin/com/apkupdater/util/play/NativeDeviceInfoProvider.kt create mode 100644 app/src/main/kotlin/com/apkupdater/util/play/PlayHttpClient.kt create mode 100644 app/src/main/kotlin/com/apkupdater/util/play/ProxyInfo.kt create mode 100644 app/src/main/res/drawable/ic_play.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f740d58e..8a8d581d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -93,9 +93,9 @@ dependencies { implementation("androidx.work:work-runtime-ktx:2.9.0") implementation("io.insert-koin:koin-android:3.4.2") implementation("io.insert-koin:koin-androidx-compose:3.4.2") - implementation("com.squareup.retrofit2:retrofit:2.9.0") - implementation("com.squareup.retrofit2:converter-gson:2.9.0") - implementation("com.squareup.okhttp3:logging-interceptor:4.11.0") + implementation("com.squareup.retrofit2:retrofit:2.10.0") + implementation("com.squareup.retrofit2:converter-gson:2.10.0") + implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") implementation("com.google.code.gson:gson:2.10.1") implementation("io.coil-kt:coil-compose:2.4.0") implementation("com.github.rumboalla.KryptoPrefs:kryptoprefs:0.4.3") @@ -103,6 +103,7 @@ dependencies { implementation("org.jsoup:jsoup:1.16.1") implementation("com.github.topjohnwu.libsu:core:5.2.1") implementation("io.github.g00fy2:versioncompare:1.5.0") + implementation("com.gitlab.AuroraOSS:gplayapi:3.2.10") testImplementation("junit:junit:4.13.2") diff --git a/app/src/main/kotlin/com/apkupdater/data/ui/Source.kt b/app/src/main/kotlin/com/apkupdater/data/ui/Source.kt index 6df68eac..d5627f99 100644 --- a/app/src/main/kotlin/com/apkupdater/data/ui/Source.kt +++ b/app/src/main/kotlin/com/apkupdater/data/ui/Source.kt @@ -15,3 +15,4 @@ val IzzySource = Source("F-Droid (Izzy)", R.drawable.ic_izzy) val AptoideSource = Source("Aptoide", R.drawable.ic_aptoide) val ApkPureSource = Source("ApkPure", R.drawable.ic_apkpure) val GitLabSource = Source("GitLab", R.drawable.ic_gitlab) +val PlaySource = Source("Play", R.drawable.ic_play) diff --git a/app/src/main/kotlin/com/apkupdater/di/MainModule.kt b/app/src/main/kotlin/com/apkupdater/di/MainModule.kt index 88ea3556..e486075e 100644 --- a/app/src/main/kotlin/com/apkupdater/di/MainModule.kt +++ b/app/src/main/kotlin/com/apkupdater/di/MainModule.kt @@ -13,6 +13,7 @@ import com.apkupdater.repository.AptoideRepository import com.apkupdater.repository.FdroidRepository import com.apkupdater.repository.GitHubRepository import com.apkupdater.repository.GitLabRepository +import com.apkupdater.repository.PlayRepository import com.apkupdater.repository.SearchRepository import com.apkupdater.repository.UpdatesRepository import com.apkupdater.service.ApkMirrorService @@ -145,13 +146,15 @@ val mainModule = module { single { AptoideRepository(get(), get(), get()) } + single { PlayRepository(get(), get(), get()) } + single(named("main")) { FdroidRepository(get(), "https://f-droid.org/repo/", FdroidSource, get()) } single(named("izzy")) { FdroidRepository(get(), "https://apt.izzysoft.de/fdroid/repo/", IzzySource, get()) } single { UpdatesRepository(get(), get(), get(), get(named("main")), get(named("izzy")), get(), get(), get(), get()) } - single { SearchRepository(get(), get(named("main")), get(named("izzy")), get(), get(), get(), get(), get()) } + single { SearchRepository(get(), get(named("main")), get(named("izzy")), get(), get(), get(), get(), get(), get()) } single { KryptoBuilder.nocrypt(get(), androidContext().getString(R.string.app_name)) } diff --git a/app/src/main/kotlin/com/apkupdater/prefs/Prefs.kt b/app/src/main/kotlin/com/apkupdater/prefs/Prefs.kt index 3737be09..1f19e91b 100644 --- a/app/src/main/kotlin/com/apkupdater/prefs/Prefs.kt +++ b/app/src/main/kotlin/com/apkupdater/prefs/Prefs.kt @@ -1,6 +1,7 @@ package com.apkupdater.prefs import com.apkupdater.data.ui.Screen +import com.aurora.gplayapi.data.models.AuthData import com.kryptoprefs.context.KryptoContext import com.kryptoprefs.gson.json import com.kryptoprefs.preferences.KryptoPrefs @@ -29,6 +30,7 @@ class Prefs( val useIzzy = boolean("useIzzy", defValue = true, backed = true) val useAptoide = boolean("useAptoide", defValue = true, backed = true) val useApkPure = boolean("useApkPure", defValue = true, backed = true) + val usePlay = boolean("usePlay", defValue = true, backed = true) val enableAlarm = boolean("enableAlarm", defValue = false, backed = true) val alarmHour = int("alarmHour", defValue = 12, backed = true) val alarmFrequency = int("alarmFrequency", 0, backed = true) @@ -36,4 +38,5 @@ class Prefs( val rootInstall = boolean("rootInstall", defValue = false, backed = true) val theme = int("theme", defValue = 0, backed = true) val lastTab = string("lastTab", defValue = Screen.Updates.route, backed = true) + val playAuthData = json("playAuthData", AuthData("", ""), true) } diff --git a/app/src/main/kotlin/com/apkupdater/repository/PlayRepository.kt b/app/src/main/kotlin/com/apkupdater/repository/PlayRepository.kt new file mode 100644 index 00000000..3db7e1f7 --- /dev/null +++ b/app/src/main/kotlin/com/apkupdater/repository/PlayRepository.kt @@ -0,0 +1,70 @@ +package com.apkupdater.repository + +import android.content.Context +import android.net.Uri +import android.util.Log +import com.apkupdater.data.ui.AppUpdate +import com.apkupdater.data.ui.PlaySource +import com.apkupdater.prefs.Prefs +import com.apkupdater.util.play.NativeDeviceInfoProvider +import com.apkupdater.util.play.PlayHttpClient +import com.aurora.gplayapi.data.models.AuthData +import com.aurora.gplayapi.helpers.AppDetailsHelper +import com.aurora.gplayapi.helpers.PurchaseHelper +import com.google.gson.Gson +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flow + + +class PlayRepository( + private val context: Context, + private val gson: Gson, + private val prefs: Prefs +) { + private suspend fun auth(): AuthData { + val savedData = prefs.playAuthData.get() + if (savedData.email.isEmpty()) { + val properties = NativeDeviceInfoProvider(context).getNativeDeviceProperties() + val playResponse = PlayHttpClient.postAuth( + "https://auroraoss.com/api/auth", + gson.toJson(properties).toByteArray() + ) + if (playResponse.isSuccessful) { + val authData = gson.fromJson(String(playResponse.responseBytes), AuthData::class.java) + prefs.playAuthData.put(authData) + return authData + } + throw IllegalStateException("Auth not successful.") + } + return savedData + } + + suspend fun search(text: String) = flow { + if (text.contains(" ") || !text.contains(".")) { + emit(Result.success(emptyList())) + return@flow + } + val authData = auth() + val details = AppDetailsHelper(authData).using(PlayHttpClient) + val app = details.getAppByPackageName(text) + val files = PurchaseHelper(authData).purchase(app.packageName, app.versionCode, app.offerType) + val link = files.joinToString(separator = ",") { it.url } + val update = AppUpdate( + app.displayName, + app.packageName, + app.versionName, + "", + app.versionCode.toLong(), + 0L, + PlaySource, + Uri.parse(app.iconArtwork.url), + link, + whatsNew = app.changes + ) + emit(Result.success(listOf(update))) + }.catch { + emit(Result.failure(it)) + Log.e("PlayRepository", "Error searching.", it) + } + +} diff --git a/app/src/main/kotlin/com/apkupdater/repository/SearchRepository.kt b/app/src/main/kotlin/com/apkupdater/repository/SearchRepository.kt index c7e05572..7533e3c9 100644 --- a/app/src/main/kotlin/com/apkupdater/repository/SearchRepository.kt +++ b/app/src/main/kotlin/com/apkupdater/repository/SearchRepository.kt @@ -17,6 +17,7 @@ class SearchRepository( private val gitHubRepository: GitHubRepository, private val apkPureRepository: ApkPureRepository, private val gitLabRepository: GitLabRepository, + private val playRepository: PlayRepository, private val prefs: Prefs ) { @@ -29,6 +30,7 @@ class SearchRepository( if (prefs.useGitHub.get()) sources.add(gitHubRepository.search(text)) if (prefs.useApkPure.get()) sources.add(apkPureRepository.search(text)) if (prefs.useGitLab.get()) sources.add(gitLabRepository.search(text)) + if (prefs.usePlay.get()) sources.add(playRepository.search(text)) if (sources.isNotEmpty()) { sources.combine { updates -> diff --git a/app/src/main/kotlin/com/apkupdater/ui/screen/SettingsScreen.kt b/app/src/main/kotlin/com/apkupdater/ui/screen/SettingsScreen.kt index e24080c5..dd1abc23 100644 --- a/app/src/main/kotlin/com/apkupdater/ui/screen/SettingsScreen.kt +++ b/app/src/main/kotlin/com/apkupdater/ui/screen/SettingsScreen.kt @@ -241,6 +241,12 @@ fun Settings(viewModel: SettingsViewModel) = LazyColumn { stringResource(R.string.source_apkpure), R.drawable.ic_apkpure ) + SwitchSetting( + { viewModel.getUsePlay() }, + { viewModel.setUsePlay(it) }, + stringResource(R.string.source_play) + " (Alpha)", + R.drawable.ic_play + ) } item { diff --git a/app/src/main/kotlin/com/apkupdater/util/SessionInstaller.kt b/app/src/main/kotlin/com/apkupdater/util/SessionInstaller.kt index 0eace10f..f4379b4b 100644 --- a/app/src/main/kotlin/com/apkupdater/util/SessionInstaller.kt +++ b/app/src/main/kotlin/com/apkupdater/util/SessionInstaller.kt @@ -110,4 +110,7 @@ class SessionInstaller(private val context: Context) { file.delete() } + suspend fun playInstall(id: Int, packageName: String, streams: List) = + install(id, packageName, streams) + } diff --git a/app/src/main/kotlin/com/apkupdater/util/play/EglExtensionProvider.kt b/app/src/main/kotlin/com/apkupdater/util/play/EglExtensionProvider.kt new file mode 100644 index 00000000..98aa49dd --- /dev/null +++ b/app/src/main/kotlin/com/apkupdater/util/play/EglExtensionProvider.kt @@ -0,0 +1,80 @@ +package com.apkupdater.util.play + +import android.opengl.GLES10 +import android.text.TextUtils +import javax.microedition.khronos.egl.EGL10 +import javax.microedition.khronos.egl.EGLConfig +import javax.microedition.khronos.egl.EGLContext +import javax.microedition.khronos.egl.EGLDisplay + + +object EglExtensionProvider { + @JvmStatic + val eglExtensions: List + get() { + val glExtensions: MutableSet = HashSet() + val egl10 = EGLContext.getEGL() as EGL10 + val display = egl10.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY) + egl10.eglInitialize(display, IntArray(2)) + val cf = IntArray(1) + if (egl10.eglGetConfigs(display, null, 0, cf)) { + val configs = arrayOfNulls(cf[0]) + if (egl10.eglGetConfigs(display, configs, cf[0], cf)) { + val a1 = intArrayOf(EGL10.EGL_WIDTH, EGL10.EGL_PBUFFER_BIT, EGL10.EGL_HEIGHT, EGL10.EGL_PBUFFER_BIT, EGL10.EGL_NONE) + val a2 = intArrayOf(12440, EGL10.EGL_PIXMAP_BIT, EGL10.EGL_NONE) + val a3 = IntArray(1) + for (i in 0 until cf[0]) { + egl10.eglGetConfigAttrib(display, configs[i], EGL10.EGL_CONFIG_CAVEAT, a3) + if (a3[0] != EGL10.EGL_SLOW_CONFIG) { + egl10.eglGetConfigAttrib(display, configs[i], EGL10.EGL_SURFACE_TYPE, a3) + if (1 and a3[0] != 0) { + egl10.eglGetConfigAttrib(display, configs[i], EGL10.EGL_RENDERABLE_TYPE, a3) + if (1 and a3[0] != 0) { + addExtensionsForConfig(egl10, display, configs[i], a1, null, glExtensions) + } + if (4 and a3[0] != 0) { + addExtensionsForConfig(egl10, display, configs[i], a1, a2, glExtensions) + } + } + } + } + } + } + egl10.eglTerminate(display) + return ArrayList(glExtensions).sorted() + } + + private fun addExtensionsForConfig( + egl10: EGL10, + eglDisplay: EGLDisplay, + eglConfig: EGLConfig?, + ai: IntArray, + ai1: IntArray?, + set: MutableSet + ) { + val eglContext = egl10.eglCreateContext(eglDisplay, eglConfig, EGL10.EGL_NO_CONTEXT, ai1) + if (eglContext === EGL10.EGL_NO_CONTEXT) { + return + } + val eglSurface = egl10.eglCreatePbufferSurface(eglDisplay, eglConfig, ai) + if (eglSurface === EGL10.EGL_NO_SURFACE) { + egl10.eglDestroyContext(eglDisplay, eglContext) + } else { + egl10.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext) + val s = GLES10.glGetString(7939) + if (!TextUtils.isEmpty(s)) { + val split = s.split(" ".toRegex()).toTypedArray() + val i = split.size + set.addAll(listOf(*split).subList(0, i)) + } + egl10.eglMakeCurrent( + eglDisplay, + EGL10.EGL_NO_SURFACE, + EGL10.EGL_NO_SURFACE, + EGL10.EGL_NO_CONTEXT + ) + egl10.eglDestroySurface(eglDisplay, eglSurface) + egl10.eglDestroyContext(eglDisplay, eglContext) + } + } +} diff --git a/app/src/main/kotlin/com/apkupdater/util/play/IProxyHttpClient.kt b/app/src/main/kotlin/com/apkupdater/util/play/IProxyHttpClient.kt new file mode 100644 index 00000000..9c2e477c --- /dev/null +++ b/app/src/main/kotlin/com/apkupdater/util/play/IProxyHttpClient.kt @@ -0,0 +1,9 @@ +package com.apkupdater.util.play + +import com.aurora.gplayapi.network.IHttpClient + + +interface IProxyHttpClient : IHttpClient { + @Throws(UnsupportedOperationException::class) + fun setProxy(proxyInfo: ProxyInfo): IHttpClient +} diff --git a/app/src/main/kotlin/com/apkupdater/util/play/NativeDeviceInfoProvider.kt b/app/src/main/kotlin/com/apkupdater/util/play/NativeDeviceInfoProvider.kt new file mode 100644 index 00000000..2cd1c70d --- /dev/null +++ b/app/src/main/kotlin/com/apkupdater/util/play/NativeDeviceInfoProvider.kt @@ -0,0 +1,152 @@ +package com.apkupdater.util.play + +import android.app.ActivityManager +import android.content.Context +import android.content.ContextWrapper +import android.content.res.Configuration +import android.os.Build +import android.text.TextUtils +import java.util.Locale +import java.util.Properties + + +class NativeDeviceInfoProvider(context: Context) : ContextWrapper(context) { + + fun getNativeDeviceProperties(): Properties { + val properties = Properties().apply { + //Build Props + setProperty("UserReadableName", "${Build.DEVICE}-default") + setProperty("Build.HARDWARE", Build.HARDWARE) + setProperty( + "Build.RADIO", + if (Build.getRadioVersion() != null) + Build.getRadioVersion() + else + "unknown" + ) + setProperty("Build.FINGERPRINT", Build.FINGERPRINT) + setProperty("Build.BRAND", Build.BRAND) + setProperty("Build.DEVICE", Build.DEVICE) + setProperty("Build.VERSION.SDK_INT", "${Build.VERSION.SDK_INT}") + setProperty("Build.VERSION.RELEASE", Build.VERSION.RELEASE) + setProperty("Build.MODEL", Build.MODEL) + setProperty("Build.MANUFACTURER", Build.MANUFACTURER) + setProperty("Build.PRODUCT", Build.PRODUCT) + setProperty("Build.ID", Build.ID) + setProperty("Build.BOOTLOADER", Build.BOOTLOADER) + + val config = applicationContext.resources.configuration + setProperty("TouchScreen", "${config.touchscreen}") + setProperty("Keyboard", "${config.keyboard}") + setProperty("Navigation", "${config.navigation}") + setProperty("ScreenLayout", "${config.screenLayout and 15}") + setProperty("HasHardKeyboard", "${config.keyboard == Configuration.KEYBOARD_QWERTY}") + setProperty( + "HasFiveWayNavigation", + "${config.navigation == Configuration.NAVIGATIONHIDDEN_YES}" + ) + + //Display Metrics + val metrics = applicationContext.resources.displayMetrics + setProperty("Screen.Density", "${metrics.densityDpi}") + setProperty("Screen.Width", "${metrics.widthPixels}") + setProperty("Screen.Height", "${metrics.heightPixels}") + + + //Supported Platforms + setProperty("Platforms", Build.SUPPORTED_ABIS.joinToString(separator = ",")) + //Supported Features + setProperty("Features", getFeatures().joinToString(separator = ",")) + //Shared Locales + setProperty("Locales", getLocales().joinToString(separator = ",")) + //Shared Libraries + setProperty("SharedLibraries", getSharedLibraries().joinToString(separator = ",")) + //GL Extensions + val activityManager = + applicationContext.getSystemService(ACTIVITY_SERVICE) as ActivityManager + setProperty( + "GL.Version", + activityManager.deviceConfigurationInfo.reqGlEsVersion.toString() + ) + setProperty( + "GL.Extensions", + EglExtensionProvider.eglExtensions.joinToString(separator = ",") + ) + + //Google Related Props + setProperty("Client", "android-google") + setProperty("GSF.version", "203615037") + setProperty("Vending.version", "82201710") + setProperty("Vending.versionString", "22.0.17-21 [0] [PR] 332555730") + + //MISC + setProperty("Roaming", "mobile-notroaming") + setProperty("TimeZone", "UTC-10") + + //Telephony (USA 3650 AT&T) + setProperty("CellOperator", "310") + setProperty("SimOperator", "38") + } + + if (isHuawei()) + stripHuaweiProperties(properties) + + return properties + } + + private fun getFeatures(): List { + val featureStringList: MutableList = ArrayList() + try { + val availableFeatures = applicationContext.packageManager.systemAvailableFeatures + for (feature in availableFeatures) { + if (feature.name.isNotEmpty()) { + featureStringList.add(feature.name) + } + } + } catch (_: Exception) {} + return featureStringList + } + + private fun getLocales(): List { + val localeList: MutableList = ArrayList() + localeList.addAll(listOf(*applicationContext.assets.locales)) + val locales: MutableList = ArrayList() + for (locale in localeList) { + if (TextUtils.isEmpty(locale)) { + continue + } + locales.add(locale.replace("-", "_")) + } + return locales + } + + private fun getSharedLibraries(): List { + val systemSharedLibraryNames = applicationContext.packageManager.systemSharedLibraryNames + val libraries: MutableList = ArrayList() + try { + if (systemSharedLibraryNames != null) { + libraries.addAll(listOf(*systemSharedLibraryNames)) + } + } catch (_: Exception) {} + return libraries + } + + private fun stripHuaweiProperties(properties: Properties): Properties { + //Add Pixel 7a properties + properties["Build.HARDWARE"] = "lynx" + properties["Build.BOOTLOADER"] = "lynx-1.0-9716681" + properties["Build.BRAND"] = "google" + properties["Build.DEVICE"] = "lynx" + properties["Build.MODEL"] = "Pixel 7a" + properties["Build.MANUFACTURER"] = "Google" + properties["Build.PRODUCT"] = "lynx" + properties["Build.ID"] = "TQ2A.230505.002" + return properties + } +} + +fun isHuawei(): Boolean { + return Build.MANUFACTURER.lowercase(Locale.getDefault()).contains("huawei") + || Build.HARDWARE.lowercase(Locale.getDefault()).contains("kirin") + || Build.HARDWARE.lowercase(Locale.getDefault()).contains("hi3") +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/apkupdater/util/play/PlayHttpClient.kt b/app/src/main/kotlin/com/apkupdater/util/play/PlayHttpClient.kt new file mode 100644 index 00000000..944dc0a6 --- /dev/null +++ b/app/src/main/kotlin/com/apkupdater/util/play/PlayHttpClient.kt @@ -0,0 +1,187 @@ +package com.apkupdater.util.play + +import android.util.Log +import com.aurora.gplayapi.data.models.PlayResponse +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import okhttp3.Credentials +import okhttp3.Headers.Companion.toHeaders +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import java.io.IOException +import java.net.InetSocketAddress +import java.net.Proxy +import java.util.concurrent.TimeUnit + + +object PlayHttpClient : IProxyHttpClient { + + private const val POST = "POST" + private const val GET = "GET" + + private val _responseCode = MutableStateFlow(100) + override val responseCode: StateFlow get() = _responseCode.asStateFlow() + private var okHttpClient = OkHttpClient() + val okHttpClientBuilder = OkHttpClient().newBuilder() + .connectTimeout(25, TimeUnit.SECONDS) + .readTimeout(25, TimeUnit.SECONDS) + .writeTimeout(25, TimeUnit.SECONDS) + .retryOnConnectionFailure(true) + .followRedirects(true) + .followSslRedirects(true) + + override fun setProxy(proxyInfo: ProxyInfo): PlayHttpClient { + val proxy = Proxy( + if (proxyInfo.protocol == "SOCKS") Proxy.Type.SOCKS else Proxy.Type.HTTP, + InetSocketAddress.createUnresolved(proxyInfo.host, proxyInfo.port) + ) + + val proxyUser = proxyInfo.proxyUser + val proxyPassword = proxyInfo.proxyPassword + + if (!proxyUser.isNullOrBlank() && !proxyPassword.isNullOrBlank()) { + okHttpClientBuilder.proxyAuthenticator { _, response -> + if (response.request.header("Proxy-Authorization") != null) { + return@proxyAuthenticator null + } + + val credential = Credentials.basic(proxyUser, proxyPassword) + response.request + .newBuilder() + .header("Proxy-Authorization", credential) + .build() + } + } + + okHttpClientBuilder.proxy(proxy) + okHttpClient = okHttpClientBuilder.build() + return this + } + + @Throws(IOException::class) + fun post(url: String, headers: Map, requestBody: RequestBody): PlayResponse { + val request = Request.Builder() + .url(url) + .headers(headers.toHeaders()) + .method(POST, requestBody) + .build() + return processRequest(request) + } + + @Throws(IOException::class) + override fun post( + url: String, + headers: Map, + params: Map + ): PlayResponse { + val request = Request.Builder() + .url(buildUrl(url, params)) + .headers(headers.toHeaders()) + .method(POST, "".toRequestBody(null)) + .build() + return processRequest(request) + } + + override fun postAuth(url: String, body: ByteArray): PlayResponse { + val requestBody = body.toRequestBody("application/json".toMediaType(), 0, body.size) + val request = Request.Builder() + .url(url) + .header("User-Agent", "com.aurora.store-4.4.2-56") + .method(POST, requestBody) + .build() + return processRequest(request) + } + + @Throws(IOException::class) + override fun post(url: String, headers: Map, body: ByteArray): PlayResponse { + val requestBody = body.toRequestBody( + "application/x-protobuf".toMediaType(), + 0, + body.size + ) + return post(url, headers, requestBody) + } + + @Throws(IOException::class) + override fun get(url: String, headers: Map): PlayResponse { + return get(url, headers, mapOf()) + } + + @Throws(IOException::class) + override fun get( + url: String, + headers: Map, + params: Map + ): PlayResponse { + val request = Request.Builder() + .url(buildUrl(url, params)) + .headers(headers.toHeaders()) + .method(GET, null) + .build() + return processRequest(request) + } + + override fun getAuth(url: String): PlayResponse { + val request = Request.Builder() + .url(url) + .header("User-Agent", "com.aurora.store-4.4.2-56") + .method(GET, null) + .build() + return processRequest(request) + } + + @Throws(IOException::class) + override fun get( + url: String, + headers: Map, + paramString: String + ): PlayResponse { + val request = Request.Builder() + .url(url + paramString) + .headers(headers.toHeaders()) + .method(GET, null) + .build() + return processRequest(request) + } + + private fun processRequest(request: Request): PlayResponse { + // Reset response code as flow doesn't sends the same value twice + _responseCode.value = 0 + + val call = okHttpClient.newCall(request) + return buildPlayResponse(call.execute()) + } + + private fun buildUrl(url: String, params: Map): HttpUrl { + val urlBuilder = url.toHttpUrl().newBuilder() + params.forEach { + urlBuilder.addQueryParameter(it.key, it.value) + } + return urlBuilder.build() + } + + private fun buildPlayResponse(response: Response): PlayResponse { + return PlayResponse().apply { + isSuccessful = response.isSuccessful + code = response.code + + if (response.body != null) { + responseBytes = response.body!!.bytes() + } + + if (!isSuccessful) { + errorString = response.message + } + }.also { + _responseCode.value = response.code + Log.i("PlayHttpClient", "OKHTTP [${response.code}] ${response.request.url}") + } + } +} diff --git a/app/src/main/kotlin/com/apkupdater/util/play/ProxyInfo.kt b/app/src/main/kotlin/com/apkupdater/util/play/ProxyInfo.kt new file mode 100644 index 00000000..86c9acea --- /dev/null +++ b/app/src/main/kotlin/com/apkupdater/util/play/ProxyInfo.kt @@ -0,0 +1,10 @@ +package com.apkupdater.util.play + + +data class ProxyInfo( + var protocol: String, + var host: String, + var port: Int, + var proxyUser: String?, + var proxyPassword: String? +) diff --git a/app/src/main/kotlin/com/apkupdater/viewmodel/InstallViewModel.kt b/app/src/main/kotlin/com/apkupdater/viewmodel/InstallViewModel.kt index 4abe15fe..3a81a1b8 100644 --- a/app/src/main/kotlin/com/apkupdater/viewmodel/InstallViewModel.kt +++ b/app/src/main/kotlin/com/apkupdater/viewmodel/InstallViewModel.kt @@ -63,6 +63,10 @@ abstract class InstallViewModel( } protected suspend fun downloadAndInstall(id: Int, packageName: String, link: String) = runCatching { + if (link.contains(",")) { + playDownloadAndInstall(id, packageName, link) + return@runCatching + } val stream = downloader.downloadStream(link) if (stream != null) { if (link.contains("/XAPK")) { @@ -78,6 +82,14 @@ abstract class InstallViewModel( cancelInstall(id) } + private suspend fun playDownloadAndInstall(id: Int, packageName: String, link: String) = runCatching { + val urls = link.split(",") + val streams = urls.map { downloader.downloadStream(it)!! } + installer.playInstall(id, packageName, streams) + }.getOrElse { + Log.e("InstallViewModel", "Error in playDownloadAndInstall.", it) + cancelInstall(id) + } protected fun sendInstallSnack(updates: List, log: AppInstallStatus) { if (log.snack) { diff --git a/app/src/main/kotlin/com/apkupdater/viewmodel/SettingsViewModel.kt b/app/src/main/kotlin/com/apkupdater/viewmodel/SettingsViewModel.kt index 73bbaa5a..58dd2763 100644 --- a/app/src/main/kotlin/com/apkupdater/viewmodel/SettingsViewModel.kt +++ b/app/src/main/kotlin/com/apkupdater/viewmodel/SettingsViewModel.kt @@ -60,6 +60,8 @@ class SettingsViewModel( fun setUseAptoide(b: Boolean) = prefs.useAptoide.put(b) fun getUseApkPure() = prefs.useApkPure.get() fun setUseApkPure(b: Boolean) = prefs.useApkPure.put(b) + fun getUsePlay() = prefs.usePlay.get() + fun setUsePlay(b: Boolean) = prefs.usePlay.put(b) fun getAndroidTvUi() = prefs.androidTvUi.get() fun setAndroidTvUi(b: Boolean) = prefs.androidTvUi.put(b) fun getEnableAlarm() = prefs.enableAlarm.get() diff --git a/app/src/main/res/drawable/ic_play.xml b/app/src/main/res/drawable/ic_play.xml new file mode 100644 index 00000000..9f02234b --- /dev/null +++ b/app/src/main/res/drawable/ic_play.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 00d1a73c..eaab6190 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -47,6 +47,7 @@ GitHub GitLab APKPure + Play About Frequency Theme