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