• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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.NonNull;
20 import android.content.res.Configuration;
21 import android.os.Environment;
22 import android.os.LocaleList;
23 import android.util.AtomicFile;
24 import android.util.Slog;
25 import android.util.SparseArray;
26 import android.util.TypedXmlPullParser;
27 import android.util.TypedXmlSerializer;
28 import android.util.Xml;
29 
30 import com.android.internal.annotations.GuardedBy;
31 import com.android.internal.util.XmlUtils;
32 
33 import org.xmlpull.v1.XmlPullParser;
34 import org.xmlpull.v1.XmlPullParserException;
35 
36 import java.io.ByteArrayOutputStream;
37 import java.io.File;
38 import java.io.FileInputStream;
39 import java.io.FileNotFoundException;
40 import java.io.FileOutputStream;
41 import java.io.IOException;
42 import java.io.InputStream;
43 import java.io.PrintWriter;
44 import java.util.HashMap;
45 
46 /**
47  * Persist configuration for each package, only persist the change if some on attributes are
48  * different from the global configuration. This class only applies to packages with Activities.
49  */
50 public class PackageConfigPersister {
51     private static final String TAG = PackageConfigPersister.class.getSimpleName();
52     private static final boolean DEBUG = false;
53 
54     private static final String TAG_CONFIG = "config";
55     private static final String ATTR_PACKAGE_NAME = "package_name";
56     private static final String ATTR_NIGHT_MODE = "night_mode";
57     private static final String ATTR_LOCALES = "locale_list";
58 
59     private static final String PACKAGE_DIRNAME = "package_configs";
60     private static final String SUFFIX_FILE_NAME = "_config.xml";
61 
62     private final PersisterQueue mPersisterQueue;
63     private final Object mLock = new Object();
64     private final ActivityTaskManagerService mAtm;
65 
66     @GuardedBy("mLock")
67     private final SparseArray<HashMap<String, PackageConfigRecord>> mPendingWrite =
68             new SparseArray<>();
69     @GuardedBy("mLock")
70     private final SparseArray<HashMap<String, PackageConfigRecord>> mModified =
71             new SparseArray<>();
72 
getUserConfigsDir(int userId)73     private static File getUserConfigsDir(int userId) {
74         return new File(Environment.getDataSystemCeDirectory(userId), PACKAGE_DIRNAME);
75     }
76 
PackageConfigPersister(PersisterQueue queue, ActivityTaskManagerService atm)77     PackageConfigPersister(PersisterQueue queue, ActivityTaskManagerService atm) {
78         mPersisterQueue = queue;
79         mAtm = atm;
80     }
81 
82     @GuardedBy("mLock")
loadUserPackages(int userId)83     void loadUserPackages(int userId) {
84         synchronized (mLock) {
85             final File userConfigsDir = getUserConfigsDir(userId);
86             final File[] configFiles = userConfigsDir.listFiles();
87             if (configFiles == null) {
88                 Slog.v(TAG, "loadPackages: empty list files from " + userConfigsDir);
89                 return;
90             }
91 
92             for (int fileIndex = 0; fileIndex < configFiles.length; ++fileIndex) {
93                 final File configFile = configFiles[fileIndex];
94                 if (DEBUG) {
95                     Slog.d(TAG, "loadPackages: userId=" + userId
96                             + ", configFile=" + configFile.getName());
97                 }
98                 if (!configFile.getName().endsWith(SUFFIX_FILE_NAME)) {
99                     continue;
100                 }
101 
102                 try (InputStream is = new FileInputStream(configFile)) {
103                     final TypedXmlPullParser in = Xml.resolvePullParser(is);
104                     int event;
105                     String packageName = null;
106                     Integer nightMode = null;
107                     LocaleList locales = null;
108                     while (((event = in.next()) != XmlPullParser.END_DOCUMENT)
109                             && event != XmlPullParser.END_TAG) {
110                         final String name = in.getName();
111                         if (event == XmlPullParser.START_TAG) {
112                             if (DEBUG) {
113                                 Slog.d(TAG, "loadPackages: START_TAG name=" + name);
114                             }
115                             if (TAG_CONFIG.equals(name)) {
116                                 for (int attIdx = in.getAttributeCount() - 1; attIdx >= 0;
117                                         --attIdx) {
118                                     final String attrName = in.getAttributeName(attIdx);
119                                     final String attrValue = in.getAttributeValue(attIdx);
120                                     switch (attrName) {
121                                         case ATTR_PACKAGE_NAME:
122                                             packageName = attrValue;
123                                             break;
124                                         case ATTR_NIGHT_MODE:
125                                             nightMode = Integer.parseInt(attrValue);
126                                             break;
127                                         case ATTR_LOCALES:
128                                             locales = LocaleList.forLanguageTags(attrValue);
129                                             break;
130                                     }
131                                 }
132                             }
133                         }
134                         XmlUtils.skipCurrentTag(in);
135                     }
136                     if (packageName != null) {
137                         final PackageConfigRecord initRecord =
138                                 findRecordOrCreate(mModified, packageName, userId);
139                         initRecord.mNightMode = nightMode;
140                         initRecord.mLocales = locales;
141                         if (DEBUG) {
142                             Slog.d(TAG, "loadPackages: load one package " + initRecord);
143                         }
144                     }
145                 } catch (FileNotFoundException e) {
146                     e.printStackTrace();
147                 } catch (IOException e) {
148                     e.printStackTrace();
149                 } catch (XmlPullParserException e) {
150                     e.printStackTrace();
151                 }
152             }
153         }
154     }
155 
156     @GuardedBy("mLock")
updateConfigIfNeeded(@onNull ConfigurationContainer container, int userId, String packageName)157     void updateConfigIfNeeded(@NonNull ConfigurationContainer container, int userId,
158             String packageName) {
159         synchronized (mLock) {
160             final PackageConfigRecord modifiedRecord = findRecord(mModified, packageName, userId);
161             if (DEBUG) {
162                 Slog.d(TAG,
163                         "updateConfigIfNeeded record " + container + " find? " + modifiedRecord);
164             }
165             if (modifiedRecord != null) {
166                 container.applyAppSpecificConfig(modifiedRecord.mNightMode,
167                         LocaleOverlayHelper.combineLocalesIfOverlayExists(
168                         modifiedRecord.mLocales, mAtm.getGlobalConfiguration().getLocales()));
169             }
170         }
171     }
172 
173     /**
174      * Returns true when the app specific configuration is successfully stored or removed based on
175      * the current requested configuration. It will return false when the requested
176      * configuration is same as the pre-existing app-specific configuration.
177      */
178     @GuardedBy("mLock")
updateFromImpl(String packageName, int userId, PackageConfigurationUpdaterImpl impl)179     boolean updateFromImpl(String packageName, int userId,
180             PackageConfigurationUpdaterImpl impl) {
181         synchronized (mLock) {
182             boolean isRecordPresent = false;
183             PackageConfigRecord record = findRecord(mModified, packageName, userId);
184             if (record != null) {
185                 isRecordPresent = true;
186             } else {
187                 record = findRecordOrCreate(mModified, packageName, userId);
188             }
189             boolean isNightModeChanged = updateNightMode(impl.getNightMode(), record);
190             boolean isLocalesChanged = updateLocales(impl.getLocales(), record);
191 
192             if ((record.mNightMode == null || record.isResetNightMode())
193                     && (record.mLocales == null || record.mLocales.isEmpty())) {
194                 // if all values default to system settings, we can remove the package.
195                 removePackage(packageName, userId);
196                 // if there was a pre-existing record for the package that was deleted,
197                 // we return true (since it was successfully deleted), else false (since there was
198                 // no change to the previous state).
199                 return isRecordPresent;
200             } else if (!isNightModeChanged && !isLocalesChanged) {
201                 return false;
202             } else {
203                 final PackageConfigRecord pendingRecord =
204                         findRecord(mPendingWrite, record.mName, record.mUserId);
205                 final PackageConfigRecord writeRecord;
206                 if (pendingRecord == null) {
207                     writeRecord = findRecordOrCreate(mPendingWrite, record.mName,
208                             record.mUserId);
209                 } else {
210                     writeRecord = pendingRecord;
211                 }
212 
213                 if (!updateNightMode(record.mNightMode, writeRecord)
214                         && !updateLocales(record.mLocales, writeRecord)) {
215                     return false;
216                 }
217 
218                 if (DEBUG) {
219                     Slog.d(TAG, "PackageConfigUpdater save config " + writeRecord);
220                 }
221                 mPersisterQueue.addItem(new WriteProcessItem(writeRecord), false /* flush */);
222                 return true;
223             }
224         }
225     }
226 
updateNightMode(Integer requestedNightMode, PackageConfigRecord record)227     private boolean updateNightMode(Integer requestedNightMode, PackageConfigRecord record) {
228         if (requestedNightMode == null || requestedNightMode.equals(record.mNightMode)) {
229             return false;
230         }
231         record.mNightMode = requestedNightMode;
232         return true;
233     }
234 
updateLocales(LocaleList requestedLocaleList, PackageConfigRecord record)235     private boolean updateLocales(LocaleList requestedLocaleList, PackageConfigRecord record) {
236         if (requestedLocaleList == null || requestedLocaleList.equals(record.mLocales)) {
237             return false;
238         }
239         record.mLocales = requestedLocaleList;
240         return true;
241     }
242 
243     @GuardedBy("mLock")
removeUser(int userId)244     void removeUser(int userId) {
245         synchronized (mLock) {
246             final HashMap<String, PackageConfigRecord> modifyRecords = mModified.get(userId);
247             final HashMap<String, PackageConfigRecord> writeRecords = mPendingWrite.get(userId);
248             if ((modifyRecords == null || modifyRecords.size() == 0)
249                     && (writeRecords == null || writeRecords.size() == 0)) {
250                 return;
251             }
252             final HashMap<String, PackageConfigRecord> tempList = new HashMap<>(modifyRecords);
253             tempList.forEach((name, record) -> {
254                 removePackage(record.mName, record.mUserId);
255             });
256         }
257     }
258 
259     @GuardedBy("mLock")
onPackageUninstall(String packageName, int userId)260     void onPackageUninstall(String packageName, int userId) {
261         synchronized (mLock) {
262             removePackage(packageName, userId);
263         }
264     }
265 
266     @GuardedBy("mLock")
onPackageDataCleared(String packageName, int userId)267     void onPackageDataCleared(String packageName, int userId) {
268         synchronized (mLock) {
269             removePackage(packageName, userId);
270         }
271     }
272 
removePackage(String packageName, int userId)273     private void removePackage(String packageName, int userId) {
274         if (DEBUG) {
275             Slog.d(TAG, "removePackage packageName :" + packageName + " userId " + userId);
276         }
277         final PackageConfigRecord record = findRecord(mPendingWrite, packageName, userId);
278         if (record != null) {
279             removeRecord(mPendingWrite, record);
280             mPersisterQueue.removeItems(item ->
281                             item.mRecord.mName == record.mName
282                                     && item.mRecord.mUserId == record.mUserId,
283                     WriteProcessItem.class);
284         }
285 
286         final PackageConfigRecord modifyRecord = findRecord(mModified, packageName, userId);
287         if (modifyRecord != null) {
288             removeRecord(mModified, modifyRecord);
289             mPersisterQueue.addItem(new DeletePackageItem(userId, packageName),
290                     false /* flush */);
291         }
292     }
293 
294     /**
295      * Retrieves and returns application configuration from persisted records if it exists, else
296      * returns null.
297      */
findPackageConfiguration(String packageName, int userId)298     ActivityTaskManagerInternal.PackageConfig findPackageConfiguration(String packageName,
299             int userId) {
300         synchronized (mLock) {
301             PackageConfigRecord packageConfigRecord = findRecord(mModified, packageName, userId);
302             if (packageConfigRecord == null) {
303                 Slog.w(TAG, "App-specific configuration not found for packageName: " + packageName
304                         + " and userId: " + userId);
305                 return null;
306             }
307             return new ActivityTaskManagerInternal.PackageConfig(
308                     packageConfigRecord.mNightMode, packageConfigRecord.mLocales);
309         }
310     }
311 
312     /**
313      * Dumps app-specific configurations for all packages for which the records
314      * exist.
315      */
dump(PrintWriter pw, int userId)316     void dump(PrintWriter pw, int userId) {
317         pw.println("INSTALLED PACKAGES HAVING APP-SPECIFIC CONFIGURATIONS");
318         pw.println("Current user ID : " + userId);
319         synchronized (mLock) {
320             HashMap<String, PackageConfigRecord> persistedPackageConfigMap = mModified.get(userId);
321             if (persistedPackageConfigMap != null) {
322                 for (PackageConfigPersister.PackageConfigRecord packageConfig
323                         : persistedPackageConfigMap.values()) {
324                     pw.println();
325                     pw.println("    PackageName : " + packageConfig.mName);
326                     pw.println("        NightMode : " + packageConfig.mNightMode);
327                     pw.println("        Locales : " + packageConfig.mLocales);
328                 }
329             }
330         }
331     }
332 
333     // store a changed data so we don't need to get the process
334     static class PackageConfigRecord {
335         final String mName;
336         final int mUserId;
337         Integer mNightMode;
338         LocaleList mLocales;
339 
PackageConfigRecord(String name, int userId)340         PackageConfigRecord(String name, int userId) {
341             mName = name;
342             mUserId = userId;
343         }
344 
isResetNightMode()345         boolean isResetNightMode() {
346             return mNightMode == Configuration.UI_MODE_NIGHT_UNDEFINED;
347         }
348 
349         @Override
toString()350         public String toString() {
351             return "PackageConfigRecord package name: " + mName + " userId " + mUserId
352                     + " nightMode " + mNightMode + " locales " + mLocales;
353         }
354     }
355 
findRecordOrCreate( SparseArray<HashMap<String, PackageConfigRecord>> list, String name, int userId)356     private PackageConfigRecord findRecordOrCreate(
357             SparseArray<HashMap<String, PackageConfigRecord>> list, String name, int userId) {
358         HashMap<String, PackageConfigRecord> records = list.get(userId);
359         if (records == null) {
360             records = new HashMap<>();
361             list.put(userId, records);
362         }
363         PackageConfigRecord record = records.get(name);
364         if (record != null) {
365             return record;
366         }
367         record = new PackageConfigRecord(name, userId);
368         records.put(name, record);
369         return record;
370     }
371 
findRecord(SparseArray<HashMap<String, PackageConfigRecord>> list, String name, int userId)372     private PackageConfigRecord findRecord(SparseArray<HashMap<String, PackageConfigRecord>> list,
373             String name, int userId) {
374         HashMap<String, PackageConfigRecord> packages = list.get(userId);
375         if (packages == null) {
376             return null;
377         }
378         return packages.get(name);
379     }
380 
removeRecord(SparseArray<HashMap<String, PackageConfigRecord>> list, PackageConfigRecord record)381     private void removeRecord(SparseArray<HashMap<String, PackageConfigRecord>> list,
382             PackageConfigRecord record) {
383         final HashMap<String, PackageConfigRecord> processes = list.get(record.mUserId);
384         if (processes != null) {
385             processes.remove(record.mName);
386         }
387     }
388 
389     private static class DeletePackageItem implements PersisterQueue.WriteQueueItem {
390         final int mUserId;
391         final String mPackageName;
392 
DeletePackageItem(int userId, String packageName)393         DeletePackageItem(int userId, String packageName) {
394             mUserId = userId;
395             mPackageName = packageName;
396         }
397 
398         @Override
process()399         public void process() {
400             File userConfigsDir = getUserConfigsDir(mUserId);
401             if (!userConfigsDir.isDirectory()) {
402                 return;
403             }
404             final AtomicFile atomicFile = new AtomicFile(new File(userConfigsDir,
405                     mPackageName + SUFFIX_FILE_NAME));
406             if (atomicFile.exists()) {
407                 atomicFile.delete();
408             }
409         }
410     }
411 
412     private class WriteProcessItem implements PersisterQueue.WriteQueueItem {
413         final PackageConfigRecord mRecord;
414 
WriteProcessItem(PackageConfigRecord record)415         WriteProcessItem(PackageConfigRecord record) {
416             mRecord = record;
417         }
418 
419         @Override
process()420         public void process() {
421             // Write out one user.
422             byte[] data = null;
423             synchronized (mLock) {
424                 try {
425                     data = saveToXml();
426                 } catch (Exception e) {
427                 }
428                 removeRecord(mPendingWrite, mRecord);
429             }
430             if (data != null) {
431                 // Write out xml file while not holding mService lock.
432                 FileOutputStream file = null;
433                 AtomicFile atomicFile = null;
434                 try {
435                     File userConfigsDir = getUserConfigsDir(mRecord.mUserId);
436                     if (!userConfigsDir.isDirectory() && !userConfigsDir.mkdirs()) {
437                         Slog.e(TAG, "Failure creating tasks directory for user " + mRecord.mUserId
438                                 + ": " + userConfigsDir);
439                         return;
440                     }
441                     atomicFile = new AtomicFile(new File(userConfigsDir,
442                             mRecord.mName + SUFFIX_FILE_NAME));
443                     file = atomicFile.startWrite();
444                     file.write(data);
445                     atomicFile.finishWrite(file);
446                 } catch (IOException e) {
447                     if (file != null) {
448                         atomicFile.failWrite(file);
449                     }
450                     Slog.e(TAG, "Unable to open " + atomicFile + " for persisting. " + e);
451                 }
452             }
453         }
454 
saveToXml()455         private byte[] saveToXml() throws IOException {
456             final ByteArrayOutputStream os = new ByteArrayOutputStream();
457             final TypedXmlSerializer xmlSerializer = Xml.resolveSerializer(os);
458 
459             xmlSerializer.startDocument(null, true);
460             if (DEBUG) {
461                 Slog.d(TAG, "Writing package configuration=" + mRecord);
462             }
463             xmlSerializer.startTag(null, TAG_CONFIG);
464             xmlSerializer.attribute(null, ATTR_PACKAGE_NAME, mRecord.mName);
465             if (mRecord.mNightMode != null) {
466                 xmlSerializer.attributeInt(null, ATTR_NIGHT_MODE, mRecord.mNightMode);
467             }
468             if (mRecord.mLocales != null) {
469                 xmlSerializer.attribute(null, ATTR_LOCALES, mRecord.mLocales
470                         .toLanguageTags());
471             }
472             xmlSerializer.endTag(null, TAG_CONFIG);
473             xmlSerializer.endDocument();
474             xmlSerializer.flush();
475 
476             return os.toByteArray();
477         }
478     }
479 }
480