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