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