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