Allow huawei variants to install microG & support silent installs.

This commit is contained in:
Rahul Patel
2025-07-05 14:59:41 +05:30
parent cfe52d5a04
commit aea945491d
44 changed files with 1662 additions and 145 deletions

View File

@@ -0,0 +1,149 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
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<BaseIPCRequest, BaseIPCResponse>(
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<BaseIPCResponse>
) {
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
}
}
}

View File

@@ -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<FragmentOnboardingMicrogBinding>() {
// 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<Dash> {
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"
)
)
}
}

View File

@@ -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<Fragment> {
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()
}
}

View File

@@ -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<SilentInstallRequest> 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;
}
}

View File

@@ -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<SilentInstallResponse> 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;
}
}