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