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