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.os.UserHandle 25 import android.util.Log 26 import android.widget.Toast 27 import androidx.compose.material.icons.Icons 28 import androidx.compose.material.icons.outlined.CloudDownload 29 import androidx.compose.runtime.Composable 30 import androidx.compose.runtime.rememberCoroutineScope 31 import androidx.lifecycle.compose.collectAsStateWithLifecycle 32 import com.android.settings.R 33 import com.android.settingslib.spa.widget.button.ActionButton 34 import com.android.settingslib.spaprivileged.framework.compose.DisposableBroadcastReceiverAsUser 35 import kotlinx.coroutines.CoroutineScope 36 import kotlinx.coroutines.Job 37 import kotlinx.coroutines.delay 38 import kotlinx.coroutines.flow.MutableStateFlow 39 import kotlinx.coroutines.flow.asStateFlow 40 import kotlinx.coroutines.isActive 41 import kotlinx.coroutines.launch 42 43 class AppRestoreButton(packageInfoPresenter: PackageInfoPresenter) { 44 private companion object { 45 private const val LOG_TAG = "AppRestoreButton" 46 private const val INTENT_ACTION = "com.android.settings.unarchive.action" 47 } 48 49 private val context = packageInfoPresenter.context 50 private val userPackageManager = packageInfoPresenter.userPackageManager 51 private val packageInstaller = userPackageManager.packageInstaller 52 private val packageName = packageInfoPresenter.packageName 53 private val userHandle = UserHandle.of(packageInfoPresenter.userId) 54 private var broadcastReceiverIsCreated = false 55 private lateinit var coroutineScope: CoroutineScope 56 private lateinit var updateButtonTextJob: Job 57 private val buttonTexts = intArrayOf( 58 R.string.restore, 59 R.string.restoring_step_one, 60 R.string.restoring_step_two, 61 R.string.restoring_step_three, 62 R.string.restoring_step_four, 63 ) 64 private var buttonTextIndexStateFlow = MutableStateFlow(0) 65 66 @Composable 67 fun getActionButton(app: ApplicationInfo): ActionButton { 68 if (!broadcastReceiverIsCreated) { 69 val intentFilter = IntentFilter(INTENT_ACTION) 70 DisposableBroadcastReceiverAsUser(intentFilter, userHandle) { intent -> 71 if (app.packageName == intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)) { 72 onReceive(intent, app) 73 } 74 } 75 broadcastReceiverIsCreated = true 76 } 77 coroutineScope = rememberCoroutineScope() 78 if (app.isArchived && ::updateButtonTextJob.isInitialized && !updateButtonTextJob.isActive) { 79 buttonTextIndexStateFlow.value = 0 80 } 81 return ActionButton( 82 text = context.getString( 83 buttonTexts[ 84 buttonTextIndexStateFlow.asStateFlow().collectAsStateWithLifecycle(0).value] 85 ), 86 imageVector = Icons.Outlined.CloudDownload, 87 enabled = app.isArchived && (!::updateButtonTextJob.isInitialized || !updateButtonTextJob.isActive) 88 ) { onRestoreClicked(app) } 89 } 90 91 private fun onRestoreClicked(app: ApplicationInfo) { 92 val intent = Intent(INTENT_ACTION) 93 intent.setPackage(context.packageName) 94 val pendingIntent = PendingIntent.getBroadcastAsUser( 95 context, 0, intent, 96 // FLAG_MUTABLE is required by PackageInstaller#requestUnarchive 97 PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_MUTABLE, 98 userHandle 99 ) 100 try { 101 packageInstaller.requestUnarchive(app.packageName, pendingIntent.intentSender) 102 } catch (e: Exception) { 103 Log.e(LOG_TAG, "Request unarchive failed", e) 104 Toast.makeText( 105 context, 106 context.getString(R.string.restoring_failed), 107 Toast.LENGTH_SHORT 108 ).show() 109 } 110 } 111 112 private fun onReceive(intent: Intent, app: ApplicationInfo) { 113 when (val unarchiveStatus = 114 intent.getIntExtra(PackageInstaller.EXTRA_UNARCHIVE_STATUS, Int.MIN_VALUE)) { 115 PackageInstaller.UNARCHIVAL_OK -> { 116 // updateButtonTextJob will be canceled automatically once 117 // AppButtonsPresenter#getActionButtons is triggered 118 updateButtonTextJob = coroutineScope.launch { 119 while (isActive) { 120 var index = buttonTextIndexStateFlow.value 121 index = (index + 1) % buttonTexts.size 122 // The initial state shouldn't be used here 123 if (index == 0) index++ 124 buttonTextIndexStateFlow.emit(index) 125 delay(1000) 126 } 127 } 128 val appLabel = userPackageManager.getApplicationLabel(app) 129 Toast.makeText( 130 context, 131 context.getString(R.string.restoring_in_progress, appLabel), 132 Toast.LENGTH_SHORT 133 ).show() 134 } 135 136 else -> { 137 Log.e( 138 LOG_TAG, 139 "Request unarchiving failed for $packageName with code $unarchiveStatus" 140 ) 141 val errorDialogIntent = 142 intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java) 143 if (errorDialogIntent != null) { 144 context.startActivityAsUser(errorDialogIntent, userHandle) 145 } else { 146 Toast.makeText( 147 context, 148 context.getString(R.string.restoring_failed), 149 Toast.LENGTH_SHORT 150 ).show() 151 } 152 } 153 } 154 } 155 } 156