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