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