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             }
173         }
174     }
175 
176     /**
177      * Returns true when the app specific configuration is successfully stored or removed based on
178      * the current requested configuration. It will return false when the requested
179      * configuration is same as the pre-existing app-specific configuration.
180      */
181     @GuardedBy("mLock")
updateFromImpl(String packageName, int userId, PackageConfigurationUpdaterImpl impl)182     boolean updateFromImpl(String packageName, int userId,
183             PackageConfigurationUpdaterImpl impl) {
184         synchronized (mLock) {
185             boolean isRecordPresent = false;
186             PackageConfigRecord record = findRecord(mModified, packageName, userId);
187             if (record != null) {
188                 isRecordPresent = true;
189             } else {
190                 record = findRecordOrCreate(mModified, packageName, userId);
191             }
192             boolean isNightModeChanged = updateNightMode(impl.getNightMode(), record);
193             boolean isLocalesChanged = updateLocales(impl.getLocales(), record);
194             boolean isGenderChanged = updateGender(impl.getGrammaticalGender(), record);
195 
196             if ((record.mNightMode == null || record.isResetNightMode())
197                     && (record.mLocales == null || record.mLocales.isEmpty())
198                     && (record.mGrammaticalGender == null
199                             || record.mGrammaticalGender == GRAMMATICAL_GENDER_NOT_SPECIFIED)) {
200                 // if all values default to system settings, we can remove the package.
201                 removePackage(packageName, userId);
202                 // if there was a pre-existing record for the package that was deleted,
203                 // we return true (since it was successfully deleted), else false (since there was
204                 // no change to the previous state).
205                 return isRecordPresent;
206             } else if (!isNightModeChanged && !isLocalesChanged && !isGenderChanged) {
207                 return false;
208             } else {
209                 final PackageConfigRecord pendingRecord =
210                         findRecord(mPendingWrite, record.mName, record.mUserId);
211                 final PackageConfigRecord writeRecord;
212                 if (pendingRecord == null) {
213                     writeRecord = findRecordOrCreate(mPendingWrite, record.mName,
214                             record.mUserId);
215                 } else {
216                     writeRecord = pendingRecord;
217                 }
218 
219                 if (!updateNightMode(record.mNightMode, writeRecord)
220                         && !updateLocales(record.mLocales, writeRecord)
221                         && !updateGender(record.mGrammaticalGender, writeRecord)) {
222                     return false;
223                 }
224 
225                 if (DEBUG) {
226                     Slog.d(TAG, "PackageConfigUpdater save config " + writeRecord);
227                 }
228                 mPersisterQueue.addItem(new WriteProcessItem(writeRecord), false /* flush */);
229                 return true;
230             }
231         }
232     }
233 
updateNightMode(Integer requestedNightMode, PackageConfigRecord record)234     private boolean updateNightMode(Integer requestedNightMode, PackageConfigRecord record) {
235         if (requestedNightMode == null || requestedNightMode.equals(record.mNightMode)) {
236             return false;
237         }
238         record.mNightMode = requestedNightMode;
239         return true;
240     }
241 
updateLocales(LocaleList requestedLocaleList, PackageConfigRecord record)242     private boolean updateLocales(LocaleList requestedLocaleList, PackageConfigRecord record) {
243         if (requestedLocaleList == null || requestedLocaleList.equals(record.mLocales)) {
244             return false;
245         }
246         record.mLocales = requestedLocaleList;
247         return true;
248     }
249 
updateGender(@onfiguration.GrammaticalGender Integer requestedGender, PackageConfigRecord record)250     private boolean updateGender(@Configuration.GrammaticalGender Integer requestedGender,
251             PackageConfigRecord record) {
252         if (requestedGender == null || requestedGender.equals(record.mGrammaticalGender)) {
253             return false;
254         }
255         record.mGrammaticalGender = requestedGender;
256         return true;
257     }
258 
259     @GuardedBy("mLock")
removeUser(int userId)260     void removeUser(int userId) {
261         synchronized (mLock) {
262             final HashMap<String, PackageConfigRecord> modifyRecords = mModified.get(userId);
263             final HashMap<String, PackageConfigRecord> writeRecords = mPendingWrite.get(userId);
264             if ((modifyRecords == null || modifyRecords.size() == 0)
265                     && (writeRecords == null || writeRecords.size() == 0)) {
266                 return;
267             }
268             final HashMap<String, PackageConfigRecord> tempList = new HashMap<>(modifyRecords);
269             tempList.forEach((name, record) -> {
270                 removePackage(record.mName, record.mUserId);
271             });
272         }
273     }
274 
275     @GuardedBy("mLock")
onPackageUninstall(String packageName, int userId)276     void onPackageUninstall(String packageName, int userId) {
277         synchronized (mLock) {
278             removePackage(packageName, userId);
279         }
280     }
281 
282     @GuardedBy("mLock")
onPackageDataCleared(String packageName, int userId)283     void onPackageDataCleared(String packageName, int userId) {
284         synchronized (mLock) {
285             removePackage(packageName, userId);
286         }
287     }
288 
removePackage(String packageName, int userId)289     private void removePackage(String packageName, int userId) {
290         if (DEBUG) {
291             Slog.d(TAG, "removePackage packageName :" + packageName + " userId " + userId);
292         }
293         final PackageConfigRecord record = findRecord(mPendingWrite, packageName, userId);
294         if (record != null) {
295             removeRecord(mPendingWrite, record);
296             mPersisterQueue.removeItems(item ->
297                             item.mRecord.mName == record.mName
298                                     && item.mRecord.mUserId == record.mUserId,
299                     WriteProcessItem.class);
300         }
301 
302         final PackageConfigRecord modifyRecord = findRecord(mModified, packageName, userId);
303         if (modifyRecord != null) {
304             removeRecord(mModified, modifyRecord);
305             mPersisterQueue.addItem(new DeletePackageItem(userId, packageName),
306                     false /* flush */);
307         }
308     }
309 
310     /**
311      * Retrieves and returns application configuration from persisted records if it exists, else
312      * returns null.
313      */
findPackageConfiguration(String packageName, int userId)314     ActivityTaskManagerInternal.PackageConfig findPackageConfiguration(String packageName,
315             int userId) {
316         synchronized (mLock) {
317             PackageConfigRecord packageConfigRecord = findRecord(mModified, packageName, userId);
318             if (packageConfigRecord == null) {
319                 Slog.w(TAG, "App-specific configuration not found for packageName: " + packageName
320                         + " and userId: " + userId);
321                 return null;
322             }
323             return new ActivityTaskManagerInternal.PackageConfig(
324                     packageConfigRecord.mNightMode,
325                     packageConfigRecord.mLocales,
326                     packageConfigRecord.mGrammaticalGender);
327         }
328     }
329 
330     /**
331      * Dumps app-specific configurations for all packages for which the records
332      * exist.
333      */
dump(PrintWriter pw, int userId)334     void dump(PrintWriter pw, int userId) {
335         pw.println("INSTALLED PACKAGES HAVING APP-SPECIFIC CONFIGURATIONS");
336         pw.println("Current user ID : " + userId);
337         synchronized (mLock) {
338             HashMap<String, PackageConfigRecord> persistedPackageConfigMap = mModified.get(userId);
339             if (persistedPackageConfigMap != null) {
340                 for (PackageConfigPersister.PackageConfigRecord packageConfig
341                         : persistedPackageConfigMap.values()) {
342                     pw.println();
343                     pw.println("    PackageName : " + packageConfig.mName);
344                     pw.println("        NightMode : " + packageConfig.mNightMode);
345                     pw.println("        Locales : " + packageConfig.mLocales);
346                 }
347             }
348         }
349     }
350 
351     // store a changed data so we don't need to get the process
352     static class PackageConfigRecord {
353         final String mName;
354         final int mUserId;
355         Integer mNightMode;
356         LocaleList mLocales;
357         @Configuration.GrammaticalGender
358         Integer mGrammaticalGender;
359 
PackageConfigRecord(String name, int userId)360         PackageConfigRecord(String name, int userId) {
361             mName = name;
362             mUserId = userId;
363         }
364 
isResetNightMode()365         boolean isResetNightMode() {
366             return mNightMode == Configuration.UI_MODE_NIGHT_UNDEFINED;
367         }
368 
369         @Override
toString()370         public String toString() {
371             return "PackageConfigRecord package name: " + mName + " userId " + mUserId
372                     + " nightMode " + mNightMode + " locales " + mLocales;
373         }
374     }
375 
findRecordOrCreate( SparseArray<HashMap<String, PackageConfigRecord>> list, String name, int userId)376     private PackageConfigRecord findRecordOrCreate(
377             SparseArray<HashMap<String, PackageConfigRecord>> list, String name, int userId) {
378         HashMap<String, PackageConfigRecord> records = list.get(userId);
379         if (records == null) {
380             records = new HashMap<>();
381             list.put(userId, records);
382         }
383         PackageConfigRecord record = records.get(name);
384         if (record != null) {
385             return record;
386         }
387         record = new PackageConfigRecord(name, userId);
388         records.put(name, record);
389         return record;
390     }
391 
findRecord(SparseArray<HashMap<String, PackageConfigRecord>> list, String name, int userId)392     private PackageConfigRecord findRecord(SparseArray<HashMap<String, PackageConfigRecord>> list,
393             String name, int userId) {
394         HashMap<String, PackageConfigRecord> packages = list.get(userId);
395         if (packages == null) {
396             return null;
397         }
398         return packages.get(name);
399     }
400 
removeRecord(SparseArray<HashMap<String, PackageConfigRecord>> list, PackageConfigRecord record)401     private void removeRecord(SparseArray<HashMap<String, PackageConfigRecord>> list,
402             PackageConfigRecord record) {
403         final HashMap<String, PackageConfigRecord> processes = list.get(record.mUserId);
404         if (processes != null) {
405             processes.remove(record.mName);
406         }
407     }
408 
409     private static class DeletePackageItem implements PersisterQueue.WriteQueueItem {
410         final int mUserId;
411         final String mPackageName;
412 
DeletePackageItem(int userId, String packageName)413         DeletePackageItem(int userId, String packageName) {
414             mUserId = userId;
415             mPackageName = packageName;
416         }
417 
418         @Override
process()419         public void process() {
420             File userConfigsDir = getUserConfigsDir(mUserId);
421             if (!userConfigsDir.isDirectory()) {
422                 return;
423             }
424             final AtomicFile atomicFile = new AtomicFile(new File(userConfigsDir,
425                     mPackageName + SUFFIX_FILE_NAME));
426             if (atomicFile.exists()) {
427                 atomicFile.delete();
428             }
429         }
430     }
431 
432     private class WriteProcessItem implements PersisterQueue.WriteQueueItem {
433         final PackageConfigRecord mRecord;
434 
WriteProcessItem(PackageConfigRecord record)435         WriteProcessItem(PackageConfigRecord record) {
436             mRecord = record;
437         }
438 
439         @Override
process()440         public void process() {
441             // Write out one user.
442             byte[] data = null;
443             synchronized (mLock) {
444                 try {
445                     data = saveToXml();
446                 } catch (Exception e) {
447                 }
448                 removeRecord(mPendingWrite, mRecord);
449             }
450             if (data != null) {
451                 // Write out xml file while not holding mService lock.
452                 FileOutputStream file = null;
453                 AtomicFile atomicFile = null;
454                 try {
455                     File userConfigsDir = getUserConfigsDir(mRecord.mUserId);
456                     if (!userConfigsDir.isDirectory() && !userConfigsDir.mkdirs()) {
457                         Slog.e(TAG, "Failure creating tasks directory for user " + mRecord.mUserId
458                                 + ": " + userConfigsDir);
459                         return;
460                     }
461                     atomicFile = new AtomicFile(new File(userConfigsDir,
462                             mRecord.mName + SUFFIX_FILE_NAME));
463                     file = atomicFile.startWrite();
464                     file.write(data);
465                     atomicFile.finishWrite(file);
466                 } catch (IOException e) {
467                     if (file != null) {
468                         atomicFile.failWrite(file);
469                     }
470                     Slog.e(TAG, "Unable to open " + atomicFile + " for persisting. " + e);
471                 }
472             }
473         }
474 
saveToXml()475         private byte[] saveToXml() throws IOException {
476             final ByteArrayOutputStream os = new ByteArrayOutputStream();
477             final TypedXmlSerializer xmlSerializer = Xml.resolveSerializer(os);
478 
479             xmlSerializer.startDocument(null, true);
480             if (DEBUG) {
481                 Slog.d(TAG, "Writing package configuration=" + mRecord);
482             }
483             xmlSerializer.startTag(null, TAG_CONFIG);
484             xmlSerializer.attribute(null, ATTR_PACKAGE_NAME, mRecord.mName);
485             if (mRecord.mNightMode != null) {
486                 xmlSerializer.attributeInt(null, ATTR_NIGHT_MODE, mRecord.mNightMode);
487             }
488             if (mRecord.mLocales != null) {
489                 xmlSerializer.attribute(null, ATTR_LOCALES, mRecord.mLocales
490                         .toLanguageTags());
491             }
492             xmlSerializer.endTag(null, TAG_CONFIG);
493             xmlSerializer.endDocument();
494             xmlSerializer.flush();
495 
496             return os.toByteArray();
497         }
498     }
499 }
500