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.app.UiModeManager.MODE_NIGHT_AUTO; 20 import static android.app.UiModeManager.MODE_NIGHT_CUSTOM; 21 22 import android.annotation.NonNull; 23 import android.os.Environment; 24 import android.util.AtomicFile; 25 import android.util.Slog; 26 import android.util.SparseArray; 27 import android.util.TypedXmlPullParser; 28 import android.util.TypedXmlSerializer; 29 import android.util.Xml; 30 31 import com.android.internal.annotations.GuardedBy; 32 import com.android.internal.util.XmlUtils; 33 34 import org.xmlpull.v1.XmlPullParser; 35 import org.xmlpull.v1.XmlPullParserException; 36 37 import java.io.ByteArrayOutputStream; 38 import java.io.File; 39 import java.io.FileInputStream; 40 import java.io.FileNotFoundException; 41 import java.io.FileOutputStream; 42 import java.io.IOException; 43 import java.io.InputStream; 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 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 64 @GuardedBy("mLock") 65 private final SparseArray<HashMap<String, PackageConfigRecord>> mPendingWrite = 66 new SparseArray<>(); 67 @GuardedBy("mLock") 68 private final SparseArray<HashMap<String, PackageConfigRecord>> mModified = 69 new SparseArray<>(); 70 getUserConfigsDir(int userId)71 private static File getUserConfigsDir(int userId) { 72 return new File(Environment.getDataSystemCeDirectory(userId), PACKAGE_DIRNAME); 73 } 74 PackageConfigPersister(PersisterQueue queue)75 PackageConfigPersister(PersisterQueue queue) { 76 mPersisterQueue = queue; 77 } 78 79 @GuardedBy("mLock") loadUserPackages(int userId)80 void loadUserPackages(int userId) { 81 synchronized (mLock) { 82 final File userConfigsDir = getUserConfigsDir(userId); 83 final File[] configFiles = userConfigsDir.listFiles(); 84 if (configFiles == null) { 85 Slog.v(TAG, "loadPackages: empty list files from " + userConfigsDir); 86 return; 87 } 88 89 for (int fileIndex = 0; fileIndex < configFiles.length; ++fileIndex) { 90 final File configFile = configFiles[fileIndex]; 91 if (DEBUG) { 92 Slog.d(TAG, "loadPackages: userId=" + userId 93 + ", configFile=" + configFile.getName()); 94 } 95 if (!configFile.getName().endsWith(SUFFIX_FILE_NAME)) { 96 continue; 97 } 98 99 try (InputStream is = new FileInputStream(configFile)) { 100 final TypedXmlPullParser in = Xml.resolvePullParser(is); 101 int event; 102 String packageName = null; 103 int nightMode = MODE_NIGHT_AUTO; 104 while (((event = in.next()) != XmlPullParser.END_DOCUMENT) 105 && event != XmlPullParser.END_TAG) { 106 final String name = in.getName(); 107 if (event == XmlPullParser.START_TAG) { 108 if (DEBUG) { 109 Slog.d(TAG, "loadPackages: START_TAG name=" + name); 110 } 111 if (TAG_CONFIG.equals(name)) { 112 for (int attIdx = in.getAttributeCount() - 1; attIdx >= 0; 113 --attIdx) { 114 final String attrName = in.getAttributeName(attIdx); 115 final String attrValue = in.getAttributeValue(attIdx); 116 switch (attrName) { 117 case ATTR_PACKAGE_NAME: 118 packageName = attrValue; 119 break; 120 case ATTR_NIGHT_MODE: 121 nightMode = Integer.parseInt(attrValue); 122 break; 123 } 124 } 125 } 126 } 127 XmlUtils.skipCurrentTag(in); 128 } 129 if (packageName != null) { 130 final PackageConfigRecord initRecord = 131 findRecordOrCreate(mModified, packageName, userId); 132 initRecord.mNightMode = nightMode; 133 if (DEBUG) { 134 Slog.d(TAG, "loadPackages: load one package " + initRecord); 135 } 136 } 137 } catch (FileNotFoundException e) { 138 e.printStackTrace(); 139 } catch (IOException e) { 140 e.printStackTrace(); 141 } catch (XmlPullParserException e) { 142 e.printStackTrace(); 143 } 144 } 145 } 146 } 147 148 @GuardedBy("mLock") updateConfigIfNeeded(@onNull ConfigurationContainer container, int userId, String packageName)149 void updateConfigIfNeeded(@NonNull ConfigurationContainer container, int userId, 150 String packageName) { 151 synchronized (mLock) { 152 final PackageConfigRecord modifiedRecord = findRecord(mModified, packageName, userId); 153 if (DEBUG) { 154 Slog.d(TAG, 155 "updateConfigIfNeeded record " + container + " find? " + modifiedRecord); 156 } 157 if (modifiedRecord != null) { 158 container.setOverrideNightMode(modifiedRecord.mNightMode); 159 } 160 } 161 } 162 163 @GuardedBy("mLock") updateFromImpl(String packageName, int userId, ActivityTaskManagerService.PackageConfigurationUpdaterImpl impl)164 void updateFromImpl(String packageName, int userId, 165 ActivityTaskManagerService.PackageConfigurationUpdaterImpl impl) { 166 synchronized (mLock) { 167 PackageConfigRecord record = findRecordOrCreate(mModified, packageName, userId); 168 record.mNightMode = impl.getNightMode(); 169 170 if (record.isResetNightMode()) { 171 removePackage(record.mName, record.mUserId); 172 } else { 173 final PackageConfigRecord pendingRecord = 174 findRecord(mPendingWrite, record.mName, record.mUserId); 175 final PackageConfigRecord writeRecord; 176 if (pendingRecord == null) { 177 writeRecord = findRecordOrCreate(mPendingWrite, record.mName, 178 record.mUserId); 179 } else { 180 writeRecord = pendingRecord; 181 } 182 if (writeRecord.mNightMode == record.mNightMode) { 183 return; 184 } 185 writeRecord.mNightMode = record.mNightMode; 186 if (DEBUG) { 187 Slog.d(TAG, "PackageConfigUpdater save config " + writeRecord); 188 } 189 mPersisterQueue.addItem(new WriteProcessItem(writeRecord), false /* flush */); 190 } 191 } 192 } 193 194 @GuardedBy("mLock") removeUser(int userId)195 void removeUser(int userId) { 196 synchronized (mLock) { 197 final HashMap<String, PackageConfigRecord> modifyRecords = mModified.get(userId); 198 final HashMap<String, PackageConfigRecord> writeRecords = mPendingWrite.get(userId); 199 if ((modifyRecords == null || modifyRecords.size() == 0) 200 && (writeRecords == null || writeRecords.size() == 0)) { 201 return; 202 } 203 final HashMap<String, PackageConfigRecord> tempList = new HashMap<>(modifyRecords); 204 tempList.forEach((name, record) -> { 205 removePackage(record.mName, record.mUserId); 206 }); 207 } 208 } 209 210 @GuardedBy("mLock") onPackageUninstall(String packageName)211 void onPackageUninstall(String packageName) { 212 synchronized (mLock) { 213 for (int i = mModified.size() - 1; i > 0; i--) { 214 final int userId = mModified.keyAt(i); 215 removePackage(packageName, userId); 216 } 217 } 218 } 219 removePackage(String packageName, int userId)220 private void removePackage(String packageName, int userId) { 221 if (DEBUG) { 222 Slog.d(TAG, "removePackage packageName :" + packageName + " userId " + userId); 223 } 224 final PackageConfigRecord record = findRecord(mPendingWrite, packageName, userId); 225 if (record != null) { 226 removeRecord(mPendingWrite, record); 227 mPersisterQueue.removeItems(item -> 228 item.mRecord.mName == record.mName 229 && item.mRecord.mUserId == record.mUserId, 230 WriteProcessItem.class); 231 } 232 233 final PackageConfigRecord modifyRecord = findRecord(mModified, packageName, userId); 234 if (modifyRecord != null) { 235 removeRecord(mModified, modifyRecord); 236 mPersisterQueue.addItem(new DeletePackageItem(userId, packageName), 237 false /* flush */); 238 } 239 } 240 241 // store a changed data so we don't need to get the process 242 static class PackageConfigRecord { 243 final String mName; 244 final int mUserId; 245 int mNightMode; 246 PackageConfigRecord(String name, int userId)247 PackageConfigRecord(String name, int userId) { 248 mName = name; 249 mUserId = userId; 250 } 251 isResetNightMode()252 boolean isResetNightMode() { 253 return mNightMode == MODE_NIGHT_AUTO || mNightMode == MODE_NIGHT_CUSTOM; 254 } 255 256 @Override toString()257 public String toString() { 258 return "PackageConfigRecord package name: " + mName + " userId " + mUserId 259 + " nightMode " + mNightMode; 260 } 261 } 262 findRecordOrCreate( SparseArray<HashMap<String, PackageConfigRecord>> list, String name, int userId)263 private PackageConfigRecord findRecordOrCreate( 264 SparseArray<HashMap<String, PackageConfigRecord>> list, String name, int userId) { 265 HashMap<String, PackageConfigRecord> records = list.get(userId); 266 if (records == null) { 267 records = new HashMap<>(); 268 list.put(userId, records); 269 } 270 PackageConfigRecord record = records.get(name); 271 if (record != null) { 272 return record; 273 } 274 record = new PackageConfigRecord(name, userId); 275 records.put(name, record); 276 return record; 277 } 278 findRecord(SparseArray<HashMap<String, PackageConfigRecord>> list, String name, int userId)279 private PackageConfigRecord findRecord(SparseArray<HashMap<String, PackageConfigRecord>> list, 280 String name, int userId) { 281 HashMap<String, PackageConfigRecord> packages = list.get(userId); 282 if (packages == null) { 283 return null; 284 } 285 return packages.get(name); 286 } 287 removeRecord(SparseArray<HashMap<String, PackageConfigRecord>> list, PackageConfigRecord record)288 private void removeRecord(SparseArray<HashMap<String, PackageConfigRecord>> list, 289 PackageConfigRecord record) { 290 final HashMap<String, PackageConfigRecord> processes = list.get(record.mUserId); 291 if (processes != null) { 292 processes.remove(record.mName); 293 } 294 } 295 296 private static class DeletePackageItem implements PersisterQueue.WriteQueueItem { 297 final int mUserId; 298 final String mPackageName; 299 DeletePackageItem(int userId, String packageName)300 DeletePackageItem(int userId, String packageName) { 301 mUserId = userId; 302 mPackageName = packageName; 303 } 304 305 @Override process()306 public void process() { 307 File userConfigsDir = getUserConfigsDir(mUserId); 308 if (!userConfigsDir.isDirectory()) { 309 return; 310 } 311 final AtomicFile atomicFile = new AtomicFile(new File(userConfigsDir, 312 mPackageName + SUFFIX_FILE_NAME)); 313 if (atomicFile.exists()) { 314 atomicFile.delete(); 315 } 316 } 317 } 318 319 private class WriteProcessItem implements PersisterQueue.WriteQueueItem { 320 final PackageConfigRecord mRecord; 321 WriteProcessItem(PackageConfigRecord record)322 WriteProcessItem(PackageConfigRecord record) { 323 mRecord = record; 324 } 325 326 @Override process()327 public void process() { 328 // Write out one user. 329 byte[] data = null; 330 synchronized (mLock) { 331 try { 332 data = saveToXml(); 333 } catch (Exception e) { 334 } 335 removeRecord(mPendingWrite, mRecord); 336 } 337 if (data != null) { 338 // Write out xml file while not holding mService lock. 339 FileOutputStream file = null; 340 AtomicFile atomicFile = null; 341 try { 342 File userConfigsDir = getUserConfigsDir(mRecord.mUserId); 343 if (!userConfigsDir.isDirectory() && !userConfigsDir.mkdirs()) { 344 Slog.e(TAG, "Failure creating tasks directory for user " + mRecord.mUserId 345 + ": " + userConfigsDir); 346 return; 347 } 348 atomicFile = new AtomicFile(new File(userConfigsDir, 349 mRecord.mName + SUFFIX_FILE_NAME)); 350 file = atomicFile.startWrite(); 351 file.write(data); 352 atomicFile.finishWrite(file); 353 } catch (IOException e) { 354 if (file != null) { 355 atomicFile.failWrite(file); 356 } 357 Slog.e(TAG, "Unable to open " + atomicFile + " for persisting. " + e); 358 } 359 } 360 } 361 saveToXml()362 private byte[] saveToXml() throws IOException { 363 final ByteArrayOutputStream os = new ByteArrayOutputStream(); 364 final TypedXmlSerializer xmlSerializer = Xml.resolveSerializer(os); 365 366 xmlSerializer.startDocument(null, true); 367 if (DEBUG) { 368 Slog.d(TAG, "Writing package configuration=" + mRecord); 369 } 370 xmlSerializer.startTag(null, TAG_CONFIG); 371 xmlSerializer.attribute(null, ATTR_PACKAGE_NAME, mRecord.mName); 372 xmlSerializer.attributeInt(null, ATTR_NIGHT_MODE, mRecord.mNightMode); 373 xmlSerializer.endTag(null, TAG_CONFIG); 374 xmlSerializer.endDocument(); 375 xmlSerializer.flush(); 376 377 return os.toByteArray(); 378 } 379 } 380 } 381