diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9e4f1291e..3290f69c5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -264,6 +264,8 @@ dependencies { implementation(libs.process.phoenix) + "huaweiImplementation"(libs.ag.coreservice) + // LeakCanary debugImplementation(libs.squareup.leakcanary.android) } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 855f7b111..bf3938b7d 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -124,3 +124,9 @@ -keep class * extends androidx.viewbinding.ViewBinding { *; } + +# Keep Huawei specific classes and methods +-keep class com.huawei.** { *; } +-dontwarn com.huawei.** +-keep class com.hihonor.** { *; } +-dontwarn com.hihonor.** \ No newline at end of file diff --git a/app/src/huawei/java/com/aurora/store/data/receiver/InstallerStatusReceiver.kt b/app/src/huawei/java/com/aurora/store/data/receiver/InstallerStatusReceiver.kt new file mode 100644 index 000000000..ebda1d116 --- /dev/null +++ b/app/src/huawei/java/com/aurora/store/data/receiver/InstallerStatusReceiver.kt @@ -0,0 +1,149 @@ +/* + * Aurora Store + * Copyright (C) 2021, Rahul Kumar Patel + * + * Aurora Store is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * Aurora Store is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Aurora Store. If not, see . + * + */ +package com.aurora.store.data.receiver + +import android.content.Context +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.util.Log +import com.aurora.Constants.PACKAGE_NAME_APP_GALLERY +import com.huawei.appgallery.coreservice.api.ApiClient +import com.huawei.appgallery.coreservice.api.ApiCode +import com.huawei.appgallery.coreservice.api.IConnectionResult +import com.huawei.appgallery.coreservice.api.PendingCall +import com.huawei.appgallery.coreservice.internal.framework.ipc.transport.data.BaseIPCRequest +import com.huawei.appgallery.coreservice.internal.framework.ipc.transport.data.BaseIPCResponse +import com.huawei.appmarket.framework.coreservice.Status +import com.huawei.appmarket.service.externalservice.distribution.thirdsilentinstall.SilentInstallRequest +import com.huawei.appmarket.service.externalservice.distribution.thirdsilentinstall.SilentInstallResponse +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class InstallerStatusReceiver : BaseInstallerStatusReceiver() { + + private val TAG = InstallerStatusReceiver::class.java.simpleName + + private lateinit var apiClient: ApiClient + + override fun onReceive(context: Context?, intent: Intent?) { + super.onReceive(context, intent) + } + + override fun doAppropriatePrompt(context: Context, intent: Intent, sessionId: Int) { + if (isHuaweiSilentInstallSupported(context)) { + Log.i( + TAG, + "Huawei silent install supported, proceeding with ApiClient connection" + ) + connectApiClient(context, intent, sessionId) + } else { + promptUser(context, intent) + } + } + + override fun postStatus(status: Int, packageName: String?, extra: String?, context: Context) { + super.postStatus(status, packageName, extra, context) + + if (::apiClient.isInitialized && apiClient.isConnected) { + apiClient.disconnect() + } + } + + private fun connectApiClient(context: Context, intent: Intent, sessionId: Int) { + // Check if the ApiClient is already initialized and connected + if (::apiClient.isInitialized && apiClient.isConnected) { + Log.i(TAG, "ApiClient already connected, requesting silent install") + requestSilentInstall(context, intent, sessionId) + return + } + + apiClient = ApiClient.Builder(context.applicationContext) + .setHomeCountry("CN") + .addConnectionCallbacks(object : ApiClient.ConnectionCallback { + override fun onConnected() { + Log.i(TAG, "ApiClient connected") + requestSilentInstall(context, intent, sessionId) + } + + override fun onConnectionSuspended(cause: Int) { + Log.w(TAG, "ApiClient connection suspended: $cause") + } + + override fun onConnectionFailed(result: IConnectionResult?) { + Log.e(TAG, "ApiClient failed to connect with result: $result, prompting user") + promptUser(context, intent) + } + }) + .build() + + apiClient.connect() + } + + private fun requestSilentInstall(context: Context, intent: Intent, sessionId: Int) { + val request = SilentInstallRequest().apply { + setSessionId(sessionId) + } + + val pendingResult = PendingCall( + apiClient, + request + ) + + if (::apiClient.isInitialized && apiClient.isConnected) { + pendingResult.setCallback { handleIPCResponse(context, intent, it) } + } else { + Log.e(TAG, "ApiClient null or not connected") + promptUser(context, intent) + } + } + + private fun handleIPCResponse( + context: Context, + intent: Intent, + ipcResponse: Status + ) { + with(ipcResponse) { + if (response is SilentInstallResponse || response is BaseIPCResponse) { + Log.i(TAG, "IPC Response: ${ApiCode.getStatusCodeString(statusCode)}") + + if (statusCode != ApiCode.SUCCESS) { + Log.e(TAG, "Silent install failed with status code: $statusCode") + promptUser(context, intent) + } + } + } + } + + private fun isHuaweiSilentInstallSupported(context: Context): Boolean { + return try { + val applicationInfo: ApplicationInfo = context.packageManager.getApplicationInfo( + PACKAGE_NAME_APP_GALLERY, + PackageManager.GET_META_DATA + ) + + val supportFunction = applicationInfo.metaData.getInt("appgallery_support_function") + Log.i(TAG, "Huawei silent install support function: $supportFunction") + + (supportFunction and (1 shl 5)) != 0 + } catch (e: Exception) { + false + } + } +} diff --git a/app/src/huawei/java/com/aurora/store/view/ui/onboarding/MicroGFragment.kt b/app/src/huawei/java/com/aurora/store/view/ui/onboarding/MicroGFragment.kt new file mode 100644 index 000000000..d4b08e51c --- /dev/null +++ b/app/src/huawei/java/com/aurora/store/view/ui/onboarding/MicroGFragment.kt @@ -0,0 +1,159 @@ +package com.aurora.store.view.ui.onboarding + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope +import com.aurora.extensions.browse +import com.aurora.store.AuroraApp +import com.aurora.store.R +import com.aurora.store.data.event.Event +import com.aurora.store.data.event.InstallerEvent +import com.aurora.store.data.model.Dash +import com.aurora.store.data.model.DownloadStatus +import com.aurora.store.databinding.FragmentOnboardingMicrogBinding +import com.aurora.store.util.PackageUtil +import com.aurora.store.view.epoxy.views.EpoxyTextViewModel_ +import com.aurora.store.view.epoxy.views.preference.DashViewModel_ +import com.aurora.store.view.ui.commons.BaseFragment +import com.aurora.store.viewmodel.onboarding.MicroGViewModel +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class MicroGFragment : BaseFragment() { + // Shared ViewModel + val microGViewModel: MicroGViewModel by activityViewModels() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + with(binding) { + // RecyclerView + epoxyRecycler.withModels { + setFilterDuplicates(true) + + add( + EpoxyTextViewModel_() + .id("microg_desc") + .title(getString(R.string.onboarding_gms_missing)) + .size(14) + .style(R.style.AuroraTextStyle) + ) + + add( + EpoxyTextViewModel_() + .id("microg_gms") + .title(getString(R.string.onboarding_gms_microg)) + .size(14) + .style(R.style.AuroraTextStyle) + ) + + + dashItems().forEach { + add( + DashViewModel_() + .id(it.id) + .dash(it) + .click { _ -> + requireContext().browse(it.url) + } + ) + } + } + + checkboxAgreement.setOnCheckedChangeListener { _, value -> + microGViewModel.markAgreement(value) + btnMicroG.isEnabled = value + } + + btnMicroG.setOnClickListener { microGViewModel.downloadMicroG() } + } + + microGViewModel.download.filterNotNull().onEach { + when (it.downloadStatus) { + DownloadStatus.DOWNLOADING -> updateProgressBar(visible = true, it.progress) + DownloadStatus.FAILED -> updateProgressBar(visible = false, 0) + DownloadStatus.QUEUED -> updateProgressBar(visible = true, -1) + DownloadStatus.COMPLETED -> updateProgressBar(visible = true, -1) + else -> {} + } + }.launchIn(viewLifecycleOwner.lifecycleScope) + + viewLifecycleOwner.lifecycleScope.launch { + AuroraApp.events.installerEvent.collect { onEvent(it) } + } + } + + private fun onEvent(event: Event) { + when (event) { + is InstallerEvent.Installed -> { + if (PackageUtil.isMicroGBundleInstalled(requireContext())) { + markInstallationComplete() + } + } + + is InstallerEvent.Failed -> markInstallationFailed() + else -> {} + } + } + + private fun updateProgressBar(visible: Boolean, downloadProgress: Int) { + with(binding.progressBar) { + if (visible) show() else hide() + isIndeterminate = downloadProgress == -1 + progress = downloadProgress + } + } + + private fun markInstallationComplete() { + with(binding) { + with(btnMicroG) { + isEnabled = false + text = getString(R.string.title_installed) + } + checkboxAgreement.isEnabled = false + progressBar.hide() + } + } + + private fun markInstallationFailed() { + with(binding) { + with(btnMicroG) { + isEnabled = false + text = getString(R.string.action_install) + } + checkboxAgreement.isChecked = false + progressBar.hide() + } + } + + private fun dashItems(): List { + return listOf( + Dash( + id = 2, + title = requireContext().getString(R.string.details_dev_website), + subtitle = requireContext().getString(R.string.microg_website), + icon = R.drawable.ic_network, + url = "https://microG.org" + ), + Dash( + id = 4, + title = requireContext().getString(R.string.privacy_policy_title), + subtitle = requireContext().getString(R.string.microg_privacy_policy), + icon = R.drawable.ic_privacy, + url = "https://microg.org/privacy.html" + ), + Dash( + id = 5, + title = requireContext().getString(R.string.menu_disclaimer), + subtitle = requireContext().getString(R.string.microg_license_agreement), + icon = R.drawable.ic_disclaimer, + url = "https://raw.githubusercontent.com/microg/GmsCore/refs/heads/master/LICENSE" + ) + ) + } +} \ No newline at end of file diff --git a/app/src/huawei/java/com/aurora/store/view/ui/onboarding/OnboardingFragment.kt b/app/src/huawei/java/com/aurora/store/view/ui/onboarding/OnboardingFragment.kt index d2119f027..2bcb5a2da 100644 --- a/app/src/huawei/java/com/aurora/store/view/ui/onboarding/OnboardingFragment.kt +++ b/app/src/huawei/java/com/aurora/store/view/ui/onboarding/OnboardingFragment.kt @@ -20,6 +20,9 @@ package com.aurora.store.view.ui.onboarding import androidx.fragment.app.Fragment +import com.aurora.extensions.isHuawei +import com.aurora.store.data.providers.BlacklistProvider +import com.aurora.store.util.PackageUtil import com.aurora.store.util.Preferences.PREFERENCE_AUTO_DELETE import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT_SELECTED_TAB import com.aurora.store.util.Preferences.PREFERENCE_DISPENSER_URLS @@ -34,9 +37,12 @@ import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_EXTENDED import com.aurora.store.util.Preferences.PREFERENCE_VENDING_VERSION import com.aurora.store.util.save import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject @AndroidEntryPoint class OnboardingFragment : BaseFlavouredOnboardingFragment() { + @Inject + lateinit var blacklistProvider: BlacklistProvider override fun loadDefaultPreferences() { /*Filters*/ @@ -44,7 +50,7 @@ class OnboardingFragment : BaseFlavouredOnboardingFragment() { save(PREFERENCE_FILTER_FDROID, true) /*Network*/ - save(PREFERENCE_DISPENSER_URLS, setOf()) + save(PREFERENCE_DISPENSER_URLS, emptySet()) save(PREFERENCE_VENDING_VERSION, 0) /*Customization*/ @@ -63,10 +69,26 @@ class OnboardingFragment : BaseFlavouredOnboardingFragment() { } override fun onboardingPages(): List { - return listOf( + var pages = mutableListOf( WelcomeFragment(), PermissionsFragment.newInstance() ) + + /** + * MicroG Fragment Preconditions: + * 1. It should be a Huawei device + * 2. Supported App Gallery should be available, i.e. v15.1.x or above + * 3. MicroG bundle should not be already installed + */ + if ( + isHuawei && + PackageUtil.hasSupportedAppGallery(requireContext()) && + !PackageUtil.isMicroGBundleInstalled(requireContext()) + ) { + pages.add(MicroGFragment()) + } + + return pages } override fun setupAutoUpdates() { @@ -76,8 +98,9 @@ class OnboardingFragment : BaseFlavouredOnboardingFragment() { } override fun finishOnboarding() { - super.finishOnboarding() + blacklistProvider.blacklist("com.android.vending") + blacklistProvider.blacklist("com.google.android.gms") - // Remove super & implement variant logic here + super.finishOnboarding() } } diff --git a/app/src/huawei/java/com/huawei/appmarket/service/externalservice/distribution/thirdsilentinstall/SilentInstallRequest.java b/app/src/huawei/java/com/huawei/appmarket/service/externalservice/distribution/thirdsilentinstall/SilentInstallRequest.java new file mode 100644 index 000000000..49d4dc25d --- /dev/null +++ b/app/src/huawei/java/com/huawei/appmarket/service/externalservice/distribution/thirdsilentinstall/SilentInstallRequest.java @@ -0,0 +1,54 @@ +package com.huawei.appmarket.service.externalservice.distribution.thirdsilentinstall; + +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.Keep; + +import com.huawei.appgallery.coreservice.internal.framework.ipc.transport.data.BaseIPCRequest; +import com.huawei.appgallery.coreservice.internal.support.parcelable.AutoParcelable; +import com.huawei.appgallery.coreservice.internal.support.parcelable.EnableAutoParcel; + +@Keep +public class SilentInstallRequest extends BaseIPCRequest { + public static final Parcelable.Creator CREATOR = new AutoParcelable.AutoCreator<>(SilentInstallRequest.class); + + public static final String METHOD = "method.requestSilentInstall"; + @EnableAutoParcel(1) + private int sessionId; + + @Override + public String getMethod() { + return METHOD; + } + + public int getSessionId() { + return sessionId; + } + + public void setSessionId(int sessionId) { + this.sessionId = sessionId; + } + + @Override + public void writeToParcel(Parcel parcel, int i) { + super.writeToParcel(parcel, i); + parcel.writeInt(sessionId); + } + + public void readFromParcel(Parcel source) { + this.sessionId = source.readInt(); + } + + public SilentInstallRequest() { + } + + protected SilentInstallRequest(Parcel in) { + this.sessionId = in.readInt(); + } + + @Override + public int describeContents() { + return 0; + } +} diff --git a/app/src/huawei/java/com/huawei/appmarket/service/externalservice/distribution/thirdsilentinstall/SilentInstallResponse.java b/app/src/huawei/java/com/huawei/appmarket/service/externalservice/distribution/thirdsilentinstall/SilentInstallResponse.java new file mode 100644 index 000000000..ccf15f017 --- /dev/null +++ b/app/src/huawei/java/com/huawei/appmarket/service/externalservice/distribution/thirdsilentinstall/SilentInstallResponse.java @@ -0,0 +1,23 @@ +package com.huawei.appmarket.service.externalservice.distribution.thirdsilentinstall; + +import android.os.Parcelable; + +import com.huawei.appgallery.coreservice.internal.framework.ipc.transport.data.BaseIPCResponse; +import com.huawei.appgallery.coreservice.internal.support.parcelable.AutoParcelable; +import com.huawei.appgallery.coreservice.internal.support.parcelable.EnableAutoParcel; + +public class SilentInstallResponse extends BaseIPCResponse { + + public static final Parcelable.Creator CREATOR = new AutoParcelable.AutoCreator<>(SilentInstallResponse.class); + + @EnableAutoParcel(1) + private int result; + + public int getResult() { + return this.result; + } + + public void setResult(int result) { + this.result = result; + } +} diff --git a/app/src/main/java/com/aurora/Constants.kt b/app/src/main/java/com/aurora/Constants.kt index d87ddcd3b..536f25e11 100644 --- a/app/src/main/java/com/aurora/Constants.kt +++ b/app/src/main/java/com/aurora/Constants.kt @@ -62,4 +62,9 @@ object Constants { const val TOP_CHART_CATEGORY = "TOP_CHART_CATEGORY" const val JSON_MIME_TYPE = "application/json" + + // PACKAGE NAMES + const val PACKAGE_NAME_GMS = "com.google.android.gms" + const val PACKAGE_NAME_PLAY_STORE = "com.android.vending" + const val PACKAGE_NAME_APP_GALLERY = "com.huawei.appmarket" } diff --git a/app/src/main/java/com/aurora/store/compose/composables/app/AppListComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/app/AppListComposable.kt index 76fdff935..128088484 100644 --- a/app/src/main/java/com/aurora/store/compose/composables/app/AppListComposable.kt +++ b/app/src/main/java/com/aurora/store/compose/composables/app/AppListComposable.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage import coil3.request.ImageRequest import coil3.request.crossfade +import com.aurora.Constants.PACKAGE_NAME_GMS import com.aurora.gplayapi.data.models.App import com.aurora.store.BuildConfig import com.aurora.store.R @@ -114,7 +115,7 @@ private fun buildExtras(app: App): List { add(stringResource(R.string.details_no_ads)) } - if (app.dependencies.dependentPackages.contains(PackageUtil.PACKAGE_NAME_GMS)) { + if (app.dependencies.dependentPackages.contains(PACKAGE_NAME_GMS)) { add(stringResource(R.string.details_gsf_dependent)) } } diff --git a/app/src/main/java/com/aurora/store/data/helper/DownloadHelper.kt b/app/src/main/java/com/aurora/store/data/helper/DownloadHelper.kt index 608ed0ac0..e619289f6 100644 --- a/app/src/main/java/com/aurora/store/data/helper/DownloadHelper.kt +++ b/app/src/main/java/com/aurora/store/data/helper/DownloadHelper.kt @@ -12,6 +12,7 @@ import com.aurora.store.AuroraApp import com.aurora.store.data.model.DownloadStatus import com.aurora.store.data.room.download.Download import com.aurora.store.data.room.download.DownloadDao +import com.aurora.store.data.room.suite.ExternalApk import com.aurora.store.data.room.update.Update import com.aurora.store.data.work.DownloadWorker import com.aurora.store.util.PathUtil @@ -90,6 +91,14 @@ class DownloadHelper @Inject constructor( downloadDao.insert(Download.fromUpdate(update)) } + /** + * Enqueues ExternalApk for download & install + * @param externalApk [ExternalApk] to download + */ + suspend fun enqueueStandalone(externalApk: ExternalApk) { + downloadDao.insert(Download.fromExternalApk(externalApk)) + } + /** * Cancels the download for the given package * @param packageName Name of the package to cancel download diff --git a/app/src/main/java/com/aurora/store/data/installer/SessionInstaller.kt b/app/src/main/java/com/aurora/store/data/installer/SessionInstaller.kt index 786871b96..53dc2dc65 100644 --- a/app/src/main/java/com/aurora/store/data/installer/SessionInstaller.kt +++ b/app/src/main/java/com/aurora/store/data/installer/SessionInstaller.kt @@ -25,6 +25,7 @@ import android.content.Context import android.content.Intent import android.content.pm.PackageInfo import android.content.pm.PackageInstaller +import android.content.pm.PackageInstaller.EXTRA_SESSION_ID import android.content.pm.PackageInstaller.PACKAGE_SOURCE_STORE import android.content.pm.PackageInstaller.SessionParams import android.content.pm.PackageManager @@ -240,24 +241,31 @@ class SessionInstaller @Inject constructor( } private fun commitInstall(sessionInfo: SessionInfo) { - Log.i(TAG, "Starting install session for ${sessionInfo.packageName}") - - val sessionId = sessionInfo.sessionId - packageInstaller.getSessionInfo(sessionId) ?: run { - Log.e(TAG, "Session $sessionId is no longer valid, skipping commit.") - removeFromInstallQueue(sessionInfo.packageName) - return - } - try { - val session = packageInstaller.openSession(sessionId) + Log.i(TAG, "Starting install session for ${sessionInfo.packageName}") + + val existingSessionInfo = packageInstaller.getSessionInfo(sessionInfo.sessionId) + if (existingSessionInfo == null) { + Log.e(TAG, "Session ${sessionInfo.sessionId} is no longer valid.") + return removeFromInstallQueue(sessionInfo.packageName) + } + + commitSession(sessionInfo) + } catch (e: Exception) { + Log.e(TAG, "Error committing session: ${e.message}") + removeFromInstallQueue(sessionInfo.packageName) + postError(sessionInfo.packageName, e.localizedMessage, e.stackTraceToString()) + } + } + + private fun commitSession(sessionInfo: SessionInfo) { + try { + val session = packageInstaller.openSession(sessionInfo.sessionId) session.commit(getCallBackIntent(sessionInfo)!!.intentSender) session.close() - } catch (e: SecurityException) { - Log.e(TAG, "Failed to commit session $sessionId: ${e.message}") - removeFromInstallQueue(sessionInfo.packageName) } catch (e: Exception) { - Log.e(TAG, "Unexpected error in commitInstall for session $sessionId", e) + Log.e(TAG, "Error committing session: ${e.message}") + } finally { removeFromInstallQueue(sessionInfo.packageName) } } @@ -266,6 +274,7 @@ class SessionInstaller @Inject constructor( val callBackIntent = Intent(context, InstallerStatusReceiver::class.java).apply { action = ACTION_INSTALL_STATUS setPackage(context.packageName) + putExtra(EXTRA_SESSION_ID, sessionInfo.sessionId) putExtra(EXTRA_PACKAGE_NAME, sessionInfo.packageName) putExtra(EXTRA_VERSION_CODE, sessionInfo.versionCode) putExtra(EXTRA_DISPLAY_NAME, sessionInfo.displayName) @@ -280,4 +289,22 @@ class SessionInstaller @Inject constructor( true ) } + + enum class ServiceResultCode(val code: Int, val reason: String) { + SUCCESS(0, "Request successful"), + SERVICE_VERSION_UPDATE_REQUIRED(2, "Interface depends on a higher version"), + SERVICE_INVALID(4, "Service is invalid"), + METHOD_UNSUPPORTED(5, "Interface is not supported"), + RESOLUTION_REQUIRED(6, "Needs to be resolved by opening PendingIntent"), + NETWORK_ERROR(7, "Network exception, unable to complete interface request"), + INTERNAL_ERROR(8, "Internal code error, incorrect parameter transmission in scenario"), + TIMEOUT(10, "Interface access timeout return"), + DEAD_CLIENT(12, "Current client is unavailable"), + RESPONSE_ERROR(13, "Server returns abnormal response"), + PROTOCOL_ERROR(15, "Not signed Huawei App Market agreement"); + + companion object { + fun fromCode(code: Int): ServiceResultCode? = entries.find { it.code == code } + } + } } diff --git a/app/src/main/java/com/aurora/store/data/installer/ShizukuInstaller.kt b/app/src/main/java/com/aurora/store/data/installer/ShizukuInstaller.kt index 71e4f53d5..c59effcb1 100644 --- a/app/src/main/java/com/aurora/store/data/installer/ShizukuInstaller.kt +++ b/app/src/main/java/com/aurora/store/data/installer/ShizukuInstaller.kt @@ -133,7 +133,10 @@ class ShizukuInstaller @Inject constructor( sharedLibPkgName: String = "", displayName: String = "" ) { - Log.i(TAG, "Received session install request for ${sharedLibPkgName.ifBlank { packageName }}") + Log.i( + TAG, + "Received session install request for ${sharedLibPkgName.ifBlank { packageName }}" + ) val (sessionId, session) = kotlin.runCatching { val params = SessionParams(SessionParams.MODE_FULL_INSTALL) @@ -168,7 +171,11 @@ class ShizukuInstaller @Inject constructor( Log.i(TAG, "Writing splits to session for ${sharedLibPkgName.ifBlank { packageName }}") getFiles(packageName, versionCode, sharedLibPkgName).forEach { it.inputStream().use { input -> - session.openWrite("${sharedLibPkgName.ifBlank { packageName }}_${System.currentTimeMillis()}", 0, -1).use { output -> + session.openWrite( + "${sharedLibPkgName.ifBlank { packageName }}_${System.currentTimeMillis()}", + 0, + -1 + ).use { output -> input.copyTo(output) session.fsync(output) } diff --git a/app/src/main/java/com/aurora/store/data/providers/AuthProvider.kt b/app/src/main/java/com/aurora/store/data/providers/AuthProvider.kt index 532f83e33..7c9d143e6 100644 --- a/app/src/main/java/com/aurora/store/data/providers/AuthProvider.kt +++ b/app/src/main/java/com/aurora/store/data/providers/AuthProvider.kt @@ -61,7 +61,7 @@ class AuthProvider @Inject constructor( return if (rawAuth.isNotBlank()) { json.decodeFromString(rawAuth) } else { - null + AuthData("BOGUS") } } diff --git a/app/src/main/java/com/aurora/store/data/providers/BlacklistProvider.kt b/app/src/main/java/com/aurora/store/data/providers/BlacklistProvider.kt index 92b38169c..d483d02f6 100644 --- a/app/src/main/java/com/aurora/store/data/providers/BlacklistProvider.kt +++ b/app/src/main/java/com/aurora/store/data/providers/BlacklistProvider.kt @@ -21,7 +21,6 @@ package com.aurora.store.data.providers import android.content.Context import android.content.SharedPreferences -import androidx.preference.PreferenceManager import com.aurora.extensions.isNAndAbove import com.aurora.store.util.Preferences import dagger.hilt.android.qualifiers.ApplicationContext @@ -39,7 +38,11 @@ class BlacklistProvider @Inject constructor( private val PREFERENCE_BLACKLIST = "PREFERENCE_BLACKLIST" var blacklist: MutableSet - set(value) = Preferences.putString(context, PREFERENCE_BLACKLIST, json.encodeToString(value)) + set(value) = Preferences.putString( + context, + PREFERENCE_BLACKLIST, + json.encodeToString(value) + ) get() { return try { val rawBlacklist = if (isNAndAbove) { @@ -54,7 +57,7 @@ class BlacklistProvider @Inject constructor( Context.MODE_PRIVATE ) as SharedPreferences - PreferenceManager.getDefaultSharedPreferences(context) + Preferences.getPrefs(context) .getString( PREFERENCE_BLACKLIST, refSharedPreferences.getString(PREFERENCE_BLACKLIST, "") diff --git a/app/src/main/java/com/aurora/store/data/receiver/InstallerStatusReceiver.kt b/app/src/main/java/com/aurora/store/data/receiver/BaseInstallerStatusReceiver.kt similarity index 71% rename from app/src/main/java/com/aurora/store/data/receiver/InstallerStatusReceiver.kt rename to app/src/main/java/com/aurora/store/data/receiver/BaseInstallerStatusReceiver.kt index 8e0c1ced1..2f5551fc0 100644 --- a/app/src/main/java/com/aurora/store/data/receiver/InstallerStatusReceiver.kt +++ b/app/src/main/java/com/aurora/store/data/receiver/BaseInstallerStatusReceiver.kt @@ -23,6 +23,7 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.pm.PackageInstaller +import android.content.pm.PackageInstaller.EXTRA_SESSION_ID import android.util.Log import androidx.core.content.IntentCompat import androidx.core.content.getSystemService @@ -35,28 +36,31 @@ import com.aurora.store.data.installer.AppInstaller.Companion.EXTRA_DISPLAY_NAME import com.aurora.store.data.installer.AppInstaller.Companion.EXTRA_PACKAGE_NAME import com.aurora.store.data.installer.AppInstaller.Companion.EXTRA_VERSION_CODE import com.aurora.store.data.installer.base.InstallerBase -import com.aurora.store.util.CommonUtil.inForeground import com.aurora.store.util.NotificationUtil import com.aurora.store.util.PackageUtil import com.aurora.store.util.PathUtil import com.aurora.store.util.Preferences import com.aurora.store.util.Preferences.PREFERENCE_AUTO_DELETE -import dagger.hilt.android.AndroidEntryPoint -@AndroidEntryPoint -class InstallerStatusReceiver : BroadcastReceiver() { +abstract class BaseInstallerStatusReceiver : BroadcastReceiver() { - private val TAG = InstallerStatusReceiver::class.java.simpleName + private val TAG = BaseInstallerStatusReceiver::class.java.simpleName override fun onReceive(context: Context?, intent: Intent?) { if (context != null && intent?.action == ACTION_INSTALL_STATUS) { - val packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME)!! - val displayName = intent.getStringExtra(EXTRA_DISPLAY_NAME)!! - val versionCode = intent.getLongExtra(EXTRA_VERSION_CODE, -1) + val packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME) ?: return + val displayName = intent.getStringExtra(EXTRA_DISPLAY_NAME) ?: packageName + val versionCode = intent.getLongExtra(EXTRA_VERSION_CODE, -1) + val sessionId = intent.getIntExtra(EXTRA_SESSION_ID, -1) val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1) val extra = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) + Log.i( + TAG, + "$packageName ($versionCode) sessionId=$sessionId, status=$status, extra=$extra" + ) + // If package was successfully installed, exit after notifying user and doing cleanup if (status == PackageInstaller.STATUS_SUCCESS) { // No post-install steps for shared libraries @@ -64,19 +68,22 @@ class InstallerStatusReceiver : BroadcastReceiver() { AuroraApp.enqueuedInstalls.remove(packageName) InstallerBase.notifyInstallation(context, displayName, packageName) + if (Preferences.getBoolean(context, PREFERENCE_AUTO_DELETE)) { PathUtil.getAppDownloadDir(context, packageName, versionCode) .deleteRecursively() } - return + + return postStatus(status, packageName, extra, context) } - if (inForeground() && status == PackageInstaller.STATUS_PENDING_USER_ACTION) { - promptUser(intent, context) + if (status == PackageInstaller.STATUS_PENDING_USER_ACTION) { + doAppropriatePrompt(context, intent, sessionId) } else { AuroraApp.enqueuedInstalls.remove(packageName) - postStatus(status, packageName, extra, context) notifyUser(context, packageName, displayName, status) + + postStatus(status, packageName, extra, context) } } } @@ -94,24 +101,38 @@ class InstallerStatusReceiver : BroadcastReceiver() { displayName, InstallerBase.getErrorString(context, status) ) - notificationManager!!.notify(packageName.hashCode(), notification) + notificationManager?.notify(packageName.hashCode(), notification) } - private fun promptUser(intent: Intent, context: Context) { - IntentCompat.getParcelableExtra(intent, Intent.EXTRA_INTENT, Intent::class.java)?.let { - it.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true) - it.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME, context.packageName) - it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + internal fun promptUser(context: Context, intent: Intent) { + runOnUiThread { + val launchIntent = IntentCompat.getParcelableExtra( + intent, + Intent.EXTRA_INTENT, + Intent::class.java + ) - try { - runOnUiThread { context.startActivity(it) } - } catch (exception: Exception) { - Log.e(TAG, "Failed to trigger installation!", exception) + if (launchIntent != null) { + launchIntent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true) + launchIntent.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME, context.packageName) + launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + try { + context.startActivity(launchIntent) + } catch (exception: Exception) { + Log.e(TAG, "Failed to launch intent!", exception) + } + } else { + Log.w(TAG, "No launch intent found in the installation request.") } } } - private fun postStatus(status: Int, packageName: String?, extra: String?, context: Context) { + open fun doAppropriatePrompt(context: Context, intent: Intent, sessionId: Int) { + promptUser(context, intent) + } + + open fun postStatus(status: Int, packageName: String?, extra: String?, context: Context) { val event = when (status) { PackageInstaller.STATUS_SUCCESS -> { InstallerEvent.Installed(packageName!!).apply { @@ -132,6 +153,7 @@ class InstallerStatusReceiver : BroadcastReceiver() { } } } + AuroraApp.events.send(event) } } diff --git a/app/src/main/java/com/aurora/store/data/room/download/Download.kt b/app/src/main/java/com/aurora/store/data/room/download/Download.kt index dadfc6202..318ac5f3b 100644 --- a/app/src/main/java/com/aurora/store/data/room/download/Download.kt +++ b/app/src/main/java/com/aurora/store/data/room/download/Download.kt @@ -6,6 +6,7 @@ import androidx.room.PrimaryKey import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.PlayFile import com.aurora.store.data.model.DownloadStatus +import com.aurora.store.data.room.suite.ExternalApk import com.aurora.store.data.room.update.Update import kotlinx.parcelize.Parcelize import java.util.Date @@ -81,5 +82,26 @@ data class Download( Date().time ) } + + fun fromExternalApk(externalApk: ExternalApk): Download { + return Download( + packageName = externalApk.packageName, + versionCode = externalApk.versionCode, + offerType = 0, + isInstalled = false, + displayName = externalApk.displayName, + iconURL = externalApk.iconURL, + size = 0, + id = 0, + downloadStatus = DownloadStatus.QUEUED, + progress = 0, + speed = 0L, + timeRemaining = 0L, + totalFiles = 1, + downloadedFiles = 0, + fileList = externalApk.fileList, + sharedLibs = emptyList(), + ) + } } } diff --git a/app/src/main/java/com/aurora/store/data/room/suite/ExternalApk.kt b/app/src/main/java/com/aurora/store/data/room/suite/ExternalApk.kt new file mode 100644 index 000000000..cda9af7c5 --- /dev/null +++ b/app/src/main/java/com/aurora/store/data/room/suite/ExternalApk.kt @@ -0,0 +1,27 @@ +package com.aurora.store.data.room.suite + +import android.content.Context +import android.os.Parcelable +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.aurora.gplayapi.data.models.PlayFile +import com.aurora.store.util.PackageUtil +import kotlinx.parcelize.Parcelize + +@Parcelize +@Entity(tableName = "externalApk") +data class ExternalApk( + @PrimaryKey + val packageName: String, + val versionCode: Long, + val versionName: String, + val displayName: String, + val iconURL: String, + val developerName: String, + var fileList: List +) : Parcelable { + + fun isInstalled(context: Context): Boolean { + return PackageUtil.isInstalled(context, packageName) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aurora/store/data/work/AuthWorker.kt b/app/src/main/java/com/aurora/store/data/work/AuthWorker.kt index 22e0fb38f..516e28fc0 100644 --- a/app/src/main/java/com/aurora/store/data/work/AuthWorker.kt +++ b/app/src/main/java/com/aurora/store/data/work/AuthWorker.kt @@ -13,6 +13,7 @@ import androidx.core.os.bundleOf import androidx.hilt.work.HiltWorker import androidx.work.CoroutineWorker import androidx.work.WorkerParameters +import com.aurora.Constants.PACKAGE_NAME_PLAY_STORE import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.helpers.AuthHelper import com.aurora.store.data.model.AccountType @@ -21,7 +22,6 @@ import com.aurora.store.data.providers.AuthProvider import com.aurora.store.util.CertUtil.GOOGLE_ACCOUNT_TYPE import com.aurora.store.util.CertUtil.GOOGLE_PLAY_AUTH_TOKEN_TYPE import com.aurora.store.util.CertUtil.GOOGLE_PLAY_CERT -import com.aurora.store.util.CertUtil.GOOGLE_PLAY_PACKAGE_NAME import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlin.coroutines.resume @@ -140,7 +140,7 @@ open class AuthWorker @AssistedInject constructor( Account(email, GOOGLE_ACCOUNT_TYPE), GOOGLE_PLAY_AUTH_TOKEN_TYPE, bundleOf( - "overridePackage" to GOOGLE_PLAY_PACKAGE_NAME, + "overridePackage" to PACKAGE_NAME_PLAY_STORE, "overrideCertificate" to Base64.decode( GOOGLE_PLAY_CERT, Base64.DEFAULT diff --git a/app/src/main/java/com/aurora/store/util/CertUtil.kt b/app/src/main/java/com/aurora/store/util/CertUtil.kt index f7709d0c2..41b7da45f 100644 --- a/app/src/main/java/com/aurora/store/util/CertUtil.kt +++ b/app/src/main/java/com/aurora/store/util/CertUtil.kt @@ -24,6 +24,8 @@ import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.util.Base64 import android.util.Log +import com.aurora.Constants.PACKAGE_NAME_APP_GALLERY +import com.aurora.Constants.PACKAGE_NAME_GMS import com.aurora.extensions.generateX509Certificate import com.aurora.extensions.getUpdateOwnerPackageNameCompat import com.aurora.extensions.isPAndAbove @@ -40,7 +42,6 @@ object CertUtil { const val GOOGLE_ACCOUNT_TYPE = "com.google" const val GOOGLE_PLAY_AUTH_TOKEN_TYPE = "oauth2:https://www.googleapis.com/auth/googleplay" - const val GOOGLE_PLAY_PACKAGE_NAME = "com.android.vending" const val GOOGLE_PLAY_CERT = "MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK" @@ -49,7 +50,7 @@ object CertUtil { } fun isAppGalleryApp(context: Context, packageName: String): Boolean { - return context.packageManager.getUpdateOwnerPackageNameCompat(packageName) == "com.huawei.appmarket" + return context.packageManager.getUpdateOwnerPackageNameCompat(packageName) == PACKAGE_NAME_APP_GALLERY } fun isAuroraStoreApp(context: Context, packageName: String): Boolean { @@ -91,16 +92,17 @@ object CertUtil { } } - fun isMicroGGMS(context: Context, packageName: String): Boolean { + fun isMicroGGms(context: Context): Boolean { return try { - val packageInfo = getPackageInfo(context, packageName, PackageManager.GET_PERMISSIONS) + val packageInfo = + getPackageInfo(context, PACKAGE_NAME_GMS, PackageManager.GET_PERMISSIONS) val hasFakePackageSignature = packageInfo.requestedPermissions?.any { permission -> permission == "android.permission.FAKE_PACKAGE_SIGNATURE" } == true return hasFakePackageSignature } catch (exception: Exception) { - Log.e(TAG, "Failed to check origin for $packageName") + Log.e(TAG, "Failed to check origin for $PACKAGE_NAME_GMS") false } } diff --git a/app/src/main/java/com/aurora/store/util/PackageUtil.kt b/app/src/main/java/com/aurora/store/util/PackageUtil.kt index 894555cdc..0eeda6428 100644 --- a/app/src/main/java/com/aurora/store/util/PackageUtil.kt +++ b/app/src/main/java/com/aurora/store/util/PackageUtil.kt @@ -36,6 +36,9 @@ import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.pm.PackageInfoCompat import androidx.core.graphics.drawable.toBitmap import androidx.core.net.toUri +import com.aurora.Constants.PACKAGE_NAME_APP_GALLERY +import com.aurora.Constants.PACKAGE_NAME_GMS +import com.aurora.Constants.PACKAGE_NAME_PLAY_STORE import com.aurora.extensions.isHuawei import com.aurora.extensions.isOAndAbove import com.aurora.extensions.isPAndAbove @@ -50,7 +53,6 @@ object PackageUtil { private const val TAG = "PackageUtil" - const val PACKAGE_NAME_GMS = "com.google.android.gms" private const val VERSION_CODE_MICRO_G: Long = 240913402 private const val VERSION_CODE_MICRO_G_HUAWEI: Long = 240913007 @@ -63,8 +65,39 @@ object PackageUtil { } } - fun hasSupportedMicroG(context: Context): Boolean { - val isMicroG = CertUtil.isMicroGGMS(context, PACKAGE_NAME_GMS) + fun hasSupportedAppGallery(context: Context): Boolean { + return try { + val result = context.packageManager.checkPermission( + android.Manifest.permission.INSTALL_PACKAGES, + PACKAGE_NAME_APP_GALLERY + ) + + if (result != PackageManager.PERMISSION_GRANTED) { + Log.w(TAG, "AppGallery does not have INSTALL_PACKAGES permission") + return false + } + + val packageInfo = context.packageManager.getPackageInfo( + PACKAGE_NAME_APP_GALLERY, + PackageManager.GET_META_DATA + ) + + @Suppress("DEPRECATION") + val versionCode = if (Build.VERSION.SDK_INT >= 28) + packageInfo.longVersionCode + else + packageInfo.versionCode.toLong() + + Log.i(TAG, "AppGallery - ${packageInfo.versionName} ($versionCode)") + + versionCode >= 15010000L + } catch (_: Exception) { + false + } + } + + fun hasSupportedMicroGVariant(context: Context): Boolean { + val isMicroG = CertUtil.isMicroGGms(context) // Do not proceed if MicroG variant is not installed if (!isMicroG) return false @@ -76,20 +109,15 @@ object PackageUtil { } } - fun isInstalled(context: Context, packageName: String): Boolean { + fun isInstalled(context: Context, packageName: String, versionCode: Long? = null): Boolean { return try { - getPackageInfo(context, packageName, PackageManager.GET_META_DATA) - true - } catch (e: PackageManager.NameNotFoundException) { - false - } - } - - fun isInstalled(context: Context, packageName: String, versionCode: Long): Boolean { - return try { - val packageInfo = getPackageInfo(context, packageName) - return PackageInfoCompat.getLongVersionCode(packageInfo) >= versionCode.toLong() - } catch (e: PackageManager.NameNotFoundException) { + val packageInfo = getPackageInfo(context, packageName, PackageManager.GET_META_DATA) + if (versionCode != null) { + PackageInfoCompat.getLongVersionCode(packageInfo) >= versionCode + } else { + true + } + } catch (_: PackageManager.NameNotFoundException) { false } } @@ -110,7 +138,11 @@ object PackageUtil { } } - fun isSharedLibraryInstalled(context: Context, packageName: String, versionCode: Long): Boolean { + fun isSharedLibraryInstalled( + context: Context, + packageName: String, + versionCode: Long + ): Boolean { return if (isOAndAbove) { val sharedLibraries = getAllSharedLibraries(context) if (isPAndAbove) { @@ -137,6 +169,16 @@ object PackageUtil { } } + fun isMicroGBundleInstalled(context: Context): Boolean { + /** + * Confirm if MicroG bundle is installed + * Considering the following: + * 1. GmsCore is installed and it is a microG huawei variant + * 2. Play Store is installed - (microG Companion) + */ + return hasSupportedMicroGVariant(context) && isInstalled(context, PACKAGE_NAME_PLAY_STORE) + } + fun getInstalledVersionName(context: Context, packageName: String): String { return try { getPackageInfo(context, packageName).versionName ?: "" diff --git a/app/src/main/java/com/aurora/store/view/epoxy/views/EpoxyTextView.kt b/app/src/main/java/com/aurora/store/view/epoxy/views/EpoxyTextView.kt new file mode 100644 index 000000000..a6b6a509b --- /dev/null +++ b/app/src/main/java/com/aurora/store/view/epoxy/views/EpoxyTextView.kt @@ -0,0 +1,56 @@ +/* + * Aurora Store + * Copyright (C) 2021, Rahul Kumar Patel + * + * Aurora Store is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * Aurora Store is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Aurora Store. If not, see . + * + */ + +package com.aurora.store.view.epoxy.views + +import android.content.Context +import android.util.AttributeSet +import android.util.TypedValue +import com.airbnb.epoxy.ModelProp +import com.airbnb.epoxy.ModelView +import com.aurora.store.R +import com.aurora.store.databinding.ViewTextBinding + +@ModelView( + autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT, + baseModelClass = BaseModel::class +) +class EpoxyTextView @JvmOverloads constructor( + context: Context?, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : BaseView(context, attrs, defStyleAttr) { + + @ModelProp + fun title(title: String) { + binding.txtView.text = title + } + + @ModelProp + @JvmOverloads + fun size(int: Int = 16) { + binding.txtView.setTextSize(TypedValue.COMPLEX_UNIT_SP, int.toFloat()) + } + + @ModelProp + @JvmOverloads + fun style(resId: Int = R.style.AuroraTextStyle) { + binding.txtView.setTextAppearance(context, resId) + } +} diff --git a/app/src/main/java/com/aurora/store/view/ui/commons/BaseFragment.kt b/app/src/main/java/com/aurora/store/view/ui/commons/BaseFragment.kt index 08bb684d1..86b52250f 100644 --- a/app/src/main/java/com/aurora/store/view/ui/commons/BaseFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/commons/BaseFragment.kt @@ -34,6 +34,7 @@ import com.aurora.gplayapi.data.models.StreamCluster import com.aurora.store.MobileNavigationDirections import com.aurora.store.data.model.MinimalApp import com.aurora.store.data.providers.PermissionProvider +import com.aurora.store.view.ui.details.AppDetailsFragmentDirections import java.lang.reflect.ParameterizedType abstract class BaseFragment : Fragment() { @@ -131,6 +132,12 @@ abstract class BaseFragment : Fragment() { findNavController().navigate(MobileNavigationDirections.actionGlobalAppMenuSheet(app)) } + fun openGMSWarningFragment() { + findNavController().navigate( + AppDetailsFragmentDirections.actionAppDetailsFragmentToGmsWarnFragment() + ) + } + private fun cleanupRecyclerViews(recyclerViews: List) { recyclerViews.forEach { recyclerView -> runCatching { diff --git a/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsFragment.kt b/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsFragment.kt index 70e87e467..ba5ebaff4 100644 --- a/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsFragment.kt @@ -31,6 +31,7 @@ import android.view.View import android.view.animation.AccelerateDecelerateInterpolator import android.widget.RelativeLayout import android.widget.Toast +import androidx.compose.ui.util.fastAny import androidx.core.content.ContextCompat import androidx.core.text.HtmlCompat import androidx.core.view.isVisible @@ -46,8 +47,10 @@ import coil3.transform.CircleCropTransformation import coil3.transform.RoundedCornersTransformation import com.aurora.Constants import com.aurora.Constants.EXODUS_SUBMIT_PAGE +import com.aurora.Constants.PACKAGE_NAME_GMS import com.aurora.extensions.browse import com.aurora.extensions.hide +import com.aurora.extensions.isHuawei import com.aurora.extensions.px import com.aurora.extensions.requiresObbDir import com.aurora.extensions.runOnUiThread @@ -397,6 +400,7 @@ class AppDetailsFragment : BaseFragment() { viewLifecycleOwner.lifecycleScope.launch { AuroraApp.events.installerEvent.collect { onEvent(it) } } + } override fun onResume() { @@ -525,6 +529,24 @@ class AppDetailsFragment : BaseFragment() { } private fun purchase(app: App) { + /** + * MicroG Fragment Preconditions: + * 1. App being installed must have GSF dependency + * 2. It should be a Huawei device + * 3. Supported App Gallery should be available, i.e. v15.1.x or above + * 4. MicroG bundle should not be already installed + * + * TODO: Extract this trigger out of Vanilla & put in Huawei flavour + */ + if ( + app.dependencies.dependentPackages.fastAny { it == PACKAGE_NAME_GMS } && + isHuawei && + PackageUtil.hasSupportedAppGallery(requireContext()) && + !PackageUtil.isMicroGBundleInstalled(requireContext()) + ) { + return openGMSWarningFragment() + } + if (app.fileList.requiresObbDir()) { if (permissionProvider.isGranted(PermissionType.STORAGE_MANAGER)) { viewModel.download(app) @@ -1036,7 +1058,7 @@ class AppDetailsFragment : BaseFragment() { } private fun updateCompatibilityInfo() { - if (app.dependencies.dependentPackages.contains(PackageUtil.PACKAGE_NAME_GMS)) { + if (app.dependencies.dependentPackages.contains(PACKAGE_NAME_GMS)) { viewModel.fetchPlexusReport(app.packageName) binding.layoutDetailsCompatibility.txtGmsDependency.apply { diff --git a/app/src/main/java/com/aurora/store/view/ui/details/DetailsMicroGFragment.kt b/app/src/main/java/com/aurora/store/view/ui/details/DetailsMicroGFragment.kt new file mode 100644 index 000000000..e9c0aace3 --- /dev/null +++ b/app/src/main/java/com/aurora/store/view/ui/details/DetailsMicroGFragment.kt @@ -0,0 +1,203 @@ +/* + * Aurora Store + * Copyright (C) 2021, Rahul Kumar Patel + * + * Aurora Store is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * Aurora Store is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Aurora Store. If not, see . + * + */ + +package com.aurora.store.view.ui.details + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import com.aurora.extensions.browse +import com.aurora.store.AuroraApp +import com.aurora.store.R +import com.aurora.store.data.event.Event +import com.aurora.store.data.event.InstallerEvent +import com.aurora.store.data.model.Dash +import com.aurora.store.data.model.DownloadStatus +import com.aurora.store.databinding.FragmentDetailsMicrogBinding +import com.aurora.store.util.PackageUtil +import com.aurora.store.view.epoxy.views.EpoxyTextViewModel_ +import com.aurora.store.view.epoxy.views.preference.DashViewModel_ +import com.aurora.store.view.ui.commons.BaseFragment +import com.aurora.store.viewmodel.onboarding.MicroGViewModel +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class DetailsMicroGFragment : BaseFragment() { + + val microGViewModel: MicroGViewModel by viewModels() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + // Toolbar + binding.toolbar.apply { + title = "" + setNavigationOnClickListener { findNavController().navigateUp() } + } + + with(binding) { + // RecyclerView + epoxyRecycler.withModels { + setFilterDuplicates(true) + + add( + EpoxyTextViewModel_() + .id("microg_title") + .title(getString(R.string.onboarding_title_gsf)) + .size(32) + .style(R.style.AuroraTextStyle) + ) + + add( + EpoxyTextViewModel_() + .id("microg_desc") + .title(getString(R.string.onboarding_title_gsf_desc)) + .size(18) + .style(R.style.AuroraTextStyle) + ) + + add( + EpoxyTextViewModel_() + .id("microg_desc") + .title(getString(R.string.onboarding_gms_missing)) + .size(14) + .style(R.style.AuroraTextStyle) + ) + + add( + EpoxyTextViewModel_() + .id("microg_gms") + .title(getString(R.string.onboarding_gms_microg)) + .size(14) + .style(R.style.AuroraTextStyle) + ) + + + dashItems().forEach { + add( + DashViewModel_() + .id(it.id) + .dash(it) + .click { _ -> + requireContext().browse(it.url) + } + ) + } + } + + checkboxAgreement.setOnCheckedChangeListener { _, value -> + microGViewModel.markAgreement(value) + btnMicroG.isEnabled = value + } + + btnMicroG.setOnClickListener { microGViewModel.downloadMicroG() } + btnSkip.setOnClickListener { findNavController().navigateUp() } + } + + microGViewModel.download.filterNotNull().onEach { + when (it.downloadStatus) { + DownloadStatus.DOWNLOADING -> updateProgressBar(visible = true, it.progress) + DownloadStatus.FAILED -> updateProgressBar(visible = false, 0) + DownloadStatus.QUEUED -> updateProgressBar(visible = true, -1) + DownloadStatus.COMPLETED -> updateProgressBar(visible = true, -1) + else -> {} + } + }.launchIn(viewLifecycleOwner.lifecycleScope) + + viewLifecycleOwner.lifecycleScope.launch { + AuroraApp.events.installerEvent.collect { onEvent(it) } + } + } + + private fun onEvent(event: Event) { + when (event) { + is InstallerEvent.Installed -> { + if (PackageUtil.isMicroGBundleInstalled(requireContext())) { + markInstallationComplete() + } + } + + is InstallerEvent.Failed -> markInstallationFailed() + else -> {} + } + } + + private fun updateProgressBar(visible: Boolean, downloadProgress: Int) { + with(binding.progressBar) { + if (visible) show() else hide() + isIndeterminate = downloadProgress == -1 + progress = downloadProgress + } + } + + private fun markInstallationComplete() { + with(binding) { + with(btnMicroG) { + isEnabled = true + text = getString(R.string.action_finish) + setOnClickListener { findNavController().navigateUp() } + } + checkboxAgreement.isEnabled = false + progressBar.hide() + } + } + + private fun markInstallationFailed() { + with(binding) { + with(btnMicroG) { + isEnabled = false + text = getString(R.string.action_install) + } + checkboxAgreement.isChecked = false + progressBar.hide() + } + } + + private fun dashItems(): List { + return listOf( + Dash( + id = 2, + title = requireContext().getString(R.string.details_dev_website), + subtitle = requireContext().getString(R.string.microg_website), + icon = R.drawable.ic_network, + url = "https://microG.org" + ), + Dash( + id = 4, + title = requireContext().getString(R.string.privacy_policy_title), + subtitle = requireContext().getString(R.string.microg_privacy_policy), + icon = R.drawable.ic_privacy, + url = "https://microg.org/privacy.html" + ), + Dash( + id = 5, + title = requireContext().getString(R.string.menu_disclaimer), + subtitle = requireContext().getString(R.string.microg_license_agreement), + icon = R.drawable.ic_disclaimer, + url = "https://raw.githubusercontent.com/microg/GmsCore/refs/heads/master/LICENSE" + ) + ) + } +} diff --git a/app/src/main/java/com/aurora/store/view/ui/onboarding/BaseFlavouredOnboardingFragment.kt b/app/src/main/java/com/aurora/store/view/ui/onboarding/BaseFlavouredOnboardingFragment.kt index 00a7b6f4a..565a2623f 100644 --- a/app/src/main/java/com/aurora/store/view/ui/onboarding/BaseFlavouredOnboardingFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/onboarding/BaseFlavouredOnboardingFragment.kt @@ -4,17 +4,22 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.annotation.StringRes import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager -import androidx.fragment.app.viewModels +import androidx.fragment.app.activityViewModels import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback import com.aurora.extensions.areNotificationsEnabled import com.aurora.extensions.isIgnoringBatteryOptimizations +import com.aurora.store.AuroraApp import com.aurora.store.R +import com.aurora.store.data.event.Event +import com.aurora.store.data.event.InstallerEvent import com.aurora.store.data.model.UpdateMode import com.aurora.store.data.work.CacheWorker import com.aurora.store.databinding.FragmentOnboardingBinding @@ -25,15 +30,20 @@ import com.aurora.store.util.Preferences.PREFERENCE_INTRO import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_AUTO import com.aurora.store.util.save import com.aurora.store.view.ui.commons.BaseFragment +import com.aurora.store.viewmodel.onboarding.MicroGViewModel +import com.aurora.store.viewmodel.onboarding.OnboardingPage import com.aurora.store.viewmodel.onboarding.OnboardingViewModel import com.google.android.material.tabs.TabLayoutMediator import com.jakewharton.processphoenix.ProcessPhoenix +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch abstract class BaseFlavouredOnboardingFragment : BaseFragment() { + // Shared ViewModels + val microGViewModel: MicroGViewModel by activityViewModels() + val onboardingViewModel: OnboardingViewModel by activityViewModels() - val viewModel: OnboardingViewModel by viewModels() - - var lastPosition = 0 + var currentPage = 0 override fun onCreateView( inflater: LayoutInflater, @@ -64,52 +74,141 @@ abstract class BaseFlavouredOnboardingFragment : BaseFragment OnboardingPage.WELCOME + 1 -> OnboardingPage.PERMISSIONS + 2 -> OnboardingPage.GSF + else -> OnboardingPage.WELCOME + } + ) + currentPage = position + } + }) + } + + TabLayoutMediator(tabLayout, viewpager2, true) { tab, position -> + tab.text = (position + 1).toString() + }.attach() + } + + updateBackwardButton(false) + updateForwardButton(true) + + viewLifecycleOwner.lifecycleScope.launch { + // Combine both relevant flows + combine( + microGViewModel.checked, + onboardingViewModel.currentPage + ) { isChecked, page -> isChecked to page }.collect { (isChecked, page) -> + when (page) { + OnboardingPage.WELCOME -> { + updateBackwardButton(enabled = false) + updateForwardButton(enabled = true) + } + + OnboardingPage.PERMISSIONS -> { + updateBackwardButton(enabled = true) + val isLastPage = pages.size == 2 + + updateForwardButton( + enabled = true, + resId = if (isLastPage) R.string.action_finish else R.string.action_next, + if (isLastPage) { + { finishOnboarding() } + } else { + null + } + ) + } + + OnboardingPage.GSF -> { + updateBackwardButton(enabled = true) + + if (isChecked) { + val isInstalled = PackageUtil.isMicroGBundleInstalled(requireContext()) + updateForwardButton( + enabled = isInstalled, + resId = R.string.action_finish, + action = if (isInstalled) { + { finishOnboarding() } + } else { + null + } + ) + } else { + updateForwardButton( + enabled = false, + resId = R.string.action_finish, + action = { finishOnboarding() } + ) + } } } - }) + } } - TabLayoutMediator(binding.tabLayout, binding.viewpager2, true) { tab, position -> - tab.text = (position + 1).toString() - }.attach() - - binding.btnForward.setOnClickListener { - binding.viewpager2.setCurrentItem(binding.viewpager2.currentItem + 1, true) - } - - binding.btnBackward.setOnClickListener { - binding.viewpager2.setCurrentItem(binding.viewpager2.currentItem - 1, true) + viewLifecycleOwner.lifecycleScope.launch { + AuroraApp.events.installerEvent.collect { onEvent(it) } } } - fun refreshButtonState() { - binding.btnBackward.isEnabled = lastPosition != 0 - binding.btnForward.isEnabled = lastPosition != 1 + private fun updateBackwardButton( + enabled: Boolean = true + ) { + with(binding.btnBackward) { + isEnabled = enabled + text = getString(R.string.action_back) + setOnClickListener({ + binding.viewpager2.setCurrentItem(binding.viewpager2.currentItem - 1, true) + }) + } + } + + private fun updateForwardButton( + enabled: Boolean = true, + @StringRes resId: Int = R.string.action_next, + action: ((View) -> Unit)? = null + ) { + with(binding.btnForward) { + isEnabled = enabled + text = getString(resId) + setOnClickListener(action ?: { + binding.viewpager2.setCurrentItem(binding.viewpager2.currentItem + 1, true) + }) + } + } + + private fun onEvent(event: Event) { + when (event) { + is InstallerEvent.Installed -> { + if (PackageUtil.isMicroGBundleInstalled(requireContext())) { + with(binding.btnForward) { + isEnabled = true + text = getString(R.string.action_finish) + setOnClickListener { + finishOnboarding() + } + } + } + } + + else -> { - if (lastPosition == 1) { - binding.btnForward.text = getString(R.string.action_finish) - binding.btnForward.isEnabled = true - binding.btnForward.setOnClickListener { finishOnboarding() } - } else { - binding.btnForward.text = getString(R.string.action_next) - binding.btnForward.setOnClickListener { - binding.viewpager2.setCurrentItem( - binding.viewpager2.currentItem + 1, true - ) } } } @@ -136,7 +235,7 @@ abstract class BaseFlavouredOnboardingFragment : BaseFragment(PREFERENCE_MICROG_AUTH)?.isEnabled = - PackageUtil.hasSupportedMicroG(requireContext()) + PackageUtil.hasSupportedMicroGVariant(requireContext()) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/app/src/main/java/com/aurora/store/view/ui/splash/BaseFlavouredSplashFragment.kt b/app/src/main/java/com/aurora/store/view/ui/splash/BaseFlavouredSplashFragment.kt index a8adb7ad0..92c807299 100644 --- a/app/src/main/java/com/aurora/store/view/ui/splash/BaseFlavouredSplashFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/splash/BaseFlavouredSplashFragment.kt @@ -16,6 +16,7 @@ import androidx.core.view.isVisible import androidx.fragment.app.activityViewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController +import com.aurora.Constants.PACKAGE_NAME_PLAY_STORE import com.aurora.extensions.getPackageName import com.aurora.extensions.isMAndAbove import com.aurora.extensions.navigate @@ -27,7 +28,6 @@ import com.aurora.store.databinding.FragmentSplashBinding import com.aurora.store.util.CertUtil.GOOGLE_ACCOUNT_TYPE import com.aurora.store.util.CertUtil.GOOGLE_PLAY_AUTH_TOKEN_TYPE import com.aurora.store.util.CertUtil.GOOGLE_PLAY_CERT -import com.aurora.store.util.CertUtil.GOOGLE_PLAY_PACKAGE_NAME import com.aurora.store.util.PackageUtil import com.aurora.store.util.Preferences import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT_SELECTED_TAB @@ -45,7 +45,7 @@ abstract class BaseFlavouredSplashFragment : BaseFragment val viewModel: AuthViewModel by activityViewModels() val canLoginWithMicroG: Boolean - get() = isMAndAbove && PackageUtil.hasSupportedMicroG(requireContext()) && + get() = isMAndAbove && PackageUtil.hasSupportedMicroGVariant(requireContext()) && Preferences.getBoolean(requireContext(), PREFERENCE_MICROG_AUTH, true) val startForAccount = @@ -215,7 +215,7 @@ abstract class BaseFlavouredSplashFragment : BaseFragment Account(accountName, GOOGLE_ACCOUNT_TYPE), GOOGLE_PLAY_AUTH_TOKEN_TYPE, bundleOf( - "overridePackage" to GOOGLE_PLAY_PACKAGE_NAME, + "overridePackage" to PACKAGE_NAME_PLAY_STORE, "overrideCertificate" to Base64.decode(GOOGLE_PLAY_CERT, Base64.DEFAULT) ), requireActivity(), diff --git a/app/src/main/java/com/aurora/store/viewmodel/onboarding/MicroGViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/onboarding/MicroGViewModel.kt new file mode 100644 index 000000000..5059925db --- /dev/null +++ b/app/src/main/java/com/aurora/store/viewmodel/onboarding/MicroGViewModel.kt @@ -0,0 +1,106 @@ +/* + * SPDX-FileCopyrightText: 2025 The Calyx Institute + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.aurora.store.viewmodel.onboarding + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.aurora.Constants.PACKAGE_NAME_GMS +import com.aurora.Constants.PACKAGE_NAME_PLAY_STORE +import com.aurora.gplayapi.data.models.PlayFile +import com.aurora.store.data.helper.DownloadHelper +import com.aurora.store.data.room.suite.ExternalApk +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class MicroGViewModel @Inject constructor( + @ApplicationContext private val context: Context, + private val downloadHelper: DownloadHelper +) : ViewModel() { + + private val _packageNames = MutableSharedFlow>() + val packageNames = _packageNames.asSharedFlow() + + private val _checked = MutableStateFlow(false) + val checked: StateFlow = _checked + + val download = combine(packageNames, downloadHelper.downloadsList) { apps, list -> + list.find { d -> apps.any { it == d.packageName } } + }.stateIn(viewModelScope, SharingStarted.Eagerly, null) + + val externalApks = listOf( + ExternalApk( + packageName = "com.google.android.gms", + versionCode = 244735012, + versionName = "v0.3.6.244735", + displayName = "microG Services", + iconURL = "https://raw.githubusercontent.com/microg/GmsCore/refs/heads/master/play-services-core/src/main/res/mipmap-xxxhdpi/ic_app.png", + developerName = "microG Team", + fileList = listOf( + PlayFile( + url = "https://github.com/microg/GmsCore/releases/download/v0.3.6.244735/com.google.android.gms-244735012-hw.apk", + name = "com.google.android.gms-244735012-hw.apk", + size = 32509431, + sha256 = "2f14df2974811b576bfafa6167a97e3b3032f2bd6e6ec3887a833fd2fa350dda" + ) + ) + ), + ExternalApk( + packageName = "com.android.vending", + versionCode = 84022612, + versionName = "v0.3.6.244735", + displayName = "microG Companion", + iconURL = "https://raw.githubusercontent.com/microg/FakeStore/refs/heads/main/fake-store/src/main/res/mipmap-xxxhdpi/ic_app.png", + developerName = "microG Team", + fileList = listOf( + PlayFile( + url = "https://github.com/microg/GmsCore/releases/download/v0.3.6.244735/com.android.vending-84022612-hw.apk", + name = "com.android.vending-84022612-hw.apk", + size = 3915551, + sha256 = "6835b09016cef0fc3469b4a36b1720427ad3f81161cf20b188f0dadb5f8594e1" + ) + ) + ) + ) + + fun markAgreement(checked: Boolean) { + viewModelScope.launch { + _checked.emit(checked) + } + } + + fun downloadMicroG() { + viewModelScope.launch { + try { + _packageNames.emit( + listOf( + PACKAGE_NAME_GMS, + PACKAGE_NAME_PLAY_STORE + ) + ) + + externalApks.forEach { + // Enqueue download only if not already installed + if (!it.isInstalled(context)) { + downloadHelper.enqueueStandalone(it) + } + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } +} diff --git a/app/src/main/java/com/aurora/store/viewmodel/onboarding/OnboardingPage.kt b/app/src/main/java/com/aurora/store/viewmodel/onboarding/OnboardingPage.kt new file mode 100644 index 000000000..8386b6ccc --- /dev/null +++ b/app/src/main/java/com/aurora/store/viewmodel/onboarding/OnboardingPage.kt @@ -0,0 +1,7 @@ +package com.aurora.store.viewmodel.onboarding + +enum class OnboardingPage { + WELCOME, + PERMISSIONS, + GSF, +} \ No newline at end of file diff --git a/app/src/main/java/com/aurora/store/viewmodel/onboarding/OnboardingViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/onboarding/OnboardingViewModel.kt index 1e1486ee1..c6266be9c 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/onboarding/OnboardingViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/onboarding/OnboardingViewModel.kt @@ -6,9 +6,23 @@ package com.aurora.store.viewmodel.onboarding import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.aurora.store.data.helper.UpdateHelper import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel -class OnboardingViewModel @Inject constructor(val updateHelper: UpdateHelper) : ViewModel() +class OnboardingViewModel @Inject constructor(val updateHelper: UpdateHelper) : ViewModel() { + + private val _page = MutableStateFlow(OnboardingPage.WELCOME) + val currentPage: StateFlow = _page + + fun setCurrentPage(page: OnboardingPage) { + viewModelScope.launch { + _page.emit(page) + } + } +} diff --git a/app/src/main/res/layout-land/fragment_onboarding_microg.xml b/app/src/main/res/layout-land/fragment_onboarding_microg.xml new file mode 100644 index 000000000..f856f8015 --- /dev/null +++ b/app/src/main/res/layout-land/fragment_onboarding_microg.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout-land/fragment_onboarding_permissions.xml b/app/src/main/res/layout-land/fragment_onboarding_permissions.xml index 34513659e..4b588c455 100644 --- a/app/src/main/res/layout-land/fragment_onboarding_permissions.xml +++ b/app/src/main/res/layout-land/fragment_onboarding_permissions.xml @@ -38,7 +38,8 @@ + android:layout_height="wrap_content" + android:orientation="horizontal"> + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_onboarding_microg.xml b/app/src/main/res/layout/fragment_onboarding_microg.xml new file mode 100644 index 000000000..124aa2144 --- /dev/null +++ b/app/src/main/res/layout/fragment_onboarding_microg.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_onboarding_permissions.xml b/app/src/main/res/layout/fragment_onboarding_permissions.xml index f40132a5f..ce8a56cde 100644 --- a/app/src/main/res/layout/fragment_onboarding_permissions.xml +++ b/app/src/main/res/layout/fragment_onboarding_permissions.xml @@ -47,7 +47,7 @@ android:text="@string/onboarding_title_permissions" android:textAlignment="textStart" android:textColor="?colorAccent" - android:textSize="42sp" /> + android:textSize="32sp" /> + android:textSize="32sp" /> + + diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index 470a907d7..56816d9be 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -91,7 +91,7 @@ android:id="@+id/settingsFragment" android:name="com.aurora.store.view.ui.preferences.SettingsFragment" android:label="@string/title_settings" - tools:layout="@layout/fragment_setting" > + tools:layout="@layout/fragment_setting"> @@ -157,6 +157,11 @@ + @@ -169,11 +174,6 @@ - @@ -183,6 +183,9 @@ + + 52dp 64dp 148dp + 96dp 8dp 10dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a05221667..e9764cc69 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -76,6 +76,7 @@ "Granted" "Ignore" "Install" + "Install microG Bundle" "Installations" "Installing" "Join" @@ -193,21 +194,6 @@ App export notification Install notification Downloads notification - "Aurora Store requires following permissions" - "Installer" - "Permissions" - "Welcome" - "How you doing?" - "External storage access" - To save APK expansion files (OBBs) for large apps & games. - "External storage manager" - "Installer permission" - "Allow installing apps from Aurora Store" - "Allow installation of apps from unknown sources" - "Notifications" - "Send notifications regarding installations status" - "Background downloads" - "Allow Aurora Store to download and update apps in background" "Extras" "Don't check updates for apps installed from F-Droid" "Filter F-Droid apps" @@ -362,6 +348,31 @@ "Minimum Android Version" "Target API Level" + + "Installer" + "App Compatibility" + "Missing dependencies" + "Permissions" + "Welcome" + "How you doing?" + "External storage access" + To save APK expansion files (OBBs) for large apps & games. + "External storage manager" + "Installer permission" + "Allow installing apps from Aurora Store" + "Allow installation of apps from unknown sources" + "Notifications" + "Send notifications regarding installations status" + "Background downloads" + "Allow Aurora Store to download and update apps in background" + "Aurora Store requires following permissions" + "I have read and agree to the microG Terms of Service and Privacy Policy" + "We couldn't find Google Play Services on your device, several popular apps now require it to function properly!" + "microG is a free and open-source implementation that provides similar functionality to run apps dependent on Google Play Services for Android devices through re-implementation.\n\nmicroG enables apps to access those Google APIs, enhancing compatibility for users while offering privacy benefits. \n\nmicroG will run automatically in the background when you open applications dependent on Google Mobile Services." + Read microG Privacy Policy + Read microG License and Agreement + Visit microG Project Website + Session based installer for bundled/split APKs Recommended, in-built and supports all Android versions diff --git a/app/src/preload/java/com/aurora/store/data/receiver/InstallerStatusReceiver.kt b/app/src/preload/java/com/aurora/store/data/receiver/InstallerStatusReceiver.kt new file mode 100644 index 000000000..440a1070d --- /dev/null +++ b/app/src/preload/java/com/aurora/store/data/receiver/InstallerStatusReceiver.kt @@ -0,0 +1,24 @@ +/* + * Aurora Store + * Copyright (C) 2021, Rahul Kumar Patel + * + * Aurora Store is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * Aurora Store is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Aurora Store. If not, see . + * + */ +package com.aurora.store.data.receiver + +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class InstallerStatusReceiver : BaseInstallerStatusReceiver() diff --git a/app/src/vanilla/java/com/aurora/store/data/receiver/InstallerStatusReceiver.kt b/app/src/vanilla/java/com/aurora/store/data/receiver/InstallerStatusReceiver.kt new file mode 100644 index 000000000..440a1070d --- /dev/null +++ b/app/src/vanilla/java/com/aurora/store/data/receiver/InstallerStatusReceiver.kt @@ -0,0 +1,24 @@ +/* + * Aurora Store + * Copyright (C) 2021, Rahul Kumar Patel + * + * Aurora Store is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * Aurora Store is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Aurora Store. If not, see . + * + */ +package com.aurora.store.data.receiver + +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class InstallerStatusReceiver : BaseInstallerStatusReceiver() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cab7394f0..d9f9d3137 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,6 @@ [versions] activity = "1.10.1" +agCoreservice = "13.3.1.300" androidGradlePlugin = "8.11.0" androidx-hilt = "1.2.0" androidx-junit = "1.2.1" @@ -38,6 +39,7 @@ viewpager2 = "1.1.0" work = "2.10.2" [libraries] +ag-coreservice = { module = "com.huawei.hms:ag-coreservice", version.ref = "agCoreservice" } airbnb-epoxy-android = { module = "com.airbnb.android:epoxy", version.ref = "epoxy" } airbnb-epoxy-processor = { module = "com.airbnb.android:epoxy-processor", version.ref = "epoxy" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity" } diff --git a/settings.gradle.kts b/settings.gradle.kts index fd017b0e2..4e37e81db 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -36,6 +36,7 @@ dependencyResolutionManagement { includeModule("com.github.topjohnwu.libsu", "core") } } + maven { url = uri("https://developer.huawei.com/repo/") } } } include(":app")