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.content.ComponentName;
20 import android.content.pm.PackageList;
21 import android.content.pm.PackageManagerInternal;
22 import android.graphics.Rect;
23 import android.os.Environment;
24 import android.util.ArrayMap;
25 import android.util.ArraySet;
26 import android.util.AtomicFile;
27 import android.util.Slog;
28 import android.util.SparseArray;
29 import android.util.Xml;
30 import android.view.DisplayInfo;
31 
32 import com.android.internal.annotations.VisibleForTesting;
33 import com.android.internal.util.FastXmlSerializer;
34 import com.android.server.LocalServices;
35 import com.android.server.wm.LaunchParamsController.LaunchParams;
36 
37 import libcore.io.IoUtils;
38 
39 import org.xmlpull.v1.XmlPullParser;
40 import org.xmlpull.v1.XmlSerializer;
41 
42 import java.io.BufferedReader;
43 import java.io.File;
44 import java.io.FileOutputStream;
45 import java.io.FileReader;
46 import java.io.IOException;
47 import java.io.StringWriter;
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  * 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  */
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 
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 
LaunchParamsPersister(PersisterQueue persisterQueue, ActivityStackSupervisor supervisor)93     LaunchParamsPersister(PersisterQueue persisterQueue, ActivityStackSupervisor supervisor) {
94         this(persisterQueue, supervisor, Environment::getDataSystemCeDirectory);
95     }
96 
97     @VisibleForTesting
LaunchParamsPersister(PersisterQueue persisterQueue, ActivityStackSupervisor supervisor, IntFunction<File> userFolderGetter)98     LaunchParamsPersister(PersisterQueue persisterQueue, ActivityStackSupervisor supervisor,
99             IntFunction<File> userFolderGetter) {
100         mPersisterQueue = persisterQueue;
101         mSupervisor = supervisor;
102         mUserFolderGetter = userFolderGetter;
103     }
104 
onSystemReady()105     void onSystemReady() {
106         PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class);
107         mPackageList = pmi.getPackageList(new PackageListObserver());
108     }
109 
onUnlockUser(int userId)110     void onUnlockUser(int userId) {
111         loadLaunchParams(userId);
112     }
113 
onCleanupUser(int userId)114     void onCleanupUser(int userId) {
115         mMap.remove(userId);
116     }
117 
loadLaunchParams(int userId)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 
saveTask(TaskRecord task)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 
saveTaskToLaunchParam(TaskRecord task, PersistableLaunchParams params)223     private boolean saveTaskToLaunchParam(TaskRecord task, PersistableLaunchParams params) {
224         final ActivityStack stack = task.getStack();
225         final int displayId = stack.mDisplayId;
226         final ActivityDisplay display =
227                 mSupervisor.mRootActivityContainer.getActivityDisplay(displayId);
228         final DisplayInfo info = new DisplayInfo();
229         display.mDisplay.getDisplayInfo(info);
230 
231         boolean changed = !Objects.equals(params.mDisplayUniqueId, info.uniqueId);
232         params.mDisplayUniqueId = info.uniqueId;
233 
234         changed |= params.mWindowingMode != stack.getWindowingMode();
235         params.mWindowingMode = stack.getWindowingMode();
236 
237         if (task.mLastNonFullscreenBounds != null) {
238             changed |= !Objects.equals(params.mBounds, task.mLastNonFullscreenBounds);
239             params.mBounds.set(task.mLastNonFullscreenBounds);
240         } else {
241             changed |= !params.mBounds.isEmpty();
242             params.mBounds.setEmpty();
243         }
244 
245         return changed;
246     }
247 
getLaunchParams(TaskRecord task, ActivityRecord activity, LaunchParams outParams)248     void getLaunchParams(TaskRecord task, ActivityRecord activity, LaunchParams outParams) {
249         final ComponentName name = task != null ? task.realActivity : activity.mActivityComponent;
250         final int userId = task != null ? task.userId : activity.mUserId;
251 
252         outParams.reset();
253         Map<ComponentName, PersistableLaunchParams> map = mMap.get(userId);
254         if (map == null) {
255             return;
256         }
257         final PersistableLaunchParams persistableParams = map.get(name);
258 
259         if (persistableParams == null) {
260             return;
261         }
262 
263         final ActivityDisplay display = mSupervisor.mRootActivityContainer.getActivityDisplay(
264                 persistableParams.mDisplayUniqueId);
265         if (display != null) {
266             outParams.mPreferredDisplayId =  display.mDisplayId;
267         }
268         outParams.mWindowingMode = persistableParams.mWindowingMode;
269         outParams.mBounds.set(persistableParams.mBounds);
270     }
271 
removeRecordForPackage(String packageName)272     void removeRecordForPackage(String packageName) {
273         final List<File> fileToDelete = new ArrayList<>();
274         for (int i = 0; i < mMap.size(); ++i) {
275             int userId = mMap.keyAt(i);
276             final File launchParamsFolder = getLaunchParamFolder(userId);
277             ArrayMap<ComponentName, PersistableLaunchParams> map = mMap.valueAt(i);
278             for (int j = map.size() - 1; j >= 0; --j) {
279                 final ComponentName name = map.keyAt(j);
280                 if (name.getPackageName().equals(packageName)) {
281                     map.removeAt(j);
282                     fileToDelete.add(getParamFile(launchParamsFolder, name));
283                 }
284             }
285         }
286 
287         synchronized (mPersisterQueue) {
288             mPersisterQueue.removeItems(
289                     item -> item.mComponentName.getPackageName().equals(packageName),
290                     LaunchParamsWriteQueueItem.class);
291 
292             mPersisterQueue.addItem(new CleanUpComponentQueueItem(fileToDelete), true);
293         }
294     }
295 
getParamFile(File launchParamFolder, ComponentName name)296     private File getParamFile(File launchParamFolder, ComponentName name) {
297         final String componentNameString = name.flattenToShortString()
298                 .replace(ORIGINAL_COMPONENT_SEPARATOR, ESCAPED_COMPONENT_SEPARATOR);
299         return new File(launchParamFolder, componentNameString + LAUNCH_PARAMS_FILE_SUFFIX);
300     }
301 
getLaunchParamFolder(int userId)302     private File getLaunchParamFolder(int userId) {
303         final File userFolder = mUserFolderGetter.apply(userId);
304         return new File(userFolder, LAUNCH_PARAMS_DIRNAME);
305     }
306 
307     private class PackageListObserver implements PackageManagerInternal.PackageListObserver {
308         @Override
onPackageAdded(String packageName, int uid)309         public void onPackageAdded(String packageName, int uid) { }
310 
311         @Override
onPackageRemoved(String packageName, int uid)312         public void onPackageRemoved(String packageName, int uid) {
313             removeRecordForPackage(packageName);
314         }
315     }
316 
317     private class LaunchParamsWriteQueueItem
318             implements PersisterQueue.WriteQueueItem<LaunchParamsWriteQueueItem> {
319         private final int mUserId;
320         private final ComponentName mComponentName;
321 
322         private PersistableLaunchParams mLaunchParams;
323 
LaunchParamsWriteQueueItem(int userId, ComponentName componentName, PersistableLaunchParams launchParams)324         private LaunchParamsWriteQueueItem(int userId, ComponentName componentName,
325                 PersistableLaunchParams launchParams) {
326             mUserId = userId;
327             mComponentName = componentName;
328             mLaunchParams = launchParams;
329         }
330 
saveParamsToXml()331         private StringWriter saveParamsToXml() {
332             final StringWriter writer = new StringWriter();
333             final XmlSerializer serializer = new FastXmlSerializer();
334 
335             try {
336                 serializer.setOutput(writer);
337                 serializer.startDocument(/* encoding */ null, /* standalone */ true);
338                 serializer.startTag(null, TAG_LAUNCH_PARAMS);
339 
340                 mLaunchParams.saveToXml(serializer);
341 
342                 serializer.endTag(null, TAG_LAUNCH_PARAMS);
343                 serializer.endDocument();
344                 serializer.flush();
345 
346                 return writer;
347             } catch (IOException e) {
348                 return null;
349             }
350         }
351 
352         @Override
process()353         public void process() {
354             final StringWriter writer = saveParamsToXml();
355 
356             final File launchParamFolder = getLaunchParamFolder(mUserId);
357             if (!launchParamFolder.isDirectory() && !launchParamFolder.mkdirs()) {
358                 Slog.w(TAG, "Failed to create folder for " + mUserId);
359                 return;
360             }
361 
362             final File launchParamFile = getParamFile(launchParamFolder, mComponentName);
363             final AtomicFile atomicFile = new AtomicFile(launchParamFile);
364 
365             FileOutputStream stream = null;
366             try {
367                 stream = atomicFile.startWrite();
368                 stream.write(writer.toString().getBytes());
369             } catch (Exception e) {
370                 Slog.e(TAG, "Failed to write param file for " + mComponentName, e);
371                 if (stream != null) {
372                     atomicFile.failWrite(stream);
373                 }
374                 return;
375             }
376             atomicFile.finishWrite(stream);
377         }
378 
379         @Override
matches(LaunchParamsWriteQueueItem item)380         public boolean matches(LaunchParamsWriteQueueItem item) {
381             return mUserId == item.mUserId && mComponentName.equals(item.mComponentName);
382         }
383 
384         @Override
updateFrom(LaunchParamsWriteQueueItem item)385         public void updateFrom(LaunchParamsWriteQueueItem item) {
386             mLaunchParams = item.mLaunchParams;
387         }
388     }
389 
390     private class CleanUpComponentQueueItem implements PersisterQueue.WriteQueueItem {
391         private final List<File> mComponentFiles;
392 
CleanUpComponentQueueItem(List<File> componentFiles)393         private CleanUpComponentQueueItem(List<File> componentFiles) {
394             mComponentFiles = componentFiles;
395         }
396 
397         @Override
process()398         public void process() {
399             for (File file : mComponentFiles) {
400                 if (!file.delete()) {
401                     Slog.w(TAG, "Failed to delete " + file.getAbsolutePath());
402                 }
403             }
404         }
405     }
406 
407     private class PersistableLaunchParams {
408         private static final String ATTR_WINDOWING_MODE = "windowing_mode";
409         private static final String ATTR_DISPLAY_UNIQUE_ID = "display_unique_id";
410         private static final String ATTR_BOUNDS = "bounds";
411 
412         /** The bounds within the parent container. */
413         final Rect mBounds = new Rect();
414 
415         /** The unique id of the display the {@link TaskRecord} would prefer to be on. */
416         String mDisplayUniqueId;
417 
418         /** The windowing mode to be in. */
419         int mWindowingMode;
420 
saveToXml(XmlSerializer serializer)421         void saveToXml(XmlSerializer serializer) throws IOException {
422             serializer.attribute(null, ATTR_DISPLAY_UNIQUE_ID, mDisplayUniqueId);
423             serializer.attribute(null, ATTR_WINDOWING_MODE,
424                     Integer.toString(mWindowingMode));
425             serializer.attribute(null, ATTR_BOUNDS, mBounds.flattenToString());
426         }
427 
restoreFromXml(XmlPullParser parser)428         void restoreFromXml(XmlPullParser parser) {
429             for (int i = 0; i < parser.getAttributeCount(); ++i) {
430                 final String attrValue = parser.getAttributeValue(i);
431                 switch (parser.getAttributeName(i)) {
432                     case ATTR_DISPLAY_UNIQUE_ID:
433                         mDisplayUniqueId = attrValue;
434                         break;
435                     case ATTR_WINDOWING_MODE:
436                         mWindowingMode = Integer.parseInt(attrValue);
437                         break;
438                     case ATTR_BOUNDS: {
439                         final Rect bounds = Rect.unflattenFromString(attrValue);
440                         if (bounds != null) {
441                             mBounds.set(bounds);
442                         }
443                         break;
444                     }
445                 }
446             }
447         }
448 
449         @Override
toString()450         public String toString() {
451             final StringBuilder builder = new StringBuilder("PersistableLaunchParams{");
452             builder.append("windowingMode=" + mWindowingMode);
453             builder.append(" displayUniqueId=" + mDisplayUniqueId);
454             builder.append(" bounds=" + mBounds);
455             builder.append(" }");
456             return builder.toString();
457         }
458     }
459 }
460