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