1 /* 2 * Copyright (C) 2015 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.tv.settings.device.apps; 18 19 import static android.content.pm.ApplicationInfo.FLAG_ALLOW_CLEAR_USER_DATA; 20 import static android.content.pm.ApplicationInfo.FLAG_SYSTEM; 21 22 import static com.android.tv.settings.util.InstrumentationUtils.logEntrySelected; 23 24 import android.app.Activity; 25 import android.app.ActivityManager; 26 import android.app.tvsettings.TvSettingsEnums; 27 import android.content.ActivityNotFoundException; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.pm.IPackageDataObserver; 31 import android.content.pm.PackageManager; 32 import android.content.pm.ResolveInfo; 33 import android.graphics.drawable.Drawable; 34 import android.os.Bundle; 35 import android.os.Handler; 36 import android.os.UserHandle; 37 import android.text.TextUtils; 38 import android.util.Log; 39 import android.widget.Toast; 40 41 import androidx.annotation.NonNull; 42 import androidx.preference.Preference; 43 import androidx.preference.PreferenceScreen; 44 45 import com.android.settingslib.applications.ApplicationsState; 46 import com.android.tv.settings.R; 47 import com.android.tv.settings.SettingsPreferenceFragment; 48 import com.android.tv.twopanelsettings.TwoPanelSettingsFragment; 49 50 import java.util.ArrayList; 51 import java.util.List; 52 53 /** 54 * Fragment for managing a single app 55 */ 56 public class AppManagementFragment extends SettingsPreferenceFragment { 57 private static final String TAG = "AppManagementFragment"; 58 59 private static final String ARG_PACKAGE_NAME = "packageName"; 60 61 private static final String KEY_VERSION = "version"; 62 private static final String KEY_OPEN = "open"; 63 private static final String KEY_FORCE_STOP = "forceStop"; 64 private static final String KEY_UNINSTALL = "uninstall"; 65 private static final String KEY_ENABLE_DISABLE = "enableDisable"; 66 private static final String KEY_APP_STORAGE = "appStorage"; 67 private static final String KEY_CLEAR_DATA = "clearData"; 68 private static final String KEY_CLEAR_CACHE = "clearCache"; 69 private static final String KEY_CLEAR_DEFAULTS = "clearDefaults"; 70 private static final String KEY_NOTIFICATIONS = "notifications"; 71 private static final String KEY_PERMISSIONS = "permissions"; 72 private static final String KEY_LICENSES = "licenses"; 73 74 // Intent action implemented by apps that have open source licenses to display under settings 75 private static final String VIEW_LICENSES_ACTION = "com.android.tv.settings.VIEW_LICENSES"; 76 77 // Result code identifiers 78 private static final int REQUEST_UNINSTALL = 1; 79 private static final int REQUEST_MANAGE_SPACE = 2; 80 private static final int REQUEST_UNINSTALL_UPDATES = 3; 81 82 private PackageManager mPackageManager; 83 private String mPackageName; 84 private ApplicationsState mApplicationsState; 85 private ApplicationsState.Session mSession; 86 private ApplicationsState.AppEntry mEntry; 87 private final ApplicationsState.Callbacks mCallbacks = new ApplicationsStateCallbacks(); 88 89 private ForceStopPreference mForceStopPreference; 90 private UninstallPreference mUninstallPreference; 91 private EnableDisablePreference mEnableDisablePreference; 92 private AppStoragePreference mAppStoragePreference; 93 private ClearDataPreference mClearDataPreference; 94 private ClearCachePreference mClearCachePreference; 95 private ClearDefaultsPreference mClearDefaultsPreference; 96 private NotificationsPreference mNotificationsPreference; 97 98 private final Handler mHandler = new Handler(); 99 private Runnable mBailoutRunnable = () -> { 100 if (isResumed() && !getFragmentManager().popBackStackImmediate()) { 101 getActivity().onBackPressed(); 102 } 103 }; 104 prepareArgs(@onNull Bundle args, String packageName)105 public static void prepareArgs(@NonNull Bundle args, String packageName) { 106 args.putString(ARG_PACKAGE_NAME, packageName); 107 } 108 109 @Override onCreate(Bundle savedInstanceState)110 public void onCreate(Bundle savedInstanceState) { 111 mPackageName = getArguments().getString(ARG_PACKAGE_NAME); 112 113 final Activity activity = getActivity(); 114 mPackageManager = activity.getPackageManager(); 115 mApplicationsState = ApplicationsState.getInstance(activity.getApplication()); 116 mSession = mApplicationsState.newSession(mCallbacks, getLifecycle()); 117 mEntry = mApplicationsState.getEntry(mPackageName, UserHandle.myUserId()); 118 119 super.onCreate(savedInstanceState); 120 } 121 122 @Override onResume()123 public void onResume() { 124 super.onResume(); 125 126 if (mEntry == null) { 127 Log.w(TAG, "App not found, trying to bail out"); 128 navigateBack(); 129 } 130 131 if (mClearDefaultsPreference != null) { 132 mClearDefaultsPreference.refresh(); 133 } 134 if (mEnableDisablePreference != null) { 135 mEnableDisablePreference.refresh(); 136 } 137 } 138 139 @Override onPause()140 public void onPause() { 141 super.onPause(); 142 mHandler.removeCallbacks(mBailoutRunnable); 143 } 144 145 @Override onActivityResult(int requestCode, int resultCode, Intent data)146 public void onActivityResult(int requestCode, int resultCode, Intent data) { 147 super.onActivityResult(requestCode, resultCode, data); 148 if (mEntry == null) { 149 return; 150 } 151 switch (requestCode) { 152 case REQUEST_UNINSTALL: 153 final int deleteResult = data != null 154 ? data.getIntExtra(Intent.EXTRA_INSTALL_RESULT, 0) : 0; 155 if (deleteResult == PackageManager.DELETE_SUCCEEDED) { 156 final int userId = UserHandle.getUserId(mEntry.info.uid); 157 mApplicationsState.removePackage(mPackageName, userId); 158 navigateBack(); 159 } else { 160 Log.e(TAG, "Uninstall failed with result " + deleteResult); 161 } 162 break; 163 case REQUEST_MANAGE_SPACE: 164 mClearDataPreference.setClearingData(false); 165 if (resultCode == Activity.RESULT_OK) { 166 final int userId = UserHandle.getUserId(mEntry.info.uid); 167 mApplicationsState.requestSize(mPackageName, userId); 168 } else { 169 Log.w(TAG, "Failed to clear data!"); 170 } 171 break; 172 case REQUEST_UNINSTALL_UPDATES: 173 mUninstallPreference.refresh(); 174 break; 175 } 176 } 177 navigateBack()178 private void navigateBack() { 179 if (getCallbackFragment() instanceof TwoPanelSettingsFragment) { 180 TwoPanelSettingsFragment parentFragment = 181 (TwoPanelSettingsFragment) getCallbackFragment(); 182 if (parentFragment.isFragmentInTheMainPanel(this)) { 183 parentFragment.navigateBack(); 184 } 185 } else { 186 // need to post this to avoid recursing in the fragment manager. 187 mHandler.removeCallbacks(mBailoutRunnable); 188 mHandler.post(mBailoutRunnable); 189 } 190 } 191 192 @Override onPreferenceTreeClick(Preference preference)193 public boolean onPreferenceTreeClick(Preference preference) { 194 if (preference.equals(mEnableDisablePreference)) { 195 // disable the preference to prevent double clicking 196 mHandler.post(() -> { 197 mEnableDisablePreference.setEnabled(false); 198 }); 199 } 200 final Intent intent = preference.getIntent(); 201 if (intent != null) { 202 try { 203 if (preference.equals(mUninstallPreference)) { 204 startActivityForResult(intent, mUninstallPreference.canUninstall() 205 ? REQUEST_UNINSTALL : REQUEST_UNINSTALL_UPDATES); 206 } else { 207 startActivity(intent); 208 } 209 } catch (ActivityNotFoundException e) { 210 Log.e(TAG, "Could not find activity to launch", e); 211 Toast.makeText(getContext(), R.string.device_apps_app_management_not_available, 212 Toast.LENGTH_SHORT).show(); 213 } 214 return true; 215 } else { 216 return super.onPreferenceTreeClick(preference); 217 } 218 } 219 220 @Override onCreatePreferences(Bundle savedInstanceState, String rootKey)221 public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 222 final Context themedContext = getPreferenceManager().getContext(); 223 final PreferenceScreen screen = 224 getPreferenceManager().createPreferenceScreen(themedContext); 225 screen.setTitle(getAppName()); 226 setPreferenceScreen(screen); 227 228 updatePrefs(); 229 230 if (Intent.ACTION_AUTO_REVOKE_PERMISSIONS.equals(getActivity().getIntent().getAction())) { 231 scrollToPreference(findPreference(KEY_PERMISSIONS)); 232 } 233 } 234 updatePrefs()235 private void updatePrefs() { 236 if (mEntry == null) { 237 final PreferenceScreen screen = getPreferenceScreen(); 238 screen.removeAll(); 239 return; 240 } 241 final Context themedContext = getPreferenceManager().getContext(); 242 243 // Version 244 Preference versionPreference = findPreference(KEY_VERSION); 245 if (versionPreference == null) { 246 versionPreference = new Preference(themedContext); 247 versionPreference.setKey(KEY_VERSION); 248 replacePreference(versionPreference); 249 versionPreference.setSelectable(false); 250 } 251 versionPreference.setTitle(getString(R.string.device_apps_app_management_version, 252 mEntry.getVersion(getActivity()))); 253 versionPreference.setSummary(mPackageName); 254 255 // Open 256 Preference openPreference = findPreference(KEY_OPEN); 257 if (openPreference == null) { 258 openPreference = new Preference(themedContext); 259 openPreference.setKey(KEY_OPEN); 260 replacePreference(openPreference); 261 } 262 Intent appLaunchIntent = 263 mPackageManager.getLeanbackLaunchIntentForPackage(mEntry.info.packageName); 264 if (appLaunchIntent == null) { 265 appLaunchIntent = mPackageManager.getLaunchIntentForPackage(mEntry.info.packageName); 266 } 267 if (appLaunchIntent != null) { 268 openPreference.setIntent(appLaunchIntent); 269 openPreference.setTitle(R.string.device_apps_app_management_open); 270 openPreference.setVisible(true); 271 openPreference.setOnPreferenceClickListener( 272 preference -> { 273 logEntrySelected(TvSettingsEnums.APPS_ALL_APPS_APP_ENTRY_OPEN); 274 return false; 275 }); 276 } else { 277 openPreference.setVisible(false); 278 } 279 280 // Force stop 281 if (mForceStopPreference == null) { 282 mForceStopPreference = new ForceStopPreference(themedContext, mEntry); 283 mForceStopPreference.setKey(KEY_FORCE_STOP); 284 replacePreference(mForceStopPreference); 285 } else { 286 mForceStopPreference.setEntry(mEntry); 287 } 288 289 // Uninstall 290 if (mUninstallPreference == null) { 291 mUninstallPreference = new UninstallPreference(themedContext, mEntry); 292 mUninstallPreference.setKey(KEY_UNINSTALL); 293 replacePreference(mUninstallPreference); 294 } else { 295 mUninstallPreference.setEntry(mEntry); 296 } 297 298 // Disable/Enable 299 if (mEnableDisablePreference == null) { 300 mEnableDisablePreference = new EnableDisablePreference(themedContext, mEntry); 301 mEnableDisablePreference.setKey(KEY_ENABLE_DISABLE); 302 replacePreference(mEnableDisablePreference); 303 } else { 304 mEnableDisablePreference.setEntry(mEntry); 305 if (!mEnableDisablePreference.isRestricted()) { 306 mEnableDisablePreference.setEnabled(true); 307 } 308 } 309 310 // Storage used 311 if (mAppStoragePreference == null) { 312 mAppStoragePreference = new AppStoragePreference(themedContext, mEntry); 313 mAppStoragePreference.setKey(KEY_APP_STORAGE); 314 replacePreference(mAppStoragePreference); 315 } else { 316 mAppStoragePreference.setEntry(mEntry); 317 } 318 319 // Clear data 320 if (clearDataAllowed()) { 321 if (mClearDataPreference == null) { 322 mClearDataPreference = new ClearDataPreference(themedContext, mEntry); 323 mClearDataPreference.setKey(KEY_CLEAR_DATA); 324 replacePreference(mClearDataPreference); 325 } else { 326 mClearDataPreference.setEntry(mEntry); 327 } 328 } 329 330 // Clear cache 331 if (mClearCachePreference == null) { 332 mClearCachePreference = new ClearCachePreference(themedContext, mEntry); 333 mClearCachePreference.setKey(KEY_CLEAR_CACHE); 334 replacePreference(mClearCachePreference); 335 } else { 336 mClearCachePreference.setEntry(mEntry); 337 } 338 339 // Clear defaults 340 if (mClearDefaultsPreference == null) { 341 mClearDefaultsPreference = new ClearDefaultsPreference(themedContext, mEntry); 342 mClearDefaultsPreference.setKey(KEY_CLEAR_DEFAULTS); 343 replacePreference(mClearDefaultsPreference); 344 } else { 345 mClearDefaultsPreference.setEntry(mEntry); 346 } 347 348 // Notifications 349 if (mNotificationsPreference == null) { 350 mNotificationsPreference = new NotificationsPreference(themedContext, mEntry); 351 mNotificationsPreference.setKey(KEY_NOTIFICATIONS); 352 replacePreference(mNotificationsPreference); 353 } else { 354 mNotificationsPreference.setEntry(mEntry); 355 } 356 357 // Open Source Licenses 358 Preference licensesPreference = findPreference(KEY_LICENSES); 359 if (licensesPreference == null) { 360 licensesPreference = new Preference(themedContext); 361 licensesPreference.setKey(KEY_LICENSES); 362 replacePreference(licensesPreference); 363 } 364 // Check if app has open source licenses to display 365 Intent licenseIntent = new Intent(VIEW_LICENSES_ACTION); 366 licenseIntent.setPackage(mEntry.info.packageName); 367 ResolveInfo resolveInfo = resolveIntent(licenseIntent); 368 if (resolveInfo == null) { 369 licensesPreference.setVisible(false); 370 } else { 371 Intent intent = new Intent(licenseIntent); 372 intent.setClassName(resolveInfo.activityInfo.packageName, 373 resolveInfo.activityInfo.name); 374 licensesPreference.setIntent(intent); 375 licensesPreference.setTitle(R.string.device_apps_app_management_licenses); 376 licensesPreference.setOnPreferenceClickListener( 377 preference -> { 378 logEntrySelected(TvSettingsEnums.APPS_ALL_APPS_APP_ENTRY_LICENSES); 379 return false; 380 }); 381 licensesPreference.setVisible(true); 382 } 383 384 // Permissions 385 Preference permissionsPreference = findPreference(KEY_PERMISSIONS); 386 if (permissionsPreference == null) { 387 permissionsPreference = new Preference(themedContext); 388 permissionsPreference.setKey(KEY_PERMISSIONS); 389 permissionsPreference.setTitle(R.string.device_apps_app_management_permissions); 390 replacePreference(permissionsPreference); 391 } 392 permissionsPreference.setOnPreferenceClickListener( 393 preference -> { 394 logEntrySelected(TvSettingsEnums.APPS_ALL_APPS_APP_ENTRY_PERMISSIONS); 395 return false; 396 }); 397 permissionsPreference.setIntent(new Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS) 398 .putExtra(Intent.EXTRA_PACKAGE_NAME, mPackageName)); 399 } 400 replacePreference(Preference preference)401 private void replacePreference(Preference preference) { 402 final String key = preference.getKey(); 403 if (TextUtils.isEmpty(key)) { 404 throw new IllegalArgumentException("Can't replace a preference without a key"); 405 } 406 final Preference old = findPreference(key); 407 if (old != null) { 408 getPreferenceScreen().removePreference(old); 409 } 410 getPreferenceScreen().addPreference(preference); 411 } 412 resolveIntent(Intent intent)413 private ResolveInfo resolveIntent(Intent intent) { 414 List<ResolveInfo> resolveInfos = mPackageManager.queryIntentActivities(intent, 0); 415 return (resolveInfos == null || resolveInfos.size() <= 0) ? null : resolveInfos.get(0); 416 } 417 getAppName()418 public String getAppName() { 419 if (mEntry == null) { 420 return null; 421 } 422 mEntry.ensureLabel(getActivity()); 423 return mEntry.label; 424 } 425 getAppIcon()426 public Drawable getAppIcon() { 427 if (mEntry == null) { 428 return null; 429 } 430 mApplicationsState.ensureIcon(mEntry); 431 return mEntry.icon; 432 } 433 clearData()434 public void clearData() { 435 if (!clearDataAllowed()) { 436 Log.e(TAG, "Attempt to clear data failed. Clear data is disabled for " + mPackageName); 437 return; 438 } 439 440 mClearDataPreference.setClearingData(true); 441 String spaceManagementActivityName = mEntry.info.manageSpaceActivityName; 442 if (spaceManagementActivityName != null) { 443 if (!ActivityManager.isUserAMonkey()) { 444 Intent intent = new Intent(Intent.ACTION_DEFAULT); 445 intent.setClassName(mEntry.info.packageName, spaceManagementActivityName); 446 startActivityForResult(intent, REQUEST_MANAGE_SPACE); 447 } 448 } else { 449 // Disabling clear cache preference while clearing data is in progress. See b/77815256 450 // for details. 451 mClearCachePreference.setClearingCache(true); 452 ActivityManager am = (ActivityManager) getActivity().getSystemService( 453 Context.ACTIVITY_SERVICE); 454 boolean success = am.clearApplicationUserData( 455 mEntry.info.packageName, new IPackageDataObserver.Stub() { 456 public void onRemoveCompleted( 457 final String packageName, final boolean succeeded) { 458 mHandler.post(new Runnable() { 459 @Override 460 public void run() { 461 mClearDataPreference.setClearingData(false); 462 mClearCachePreference.setClearingCache(false); 463 if (succeeded) { 464 dataCleared(true); 465 } else { 466 dataCleared(false); 467 } 468 } 469 }); 470 } 471 }); 472 if (!success) { 473 mClearDataPreference.setClearingData(false); 474 dataCleared(false); 475 } 476 } 477 mClearDataPreference.refresh(); 478 } 479 dataCleared(boolean succeeded)480 private void dataCleared(boolean succeeded) { 481 if (succeeded) { 482 final int userId = UserHandle.getUserId(mEntry.info.uid); 483 mApplicationsState.requestSize(mPackageName, userId); 484 } else { 485 Log.w(TAG, "Failed to clear data!"); 486 mClearDataPreference.refresh(); 487 } 488 } 489 clearCache()490 public void clearCache() { 491 mClearCachePreference.setClearingCache(true); 492 mPackageManager.deleteApplicationCacheFiles(mEntry.info.packageName, 493 new IPackageDataObserver.Stub() { 494 public void onRemoveCompleted(final String packageName, 495 final boolean succeeded) { 496 mHandler.post(new Runnable() { 497 @Override 498 public void run() { 499 mClearCachePreference.setClearingCache(false); 500 cacheCleared(succeeded); 501 } 502 }); 503 } 504 }); 505 mClearCachePreference.refresh(); 506 } 507 cacheCleared(boolean succeeded)508 private void cacheCleared(boolean succeeded) { 509 if (succeeded) { 510 final int userId = UserHandle.getUserId(mEntry.info.uid); 511 mApplicationsState.requestSize(mPackageName, userId); 512 } else { 513 Log.w(TAG, "Failed to clear cache!"); 514 mClearCachePreference.refresh(); 515 } 516 } 517 518 /** 519 * Clearing data can only be disabled for system apps. For all non-system apps it is enabled. 520 * System apps disable it explicitly via the android:allowClearUserData tag. 521 **/ clearDataAllowed()522 private boolean clearDataAllowed() { 523 boolean sysApp = (mEntry.info.flags & FLAG_SYSTEM) == FLAG_SYSTEM; 524 boolean allowClearData = 525 (mEntry.info.flags & FLAG_ALLOW_CLEAR_USER_DATA) == FLAG_ALLOW_CLEAR_USER_DATA; 526 return !sysApp || allowClearData; 527 } 528 529 @Override getPageId()530 protected int getPageId() { 531 return TvSettingsEnums.APPS_ALL_APPS_APP_ENTRY; 532 } 533 534 private class ApplicationsStateCallbacks implements ApplicationsState.Callbacks { 535 536 @Override onRunningStateChanged(boolean running)537 public void onRunningStateChanged(boolean running) { 538 if (mForceStopPreference != null) { 539 mForceStopPreference.refresh(); 540 } 541 } 542 543 @Override onPackageListChanged()544 public void onPackageListChanged() { 545 if (mEntry == null || mEntry.info == null) { 546 return; 547 } 548 final int userId = UserHandle.getUserId(mEntry.info.uid); 549 mEntry = mApplicationsState.getEntry(mPackageName, userId); 550 if (mEntry == null) { 551 navigateBack(); 552 } 553 updatePrefs(); 554 } 555 556 @Override onRebuildComplete(ArrayList<ApplicationsState.AppEntry> apps)557 public void onRebuildComplete(ArrayList<ApplicationsState.AppEntry> apps) {} 558 559 @Override onPackageIconChanged()560 public void onPackageIconChanged() {} 561 562 @Override onPackageSizeChanged(String packageName)563 public void onPackageSizeChanged(String packageName) { 564 if (mAppStoragePreference == null) { 565 // Nothing to do here. 566 return; 567 } 568 mAppStoragePreference.refresh(); 569 mClearCachePreference.refresh(); 570 571 if (mClearDataPreference != null) { 572 mClearDataPreference.refresh(); 573 } 574 } 575 576 @Override onAllSizesComputed()577 public void onAllSizesComputed() { 578 if (mAppStoragePreference == null) { 579 // Nothing to do here. 580 return; 581 } 582 mAppStoragePreference.refresh(); 583 mClearCachePreference.refresh(); 584 585 if (mClearDataPreference != null) { 586 mClearDataPreference.refresh(); 587 } 588 } 589 590 @Override onLauncherInfoChanged()591 public void onLauncherInfoChanged() { 592 updatePrefs(); 593 } 594 595 @Override onLoadEntriesCompleted()596 public void onLoadEntriesCompleted() { 597 mEntry = mApplicationsState.getEntry(mPackageName, UserHandle.myUserId()); 598 updatePrefs(); 599 if (mAppStoragePreference == null) { 600 // Nothing to do here. 601 return; 602 } 603 mAppStoragePreference.refresh(); 604 mClearCachePreference.refresh(); 605 606 if (mClearDataPreference != null) { 607 mClearDataPreference.refresh(); 608 } 609 } 610 } 611 } 612