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