Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 4db02dd8 authored by dev-12's avatar dev-12
Browse files

Fix: handle update ownership transfer consent

Add support for update ownership transfer consent required by newer versions android. This handles the system intent that allows users to consent to transferring update ownership
to our app store.

See: https://source.android.com/docs/setup/create/app-ownership
parent 77157730
Loading
Loading
Loading
Loading
Loading
+86 −0
Original line number Original line Diff line number Diff line
@@ -18,11 +18,21 @@


package foundation.e.apps
package foundation.e.apps


import android.Manifest
import android.app.Application
import android.app.Application
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Build
import android.util.Log
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.hilt.work.HiltWorkerFactory
import androidx.hilt.work.HiltWorkerFactory
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.work.Configuration
import androidx.work.Configuration
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.ExistingPeriodicWorkPolicy
import dagger.hilt.android.HiltAndroidApp
import dagger.hilt.android.HiltAndroidApp
@@ -30,22 +40,31 @@ import foundation.e.apps.data.Constants.TAG_APP_INSTALL_STATE
import foundation.e.apps.data.Constants.TAG_AUTHDATA_DUMP
import foundation.e.apps.data.Constants.TAG_AUTHDATA_DUMP
import foundation.e.apps.data.preference.AppLoungeDataStore
import foundation.e.apps.data.preference.AppLoungeDataStore
import foundation.e.apps.data.preference.AppLoungePreference
import foundation.e.apps.data.preference.AppLoungePreference
import foundation.e.apps.di.NotificationManagerModule
import foundation.e.apps.install.pkg.AppLoungePackageManager
import foundation.e.apps.install.pkg.AppLoungePackageManager
import foundation.e.apps.install.pkg.PkgManagerBR
import foundation.e.apps.install.pkg.PkgManagerBR
import foundation.e.apps.install.updates.UpdatesWorkManager
import foundation.e.apps.install.updates.UpdatesWorkManager
import foundation.e.apps.install.workmanager.InstallWorkManager
import foundation.e.apps.install.workmanager.InstallWorkManager
import foundation.e.apps.ui.setup.tos.TOS_VERSION
import foundation.e.apps.ui.setup.tos.TOS_VERSION
import foundation.e.apps.utils.CustomUncaughtExceptionHandler
import foundation.e.apps.utils.CustomUncaughtExceptionHandler
import foundation.e.apps.utils.eventBus.AppEvent
import foundation.e.apps.utils.eventBus.EventBus
import foundation.e.lib.telemetry.Telemetry
import foundation.e.lib.telemetry.Telemetry
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.runBlocking
import timber.log.Timber
import timber.log.Timber
import timber.log.Timber.Forest.plant
import timber.log.Timber.Forest.plant
import java.util.concurrent.Executors
import java.util.concurrent.Executors
import javax.inject.Inject
import javax.inject.Inject
import kotlin.jvm.optionals.getOrDefault


