1 /* <lambda>null2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.spa.app.appinfo 18 19 import android.app.PendingIntent 20 import android.content.Intent 21 import android.content.IntentFilter 22 import android.content.pm.ApplicationInfo 23 import android.content.pm.PackageInstaller 24 import android.content.pm.PackageManager 25 import android.os.UserHandle 26 import android.util.Log 27 import android.widget.Toast 28 import androidx.compose.material.icons.Icons 29 import androidx.compose.material.icons.outlined.CloudUpload 30 import androidx.compose.runtime.Composable 31 import androidx.compose.runtime.remember 32 import androidx.lifecycle.compose.collectAsStateWithLifecycle 33 import com.android.settings.R 34 import com.android.settingslib.spa.widget.button.ActionButton 35 import com.android.settingslib.spaprivileged.framework.compose.DisposableBroadcastReceiverAsUser 36 import kotlinx.coroutines.Dispatchers 37 import kotlinx.coroutines.flow.MutableStateFlow 38 import kotlinx.coroutines.flow.asStateFlow 39 import kotlinx.coroutines.flow.flowOn 40 import kotlinx.coroutines.flow.map 41 42 class AppArchiveButton( 43 packageInfoPresenter: PackageInfoPresenter, 44 private val isHibernationSwitchEnabledStateFlow: MutableStateFlow<Boolean>, 45 ) { 46 private companion object { 47 private const val LOG_TAG = "AppArchiveButton" 48 private const val INTENT_ACTION = "com.android.settings.archive.action" 49 } 50 51 private val context = packageInfoPresenter.context 52 private val appButtonRepository = AppButtonRepository(context) 53 private val userPackageManager = packageInfoPresenter.userPackageManager 54 private val packageInstaller = userPackageManager.packageInstaller 55 private val packageName = packageInfoPresenter.packageName 56 private val userHandle = UserHandle.of(packageInfoPresenter.userId) 57 private var broadcastReceiverIsCreated = false 58 private lateinit var appLabel: CharSequence 59 60 @Composable 61 fun getActionButton(app: ApplicationInfo): ActionButton { 62 if (!broadcastReceiverIsCreated) { 63 val intentFilter = IntentFilter(INTENT_ACTION) 64 DisposableBroadcastReceiverAsUser(intentFilter, userHandle) { intent -> 65 if (app.packageName == intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)) { 66 onReceive(intent) 67 } 68 } 69 broadcastReceiverIsCreated = true 70 } 71 appLabel = userPackageManager.getApplicationLabel(app) 72 return ActionButton( 73 text = context.getString(R.string.archive), 74 imageVector = Icons.Outlined.CloudUpload, 75 enabled = remember(app) { 76 isHibernationSwitchEnabledStateFlow.asStateFlow().map { 77 it && isActionButtonEnabledForApp(app) 78 }.flowOn(Dispatchers.Default) 79 }.collectAsStateWithLifecycle(false).value 80 ) { onArchiveClicked(app) } 81 } 82 83 private fun isActionButtonEnabledForApp(app: ApplicationInfo): Boolean { 84 return app.isActionButtonEnabled() && appButtonRepository.isAllowUninstallOrArchive( 85 context, 86 app 87 ) 88 } 89 90 private fun ApplicationInfo.isActionButtonEnabled(): Boolean { 91 return try { 92 (!isArchived 93 && userPackageManager.isAppArchivable(packageName) 94 // We apply the same device policy for both the uninstallation and archive 95 // button. 96 && !appButtonRepository.isUninstallBlockedByAdmin(this)) 97 } catch (e: PackageManager.NameNotFoundException) { 98 false 99 } 100 } 101 102 private fun onArchiveClicked(app: ApplicationInfo) { 103 val intent = Intent(INTENT_ACTION) 104 intent.setPackage(context.packageName) 105 val pendingIntent = PendingIntent.getBroadcastAsUser( 106 context, 0, intent, 107 // FLAG_MUTABLE is required by PackageInstaller#requestArchive 108 PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_MUTABLE, 109 userHandle 110 ) 111 try { 112 packageInstaller.requestArchive(app.packageName, pendingIntent.intentSender) 113 } catch (e: Exception) { 114 Log.e(LOG_TAG, "Request archive failed", e) 115 Toast.makeText( 116 context, 117 context.getString(R.string.archiving_failed), 118 Toast.LENGTH_SHORT 119 ).show() 120 } 121 } 122 123 private fun onReceive(intent: Intent) { 124 when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, Int.MIN_VALUE)) { 125 PackageInstaller.STATUS_SUCCESS -> { 126 Toast.makeText( 127 context, 128 context.getString(R.string.archiving_succeeded, appLabel), 129 Toast.LENGTH_SHORT 130 ).show() 131 } 132 133 else -> { 134 Log.e(LOG_TAG, "Request archiving failed for $packageName with code $status") 135 Toast.makeText( 136 context, 137 context.getString(R.string.archiving_failed), 138 Toast.LENGTH_SHORT 139 ).show() 140 } 141 } 142 } 143 } 144