1 /* 2 * Copyright (C) 2018 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.server.wm; 18 19 import android.annotation.Nullable; 20 import android.content.ComponentName; 21 import android.content.pm.ActivityInfo; 22 import android.content.pm.PackageManagerInternal; 23 import android.graphics.Rect; 24 import android.os.Environment; 25 import android.util.ArrayMap; 26 import android.util.ArraySet; 27 import android.util.AtomicFile; 28 import android.util.Slog; 29 import android.util.SparseArray; 30 import android.util.Xml; 31 import android.view.DisplayInfo; 32 33 import com.android.internal.annotations.VisibleForTesting; 34 import com.android.modules.utils.TypedXmlPullParser; 35 import com.android.modules.utils.TypedXmlSerializer; 36 import com.android.server.LocalServices; 37 import com.android.server.pm.PackageList; 38 import com.android.server.wm.LaunchParamsController.LaunchParams; 39 40 import org.xmlpull.v1.XmlPullParser; 41 42 import java.io.ByteArrayOutputStream; 43 import java.io.File; 44 import java.io.FileInputStream; 45 import java.io.FileOutputStream; 46 import java.io.IOException; 47 import java.io.InputStream; 48 import java.util.ArrayList; 49 import java.util.List; 50 import java.util.Map; 51 import java.util.Objects; 52 import java.util.Set; 53 import java.util.function.IntFunction; 54 55 /** 56 * Persister that saves launch parameters in memory and in storage. It saves the last seen state of 57 * tasks key-ed on task's user ID and the activity used to launch the task ({@link 58 * Task#realActivity}) and that's used to determine the launch params when the activity is 59 * being launched again in {@link LaunchParamsController}. 60 * 61 * Need to hold {@link ActivityTaskManagerService#getGlobalLock()} to access this class. 62 */ 63 class LaunchParamsPersister { 64 private static final String TAG = "LaunchParamsPersister"; 65 private static final String LAUNCH_PARAMS_DIRNAME = "launch_params"; 66 private static final String LAUNCH_PARAMS_FILE_SUFFIX = ".xml"; 67 68 // Chars below are used to escape the backslash in component name to underscore. 69 private static final char ORIGINAL_COMPONENT_SEPARATOR = '/'; 70 private static final char ESCAPED_COMPONENT_SEPARATOR = '-'; 71 private static final char OLD_ESCAPED_COMPONENT_SEPARATOR = '_'; 72 73 private static final String TAG_LAUNCH_PARAMS = "launch_params"; 74 75 private final PersisterQueue mPersisterQueue; 76 private final ActivityTaskSupervisor mSupervisor; 77 78 /** 79 * A function that takes in user ID and returns a folder to store information of that user. Used 80 * to differentiate storage location in test environment and production environment. 81 */ 82 private final IntFunction<File> mUserFolderGetter; 83 84 private PackageList mPackageList; 85 86 /** 87 * A dual layer map that first maps user ID to a secondary map, which maps component name (the 88 * launching activity of tasks) to {@link PersistableLaunchParams} that stores launch metadata 89 * that are stable across reboots. 90 */ 91 private final SparseArray<ArrayMap<ComponentName, PersistableLaunchParams>> mLaunchParamsMap = 92 new SparseArray<>(); 93 94 /** 95 * A map from {@link android.content.pm.ActivityInfo.WindowLayout#windowLayoutAffinity} to 96 * activity's component name for reverse queries from window layout affinities to activities. 97 * Used to decide if we should use another activity's record with the same affinity. 98 */ 99 private final ArrayMap<String, ArraySet<ComponentName>> mWindowLayoutAffinityMap = 100 new ArrayMap<>(); 101 LaunchParamsPersister(PersisterQueue persisterQueue, ActivityTaskSupervisor supervisor)102 LaunchParamsPersister(PersisterQueue persisterQueue, ActivityTaskSupervisor supervisor) { 103 this(persisterQueue, supervisor, Environment::getDataSystemCeDirectory); 104 } 105 106 @VisibleForTesting LaunchParamsPersister(PersisterQueue persisterQueue, ActivityTaskSupervisor supervisor, IntFunction<File> userFolderGetter)107 LaunchParamsPersister(PersisterQueue persisterQueue, ActivityTaskSupervisor supervisor, 108 IntFunction<File> userFolderGetter) { 109 mPersisterQueue = persisterQueue; 110 mSupervisor = supervisor; 111 mUserFolderGetter = userFolderGetter; 112 } 113 onSystemReady()114 void onSystemReady() { 115 PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class); 116 mPackageList = pmi.getPackageList(new PackageListObserver()); 117 } 118 onUnlockUser(int userId)119 void onUnlockUser(int userId) { 120 loadLaunchParams(userId); 121 } 122 onCleanupUser(int userId)123 void onCleanupUser(int userId) { 124 mLaunchParamsMap.remove(userId); 125 } 126 loadLaunchParams(int userId)127 private void loadLaunchParams(int userId) { 128 final List<File> filesToDelete = new ArrayList<>(); 129 final File launchParamsFolder = getLaunchParamFolder(userId); 130 if (!launchParamsFolder.isDirectory()) { 131 Slog.i(TAG, "Didn't find launch param folder for user " + userId); 132 return; 133 } 134 135 final Set<String> packages = new ArraySet<>(mPackageList.getPackageNames()); 136 137 final File[] paramsFiles = launchParamsFolder.listFiles(); 138 final ArrayMap<ComponentName, PersistableLaunchParams> map = 139 new ArrayMap<>(paramsFiles.length); 140 mLaunchParamsMap.put(userId, map); 141 142 for (File paramsFile : paramsFiles) { 143 if (!paramsFile.isFile()) { 144 Slog.w(TAG, paramsFile.getAbsolutePath() + " is not a file."); 145 continue; 146 } 147 if (!paramsFile.getName().endsWith(LAUNCH_PARAMS_FILE_SUFFIX)) { 148 Slog.w(TAG, "Unexpected params file name: " + paramsFile.getName()); 149 filesToDelete.add(paramsFile); 150 continue; 151 } 152 String paramsFileName = paramsFile.getName(); 153 // Migrate all records from old separator to new separator. 154 final int oldSeparatorIndex = 155 paramsFileName.indexOf(OLD_ESCAPED_COMPONENT_SEPARATOR); 156 if (oldSeparatorIndex != -1) { 157 if (paramsFileName.indexOf( 158 OLD_ESCAPED_COMPONENT_SEPARATOR, oldSeparatorIndex + 1) != -1) { 159 // Rare case. We have more than one old escaped component separator probably 160 // because this app uses underscore in their package name. We can't distinguish 161 // which one is the real separator so let's skip it. 162 filesToDelete.add(paramsFile); 163 continue; 164 } 165 paramsFileName = paramsFileName.replace( 166 OLD_ESCAPED_COMPONENT_SEPARATOR, ESCAPED_COMPONENT_SEPARATOR); 167 final File newFile = new File(launchParamsFolder, paramsFileName); 168 if (paramsFile.renameTo(newFile)) { 169 paramsFile = newFile; 170 } else { 171 // Rare case. For some reason we can't rename the file. Let's drop this record 172 // instead. 173 filesToDelete.add(paramsFile); 174 continue; 175 } 176 } 177 final String componentNameString = paramsFileName.substring( 178 0 /* beginIndex */, 179 paramsFileName.length() - LAUNCH_PARAMS_FILE_SUFFIX.length()) 180 .replace(ESCAPED_COMPONENT_SEPARATOR, ORIGINAL_COMPONENT_SEPARATOR); 181 final ComponentName name = ComponentName.unflattenFromString( 182 componentNameString); 183 if (name == null) { 184 Slog.w(TAG, "Unexpected file name: " + paramsFileName); 185 filesToDelete.add(paramsFile); 186 continue; 187 } 188 189 if (!packages.contains(name.getPackageName())) { 190 // Rare case. PersisterQueue doesn't have a chance to remove files for removed 191 // packages last time. 192 filesToDelete.add(paramsFile); 193 continue; 194 } 195 196 try (InputStream in = new FileInputStream(paramsFile)) { 197 final PersistableLaunchParams params = new PersistableLaunchParams(); 198 final TypedXmlPullParser parser = Xml.resolvePullParser(in); 199 int event; 200 while ((event = parser.next()) != XmlPullParser.END_DOCUMENT 201 && event != XmlPullParser.END_TAG) { 202 if (event != XmlPullParser.START_TAG) { 203 continue; 204 } 205 206 final String tagName = parser.getName(); 207 if (!TAG_LAUNCH_PARAMS.equals(tagName)) { 208 Slog.w(TAG, "Unexpected tag name: " + tagName); 209 continue; 210 } 211 212 params.restore(paramsFile, parser); 213 } 214 215 map.put(name, params); 216 addComponentNameToLaunchParamAffinityMapIfNotNull( 217 name, params.mWindowLayoutAffinity); 218 } catch (Exception e) { 219 Slog.w(TAG, "Failed to restore launch params for " + name, e); 220 filesToDelete.add(paramsFile); 221 } 222 } 223 224 if (!filesToDelete.isEmpty()) { 225 mPersisterQueue.addItem(new CleanUpComponentQueueItem(filesToDelete), true); 226 } 227 } 228 saveTask(Task task)229 void saveTask(Task task) { 230 saveTask(task, task.getDisplayContent()); 231 } 232 saveTask(Task task, DisplayContent display)233 void saveTask(Task task, DisplayContent display) { 234 final ComponentName name = task.realActivity; 235 if (name == null) { 236 return; 237 } 238 final int userId = task.mUserId; 239 PersistableLaunchParams params; 240 ArrayMap<ComponentName, PersistableLaunchParams> map = mLaunchParamsMap.get(userId); 241 if (map == null) { 242 map = new ArrayMap<>(); 243 mLaunchParamsMap.put(userId, map); 244 } 245 246 params = map.computeIfAbsent(name, componentName -> new PersistableLaunchParams()); 247 final boolean changed = saveTaskToLaunchParam(task, display, params); 248 249 addComponentNameToLaunchParamAffinityMapIfNotNull(name, params.mWindowLayoutAffinity); 250 251 if (changed) { 252 mPersisterQueue.updateLastOrAddItem( 253 new LaunchParamsWriteQueueItem(userId, name, params), 254 /* flush */ false); 255 } 256 } 257 saveTaskToLaunchParam( Task task, DisplayContent display, PersistableLaunchParams params)258 private boolean saveTaskToLaunchParam( 259 Task task, DisplayContent display, PersistableLaunchParams params) { 260 final DisplayInfo info = new DisplayInfo(); 261 display.mDisplay.getDisplayInfo(info); 262 263 boolean changed = !Objects.equals(params.mDisplayUniqueId, info.uniqueId); 264 params.mDisplayUniqueId = info.uniqueId; 265 266 changed |= params.mWindowingMode != task.getWindowingMode(); 267 params.mWindowingMode = task.getWindowingMode(); 268 269 if (task.mLastNonFullscreenBounds != null) { 270 changed |= !Objects.equals(params.mBounds, task.mLastNonFullscreenBounds); 271 params.mBounds.set(task.mLastNonFullscreenBounds); 272 } else { 273 changed |= !params.mBounds.isEmpty(); 274 params.mBounds.setEmpty(); 275 } 276 277 String launchParamAffinity = task.mWindowLayoutAffinity; 278 changed |= Objects.equals(launchParamAffinity, params.mWindowLayoutAffinity); 279 params.mWindowLayoutAffinity = launchParamAffinity; 280 281 if (changed) { 282 params.mTimestamp = System.currentTimeMillis(); 283 } 284 285 return changed; 286 } 287 addComponentNameToLaunchParamAffinityMapIfNotNull( ComponentName name, String launchParamAffinity)288 private void addComponentNameToLaunchParamAffinityMapIfNotNull( 289 ComponentName name, String launchParamAffinity) { 290 if (launchParamAffinity == null) { 291 return; 292 } 293 mWindowLayoutAffinityMap.computeIfAbsent(launchParamAffinity, affinity -> new ArraySet<>()) 294 .add(name); 295 } 296 getLaunchParams(Task task, ActivityRecord activity, LaunchParams outParams)297 void getLaunchParams(Task task, ActivityRecord activity, LaunchParams outParams) { 298 final ComponentName name = task != null ? task.realActivity : activity.mActivityComponent; 299 final int userId = task != null ? task.mUserId : activity.mUserId; 300 final String windowLayoutAffinity; 301 if (task != null) { 302 windowLayoutAffinity = task.mWindowLayoutAffinity; 303 } else { 304 ActivityInfo.WindowLayout layout = activity.info.windowLayout; 305 windowLayoutAffinity = layout == null ? null : layout.windowLayoutAffinity; 306 } 307 308 outParams.reset(); 309 Map<ComponentName, PersistableLaunchParams> map = mLaunchParamsMap.get(userId); 310 if (map == null) { 311 return; 312 } 313 314 // First use its own record as a reference. 315 PersistableLaunchParams persistableParams = map.get(name); 316 // Next we'll compare these params against all existing params with the same affinity and 317 // use the newest one. 318 if (windowLayoutAffinity != null 319 && mWindowLayoutAffinityMap.get(windowLayoutAffinity) != null) { 320 ArraySet<ComponentName> candidates = mWindowLayoutAffinityMap.get(windowLayoutAffinity); 321 for (int i = 0; i < candidates.size(); ++i) { 322 ComponentName candidate = candidates.valueAt(i); 323 final PersistableLaunchParams candidateParams = map.get(candidate); 324 if (candidateParams == null) { 325 continue; 326 } 327 328 if (persistableParams == null 329 || candidateParams.mTimestamp > persistableParams.mTimestamp) { 330 persistableParams = candidateParams; 331 } 332 } 333 } 334 335 if (persistableParams == null) { 336 return; 337 } 338 339 final DisplayContent display = mSupervisor.mRootWindowContainer.getDisplayContent( 340 persistableParams.mDisplayUniqueId); 341 if (display != null) { 342 // TODO(b/153764726): Investigate if task display area needs to be persisted vs 343 // always choosing the default one. 344 outParams.mPreferredTaskDisplayArea = display.getDefaultTaskDisplayArea(); 345 } 346 outParams.mWindowingMode = persistableParams.mWindowingMode; 347 outParams.mBounds.set(persistableParams.mBounds); 348 } 349 removeRecordForPackage(String packageName)350 void removeRecordForPackage(String packageName) { 351 final List<File> fileToDelete = new ArrayList<>(); 352 for (int i = 0; i < mLaunchParamsMap.size(); ++i) { 353 int userId = mLaunchParamsMap.keyAt(i); 354 final File launchParamsFolder = getLaunchParamFolder(userId); 355 ArrayMap<ComponentName, PersistableLaunchParams> map = mLaunchParamsMap.valueAt(i); 356 for (int j = map.size() - 1; j >= 0; --j) { 357 final ComponentName name = map.keyAt(j); 358 if (name.getPackageName().equals(packageName)) { 359 map.removeAt(j); 360 fileToDelete.add(getParamFile(launchParamsFolder, name)); 361 } 362 } 363 } 364 365 synchronized (mPersisterQueue) { 366 mPersisterQueue.removeItems( 367 item -> item.mComponentName.getPackageName().equals(packageName), 368 LaunchParamsWriteQueueItem.class); 369 370 mPersisterQueue.addItem(new CleanUpComponentQueueItem(fileToDelete), true); 371 } 372 } 373 getParamFile(File launchParamFolder, ComponentName name)374 private File getParamFile(File launchParamFolder, ComponentName name) { 375 final String componentNameString = name.flattenToShortString() 376 .replace(ORIGINAL_COMPONENT_SEPARATOR, ESCAPED_COMPONENT_SEPARATOR); 377 return new File(launchParamFolder, componentNameString + LAUNCH_PARAMS_FILE_SUFFIX); 378 } 379 getLaunchParamFolder(int userId)380 private File getLaunchParamFolder(int userId) { 381 final File userFolder = mUserFolderGetter.apply(userId); 382 return new File(userFolder, LAUNCH_PARAMS_DIRNAME); 383 } 384 385 private class PackageListObserver implements PackageManagerInternal.PackageListObserver { 386 @Override onPackageAdded(String packageName, int uid)387 public void onPackageAdded(String packageName, int uid) {} 388 389 @Override onPackageRemoved(String packageName, int uid)390 public void onPackageRemoved(String packageName, int uid) { 391 synchronized (mSupervisor.mService.getGlobalLock()) { 392 removeRecordForPackage(packageName); 393 } 394 } 395 } 396 397 private class LaunchParamsWriteQueueItem 398 implements PersisterQueue.WriteQueueItem<LaunchParamsWriteQueueItem> { 399 private final int mUserId; 400 private final ComponentName mComponentName; 401 402 private PersistableLaunchParams mLaunchParams; 403 LaunchParamsWriteQueueItem(int userId, ComponentName componentName, PersistableLaunchParams launchParams)404 private LaunchParamsWriteQueueItem(int userId, ComponentName componentName, 405 PersistableLaunchParams launchParams) { 406 mUserId = userId; 407 mComponentName = componentName; 408 mLaunchParams = launchParams; 409 } 410 saveParamsToXml()411 private byte[] saveParamsToXml() { 412 try { 413 final ByteArrayOutputStream os = new ByteArrayOutputStream(); 414 final TypedXmlSerializer serializer = Xml.resolveSerializer(os); 415 416 serializer.startDocument(/* encoding */ null, /* standalone */ true); 417 serializer.startTag(null, TAG_LAUNCH_PARAMS); 418 419 mLaunchParams.saveToXml(serializer); 420 421 serializer.endTag(null, TAG_LAUNCH_PARAMS); 422 serializer.endDocument(); 423 serializer.flush(); 424 425 return os.toByteArray(); 426 } catch (IOException e) { 427 return null; 428 } 429 } 430 431 @Override process()432 public void process() { 433 final byte[] data = saveParamsToXml(); 434 435 final File launchParamFolder = getLaunchParamFolder(mUserId); 436 if (!launchParamFolder.isDirectory() && !launchParamFolder.mkdir()) { 437 Slog.w(TAG, "Failed to create folder for " + mUserId); 438 return; 439 } 440 441 final File launchParamFile = getParamFile(launchParamFolder, mComponentName); 442 final AtomicFile atomicFile = new AtomicFile(launchParamFile); 443 444 FileOutputStream stream = null; 445 try { 446 stream = atomicFile.startWrite(); 447 stream.write(data); 448 } catch (Exception e) { 449 Slog.e(TAG, "Failed to write param file for " + mComponentName, e); 450 if (stream != null) { 451 atomicFile.failWrite(stream); 452 } 453 return; 454 } 455 atomicFile.finishWrite(stream); 456 } 457 458 @Override matches(LaunchParamsWriteQueueItem item)459 public boolean matches(LaunchParamsWriteQueueItem item) { 460 return mUserId == item.mUserId && mComponentName.equals(item.mComponentName); 461 } 462 463 @Override updateFrom(LaunchParamsWriteQueueItem item)464 public void updateFrom(LaunchParamsWriteQueueItem item) { 465 mLaunchParams = item.mLaunchParams; 466 } 467 } 468 469 private class CleanUpComponentQueueItem implements PersisterQueue.WriteQueueItem { 470 private final List<File> mComponentFiles; 471 CleanUpComponentQueueItem(List<File> componentFiles)472 private CleanUpComponentQueueItem(List<File> componentFiles) { 473 mComponentFiles = componentFiles; 474 } 475 476 @Override process()477 public void process() { 478 for (File file : mComponentFiles) { 479 if (!file.delete()) { 480 Slog.w(TAG, "Failed to delete " + file.getAbsolutePath()); 481 } 482 } 483 } 484 } 485 486 private class PersistableLaunchParams { 487 private static final String ATTR_WINDOWING_MODE = "windowing_mode"; 488 private static final String ATTR_DISPLAY_UNIQUE_ID = "display_unique_id"; 489 private static final String ATTR_BOUNDS = "bounds"; 490 private static final String ATTR_WINDOW_LAYOUT_AFFINITY = "window_layout_affinity"; 491 492 /** The bounds within the parent container. */ 493 final Rect mBounds = new Rect(); 494 495 /** The unique id of the display the {@link Task} would prefer to be on. */ 496 String mDisplayUniqueId; 497 498 /** The windowing mode to be in. */ 499 int mWindowingMode; 500 501 /** 502 * Last {@link android.content.pm.ActivityInfo.WindowLayout#windowLayoutAffinity} of the 503 * window. 504 */ 505 @Nullable String mWindowLayoutAffinity; 506 507 /** 508 * Timestamp from {@link System#currentTimeMillis()} when this record is captured, or last 509 * modified time when the record is restored from storage. 510 */ 511 long mTimestamp; 512 saveToXml(TypedXmlSerializer serializer)513 void saveToXml(TypedXmlSerializer serializer) throws IOException { 514 serializer.attribute(null, ATTR_DISPLAY_UNIQUE_ID, mDisplayUniqueId); 515 serializer.attributeInt(null, ATTR_WINDOWING_MODE, mWindowingMode); 516 serializer.attribute(null, ATTR_BOUNDS, mBounds.flattenToString()); 517 if (mWindowLayoutAffinity != null) { 518 serializer.attribute(null, ATTR_WINDOW_LAYOUT_AFFINITY, mWindowLayoutAffinity); 519 } 520 } 521 restore(File xmlFile, TypedXmlPullParser parser)522 void restore(File xmlFile, TypedXmlPullParser parser) { 523 for (int i = 0; i < parser.getAttributeCount(); ++i) { 524 final String attrValue = parser.getAttributeValue(i); 525 switch (parser.getAttributeName(i)) { 526 case ATTR_DISPLAY_UNIQUE_ID: 527 mDisplayUniqueId = attrValue; 528 break; 529 case ATTR_WINDOWING_MODE: 530 mWindowingMode = Integer.parseInt(attrValue); 531 break; 532 case ATTR_BOUNDS: { 533 final Rect bounds = Rect.unflattenFromString(attrValue); 534 if (bounds != null) { 535 mBounds.set(bounds); 536 } 537 break; 538 } 539 case ATTR_WINDOW_LAYOUT_AFFINITY: 540 mWindowLayoutAffinity = attrValue; 541 break; 542 } 543 } 544 545 // The modified time could be a few seconds later than the timestamp when the record is 546 // captured, which is a good enough estimate to the capture time after a reboot or a 547 // user switch. 548 mTimestamp = xmlFile.lastModified(); 549 } 550 551 @Override toString()552 public String toString() { 553 final StringBuilder builder = new StringBuilder("PersistableLaunchParams{"); 554 builder.append(" windowingMode=" + mWindowingMode); 555 builder.append(" displayUniqueId=" + mDisplayUniqueId); 556 builder.append(" bounds=" + mBounds); 557 if (mWindowLayoutAffinity != null) { 558 builder.append(" launchParamsAffinity=" + mWindowLayoutAffinity); 559 } 560 builder.append(" timestamp=" + mTimestamp); 561 builder.append(" }"); 562 return builder.toString(); 563 } 564 } 565 } 566