@HiltAndroidApp
@HiltAndroidApp
@DelicateCoroutinesApi
@DelicateCoroutinesApi
@@ -109,6 +128,73 @@ class AppLoungeApplication : Application(), Configuration.Provider {
            appLoungePreference.getUpdateInterval(),
            appLoungePreference.getUpdateInterval(),
            ExistingPeriodicWorkPolicy.KEEP
            ExistingPeriodicWorkPolicy.KEEP
        )
        )
        listenForUserConformationEvent(this)
    }

    private fun listenForUserConformationEvent(context: Context) {
        fun isOnForeground(): Boolean {
            val lifecycle = ProcessLifecycleOwner.get().lifecycle.currentState
            return lifecycle.isAtLeast(Lifecycle.State.RESUMED)
        }

        fun sendNotification(event: AppEvent.NeedUserConfirmation) {
            if (ActivityCompat.checkSelfPermission(
                    this,
                    Manifest.permission.POST_NOTIFICATIONS
                ) != PackageManager.PERMISSION_GRANTED
            ) {
                val errorMsg = "app lounge doesn't have notification permission?"
                Timber.i(errorMsg)
                throw IllegalStateException(errorMsg)
            }

            val intent = event.confirmation

            val appName = event.appName.getOrDefault(getString(R.string.default_updatable_app_name))
            val storeName = with(event) {
                installerName.getOrDefault(getString(R.string.default_other_store_name))
            }
            val notificationTitle = getString(
                R.string.notification_update_owner_request_title,
                appName
            )
            val notificationContent = getString(
                R.string.notification_update_owner_request_content,
                appName,
                storeName
            )
            val uniqueActionId = intent.hashCode()
            val notificationManager = NotificationManagerCompat.from(context)
            val notification =
                NotificationCompat.Builder(context, NotificationManagerModule.UPDATES)
                    .setContentText(notificationContent)
                    .setContentTitle(notificationTitle)
                    .setAutoCancel(true)
                    .setSmallIcon(R.drawable.ic_app_updated_on)
                    .addAction(
                        NotificationCompat.Action(
                            R.drawable.ic_check_mark,
                            getString(android.R.string.ok),
                            PendingIntent.getActivity(
                                context,
                                uniqueActionId,
                                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
                                PendingIntent.FLAG_IMMUTABLE
                            )
                        )
                    )
                    .build()
            notificationManager.notify(uniqueActionId, notification)
        }

        GlobalScope.plus(Dispatchers.Default).launch {
            EventBus.events.filterIsInstance<AppEvent.NeedUserConfirmation>()
                .collectLatest { event ->
                    if (!isOnForeground()) {
                        sendNotification(event)
                    }
                }
        }
    }
    }


    override val workManagerConfiguration: Configuration
    override val workManagerConfiguration: Configuration
+52 −2
Original line number Original line Diff line number Diff line
@@ -21,6 +21,8 @@ package foundation.e.apps.install.pkg
import android.app.Service
import android.app.Service
import android.content.Intent
import android.content.Intent
import android.content.pm.PackageInstaller
import android.content.pm.PackageInstaller
import android.content.pm.PackageManager
import android.os.Build
import android.os.IBinder
import android.os.IBinder
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.AndroidEntryPoint
import foundation.e.apps.data.application.UpdatesDao
import foundation.e.apps.data.application.UpdatesDao
@@ -33,6 +35,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.launch
import timber.log.Timber
import timber.log.Timber
import java.util.Optional
import javax.inject.Inject
import javax.inject.Inject


