blob: c7089ac8ae05c4f490fefcc367f4e06916ac79d0 [file] [log] [blame]
Garfield Tan891146c2018-10-09 12:14:00 -07001/*
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
17package com.android.server.wm;
18
19import android.content.ComponentName;
20import android.content.pm.PackageList;
21import android.content.pm.PackageManagerInternal;
22import android.graphics.Rect;
23import android.os.Environment;
24import android.util.ArrayMap;
25import android.util.ArraySet;
26import android.util.AtomicFile;
27import android.util.Slog;
28import android.util.SparseArray;
29import android.util.Xml;
30import android.view.DisplayInfo;
31
32import com.android.internal.annotations.VisibleForTesting;
33import com.android.internal.util.FastXmlSerializer;
34import com.android.server.LocalServices;
35import com.android.server.wm.LaunchParamsController.LaunchParams;
36
37import libcore.io.IoUtils;
38
39import org.xmlpull.v1.XmlPullParser;
40import org.xmlpull.v1.XmlSerializer;
41
42import java.io.BufferedReader;
43import java.io.File;
44import java.io.FileOutputStream;
45import java.io.FileReader;
46import java.io.IOException;
47import java.io.StringWriter;
48import java.util.ArrayList;
49import java.util.List;
50import java.util.Map;
51import java.util.Objects;
52import java.util.Set;
53import 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 * TaskRecord#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 */
63class 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
72 private static final String TAG_LAUNCH_PARAMS = "launch_params";
73
74 private final PersisterQueue mPersisterQueue;
75 private final ActivityStackSupervisor mSupervisor;
76
77 /**
78 * A function that takes in user ID and returns a folder to store information of that user. Used
79 * to differentiate storage location in test environment and production environment.
80 */
81 private final IntFunction<File> mUserFolderGetter;
82
83 private PackageList mPackageList;
84
85 /**
86 * A dual layer map that first maps user ID to a secondary map, which maps component name (the
87 * launching activity of tasks) to {@link PersistableLaunchParams} that stores launch metadata
88 * that are stable across reboots.
89 */
90 private final SparseArray<ArrayMap<ComponentName, PersistableLaunchParams>> mMap =
91 new SparseArray<>();
92
93 LaunchParamsPersister(PersisterQueue persisterQueue, ActivityStackSupervisor supervisor) {
94 this(persisterQueue, supervisor, Environment::getDataSystemCeDirectory);
95 }
96
97 @VisibleForTesting
98 LaunchParamsPersister(PersisterQueue persisterQueue, ActivityStackSupervisor supervisor,
99 IntFunction<File> userFolderGetter) {
100 mPersisterQueue = persisterQueue;
101 mSupervisor = supervisor;
102 mUserFolderGetter = userFolderGetter;
103 }
104
105 void onSystemReady() {
106 PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class);
107 mPackageList = pmi.getPackageList(new PackageListObserver());
108 }
109
110 void onUnlockUser(int userId) {
111 loadLaunchParams(userId);
112 }
113
114 void onCleanupUser(int userId) {
115 mMap.remove(userId);
116 }
117
118 private void loadLaunchParams(int userId) {
119 final List<File> filesToDelete = new ArrayList<>();
120 final File launchParamsFolder = getLaunchParamFolder(userId);
121 if (!launchParamsFolder.isDirectory()) {
122 Slog.i(TAG, "Didn't find launch param folder for user " + userId);
123 return;
124 }
125
126 final Set<String> packages = new ArraySet<>(mPackageList.getPackageNames());
127
128 final File[] paramsFiles = launchParamsFolder.listFiles();
129 final ArrayMap<ComponentName, PersistableLaunchParams> map =
130 new ArrayMap<>(paramsFiles.length);
131 mMap.put(userId, map);
132
133 for (File paramsFile : paramsFiles) {
134 if (!paramsFile.isFile()) {
135 Slog.w(TAG, paramsFile.getAbsolutePath() + " is not a file.");
136 continue;
137 }
138 if (!paramsFile.getName().endsWith(LAUNCH_PARAMS_FILE_SUFFIX)) {
139 Slog.w(TAG, "Unexpected params file name: " + paramsFile.getName());
140 filesToDelete.add(paramsFile);
141 continue;
142 }
143 final String paramsFileName = paramsFile.getName();
144 final String componentNameString = paramsFileName.substring(
145 0 /* beginIndex */,
146 paramsFileName.length() - LAUNCH_PARAMS_FILE_SUFFIX.length())
147 .replace(ESCAPED_COMPONENT_SEPARATOR, ORIGINAL_COMPONENT_SEPARATOR);
148 final ComponentName name = ComponentName.unflattenFromString(
149 componentNameString);
150 if (name == null) {
151 Slog.w(TAG, "Unexpected file name: " + paramsFileName);
152 filesToDelete.add(paramsFile);
153 continue;
154 }
155
156 if (!packages.contains(name.getPackageName())) {
157 // Rare case. PersisterQueue doesn't have a chance to remove files for removed
158 // packages last time.
159 filesToDelete.add(paramsFile);
160 continue;
161 }
162
163 BufferedReader reader = null;
164 try {
165 reader = new BufferedReader(new FileReader(paramsFile));
166 final PersistableLaunchParams params = new PersistableLaunchParams();
167 final XmlPullParser parser = Xml.newPullParser();
168 parser.setInput(reader);
169 int event;
170 while ((event = parser.next()) != XmlPullParser.END_DOCUMENT
171 && event != XmlPullParser.END_TAG) {
172 if (event != XmlPullParser.START_TAG) {
173 continue;
174 }
175
176 final String tagName = parser.getName();
177 if (!TAG_LAUNCH_PARAMS.equals(tagName)) {
178 Slog.w(TAG, "Unexpected tag name: " + tagName);
179 continue;
180 }
181
182 params.restoreFromXml(parser);
183 }
184
185 map.put(name, params);
186 } catch (Exception e) {
187 Slog.w(TAG, "Failed to restore launch params for " + name, e);
188 filesToDelete.add(paramsFile);
189 } finally {
190 IoUtils.closeQuietly(reader);
191 }
192 }
193
194 if (!filesToDelete.isEmpty()) {
195 mPersisterQueue.addItem(new CleanUpComponentQueueItem(filesToDelete), true);
196 }
197 }
198
199 void saveTask(TaskRecord task) {
200 final ComponentName name = task.realActivity;
201 final int userId = task.userId;
202 PersistableLaunchParams params;
203 ArrayMap<ComponentName, PersistableLaunchParams> map = mMap.get(userId);
204 if (map == null) {
205 map = new ArrayMap<>();
206 mMap.put(userId, map);
207 }
208
209 params = map.get(name);
210 if (params == null) {
211 params = new PersistableLaunchParams();
212 map.put(name, params);
213 }
214 final boolean changed = saveTaskToLaunchParam(task, params);
215
216 if (changed) {
217 mPersisterQueue.updateLastOrAddItem(
218 new LaunchParamsWriteQueueItem(userId, name, params),
219 /* flush */ false);
220 }
221 }
222
223 private boolean saveTaskToLaunchParam(TaskRecord task, PersistableLaunchParams params) {
224 final ActivityStack<?> stack = task.getStack();
225 final int displayId = stack.mDisplayId;
226 final ActivityDisplay display = mSupervisor.getActivityDisplay(displayId);
227 final DisplayInfo info = new DisplayInfo();
228 display.mDisplay.getDisplayInfo(info);
229
230 boolean changed = !Objects.equals(params.mDisplayUniqueId, info.uniqueId);
231 params.mDisplayUniqueId = info.uniqueId;
232
233 changed |= params.mWindowingMode != stack.getWindowingMode();
234 params.mWindowingMode = stack.getWindowingMode();
235
236 if (task.mLastNonFullscreenBounds != null) {
237 changed |= !Objects.equals(params.mBounds, task.mLastNonFullscreenBounds);
238 params.mBounds.set(task.mLastNonFullscreenBounds);
239 } else {
240 changed |= !params.mBounds.isEmpty();
241 params.mBounds.setEmpty();
242 }
243
244 return changed;
245 }
246
247 void getLaunchParams(TaskRecord task, ActivityRecord activity, LaunchParams outParams) {
248 final ComponentName name = task != null ? task.realActivity : activity.realActivity;
249 final int userId = task != null ? task.userId : activity.userId;
250
251 outParams.reset();
252 Map<ComponentName, PersistableLaunchParams> map = mMap.get(userId);
253 if (map == null) {
254 return;
255 }
256 final PersistableLaunchParams persistableParams = map.get(name);
257
258 if (persistableParams == null) {
259 return;
260 }
261
262 final ActivityDisplay display = mSupervisor.getActivityDisplay(
263 persistableParams.mDisplayUniqueId);
264 if (display != null) {
265 outParams.mPreferredDisplayId = display.mDisplayId;
266 }
267 outParams.mWindowingMode = persistableParams.mWindowingMode;
268 outParams.mBounds.set(persistableParams.mBounds);
269 }
270
Garfield Tan01548632018-11-27 10:15:48 -0800271 void removeRecordForPackage(String packageName) {
Garfield Tan891146c2018-10-09 12:14:00 -0700272 final List<File> fileToDelete = new ArrayList<>();
273 for (int i = 0; i < mMap.size(); ++i) {
274 int userId = mMap.keyAt(i);
275 final File launchParamsFolder = getLaunchParamFolder(userId);
276 ArrayMap<ComponentName, PersistableLaunchParams> map = mMap.valueAt(i);
277 for (int j = map.size() - 1; j >= 0; --j) {
278 final ComponentName name = map.keyAt(j);
279 if (name.getPackageName().equals(packageName)) {
280 map.removeAt(j);
281 fileToDelete.add(getParamFile(launchParamsFolder, name));
282 }
283 }
284 }
285
286 synchronized (mPersisterQueue) {
287 mPersisterQueue.removeItems(
288 item -> item.mComponentName.getPackageName().equals(packageName),
289 LaunchParamsWriteQueueItem.class);
290
291 mPersisterQueue.addItem(new CleanUpComponentQueueItem(fileToDelete), true);
292 }
293 }
294
295 private File getParamFile(File launchParamFolder, ComponentName name) {
296 final String componentNameString = name.flattenToShortString()
297 .replace(ORIGINAL_COMPONENT_SEPARATOR, ESCAPED_COMPONENT_SEPARATOR);
298 return new File(launchParamFolder, componentNameString + LAUNCH_PARAMS_FILE_SUFFIX);
299 }
300
301 private File getLaunchParamFolder(int userId) {
302 final File userFolder = mUserFolderGetter.apply(userId);
303 return new File(userFolder, LAUNCH_PARAMS_DIRNAME);
304 }
305
306 private class PackageListObserver implements PackageManagerInternal.PackageListObserver {
307 @Override
308 public void onPackageAdded(String packageName) { }
309
310 @Override
311 public void onPackageRemoved(String packageName) {
Garfield Tan01548632018-11-27 10:15:48 -0800312 removeRecordForPackage(packageName);
Garfield Tan891146c2018-10-09 12:14:00 -0700313 }
314 }
315
316 private class LaunchParamsWriteQueueItem
317 implements PersisterQueue.WriteQueueItem<LaunchParamsWriteQueueItem> {
318 private final int mUserId;
319 private final ComponentName mComponentName;
320
321 private PersistableLaunchParams mLaunchParams;
322
323 private LaunchParamsWriteQueueItem(int userId, ComponentName componentName,
324 PersistableLaunchParams launchParams) {
325 mUserId = userId;
326 mComponentName = componentName;
327 mLaunchParams = launchParams;
328 }
329
330 private StringWriter saveParamsToXml() {
331 final StringWriter writer = new StringWriter();
332 final XmlSerializer serializer = new FastXmlSerializer();
333
334 try {
335 serializer.setOutput(writer);
336 serializer.startDocument(/* encoding */ null, /* standalone */ true);
337 serializer.startTag(null, TAG_LAUNCH_PARAMS);
338
339 mLaunchParams.saveToXml(serializer);
340
341 serializer.endTag(null, TAG_LAUNCH_PARAMS);
342 serializer.endDocument();
343 serializer.flush();
344
345 return writer;
346 } catch (IOException e) {
347 return null;
348 }
349 }
350
351 @Override
352 public void process() {
353 final StringWriter writer = saveParamsToXml();
354
355 final File launchParamFolder = getLaunchParamFolder(mUserId);
356 if (!launchParamFolder.isDirectory() && !launchParamFolder.mkdirs()) {
357 Slog.w(TAG, "Failed to create folder for " + mUserId);
358 return;
359 }
360
361 final File launchParamFile = getParamFile(launchParamFolder, mComponentName);
362 final AtomicFile atomicFile = new AtomicFile(launchParamFile);
363
364 FileOutputStream stream = null;
365 try {
366 stream = atomicFile.startWrite();
367 stream.write(writer.toString().getBytes());
368 } catch (Exception e) {
369 Slog.e(TAG, "Failed to write param file for " + mComponentName, e);
370 if (stream != null) {
371 atomicFile.failWrite(stream);
372 }
373 return;
374 }
375 atomicFile.finishWrite(stream);
376 }
377
378 @Override
379 public boolean matches(LaunchParamsWriteQueueItem item) {
380 return mUserId == item.mUserId && mComponentName.equals(item.mComponentName);
381 }
382
383 @Override
384 public void updateFrom(LaunchParamsWriteQueueItem item) {
385 mLaunchParams = item.mLaunchParams;
386 }
387 }
388
389 private class CleanUpComponentQueueItem implements PersisterQueue.WriteQueueItem {
390 private final List<File> mComponentFiles;
391
392 private CleanUpComponentQueueItem(List<File> componentFiles) {
393 mComponentFiles = componentFiles;
394 }
395
396 @Override
397 public void process() {
398 for (File file : mComponentFiles) {
399 if (!file.delete()) {
400 Slog.w(TAG, "Failed to delete " + file.getAbsolutePath());
401 }
402 }
403 }
404 }
405
406 private class PersistableLaunchParams {
407 private static final String ATTR_WINDOWING_MODE = "windowing_mode";
408 private static final String ATTR_DISPLAY_UNIQUE_ID = "display_unique_id";
409 private static final String ATTR_BOUNDS = "bounds";
410
411 /** The bounds within the parent container. */
412 final Rect mBounds = new Rect();
413
414 /** The unique id of the display the {@link TaskRecord} would prefer to be on. */
415 String mDisplayUniqueId;
416
417 /** The windowing mode to be in. */
418 int mWindowingMode;
419
420 void saveToXml(XmlSerializer serializer) throws IOException {
421 serializer.attribute(null, ATTR_DISPLAY_UNIQUE_ID, mDisplayUniqueId);
422 serializer.attribute(null, ATTR_WINDOWING_MODE,
423 Integer.toString(mWindowingMode));
424 serializer.attribute(null, ATTR_BOUNDS, mBounds.flattenToString());
425 }
426
427 void restoreFromXml(XmlPullParser parser) {
428 for (int i = 0; i < parser.getAttributeCount(); ++i) {
429 final String attrValue = parser.getAttributeValue(i);
430 switch (parser.getAttributeName(i)) {
431 case ATTR_DISPLAY_UNIQUE_ID:
432 mDisplayUniqueId = attrValue;
433 break;
434 case ATTR_WINDOWING_MODE:
435 mWindowingMode = Integer.parseInt(attrValue);
436 break;
437 case ATTR_BOUNDS: {
438 final Rect bounds = Rect.unflattenFromString(attrValue);
439 if (bounds != null) {
440 mBounds.set(bounds);
441 }
442 break;
443 }
444 }
445 }
446 }
447
448 @Override
449 public String toString() {
450 final StringBuilder builder = new StringBuilder("PersistableLaunchParams{");
451 builder.append("windowingMode=" + mWindowingMode);
452 builder.append(" displayUniqueId=" + mDisplayUniqueId);
453 builder.append(" bounds=" + mBounds);
454 builder.append(" }");
455 return builder.toString();
456 }
457 }
458}