Merge branch 'dev' into 'master'

Allow huawei variants to install microG & support silent installs.

See merge request AuroraOSS/AuroraStore!500
This commit is contained in:
Rahul Patel
2025-07-05 16:04:24 +05:30
44 changed files with 1662 additions and 145 deletions

View File

@@ -264,6 +264,8 @@ dependencies {
implementation(libs.process.phoenix)
"huaweiImplementation"(libs.ag.coreservice)
// LeakCanary
debugImplementation(libs.squareup.leakcanary.android)
}

View File

@@ -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.**

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

View File

@@ -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"
}

View File

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

View File

@@ -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

View File

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

View File

@@ -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)
}

View File

@@ -61,7 +61,7 @@ class AuthProvider @Inject constructor(
return if (rawAuth.isNotBlank()) {
json.decodeFromString<AuthData>(rawAuth)
} else {
null
AuthData("BOGUS")
}
}

View File

@@ -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<String>
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, "")

View File

@@ -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)
}
}

View File

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

View File

@@ -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<PlayFile>
) : Parcelable {
fun isInstalled(context: Context): Boolean {
return PackageUtil.isInstalled(context, packageName)
}
}

View File

@@ -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

View File

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

View File

@@ -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 ?: ""

View File

@@ -0,0 +1,56 @@
/*
* 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.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<ViewTextBinding>(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)
}
}

View File

@@ -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<ViewBindingType : ViewBinding> : Fragment() {
@@ -131,6 +132,12 @@ abstract class BaseFragment<ViewBindingType : ViewBinding> : Fragment() {
findNavController().navigate(MobileNavigationDirections.actionGlobalAppMenuSheet(app))
}
fun openGMSWarningFragment() {
findNavController().navigate(
AppDetailsFragmentDirections.actionAppDetailsFragmentToGmsWarnFragment()
)
}
private fun cleanupRecyclerViews(recyclerViews: List<EpoxyRecyclerView>) {
recyclerViews.forEach { recyclerView ->
runCatching {

View File

@@ -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<FragmentDetailsBinding>() {
viewLifecycleOwner.lifecycleScope.launch {
AuroraApp.events.installerEvent.collect { onEvent(it) }
}
}
override fun onResume() {
@@ -525,6 +529,24 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
}
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<FragmentDetailsBinding>() {
}
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 {

View File

@@ -0,0 +1,203 @@
/*
* 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.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<FragmentDetailsMicrogBinding>() {
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<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

@@ -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<FragmentOnboardingBinding>() {
// 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<FragmentOnboarding
if (PackageUtil.isTv(view.context)) finishOnboarding()
}
// ViewPager2
binding.viewpager2.apply {
adapter = PagerAdapter(
childFragmentManager,
viewLifecycleOwner.lifecycle,
onboardingPages()
)
isUserInputEnabled = false
setCurrentItem(0, true)
registerOnPageChangeCallback(object : OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
activity?.runOnUiThread {
lastPosition = position
refreshButtonState()
val pages = onboardingPages()
with(binding) {
// ViewPager2
with(viewpager2) {
adapter = PagerAdapter(
childFragmentManager,
viewLifecycleOwner.lifecycle,
pages
)
isUserInputEnabled = false
setCurrentItem(0, true)
registerOnPageChangeCallback(object : OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
onboardingViewModel.setCurrentPage(
when (position) {
0 -> 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<FragmentOnboarding
save(PREFERENCE_UPDATES_AUTO, updateMode.ordinal)
viewModel.updateHelper.scheduleAutomatedCheck()
onboardingViewModel.updateHelper.scheduleAutomatedCheck()
}
internal class PagerAdapter(

View File

@@ -66,7 +66,7 @@ class NetworkPreference : BasePreferenceFragment() {
}
findPreference<SwitchPreferenceCompat>(PREFERENCE_MICROG_AUTH)?.isEnabled =
PackageUtil.hasSupportedMicroG(requireContext())
PackageUtil.hasSupportedMicroGVariant(requireContext())
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

View File

@@ -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<FragmentSplashBinding>
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<FragmentSplashBinding>
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(),

View File

@@ -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<List<String>>()
val packageNames = _packageNames.asSharedFlow()
private val _checked = MutableStateFlow(false)
val checked: StateFlow<Boolean> = _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>(
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()
}
}
}
}

View File

@@ -0,0 +1,7 @@
package com.aurora.store.viewmodel.onboarding
enum class OnboardingPage {
WELCOME,
PERMISSIONS,
GSF,
}

View File

@@ -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>(OnboardingPage.WELCOME)
val currentPage: StateFlow<OnboardingPage> = _page
fun setCurrentPage(page: OnboardingPage) {
viewModelScope.launch {
_page.emit(page)
}
}
}

View File

@@ -0,0 +1,109 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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/>.
~
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/top_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@id/bottom_layout"
android:layout_alignParentTop="true"
android:gravity="center"
android:orientation="horizontal">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:includeFontPadding="true"
android:text="@string/onboarding_title_gsf"
android:textAlignment="textStart"
android:textColor="?colorAccent"
android:textSize="42sp" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/AuroraTextStyle.Subtitle.Alt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/onboarding_title_gsf_desc"
android:textAlignment="textStart" />
</LinearLayout>
<com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/epoxy_recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:clipToPadding="false"
android:nestedScrollingEnabled="false"
android:overScrollMode="never"
android:padding="@dimen/padding_medium"
android:scrollbars="none"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/view_dash" />
</LinearLayout>
<LinearLayout
android:id="@+id/bottom_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:divider="@drawable/divider"
android:orientation="vertical"
android:paddingStart="@dimen/padding_large"
android:paddingEnd="@dimen/padding_large"
android:showDividers="middle">
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/checkbox_agreement"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/padding_large"
android:paddingEnd="@dimen/padding_xxsmall"
android:text="@string/onboarding_gms_agreement" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_microG"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:enabled="false"
android:maxWidth="@dimen/width_button"
android:text="@string/action_install_microG" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="24dp"
android:visibility="invisible" />
</LinearLayout>
</RelativeLayout>

View File

@@ -38,7 +38,8 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:orientation="horizontal">
<LinearLayout
android:layout_width="match_parent"

View File

@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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/>.
~
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_arrow_back" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/epoxy_recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@id/bottom_layout"
android:clipToPadding="false"
android:nestedScrollingEnabled="false"
android:overScrollMode="never"
android:padding="@dimen/padding_medium"
android:scrollbars="none"
app:itemSpacing="@dimen/margin_small"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/view_dash" />
<LinearLayout
android:id="@+id/bottom_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="?attr/colorSurface"
android:divider="@drawable/divider"
android:orientation="vertical"
android:showDividers="middle">
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/checkbox_agreement"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/padding_large"
android:paddingEnd="@dimen/padding_xxsmall"
android:text="@string/onboarding_gms_agreement" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="24dp"
android:visibility="invisible" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/layout_bottom"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_microG"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="0dp"
android:layout_height="56dp"
android:layout_margin="@dimen/margin_xsmall"
android:enabled="false"
android:text="@string/action_install_microG"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/btn_skip"
app:layout_constraintTop_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_skip"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="0dp"
android:layout_height="56dp"
android:layout_margin="@dimen/margin_xsmall"
android:text="@string/action_ignore"
app:layout_constraintEnd_toStartOf="@id/btn_microG"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</RelativeLayout>
</LinearLayout>

View File

@@ -0,0 +1,106 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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/>.
~
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/top_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:divider="@drawable/divider"
android:orientation="vertical"
android:showDividers="middle">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:includeFontPadding="true"
android:paddingStart="@dimen/padding_normal"
android:paddingEnd="@dimen/padding_normal"
android:text="@string/onboarding_title_gsf"
android:textAlignment="textStart"
android:textColor="?colorAccent"
android:textSize="32sp" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/AuroraTextStyle.Subtitle.Alt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/padding_normal"
android:paddingEnd="@dimen/padding_normal"
android:text="@string/onboarding_title_gsf_desc"
android:textAlignment="textStart" />
</LinearLayout>
<com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/epoxy_recycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@id/bottom_layout"
android:layout_below="@+id/top_layout"
android:clipToPadding="false"
android:nestedScrollingEnabled="false"
android:overScrollMode="never"
android:padding="@dimen/padding_medium"
android:scrollbars="none"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/view_dash" />
<LinearLayout
android:id="@+id/bottom_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="?attr/colorSurface"
android:divider="@drawable/divider"
android:orientation="vertical"
android:paddingStart="@dimen/padding_large"
android:paddingEnd="@dimen/padding_large"
android:showDividers="middle">
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/checkbox_agreement"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/padding_large"
android:paddingEnd="@dimen/padding_xxsmall"
android:text="@string/onboarding_gms_agreement" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_microG"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:enabled="false"
android:text="@string/action_install_microG" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="24dp"
android:visibility="invisible" />
</LinearLayout>
</RelativeLayout>

View File

@@ -47,7 +47,7 @@
android:text="@string/onboarding_title_permissions"
android:textAlignment="textStart"
android:textColor="?colorAccent"
android:textSize="42sp" />
android:textSize="32sp" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/subtitle"

View File

@@ -36,7 +36,7 @@
android:text="@string/onboarding_title_welcome"
android:textAlignment="textStart"
android:textColor="?colorAccent"
android:textSize="42sp" />
android:textSize="32sp" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/AuroraTextStyle.Subtitle.Alt"

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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/>.
~
-->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/txt_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Some text" />

View File

@@ -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">
<action
android:id="@+id/action_settingsFragment_to_permissionsFragment"
app:destination="@id/permissionsFragment" />
@@ -157,6 +157,11 @@
<argument
android:name="packageName"
app:argType="string" />
<argument
android:name="app"
android:defaultValue="@null"
app:argType="com.aurora.gplayapi.data.models.App"
app:nullable="true" />
<action
android:id="@+id/action_appDetailsFragment_to_devAppsFragment"
app:destination="@id/devAppsFragment" />
@@ -169,11 +174,6 @@
<action
android:id="@+id/action_appDetailsFragment_to_detailsExodusFragment"
app:destination="@id/detailsExodusFragment" />
<argument
android:name="app"
android:defaultValue="@null"
app:argType="com.aurora.gplayapi.data.models.App"
app:nullable="true" />
<action
android:id="@+id/action_appDetailsFragment_to_manualDownloadSheet"
app:destination="@id/manualDownloadSheet" />
@@ -183,6 +183,9 @@
<action
android:id="@+id/action_appDetailsFragment_to_installErrorDialogSheet"
app:destination="@id/installErrorDialogSheet" />
<action
android:id="@+id/action_appDetailsFragment_to_gmsWarnFragment"
app:destination="@id/detailsMicroGFragment" />
</fragment>
<fragment
android:id="@+id/categoryBrowseFragment"
@@ -375,6 +378,11 @@
android:defaultValue="true"
app:argType="boolean" />
</fragment>
<fragment
android:id="@+id/detailsMicroGFragment"
android:name="com.aurora.store.view.ui.details.DetailsMicroGFragment"
android:label="MicroGFragment"
tools:layout="@layout/fragment_details_microg" />
<dialog
android:id="@+id/appMenuSheet"
android:name="com.aurora.store.view.ui.sheets.AppMenuSheet"

View File

@@ -62,6 +62,7 @@
<dimen name="height_bottomsheet_button">52dp</dimen>
<dimen name="height_peek">64dp</dimen>
<dimen name="height_nav_header">148dp</dimen>
<dimen name="height_microg_action">96dp</dimen>
<dimen name="radius_small">8dp</dimen>
<dimen name="radius_medium">10dp</dimen>

View File

@@ -76,6 +76,7 @@
<string name="action_granted">"Granted"</string>
<string name="action_ignore">"Ignore"</string>
<string name="action_install">"Install"</string>
<string name="action_install_microG">"Install microG Bundle"</string>
<string name="action_installations">"Installations"</string>
<string name="action_installing">"Installing"</string>
<string name="action_join">"Join"</string>
@@ -193,21 +194,6 @@
<string name="notification_channel_export">App export notification</string>
<string name="notification_channel_install">Install notification</string>
<string name="notification_channel_downloads">Downloads notification</string>
<string name="onboarding_permission_select">"Aurora Store requires following permissions"</string>
<string name="onboarding_title_installer">"Installer"</string>
<string name="onboarding_title_permissions">"Permissions"</string>
<string name="onboarding_title_welcome">"Welcome"</string>
<string name="onboarding_welcome_select">"How you doing?"</string>
<string name="onboarding_permission_esa">"External storage access"</string>
<string name="onboarding_permission_esa_desc">To save APK expansion files (OBBs) for large apps &amp; games.</string>
<string name="onboarding_permission_esm">"External storage manager"</string>
<string name="onboarding_permission_installer">"Installer permission"</string>
<string name="onboarding_permission_installer_desc">"Allow installing apps from Aurora Store"</string>
<string name="onboarding_permission_installer_legacy_desc">"Allow installation of apps from unknown sources"</string>
<string name="onboarding_permission_notifications">"Notifications"</string>
<string name="onboarding_permission_notifications_desc">"Send notifications regarding installations status"</string>
<string name="onboarding_permission_doze">"Background downloads"</string>
<string name="onboarding_permission_doze_desc">"Allow Aurora Store to download and update apps in background"</string>
<string name="pref_common_extra">"Extras"</string>
<string name="pref_filter_fdroid_summary">"Don't check updates for apps installed from F-Droid"</string>
<string name="pref_filter_fdroid_title">"Filter F-Droid apps"</string>
@@ -362,6 +348,31 @@
<string name="app_info_min_android">"Minimum Android Version"</string>
<string name="app_info_target_android">"Target API Level"</string>
<!-- OnBoarding Fragment -->
<string name="onboarding_title_installer">"Installer"</string>
<string name="onboarding_title_gsf">"App Compatibility"</string>
<string name="onboarding_title_gsf_desc">"Missing dependencies"</string>
<string name="onboarding_title_permissions">"Permissions"</string>
<string name="onboarding_title_welcome">"Welcome"</string>
<string name="onboarding_welcome_select">"How you doing?"</string>
<string name="onboarding_permission_esa">"External storage access"</string>
<string name="onboarding_permission_esa_desc">To save APK expansion files (OBBs) for large apps &amp; games.</string>
<string name="onboarding_permission_esm">"External storage manager"</string>
<string name="onboarding_permission_installer">"Installer permission"</string>
<string name="onboarding_permission_installer_desc">"Allow installing apps from Aurora Store"</string>
<string name="onboarding_permission_installer_legacy_desc">"Allow installation of apps from unknown sources"</string>
<string name="onboarding_permission_notifications">"Notifications"</string>
<string name="onboarding_permission_notifications_desc">"Send notifications regarding installations status"</string>
<string name="onboarding_permission_doze">"Background downloads"</string>
<string name="onboarding_permission_doze_desc">"Allow Aurora Store to download and update apps in background"</string>
<string name="onboarding_permission_select">"Aurora Store requires following permissions"</string>
<string name="onboarding_gms_agreement">"I have read and agree to the microG Terms of Service and Privacy Policy"</string>
<string name="onboarding_gms_missing">"We couldn't find Google Play Services on your device, several popular apps now require it to function properly!"</string>
<string name="onboarding_gms_microg">"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."</string>
<string name="microg_privacy_policy">Read microG Privacy Policy</string>
<string name="microg_license_agreement">Read microG License and Agreement</string>
<string name="microg_website">Visit microG Project Website</string>
<!-- InstallerFragment -->
<string name="session_installer_subtitle">Session based installer for bundled/split APKs</string>
<string name="session_installer_desc">Recommended, in-built and supports all Android versions</string>

View File

@@ -0,0 +1,24 @@
/*
* 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 dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class InstallerStatusReceiver : BaseInstallerStatusReceiver()

View File

@@ -0,0 +1,24 @@
/*
* 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 dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class InstallerStatusReceiver : BaseInstallerStatusReceiver()

View File

@@ -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" }

View File

@@ -36,6 +36,7 @@ dependencyResolutionManagement {
includeModule("com.github.topjohnwu.libsu", "core")
}
}
maven { url = uri("https://developer.huawei.com/repo/") }
}
}
include(":app")