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