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