@AndroidEntryPoint
@AndroidEntryPoint
@@ -67,11 +70,58 @@ class InstallerService : Service() {
         the packageName from AppLoungePackageManager will be used in this error case.
         the packageName from AppLoungePackageManager will be used in this error case.
         */
         */
        val packageNamePackageManagerModule = intent.getStringExtra(AppLoungePackageManager.PACKAGE_NAME)
        val packageNamePackageManagerModule = intent.getStringExtra(AppLoungePackageManager.PACKAGE_NAME)

        val startFlag = START_NOT_STICKY
        packageName = packageName ?: packageNamePackageManagerModule
        packageName = packageName ?: packageNamePackageManagerModule

        fun optionallyGetAppName(pkgName: String?): Optional<String> {
            if (pkgName.isNullOrBlank()) {
                return Optional.empty()
            }
            return try {
                val info = packageManager.getApplicationInfo(pkgName, 0)
                val name = packageManager.getApplicationLabel(info)
                Optional.of(name.toString())
            } catch (_: PackageManager.NameNotFoundException) {
                Optional.empty()
            }
        }

        fun optionallyGetInstallerName(pkgName: String?): Optional<String> {
            if (pkgName.isNullOrBlank() || Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
                return Optional.empty()
            }
            return try {
                val info = packageManager.getInstallSourceInfo(pkgName)
                optionallyGetAppName(info.updateOwnerPackageName)
            } catch (_: PackageManager.NameNotFoundException) {
                Optional.empty()
            }
        }

        if (status == PackageInstaller.STATUS_PENDING_USER_ACTION) {
            val confirmationIntent = intent.getParcelableExtra(
                Intent.EXTRA_INTENT,
                Intent::class.java
            )
            Timber.i(TAG, "need user input for $packageName")
            if (confirmationIntent != null) {
                MainScope().launch {
                    EventBus.invokeEvent(
                        AppEvent.NeedUserConfirmation(
                            confirmationIntent,
                            optionallyGetAppName(packageNamePackageManagerModule),
                            optionallyGetInstallerName(packageNamePackageManagerModule)
                        )
                    )
                }
            }
            stopSelf()
            return startFlag
        }

        postStatus(status, packageName, extra)
        postStatus(status, packageName, extra)
        stopSelf()
        stopSelf()
        return START_NOT_STICKY
        return startFlag
    }
    }


    private fun postStatus(status: Int, packageName: String?, extra: String?) {
    private fun postStatus(status: Int, packageName: String?, extra: String?) {
+37 −26
Original line number Original line Diff line number Diff line
@@ -234,6 +234,10 @@ class MainActivity : AppCompatActivity() {
                    observeAppUnavailable()
                    observeAppUnavailable()
                }
                }


                launch {
                    observeNeedUserConformation()
                }

                launch {
                launch {
                    observeTooManyRequests()
                    observeTooManyRequests()
                }
                }
@@ -524,6 +528,13 @@ class MainActivity : AppCompatActivity() {
            }
            }
    }
    }


    private suspend fun observeNeedUserConformation() {
        EventBus.events.filterIsInstance<AppEvent.NeedUserConfirmation>()
            .collectLatest { event ->
                startActivity(event.confirmation)
            }
    }

    private fun validatedAuthObject(appEvent: AppEvent) {
    private fun validatedAuthObject(appEvent: AppEvent) {
        val data = appEvent.data as String
        val data = appEvent.data as String
        if (data.isNotBlank()) {
        if (data.isNotBlank()) {
+7 −1
Original line number Original line Diff line number Diff line
@@ -20,16 +20,22 @@


package foundation.e.apps.utils.eventBus
package foundation.e.apps.utils.eventBus


import android.content.Intent
import foundation.e.apps.data.ResultSupreme
import foundation.e.apps.data.ResultSupreme
import foundation.e.apps.data.enums.ResultStatus
import foundation.e.apps.data.enums.ResultStatus
import foundation.e.apps.data.enums.User
import foundation.e.apps.data.enums.User
import foundation.e.apps.data.install.models.AppInstall
import foundation.e.apps.data.install.models.AppInstall
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CompletableDeferred
import java.util.Optional


sealed class AppEvent(val data: Any) {
sealed class AppEvent(val data: Any) {
    class SignatureMissMatchError(packageName: String) : AppEvent(packageName)
    class SignatureMissMatchError(packageName: String) : AppEvent(packageName)
    class UpdateEvent(result: ResultSupreme.WorkError<ResultStatus>) : AppEvent(result)
    class UpdateEvent(result: ResultSupreme.WorkError<ResultStatus>) : AppEvent(result)

    class NeedUserConfirmation(
        val confirmation: Intent,
        val appName: Optional<String>,
        val installerName: Optional<String>
    ) : AppEvent(confirmation)
    class InvalidAuthEvent(authName: String) : AppEvent(authName)
    class InvalidAuthEvent(authName: String) : AppEvent(authName)
    class ErrorMessageEvent(stringResourceId: Int) : AppEvent(stringResourceId)
    class ErrorMessageEvent(stringResourceId: Int) : AppEvent(stringResourceId)
    class ErrorMessageDialogEvent(stringResourceId: Int) : AppEvent(stringResourceId)
    class ErrorMessageDialogEvent(stringResourceId: Int) : AppEvent(stringResourceId)
+5 −0
Original line number Original line Diff line number Diff line
@@ -233,4 +233,9 @@
    <!--App Unavailable-->
    <!--App Unavailable-->
    <string name="app_unavailable_title">App Unavailable</string>
    <string name="app_unavailable_title">App Unavailable</string>
    <string name="app_unavailable_description">This app is not available for installation. This is typically due to content restrictions based on your location (region) or the account\'s age settings (including age verification and age/content rating)</string>
    <string name="app_unavailable_description">This app is not available for installation. This is typically due to content restrictions based on your location (region) or the account\'s age settings (including age verification and age/content rating)</string>

    <string name="default_updatable_app_name">the app</string>
    <string name="default_other_store_name">another app store</string>
    <string name="notification_update_owner_request_title">Approve update for %s</string>
    <string name="notification_update_owner_request_content">%s was installed from %s. Approval is required to update it.</string>
</resources>
</resources>