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