diff --git a/app/src/main/java/foundation/e/apps/AppLoungeApplication.kt b/app/src/main/java/foundation/e/apps/AppLoungeApplication.kt index 6adbcb225ebc93346dab76833a7a1f52c79cf2fd..4ecd9bf2d79dc3b2bcfbd7e990acd13ff8b4ea58 100644 --- a/app/src/main/java/foundation/e/apps/AppLoungeApplication.kt +++ b/app/src/main/java/foundation/e/apps/AppLoungeApplication.kt @@ -18,11 +18,21 @@ package foundation.e.apps +import android.Manifest 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.util.Log 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.lifecycle.Lifecycle +import androidx.lifecycle.ProcessLifecycleOwner import androidx.work.Configuration import androidx.work.ExistingPeriodicWorkPolicy 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.preference.AppLoungeDataStore 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.PkgManagerBR import foundation.e.apps.install.updates.UpdatesWorkManager import foundation.e.apps.install.workmanager.InstallWorkManager import foundation.e.apps.ui.setup.tos.TOS_VERSION 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 kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.MainScope +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import kotlinx.coroutines.plus import kotlinx.coroutines.runBlocking import timber.log.Timber import timber.log.Timber.Forest.plant import java.util.concurrent.Executors import javax.inject.Inject +import kotlin.jvm.optionals.getOrDefault @HiltAndroidApp @DelicateCoroutinesApi @@ -109,6 +128,73 @@ class AppLoungeApplication : Application(), Configuration.Provider { appLoungePreference.getUpdateInterval(), 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() + .collectLatest { event -> + if (!isOnForeground()) { + sendNotification(event) + } + } + } } override val workManagerConfiguration: Configuration diff --git a/app/src/main/java/foundation/e/apps/data/preference/AppLoungePreference.kt b/app/src/main/java/foundation/e/apps/data/preference/AppLoungePreference.kt index 8571d005cbfcec3f171c3af493a029c005788732..6780aa649ac8186e38914f0c879f019c94b4191b 100644 --- a/app/src/main/java/foundation/e/apps/data/preference/AppLoungePreference.kt +++ b/app/src/main/java/foundation/e/apps/data/preference/AppLoungePreference.kt @@ -20,6 +20,7 @@ package foundation.e.apps.data.preference import android.content.Context +import android.os.Build import androidx.core.content.edit import androidx.preference.PreferenceManager import dagger.hilt.android.qualifiers.ApplicationContext @@ -40,6 +41,10 @@ class AppLoungePreference @Inject constructor( private val appLoungeDataStore: AppLoungeDataStore ) { + companion object { + val isUpdateFromOtherStoreSupported = Build.VERSION.SDK_INT <= Build.VERSION_CODES.UPSIDE_DOWN_CAKE + } + private val preferenceManager = PreferenceManager.getDefaultSharedPreferences(context) fun preferredApplicationType(): String { @@ -79,7 +84,7 @@ class AppLoungePreference @Inject constructor( } } - fun shouldUpdateAppsFromOtherStores() = preferenceManager.getBoolean( + fun shouldUpdateAppsFromOtherStores() = isUpdateFromOtherStoreSupported && preferenceManager.getBoolean( context.getString(R.string.update_apps_from_other_stores), true ) diff --git a/app/src/main/java/foundation/e/apps/install/pkg/InstallerService.kt b/app/src/main/java/foundation/e/apps/install/pkg/InstallerService.kt index 6bb4a9efdfc22cd6ef85a7fc8d7e61d7cc463e9e..d6239adb41b05b2c00724a05a97da323eefa0bd0 100644 --- a/app/src/main/java/foundation/e/apps/install/pkg/InstallerService.kt +++ b/app/src/main/java/foundation/e/apps/install/pkg/InstallerService.kt @@ -21,6 +21,8 @@ package foundation.e.apps.install.pkg import android.app.Service import android.content.Intent import android.content.pm.PackageInstaller +import android.content.pm.PackageManager +import android.os.Build import android.os.IBinder import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.data.application.UpdatesDao @@ -33,6 +35,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import timber.log.Timber +import java.util.Optional import javax.inject.Inject @AndroidEntryPoint @@ -67,11 +70,58 @@ class InstallerService : Service() { the packageName from AppLoungePackageManager will be used in this error case. */ val packageNamePackageManagerModule = intent.getStringExtra(AppLoungePackageManager.PACKAGE_NAME) - + val startFlag = START_NOT_STICKY packageName = packageName ?: packageNamePackageManagerModule + + fun optionallyGetAppName(pkgName: String?): Optional { + 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 { + 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) stopSelf() - return START_NOT_STICKY + return startFlag } private fun postStatus(status: Int, packageName: String?, extra: String?) { diff --git a/app/src/main/java/foundation/e/apps/ui/MainActivity.kt b/app/src/main/java/foundation/e/apps/ui/MainActivity.kt index 74e3468891c429e83f14a740fba440b594176a44..e51d3c5424aaec9816e210d14a11237dc4ec1838 100644 --- a/app/src/main/java/foundation/e/apps/ui/MainActivity.kt +++ b/app/src/main/java/foundation/e/apps/ui/MainActivity.kt @@ -87,18 +87,18 @@ class MainActivity : AppCompatActivity() { private const val SESSION_DIALOG_TAG = "session_dialog" } - private val parentalControlAuthenticatorLauncher = registerForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { result -> - if (result.resultCode == RESULT_OK) { - val authenticationResult = result.data?.getBooleanExtra( - ParentalControlAuthenticator.KEY_PARENTAL_CONTROL_RESULT, false - ) == true - ParentalControlAuthenticator.setResult(authenticationResult) - } else { - ParentalControlAuthenticator.setResult(false) - } - } + private val parentalControlAuthenticatorLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == RESULT_OK) { + val authenticationResult = result.data?.getBooleanExtra( + ParentalControlAuthenticator.KEY_PARENTAL_CONTROL_RESULT, false + ) == true + ParentalControlAuthenticator.setResult(authenticationResult) + } else { + ParentalControlAuthenticator.setResult(false) + } + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -234,6 +234,10 @@ class MainActivity : AppCompatActivity() { observeAppUnavailable() } + launch { + observeNeedUserConformation() + } + launch { observeTooManyRequests() } @@ -524,6 +528,13 @@ class MainActivity : AppCompatActivity() { } } + private suspend fun observeNeedUserConformation() { + EventBus.events.filterIsInstance() + .collectLatest { event -> + startActivity(event.confirmation) + } + } + private fun validatedAuthObject(appEvent: AppEvent) { val data = appEvent.data as String if (data.isNotBlank()) { @@ -570,20 +581,20 @@ class MainActivity : AppCompatActivity() { private fun setupBottomNavItemSelectedListener( bottomNavigationView: BottomNavigationView, - navHostFragment: NavHostFragment, - navController: NavController - ) { - bottomNavigationView.setOnItemSelectedListener { - val fragment = navHostFragment.childFragmentManager.fragments - .find { fragment -> fragment is SettingsFragment } - if ( - bottomNavigationView.selectedItemId == R.id.settingsFragment && - fragment is SettingsFragment && - !fragment.isAnyAppSourceSelected() - ) { - ApplicationDialogFragment( - title = "", - message = getString(R.string.select_one_source_of_applications), + navHostFragment: NavHostFragment, + navController: NavController + ) { + bottomNavigationView.setOnItemSelectedListener { + val fragment = navHostFragment.childFragmentManager.fragments + .find { fragment -> fragment is SettingsFragment } + if ( + bottomNavigationView.selectedItemId == R.id.settingsFragment && + fragment is SettingsFragment && + !fragment.isAnyAppSourceSelected() + ) { + ApplicationDialogFragment( + title = "", + message = getString(R.string.select_one_source_of_applications), positiveButtonText = getString(R.string.ok) ).show(supportFragmentManager, TAG) return@setOnItemSelectedListener false diff --git a/app/src/main/java/foundation/e/apps/ui/settings/SettingsFragment.kt b/app/src/main/java/foundation/e/apps/ui/settings/SettingsFragment.kt index 95731e092da2e6660d0f1e455face9499297c795..ab9834eb04ad0bbbda75d8f466cf952b1e03daa4 100644 --- a/app/src/main/java/foundation/e/apps/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/settings/SettingsFragment.kt @@ -46,6 +46,7 @@ import foundation.e.apps.data.Stores import foundation.e.apps.data.application.UpdatesDao import foundation.e.apps.data.enums.Source import foundation.e.apps.data.enums.User +import foundation.e.apps.data.preference.AppLoungePreference import foundation.e.apps.databinding.CustomPreferenceBinding import foundation.e.apps.install.updates.UpdatesWorkManager import foundation.e.apps.ui.LoginViewModel @@ -139,6 +140,11 @@ class SettingsFragment : PreferenceFragmentCompat() { it?.onPreferenceChangeListener = sourceCheckboxListener } + if (!AppLoungePreference.isUpdateFromOtherStoreSupported) { + val key = getString(R.string.update_apps_from_other_stores) + findPreference(key)?.isVisible = false + } + val troubleshootUrl = getString(R.string.troubleshootURL, Locale.getDefault().language) troubleShootPreference?.intent = Intent(Intent.ACTION_VIEW, Uri.parse(troubleshootUrl)) } diff --git a/app/src/main/java/foundation/e/apps/utils/eventBus/AppEvent.kt b/app/src/main/java/foundation/e/apps/utils/eventBus/AppEvent.kt index 8d5b38673348732969ab2319f266ee5179101018..1d7188d845ac0cf6d6b06708e62771162ab1d356 100644 --- a/app/src/main/java/foundation/e/apps/utils/eventBus/AppEvent.kt +++ b/app/src/main/java/foundation/e/apps/utils/eventBus/AppEvent.kt @@ -20,16 +20,22 @@ package foundation.e.apps.utils.eventBus +import android.content.Intent import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.enums.User import foundation.e.apps.data.install.models.AppInstall import kotlinx.coroutines.CompletableDeferred +import java.util.Optional sealed class AppEvent(val data: Any) { class SignatureMissMatchError(packageName: String) : AppEvent(packageName) class UpdateEvent(result: ResultSupreme.WorkError) : AppEvent(result) - + class NeedUserConfirmation( + val confirmation: Intent, + val appName: Optional, + val installerName: Optional + ) : AppEvent(confirmation) class InvalidAuthEvent(authName: String) : AppEvent(authName) class ErrorMessageEvent(stringResourceId: Int) : AppEvent(stringResourceId) class ErrorMessageDialogEvent(stringResourceId: Int) : AppEvent(stringResourceId) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3dcc7bf22d03f94e0f06d09a37c090e085d8dc3f..d841df8d28c9f913d1010332abbba0a13aa84a5e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -233,4 +233,9 @@ App Unavailable 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) + + the app + another app store + Approve update for %s + %s was installed from %s. Approval is required to update it.