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.packageinstaller.v2.ui
18 
19 import android.app.Activity
20 import android.app.AppOpsManager
21 import android.content.ActivityNotFoundException
22 import android.content.Intent
23 import android.net.Uri
24 import android.os.Bundle
25 import android.os.Handler
26 import android.os.Looper
27 import android.os.Process
28 import android.os.UserManager
29 import android.provider.Settings
30 import android.util.Log
31 import android.view.Window
32 import androidx.activity.result.ActivityResultLauncher
33 import androidx.activity.result.contract.ActivityResultContracts
34 import androidx.fragment.app.DialogFragment
35 import androidx.fragment.app.FragmentActivity
36 import androidx.fragment.app.FragmentManager
37 import androidx.lifecycle.ViewModelProvider
38 import com.android.packageinstaller.R
39 import com.android.packageinstaller.v2.model.InstallAborted
40 import com.android.packageinstaller.v2.model.InstallFailed
41 import com.android.packageinstaller.v2.model.InstallInstalling
42 import com.android.packageinstaller.v2.model.InstallRepository
43 import com.android.packageinstaller.v2.model.InstallStage
44 import com.android.packageinstaller.v2.model.InstallSuccess
45 import com.android.packageinstaller.v2.model.InstallUserActionRequired
46 import com.android.packageinstaller.v2.model.PackageUtil.localLogv
47 import com.android.packageinstaller.v2.ui.fragments.AnonymousSourceFragment
48 import com.android.packageinstaller.v2.ui.fragments.ExternalSourcesBlockedFragment
49 import com.android.packageinstaller.v2.ui.fragments.InstallConfirmationFragment
50 import com.android.packageinstaller.v2.ui.fragments.InstallFailedFragment
51 import com.android.packageinstaller.v2.ui.fragments.InstallInstallingFragment
52 import com.android.packageinstaller.v2.ui.fragments.InstallStagingFragment
53 import com.android.packageinstaller.v2.ui.fragments.InstallSuccessFragment
54 import com.android.packageinstaller.v2.ui.fragments.ParseErrorFragment
55 import com.android.packageinstaller.v2.ui.fragments.SimpleErrorFragment
56 import com.android.packageinstaller.v2.viewmodel.InstallViewModel
57 import com.android.packageinstaller.v2.viewmodel.InstallViewModelFactory
58 
59 class InstallLaunch : FragmentActivity(), InstallActionListener {
60 
61     companion object {
62         @JvmField val EXTRA_CALLING_PKG_UID =
63             InstallLaunch::class.java.packageName + ".callingPkgUid"
64         @JvmField val EXTRA_CALLING_PKG_NAME =
65             InstallLaunch::class.java.packageName + ".callingPkgName"
66         private val LOG_TAG = InstallLaunch::class.java.simpleName
67         private const val TAG_DIALOG = "dialog"
68     }
69 
70     /**
71      * A collection of unknown sources listeners that are actively listening for app ops mode
72      * changes
73      */
74     private val activeUnknownSourcesListeners: MutableList<UnknownSourcesListener> = ArrayList(1)
75     private var installViewModel: InstallViewModel? = null
76     private var installRepository: InstallRepository? = null
77     private var fragmentManager: FragmentManager? = null
78     private var appOpsManager: AppOpsManager? = null
79     private lateinit var unknownAppsIntentLauncher: ActivityResultLauncher<Intent>
80 
81 
82     override fun onCreate(savedInstanceState: Bundle?) {
83         super.onCreate(savedInstanceState)
84         requestWindowFeature(Window.FEATURE_NO_TITLE)
85         fragmentManager = supportFragmentManager
86         appOpsManager = getSystemService(AppOpsManager::class.java)
87         installRepository = InstallRepository(applicationContext)
88         installViewModel = ViewModelProvider(
89             this, InstallViewModelFactory(this.application, installRepository!!)
90         )[InstallViewModel::class.java]
91 
92         val intent = intent
93         val info = InstallRepository.CallerInfo(
94             intent.getStringExtra(EXTRA_CALLING_PKG_NAME),
95             intent.getIntExtra(EXTRA_CALLING_PKG_UID, Process.INVALID_UID)
96         )
97         installViewModel!!.preprocessIntent(intent, info)
98         installViewModel!!.currentInstallStage.observe(this) { installStage: InstallStage ->
99             onInstallStageChange(installStage)
100         }
101 
102         // Used to launch intent for Settings, to manage "install unknown apps" permission
103         unknownAppsIntentLauncher =
104             registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
105                 // Reattempt installation on coming back from Settings, after toggling
106                 // "install unknown apps" permission
107                 installViewModel!!.reattemptInstall()
108             }
109     }
110 
111     /**
112      * Main controller of the UI. This method shows relevant dialogs based on the install stage
113      */
114     private fun onInstallStageChange(installStage: InstallStage) {
115         when (installStage.stageCode) {
116             InstallStage.STAGE_STAGING -> {
117                 val stagingDialog = InstallStagingFragment()
118                 showDialogInner(stagingDialog)
119                 installViewModel!!.stagingProgress.observe(this) { progress: Int ->
120                     stagingDialog.setProgress(progress)
121                 }
122             }
123 
124             InstallStage.STAGE_ABORTED -> {
125                 val aborted = installStage as InstallAborted
126                 when (aborted.abortReason) {
127                     InstallAborted.ABORT_REASON_DONE,
128                     InstallAborted.ABORT_REASON_INTERNAL_ERROR -> {
129                         if (aborted.errorDialogType == InstallAborted.DLG_PACKAGE_ERROR) {
130                             val parseErrorDialog = ParseErrorFragment(aborted)
131                             showDialogInner(parseErrorDialog)
132                         } else {
133                             setResult(aborted.activityResultCode, aborted.resultIntent, true)
134                         }
135                     }
136 
137                     InstallAborted.ABORT_REASON_POLICY -> showPolicyRestrictionDialog(aborted)
138                     else -> setResult(Activity.RESULT_CANCELED, null, true)
139                 }
140             }
141 
142             InstallStage.STAGE_USER_ACTION_REQUIRED -> {
143                 val uar = installStage as InstallUserActionRequired
144                 when (uar.actionReason) {
145                     InstallUserActionRequired.USER_ACTION_REASON_INSTALL_CONFIRMATION -> {
146                         val actionDialog = InstallConfirmationFragment(uar)
147                         showDialogInner(actionDialog)
148                     }
149 
150                     InstallUserActionRequired.USER_ACTION_REASON_UNKNOWN_SOURCE -> {
151                         val externalSourceDialog = ExternalSourcesBlockedFragment(uar)
152                         showDialogInner(externalSourceDialog)
153                     }
154 
155                     InstallUserActionRequired.USER_ACTION_REASON_ANONYMOUS_SOURCE -> {
156                         val anonymousSourceDialog = AnonymousSourceFragment()
157                         showDialogInner(anonymousSourceDialog)
158                     }
159                 }
160             }
161 
162             InstallStage.STAGE_INSTALLING -> {
163                 val installing = installStage as InstallInstalling
164                 val installingDialog = InstallInstallingFragment(installing)
165                 showDialogInner(installingDialog)
166             }
167 
168             InstallStage.STAGE_SUCCESS -> {
169                 val success = installStage as InstallSuccess
170                 if (success.shouldReturnResult) {
171                     val successIntent = success.resultIntent
172                     setResult(Activity.RESULT_OK, successIntent, true)
173                 } else {
174                     val successFragment = InstallSuccessFragment(success)
175                     showDialogInner(successFragment)
176                 }
177             }
178 
179             InstallStage.STAGE_FAILED -> {
180                 val failed = installStage as InstallFailed
181                 val failedDialog = InstallFailedFragment(failed)
182                 showDialogInner(failedDialog)
183             }
184 
185             else -> {
186                 Log.d(LOG_TAG, "Unimplemented stage: " + installStage.stageCode)
187                 showDialogInner(null)
188             }
189         }
190     }
191 
192     private fun showPolicyRestrictionDialog(aborted: InstallAborted) {
193         val restriction = aborted.message
194         val adminSupportIntent = aborted.resultIntent
195         var shouldFinish: Boolean = false
196 
197         // If the given restriction is set by an admin, display information about the
198         // admin enforcing the restriction for the affected user. If not enforced by the admin,
199         // show the system dialog.
200         if (adminSupportIntent != null) {
201             if (localLogv) {
202                 Log.i(LOG_TAG, "Restriction set by admin, starting $adminSupportIntent")
203             }
204             startActivity(adminSupportIntent)
205             // Finish the package installer app since the next dialog will not be shown by this app
206             shouldFinish = true
207         } else {
208             if (localLogv) {
209                 Log.i(LOG_TAG, "Restriction set by system: $restriction")
210             }
211             val blockedByPolicyDialog = createDevicePolicyRestrictionDialog(restriction)
212             // Don't finish the package installer app since the next dialog
213             // will be shown by this app
214             shouldFinish = blockedByPolicyDialog == null
215             showDialogInner(blockedByPolicyDialog)
216         }
217         setResult(Activity.RESULT_CANCELED, null, shouldFinish)
218     }
219 
220     /**
221      * Create a new dialog based on the install restriction enforced.
222      *
223      * @param restriction The restriction to create the dialog for
224      * @return The dialog
225      */
226     private fun createDevicePolicyRestrictionDialog(restriction: String?): DialogFragment? {
227         if (localLogv) {
228             Log.i(LOG_TAG, "createDialog($restriction)")
229         }
230         return when (restriction) {
231             UserManager.DISALLOW_INSTALL_APPS ->
232                 SimpleErrorFragment(R.string.install_apps_user_restriction_dlg_text)
233 
234             UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES,
235             UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY ->
236                 SimpleErrorFragment(R.string.unknown_apps_user_restriction_dlg_text)
237 
238             else -> null
239         }
240     }
241 
242     /**
243      * Replace any visible dialog by the dialog returned by InstallRepository
244      *
245      * @param newDialog The new dialog to display
246      */
247     private fun showDialogInner(newDialog: DialogFragment?) {
248         val currentDialog = fragmentManager!!.findFragmentByTag(TAG_DIALOG) as DialogFragment?
249         currentDialog?.dismissAllowingStateLoss()
250         newDialog?.show(fragmentManager!!, TAG_DIALOG)
251     }
252 
253     fun setResult(resultCode: Int, data: Intent?, shouldFinish: Boolean) {
254         super.setResult(resultCode, data)
255         if (shouldFinish) {
256             finish()
257         }
258     }
259 
260     override fun onPositiveResponse(reasonCode: Int) {
261         if (localLogv) {
262             Log.d(LOG_TAG, "Positive button clicked. ReasonCode: $reasonCode")
263         }
264         when (reasonCode) {
265             InstallUserActionRequired.USER_ACTION_REASON_ANONYMOUS_SOURCE ->
266                 installViewModel!!.forcedSkipSourceCheck()
267 
268             InstallUserActionRequired.USER_ACTION_REASON_INSTALL_CONFIRMATION ->
269                 installViewModel!!.initiateInstall()
270         }
271     }
272 
273     override fun onNegativeResponse(stageCode: Int) {
274         if (localLogv) {
275             Log.d(LOG_TAG, "Negative button clicked. StageCode: $stageCode")
276         }
277         if (stageCode == InstallStage.STAGE_USER_ACTION_REQUIRED) {
278             installViewModel!!.cleanupInstall()
279         }
280         setResult(Activity.RESULT_CANCELED, null, true)
281     }
282 
283     override fun onNegativeResponse(resultCode: Int, data: Intent?) {
284         if (localLogv) {
285             Log.d(LOG_TAG, "Negative button clicked. resultCode: $resultCode; Intent: $data")
286         }
287         setResult(resultCode, data, true)
288     }
289 
290     override fun sendUnknownAppsIntent(sourcePackageName: String) {
291         if (localLogv) {
292             Log.d(LOG_TAG, "Launching unknown-apps settings intent for $sourcePackageName")
293         }
294         val settingsIntent = Intent()
295         settingsIntent.setAction(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES)
296         val packageUri = Uri.parse("package:$sourcePackageName")
297         settingsIntent.setData(packageUri)
298         settingsIntent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
299         try {
300             registerAppOpChangeListener(
301                 UnknownSourcesListener(sourcePackageName), sourcePackageName
302             )
303             unknownAppsIntentLauncher.launch(settingsIntent)
304         } catch (exc: ActivityNotFoundException) {
305             Log.e(
306                 LOG_TAG, "Settings activity not found for action: "
307                     + Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES
308             )
309         }
310     }
311 
312     override fun openInstalledApp(intent: Intent?) {
313         if (localLogv) {
314             Log.d(LOG_TAG, "Opening $intent")
315         }
316         setResult(Activity.RESULT_OK, intent, true)
317         if (intent != null && intent.hasCategory(Intent.CATEGORY_LAUNCHER)) {
318             startActivity(intent)
319         }
320     }
321 
322     private fun registerAppOpChangeListener(listener: UnknownSourcesListener, packageName: String) {
323         appOpsManager!!.startWatchingMode(
324             AppOpsManager.OPSTR_REQUEST_INSTALL_PACKAGES,
325             packageName,
326             listener
327         )
328         activeUnknownSourcesListeners.add(listener)
329     }
330 
331     private fun unregisterAppOpChangeListener(listener: UnknownSourcesListener) {
332         activeUnknownSourcesListeners.remove(listener)
333         appOpsManager!!.stopWatchingMode(listener)
334     }
335 
336     override fun onDestroy() {
337         super.onDestroy()
338         while (activeUnknownSourcesListeners.isNotEmpty()) {
339             unregisterAppOpChangeListener(activeUnknownSourcesListeners[0])
340         }
341     }
342 
343     private inner class UnknownSourcesListener(private val mOriginatingPackage: String) :
344         AppOpsManager.OnOpChangedListener {
345         override fun onOpChanged(op: String, packageName: String) {
346             if (mOriginatingPackage != packageName) {
347                 return
348             }
349             unregisterAppOpChangeListener(this)
350             activeUnknownSourcesListeners.remove(this)
351             if (isDestroyed) {
352                 return
353             }
354             Handler(Looper.getMainLooper()).postDelayed({
355                 if (!isDestroyed) {
356                     // Relaunch Pia to continue installation.
357                     startActivity(
358                         intent.putExtra(
359                             InstallRepository.EXTRA_STAGED_SESSION_ID,
360                             installViewModel!!.stagedSessionId
361                         )
362                     )
363 
364                     // If the userId of the root of activity stack is different from current userId,
365                     // starting Pia again lead to duplicate instances of the app in the stack.
366                     // As such, finish the old instance. Old Pia is finished even if the userId of
367                     // the root is the same, since there is no way to determine the difference in
368                     // userIds.
369                     finish()
370                 }
371             }, 500)
372         }
373     }
374 }
375