summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Nishant Singh <nishantks@google.com> 2022-01-13 07:10:07 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2022-01-13 07:10:07 +0000
commit7f994312d9383465308bd4d02db32ab55f2dce1c (patch)
tree903b59580c7c542968f2050ce3478ccd072ea939
parent4217ed289c1ee560e3887a91c71b1ac3ca01be29 (diff)
parent50b6f34e2f386b74cafebb960343b9a56abd7602 (diff)
Merge "Simplify app-locales B&R stage data retention."
-rw-r--r--services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java185
-rw-r--r--services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java344
-rw-r--r--services/tests/servicestests/src/com/android/server/locales/ShadowLocaleManagerBackupHelper.java7
3 files changed, 155 insertions, 381 deletions
diff --git a/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java b/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java
index b2bd47b55cfb..134fb967cac5 100644
--- a/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java
+++ b/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java
@@ -21,7 +21,6 @@ import static android.os.UserHandle.USER_NULL;
import static com.android.server.locales.LocaleManagerService.DEBUG;
import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.backup.BackupManager;
import android.content.BroadcastReceiver;
@@ -34,7 +33,6 @@ import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.os.BestClock;
import android.os.Binder;
-import android.os.Environment;
import android.os.HandlerThread;
import android.os.LocaleList;
import android.os.Process;
@@ -42,7 +40,6 @@ import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserHandle;
import android.text.TextUtils;
-import android.util.AtomicFile;
import android.util.Slog;
import android.util.SparseArray;
import android.util.TypedXmlPullParser;
@@ -53,17 +50,12 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.content.PackageMonitor;
import com.android.internal.util.XmlUtils;
-import libcore.io.IoUtils;
-
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileOutputStream;
import java.io.IOException;
-import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
@@ -71,8 +63,6 @@ import java.time.Clock;
import java.time.Duration;
import java.time.ZoneOffset;
import java.util.HashMap;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
/**
* Helper class for managing backup and restore of app-specific locales.
@@ -87,33 +77,28 @@ class LocaleManagerBackupHelper {
private static final String ATTR_LOCALES = "locales";
private static final String ATTR_CREATION_TIME_MILLIS = "creationTimeMillis";
- private static final String STAGE_FILE_NAME = "staged_locales";
private static final String SYSTEM_BACKUP_PACKAGE_KEY = "android";
-
- private static final Pattern STAGE_FILE_NAME_PATTERN = Pattern.compile(
- TextUtils.formatSimple("(^%s_)(\\d+)(\\.xml$)", STAGE_FILE_NAME));
- private static final int USER_ID_GROUP_INDEX_IN_PATTERN = 2;
- private static final Duration STAGE_FILE_RETENTION_PERIOD = Duration.ofDays(3);
+ // Stage data would be deleted on reboot since it's stored in memory. So it's retained until
+ // retention period OR next reboot, whichever happens earlier.
+ private static final Duration STAGE_DATA_RETENTION_PERIOD = Duration.ofDays(3);
private final LocaleManagerService mLocaleManagerService;
private final PackageManagerInternal mPackageManagerInternal;
- private final File mStagedLocalesDir;
private final Clock mClock;
private final Context mContext;
private final Object mStagedDataLock = new Object();
// Staged data map keyed by user-id to handle multi-user scenario / work profiles. We are using
// SparseArray because it is more memory-efficient than a HashMap.
- private final SparseArray<StagedData> mStagedData = new SparseArray<>();
+ private final SparseArray<StagedData> mStagedData;
private final PackageMonitor mPackageMonitor;
private final BroadcastReceiver mUserMonitor;
LocaleManagerBackupHelper(LocaleManagerService localeManagerService,
PackageManagerInternal pmInternal) {
- this(localeManagerService.mContext, localeManagerService, pmInternal,
- new File(Environment.getDataSystemCeDirectory(),
- "app_locales"), getDefaultClock());
+ this(localeManagerService.mContext, localeManagerService, pmInternal, getDefaultClock(),
+ new SparseArray<>());
}
private static @NonNull Clock getDefaultClock() {
@@ -123,14 +108,12 @@ class LocaleManagerBackupHelper {
@VisibleForTesting LocaleManagerBackupHelper(Context context,
LocaleManagerService localeManagerService,
- PackageManagerInternal pmInternal, File stagedLocalesDir, Clock clock) {
+ PackageManagerInternal pmInternal, Clock clock, SparseArray<StagedData> stagedData) {
mContext = context;
mLocaleManagerService = localeManagerService;
mPackageManagerInternal = pmInternal;
mClock = clock;
- mStagedLocalesDir = stagedLocalesDir;
-
- loadAllStageFiles();
+ mStagedData = stagedData;
HandlerThread broadcastHandlerThread = new HandlerThread(TAG,
Process.THREAD_PRIORITY_BACKGROUND);
@@ -158,67 +141,6 @@ class LocaleManagerBackupHelper {
}
/**
- * Loads the staged data into memory by reading all the files in the staged directory.
- *
- * <p><b>Note:</b> We don't ned to hold the lock here because this is only called in the
- * constructor (before any broadcast receivers are registered).
- */
- private void loadAllStageFiles() {
- File[] files = mStagedLocalesDir.listFiles();
- if (files == null) {
- return;
- }
- for (File file : files) {
- String fileName = file.getName();
- Matcher matcher = STAGE_FILE_NAME_PATTERN.matcher(fileName);
- if (!matcher.matches()) {
- file.delete();
- Slog.w(TAG, TextUtils.formatSimple("Deleted %s. Reason: %s.", fileName,
- "Unrecognized file"));
- continue;
- }
- try {
- final int userId = Integer.parseInt(matcher.group(USER_ID_GROUP_INDEX_IN_PATTERN));
- StagedData stagedData = readStageFile(file);
- if (stagedData != null) {
- mStagedData.put(userId, stagedData);
- } else {
- file.delete();
- Slog.w(TAG, TextUtils.formatSimple("Deleted %s. Reason: %s.", fileName,
- "Could not read file"));
- }
- } catch (NumberFormatException e) {
- file.delete();
- Slog.w(TAG, TextUtils.formatSimple("Deleted %s. Reason: %s.", fileName,
- "Could not parse user id from file name"));
- }
- }
- }
-
- /**
- * Loads the stage file from the disk and parses it into a list of app backups.
- */
- private @Nullable StagedData readStageFile(@NonNull File file) {
- InputStream stagedDataInputStream = null;
- AtomicFile stageFile = new AtomicFile(file);
- try {
- stagedDataInputStream = stageFile.openRead();
- final TypedXmlPullParser parser = Xml.newFastPullParser();
- parser.setInput(stagedDataInputStream, StandardCharsets.UTF_8.name());
-
- XmlUtils.beginDocument(parser, LOCALES_XML_TAG);
- long creationTimeMillis = parser.getAttributeLong(/* namespace= */ null,
- ATTR_CREATION_TIME_MILLIS);
- return new StagedData(creationTimeMillis, readFromXml(parser));
- } catch (IOException | XmlPullParserException e) {
- Slog.e(TAG, "Could not parse stage file ", e);
- } finally {
- IoUtils.closeQuietly(stagedDataInputStream);
- }
- return null;
- }
-
- /**
* @see LocaleManagerInternal#getBackupPayload(int userId)
*/
public byte[] getBackupPayload(int userId) {
@@ -261,9 +183,7 @@ class LocaleManagerBackupHelper {
final ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
- // Passing arbitrary value for creationTimeMillis since it is ignored when forStage
- // is false.
- writeToXml(out, pkgStates, /* forStage= */ false, /* creationTimeMillis= */ -1);
+ writeToXml(out, pkgStates);
} catch (IOException e) {
Slog.e(TAG, "Could not write to xml for backup ", e);
return null;
@@ -284,7 +204,7 @@ class LocaleManagerBackupHelper {
int userId = mStagedData.keyAt(i);
StagedData stagedData = mStagedData.get(userId);
if (stagedData.mCreationTimeMillis
- < mClock.millis() - STAGE_FILE_RETENTION_PERIOD.toMillis()) {
+ < mClock.millis() - STAGE_DATA_RETENTION_PERIOD.toMillis()) {
deleteStagedDataLocked(userId);
}
}
@@ -305,7 +225,7 @@ class LocaleManagerBackupHelper {
final ByteArrayInputStream inputStream = new ByteArrayInputStream(payload);
- HashMap<String, String> pkgStates = new HashMap<>();
+ HashMap<String, String> pkgStates;
try {
// Parse the input blob into a list of BackupPackageState.
final TypedXmlPullParser parser = Xml.newFastPullParser();
@@ -315,6 +235,7 @@ class LocaleManagerBackupHelper {
pkgStates = readFromXml(parser);
} catch (IOException | XmlPullParserException e) {
Slog.e(TAG, "Could not parse payload ", e);
+ return;
}
// We need a lock here to prevent race conditions when accessing the stage file.
@@ -323,7 +244,7 @@ class LocaleManagerBackupHelper {
// performed simultaneously.
synchronized (mStagedDataLock) {
// Backups for apps which are yet to be installed.
- mStagedData.put(userId, new StagedData(mClock.millis(), new HashMap<>()));
+ StagedData stagedData = new StagedData(mClock.millis(), new HashMap<>());
for (String pkgName : pkgStates.keySet()) {
String languageTags = pkgStates.get(pkgName);
@@ -333,7 +254,7 @@ class LocaleManagerBackupHelper {
checkExistingLocalesAndApplyRestore(pkgName, languageTags, userId);
} else {
// Stage the data if the app isn't installed.
- mStagedData.get(userId).mPackageStates.put(pkgName, languageTags);
+ stagedData.mPackageStates.put(pkgName, languageTags);
if (DEBUG) {
Slog.d(TAG, "Add locales=" + languageTags
+ " package=" + pkgName + " for lazy restore.");
@@ -341,7 +262,9 @@ class LocaleManagerBackupHelper {
}
}
- writeStageFileLocked(userId);
+ if (!stagedData.mPackageStates.isEmpty()) {
+ mStagedData.put(userId, stagedData);
+ }
}
}
@@ -396,55 +319,10 @@ class LocaleManagerBackupHelper {
}
}
- /**
- * Converts the list of app backups into xml and writes it onto the disk.
- */
- private void writeStageFileLocked(int userId) {
- StagedData stagedData = mStagedData.get(userId);
- if (stagedData.mPackageStates.isEmpty()) {
- deleteStagedDataLocked(userId);
- return;
- }
-
- final FileOutputStream stagedDataOutputStream;
- AtomicFile stageFile = new AtomicFile(
- new File(mStagedLocalesDir,
- TextUtils.formatSimple("%s_%d.xml", STAGE_FILE_NAME, userId)));
- try {
- stagedDataOutputStream = stageFile.startWrite();
- } catch (IOException e) {
- Slog.e(TAG, "Failed to save stage file");
- return;
- }
-
- try {
- writeToXml(stagedDataOutputStream, stagedData.mPackageStates, /* forStage= */ true,
- stagedData.mCreationTimeMillis);
- stageFile.finishWrite(stagedDataOutputStream);
- if (DEBUG) {
- Slog.d(TAG, "Stage file written.");
- }
- } catch (IOException e) {
- Slog.e(TAG, "Could not write stage file", e);
- stageFile.failWrite(stagedDataOutputStream);
- }
- }
-
private void deleteStagedDataLocked(@UserIdInt int userId) {
- AtomicFile stageFile = getStageFileIfExistsLocked(userId);
- if (stageFile != null) {
- stageFile.delete();
- }
mStagedData.remove(userId);
}
- private @Nullable AtomicFile getStageFileIfExistsLocked(@UserIdInt int userId) {
- final File stageFile = new File(mStagedLocalesDir,
- TextUtils.formatSimple("%s_%d.xml", STAGE_FILE_NAME, userId));
- return stageFile.isFile() ? new AtomicFile(stageFile)
- : null;
- }
-
/**
* Parses the backup data from the serialized xml input stream.
*/
@@ -468,15 +346,8 @@ class LocaleManagerBackupHelper {
/**
* Converts the list of app backup data into a serialized xml stream.
- *
- * @param forStage Flag to indicate whether this method is called for the purpose of
- * staging the data. Note that if this is false, {@code creationTimeMillis} is ignored because
- * we only need it for the stage data.
- * @param creationTimeMillis The timestamp when the stage data was created. This is required
- * to determine when to delete the stage data.
*/
- private static void writeToXml(OutputStream stream,
- @NonNull HashMap<String, String> pkgStates, boolean forStage, long creationTimeMillis)
+ private static void writeToXml(OutputStream stream, @NonNull HashMap<String, String> pkgStates)
throws IOException {
if (pkgStates.isEmpty()) {
// No need to write anything at all if pkgStates is empty.
@@ -488,11 +359,6 @@ class LocaleManagerBackupHelper {
out.startDocument(/* encoding= */ null, /* standalone= */ true);
out.startTag(/* namespace= */ null, LOCALES_XML_TAG);
- if (forStage) {
- out.attribute(/* namespace= */ null, ATTR_CREATION_TIME_MILLIS,
- Long.toString(creationTimeMillis));
- }
-
for (String pkg : pkgStates.keySet()) {
out.startTag(/* namespace= */ null, PACKAGE_XML_TAG);
out.attribute(/* namespace= */ null, ATTR_PACKAGE_NAME, pkg);
@@ -504,7 +370,7 @@ class LocaleManagerBackupHelper {
out.endDocument();
}
- private static class StagedData {
+ static class StagedData {
final long mCreationTimeMillis;
final HashMap<String, String> mPackageStates;
@@ -517,7 +383,7 @@ class LocaleManagerBackupHelper {
/**
* Broadcast listener to capture user removed event.
*
- * <p>The stage file is deleted when a user is removed.
+ * <p>The stage data is deleted when a user is removed.
*/
private final class UserMonitor extends BroadcastReceiver {
@Override
@@ -546,6 +412,8 @@ class LocaleManagerBackupHelper {
public void onPackageAdded(String packageName, int uid) {
try {
synchronized (mStagedDataLock) {
+ cleanStagedDataForOldEntriesLocked();
+
int userId = UserHandle.getUserId(uid);
if (mStagedData.contains(userId)) {
// Perform lazy restore only if the staged data exists.
@@ -589,7 +457,7 @@ class LocaleManagerBackupHelper {
// Check if the package is installed indeed
if (!isPackageInstalledForUser(packageName, userId)) {
Slog.e(TAG, packageName + " not installed for user " + userId
- + ". Could not restore locales from stage file");
+ + ". Could not restore locales from stage data");
return;
}
@@ -603,8 +471,11 @@ class LocaleManagerBackupHelper {
// Remove the restored entry from the staged data list.
stagedData.mPackageStates.remove(pkgName);
- // Update the file on the disk.
- writeStageFileLocked(userId);
+
+ // Remove the stage data entry for user if there are no more packages to restore.
+ if (stagedData.mPackageStates.isEmpty()) {
+ mStagedData.remove(userId);
+ }
// No need to loop further after restoring locales because the staged data will
// contain at most one entry for the newly added package.
diff --git a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java
index 70e78eb41999..c77100045118 100644
--- a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java
@@ -17,9 +17,7 @@
package com.android.server.locales;
import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNull;
-import static junit.framework.Assert.assertTrue;
import static org.junit.Assert.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
@@ -43,11 +41,10 @@ import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.os.Binder;
-import android.os.Environment;
import android.os.LocaleList;
import android.os.RemoteException;
import android.os.SimpleClock;
-import android.util.AtomicFile;
+import android.util.SparseArray;
import android.util.TypedXmlPullParser;
import android.util.TypedXmlSerializer;
import android.util.Xml;
@@ -57,6 +54,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.android.internal.content.PackageMonitor;
import com.android.internal.util.XmlUtils;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -65,10 +63,7 @@ import org.xmlpull.v1.XmlPullParserException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileOutputStream;
import java.io.IOException;
-import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.time.Clock;
@@ -95,9 +90,8 @@ public class LocaleManagerBackupRestoreTest {
LocaleList.forLanguageTags(DEFAULT_LOCALE_TAGS);
private static final Map<String, String> DEFAULT_PACKAGE_LOCALES_MAP = Map.of(
DEFAULT_PACKAGE_NAME, DEFAULT_LOCALE_TAGS);
- private static final File STAGED_LOCALES_DIR = new File(
- Environment.getExternalStorageDirectory(), "lmsUnitTests");
-
+ private static final SparseArray<LocaleManagerBackupHelper.StagedData> STAGE_DATA =
+ new SparseArray<>();
private LocaleManagerBackupHelper mBackupHelper;
private long mCurrentTimeMillis;
@@ -138,14 +132,17 @@ public class LocaleManagerBackupRestoreTest {
doReturn(mMockPackageManager).when(mMockContext).getPackageManager();
mBackupHelper = spy(new ShadowLocaleManagerBackupHelper(mMockContext,
- mMockLocaleManagerService, mMockPackageManagerInternal,
- new File(Environment.getExternalStorageDirectory(), "lmsUnitTests"), mClock));
+ mMockLocaleManagerService, mMockPackageManagerInternal, mClock, STAGE_DATA));
doNothing().when(mBackupHelper).notifyBackupManager();
mUserMonitor = mBackupHelper.getUserMonitor();
mPackageMonitor = mBackupHelper.getPackageMonitor();
setCurrentTimeMillis(DEFAULT_CREATION_TIME_MILLIS);
- cleanStagedFiles();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ STAGE_DATA.clear();
}
@Test
@@ -203,25 +200,25 @@ public class LocaleManagerBackupRestoreTest {
}
@Test
- public void testRestore_nullPayload_nothingRestoredAndNoStageFile() throws Exception {
+ public void testRestore_nullPayload_nothingRestoredAndNoStageData() throws Exception {
mBackupHelper.stageAndApplyRestoredPayload(/* payload= */ null, DEFAULT_USER_ID);
verifyNothingRestored();
- checkStageFileDoesNotExist(DEFAULT_USER_ID);
+ checkStageDataDoesNotExist(DEFAULT_USER_ID);
}
@Test
- public void testRestore_zeroLengthPayload_nothingRestoredAndNoStageFile() throws Exception {
+ public void testRestore_zeroLengthPayload_nothingRestoredAndNoStageData() throws Exception {
final ByteArrayOutputStream out = new ByteArrayOutputStream();
mBackupHelper.stageAndApplyRestoredPayload(/* payload= */ out.toByteArray(),
DEFAULT_USER_ID);
verifyNothingRestored();
- checkStageFileDoesNotExist(DEFAULT_USER_ID);
+ checkStageDataDoesNotExist(DEFAULT_USER_ID);
}
@Test
- public void testRestore_allAppsInstalled_noStageFileCreated() throws Exception {
+ public void testRestore_allAppsInstalled_noStageDataCreated() throws Exception {
final ByteArrayOutputStream out = new ByteArrayOutputStream();
writeTestPayload(out, DEFAULT_PACKAGE_LOCALES_MAP);
@@ -234,8 +231,7 @@ public class LocaleManagerBackupRestoreTest {
verify(mMockLocaleManagerService, times(1)).setApplicationLocales(DEFAULT_PACKAGE_NAME,
DEFAULT_USER_ID, DEFAULT_LOCALES);
- // Stage file wasn't created.
- checkStageFileDoesNotExist(DEFAULT_USER_ID);
+ checkStageDataDoesNotExist(DEFAULT_USER_ID);
}
@Test
@@ -248,8 +244,8 @@ public class LocaleManagerBackupRestoreTest {
mBackupHelper.stageAndApplyRestoredPayload(out.toByteArray(), DEFAULT_USER_ID);
verifyNothingRestored();
- verifyStageFileContent(DEFAULT_PACKAGE_LOCALES_MAP,
- getStageFileIfExists(DEFAULT_USER_ID), DEFAULT_CREATION_TIME_MILLIS);
+ verifyStageDataForUser(DEFAULT_PACKAGE_LOCALES_MAP,
+ DEFAULT_CREATION_TIME_MILLIS, DEFAULT_USER_ID);
}
@Test
@@ -257,8 +253,10 @@ public class LocaleManagerBackupRestoreTest {
final ByteArrayOutputStream out = new ByteArrayOutputStream();
HashMap<String, String> pkgLocalesMap = new HashMap<>();
- String pkgNameA = "com.android.myAppA", pkgNameB = "com.android.myAppB";
- String langTagsA = "ru", langTagsB = "hi,fr";
+ String pkgNameA = "com.android.myAppA";
+ String pkgNameB = "com.android.myAppB";
+ String langTagsA = "ru";
+ String langTagsB = "hi,fr";
pkgLocalesMap.put(pkgNameA, langTagsA);
pkgLocalesMap.put(pkgNameB, langTagsB);
writeTestPayload(out, pkgLocalesMap);
@@ -273,12 +271,12 @@ public class LocaleManagerBackupRestoreTest {
LocaleList.forLanguageTags(langTagsA));
pkgLocalesMap.remove(pkgNameA);
- verifyStageFileContent(pkgLocalesMap, getStageFileIfExists(DEFAULT_USER_ID),
- DEFAULT_CREATION_TIME_MILLIS);
+ verifyStageDataForUser(pkgLocalesMap,
+ DEFAULT_CREATION_TIME_MILLIS, DEFAULT_USER_ID);
}
@Test
- public void testRestore_appLocalesAlreadySet_nothingRestoredAndNoStageFile() throws Exception {
+ public void testRestore_appLocalesAlreadySet_nothingRestoredAndNoStageData() throws Exception {
final ByteArrayOutputStream out = new ByteArrayOutputStream();
writeTestPayload(out, DEFAULT_PACKAGE_LOCALES_MAP);
@@ -289,8 +287,7 @@ public class LocaleManagerBackupRestoreTest {
// Since locales are already set, we should not restore anything for it.
verifyNothingRestored();
- // Stage file wasn't created
- checkStageFileDoesNotExist(DEFAULT_USER_ID);
+ checkStageDataDoesNotExist(DEFAULT_USER_ID);
}
@Test
@@ -299,9 +296,12 @@ public class LocaleManagerBackupRestoreTest {
final ByteArrayOutputStream out = new ByteArrayOutputStream();
HashMap<String, String> pkgLocalesMap = new HashMap<>();
- String pkgNameA = "com.android.myAppA", pkgNameB = "com.android.myAppB", pkgNameC =
- "com.android.myAppC";
- String langTagsA = "ru", langTagsB = "hi,fr", langTagsC = "zh,es";
+ String pkgNameA = "com.android.myAppA";
+ String pkgNameB = "com.android.myAppB";
+ String pkgNameC = "com.android.myAppC";
+ String langTagsA = "ru";
+ String langTagsB = "hi,fr";
+ String langTagsC = "zh,es";
pkgLocalesMap.put(pkgNameA, langTagsA);
pkgLocalesMap.put(pkgNameB, langTagsB);
pkgLocalesMap.put(pkgNameC, langTagsC);
@@ -328,8 +328,8 @@ public class LocaleManagerBackupRestoreTest {
// App C is staged.
pkgLocalesMap.remove(pkgNameA);
pkgLocalesMap.remove(pkgNameB);
- verifyStageFileContent(pkgLocalesMap, getStageFileIfExists(DEFAULT_USER_ID),
- DEFAULT_CREATION_TIME_MILLIS);
+ verifyStageDataForUser(pkgLocalesMap,
+ DEFAULT_CREATION_TIME_MILLIS, DEFAULT_USER_ID);
}
@Test
@@ -341,15 +341,15 @@ public class LocaleManagerBackupRestoreTest {
mBackupHelper.stageAndApplyRestoredPayload(out.toByteArray(), DEFAULT_USER_ID);
- verifyStageFileContent(DEFAULT_PACKAGE_LOCALES_MAP, getStageFileIfExists(DEFAULT_USER_ID),
- DEFAULT_CREATION_TIME_MILLIS);
+ verifyStageDataForUser(DEFAULT_PACKAGE_LOCALES_MAP,
+ DEFAULT_CREATION_TIME_MILLIS, DEFAULT_USER_ID);
final long newCreationTime = DEFAULT_CREATION_TIME_MILLIS + 100;
setCurrentTimeMillis(newCreationTime);
mBackupHelper.stageAndApplyRestoredPayload(out.toByteArray(), DEFAULT_USER_ID);
- verifyStageFileContent(DEFAULT_PACKAGE_LOCALES_MAP, getStageFileIfExists(DEFAULT_USER_ID),
- newCreationTime);
+ verifyStageDataForUser(DEFAULT_PACKAGE_LOCALES_MAP,
+ newCreationTime, DEFAULT_USER_ID);
}
@Test
@@ -357,8 +357,10 @@ public class LocaleManagerBackupRestoreTest {
final ByteArrayOutputStream out = new ByteArrayOutputStream();
HashMap<String, String> pkgLocalesMap = new HashMap<>();
- String pkgNameA = "com.android.myAppA", pkgNameB = "com.android.myAppB";
- String langTagsA = "ru", langTagsB = "hi,fr";
+ String pkgNameA = "com.android.myAppA";
+ String pkgNameB = "com.android.myAppB";
+ String langTagsA = "ru";
+ String langTagsB = "hi,fr";
pkgLocalesMap.put(pkgNameA, langTagsA);
pkgLocalesMap.put(pkgNameB, langTagsB);
writeTestPayload(out, pkgLocalesMap);
@@ -380,8 +382,7 @@ public class LocaleManagerBackupRestoreTest {
LocaleList.forLanguageTags(langTagsA));
pkgLocalesMap.remove(pkgNameA);
- verifyStageFileContent(pkgLocalesMap, getStageFileIfExists(DEFAULT_USER_ID),
- DEFAULT_CREATION_TIME_MILLIS);
+ verifyStageDataForUser(pkgLocalesMap, DEFAULT_CREATION_TIME_MILLIS, DEFAULT_USER_ID);
setUpPackageInstalled(pkgNameB);
@@ -389,7 +390,7 @@ public class LocaleManagerBackupRestoreTest {
verify(mMockLocaleManagerService, times(1)).setApplicationLocales(pkgNameB, DEFAULT_USER_ID,
LocaleList.forLanguageTags(langTagsB));
- checkStageFileDoesNotExist(DEFAULT_USER_ID);
+ checkStageDataDoesNotExist(DEFAULT_USER_ID);
}
@Test
@@ -404,8 +405,8 @@ public class LocaleManagerBackupRestoreTest {
mBackupHelper.stageAndApplyRestoredPayload(out.toByteArray(), DEFAULT_USER_ID);
verifyNothingRestored();
- verifyStageFileContent(DEFAULT_PACKAGE_LOCALES_MAP, getStageFileIfExists(DEFAULT_USER_ID),
- DEFAULT_CREATION_TIME_MILLIS);
+ verifyStageDataForUser(DEFAULT_PACKAGE_LOCALES_MAP,
+ DEFAULT_CREATION_TIME_MILLIS, DEFAULT_USER_ID);
// App is installed later (post SUW).
setUpPackageInstalled(DEFAULT_PACKAGE_NAME);
@@ -415,11 +416,11 @@ public class LocaleManagerBackupRestoreTest {
// Since locales are already set, we should not restore anything for it.
verifyNothingRestored();
- checkStageFileDoesNotExist(DEFAULT_USER_ID);
+ checkStageDataDoesNotExist(DEFAULT_USER_ID);
}
@Test
- public void testStageFileDeletion_backupPassRunAfterRetentionPeriod_stageFileDeleted()
+ public void testStageDataDeletion_backupPassRunAfterRetentionPeriod_stageDataDeleted()
throws Exception {
final ByteArrayOutputStream out = new ByteArrayOutputStream();
writeTestPayload(out, DEFAULT_PACKAGE_LOCALES_MAP);
@@ -429,8 +430,8 @@ public class LocaleManagerBackupRestoreTest {
mBackupHelper.stageAndApplyRestoredPayload(out.toByteArray(), DEFAULT_USER_ID);
verifyNothingRestored();
- verifyStageFileContent(DEFAULT_PACKAGE_LOCALES_MAP, getStageFileIfExists(DEFAULT_USER_ID),
- DEFAULT_CREATION_TIME_MILLIS);
+ verifyStageDataForUser(DEFAULT_PACKAGE_LOCALES_MAP,
+ DEFAULT_CREATION_TIME_MILLIS, DEFAULT_USER_ID);
// Retention period has not elapsed.
setCurrentTimeMillis(
@@ -439,32 +440,78 @@ public class LocaleManagerBackupRestoreTest {
.getInstalledApplications(anyLong(), anyInt(), anyInt());
assertNull(mBackupHelper.getBackupPayload(DEFAULT_USER_ID));
- // Stage file should NOT be deleted.
- checkStageFileExists(DEFAULT_USER_ID);
+ checkStageDataExists(DEFAULT_USER_ID);
- // Exactly RETENTION_PERIOD amount of time has passed so stage file should still not be
+ // Exactly RETENTION_PERIOD amount of time has passed so stage data should still not be
// removed.
setCurrentTimeMillis(DEFAULT_CREATION_TIME_MILLIS + RETENTION_PERIOD.toMillis());
doReturn(List.of()).when(mMockPackageManagerInternal)
.getInstalledApplications(anyLong(), anyInt(), anyInt());
assertNull(mBackupHelper.getBackupPayload(DEFAULT_USER_ID));
- // Stage file should NOT be deleted.
- checkStageFileExists(DEFAULT_USER_ID);
+ checkStageDataExists(DEFAULT_USER_ID);
- // Retention period has now expired, stage file should be deleted.
+ // Retention period has now expired, stage data should be deleted.
setCurrentTimeMillis(
DEFAULT_CREATION_TIME_MILLIS + RETENTION_PERIOD.plusSeconds(1).toMillis());
doReturn(List.of()).when(mMockPackageManagerInternal)
.getInstalledApplications(anyLong(), anyInt(), anyInt());
assertNull(mBackupHelper.getBackupPayload(DEFAULT_USER_ID));
- // Stage file should be deleted.
- checkStageFileDoesNotExist(DEFAULT_USER_ID);
+ checkStageDataDoesNotExist(DEFAULT_USER_ID);
}
@Test
- public void testUserRemoval_userRemoved_stageFileDeleted() throws Exception {
+ public void testStageDataDeletion_lazyRestoreAfterRetentionPeriod_stageDataDeleted()
+ throws Exception {
+ final ByteArrayOutputStream out = new ByteArrayOutputStream();
+ HashMap<String, String> pkgLocalesMap = new HashMap<>();
+
+ String pkgNameA = "com.android.myAppA";
+ String pkgNameB = "com.android.myAppB";
+ String langTagsA = "ru";
+ String langTagsB = "hi,fr";
+ pkgLocalesMap.put(pkgNameA, langTagsA);
+ pkgLocalesMap.put(pkgNameB, langTagsB);
+ writeTestPayload(out, pkgLocalesMap);
+
+ setUpPackageNotInstalled(pkgNameA);
+ setUpPackageNotInstalled(pkgNameB);
+ setUpLocalesForPackage(pkgNameA, LocaleList.getEmptyLocaleList());
+ setUpLocalesForPackage(pkgNameB, LocaleList.getEmptyLocaleList());
+
+ mBackupHelper.stageAndApplyRestoredPayload(out.toByteArray(), DEFAULT_USER_ID);
+
+ verifyNothingRestored();
+ verifyStageDataForUser(pkgLocalesMap, DEFAULT_CREATION_TIME_MILLIS, DEFAULT_USER_ID);
+
+ // Retention period has not elapsed.
+ setCurrentTimeMillis(
+ DEFAULT_CREATION_TIME_MILLIS + RETENTION_PERIOD.minusHours(1).toMillis());
+
+ setUpPackageInstalled(pkgNameA);
+ mPackageMonitor.onPackageAdded(pkgNameA, DEFAULT_UID);
+
+ verify(mMockLocaleManagerService, times(1)).setApplicationLocales(pkgNameA, DEFAULT_USER_ID,
+ LocaleList.forLanguageTags(langTagsA));
+
+ pkgLocalesMap.remove(pkgNameA);
+ verifyStageDataForUser(pkgLocalesMap, DEFAULT_CREATION_TIME_MILLIS, DEFAULT_USER_ID);
+
+ // Retention period has now expired, stage data should be deleted.
+ setCurrentTimeMillis(
+ DEFAULT_CREATION_TIME_MILLIS + RETENTION_PERIOD.plusSeconds(1).toMillis());
+ setUpPackageInstalled(pkgNameB);
+ mPackageMonitor.onPackageAdded(pkgNameB, DEFAULT_UID);
+
+ verify(mMockLocaleManagerService, times(0)).setApplicationLocales(eq(pkgNameB), anyInt(),
+ any());
+
+ checkStageDataDoesNotExist(DEFAULT_USER_ID);
+ }
+
+ @Test
+ public void testUserRemoval_userRemoved_stageDataDeleted() throws Exception {
final ByteArrayOutputStream outDefault = new ByteArrayOutputStream();
writeTestPayload(outDefault, DEFAULT_PACKAGE_LOCALES_MAP);
@@ -485,119 +532,20 @@ public class LocaleManagerBackupRestoreTest {
verifyNothingRestored();
- // Verify stage file contents.
- AtomicFile stageFileDefaultUser = getStageFileIfExists(DEFAULT_USER_ID);
- verifyStageFileContent(DEFAULT_PACKAGE_LOCALES_MAP, stageFileDefaultUser,
- DEFAULT_CREATION_TIME_MILLIS);
-
- AtomicFile stageFileWorkProfile = getStageFileIfExists(WORK_PROFILE_USER_ID);
- verifyStageFileContent(pkgLocalesMapWorkProfile, stageFileWorkProfile,
- DEFAULT_CREATION_TIME_MILLIS);
+ verifyStageDataForUser(DEFAULT_PACKAGE_LOCALES_MAP,
+ DEFAULT_CREATION_TIME_MILLIS, DEFAULT_USER_ID);
+ verifyStageDataForUser(pkgLocalesMapWorkProfile,
+ DEFAULT_CREATION_TIME_MILLIS, WORK_PROFILE_USER_ID);
Intent intent = new Intent();
intent.setAction(Intent.ACTION_USER_REMOVED);
intent.putExtra(Intent.EXTRA_USER_HANDLE, DEFAULT_USER_ID);
mUserMonitor.onReceive(mMockContext, intent);
- // Stage file should be removed only for DEFAULT_USER_ID.
- checkStageFileDoesNotExist(DEFAULT_USER_ID);
- verifyStageFileContent(pkgLocalesMapWorkProfile, stageFileWorkProfile,
- DEFAULT_CREATION_TIME_MILLIS);
- }
-
- @Test
- public void testLoadStageFiles_invalidNameFormat_stageFileDeleted() throws Exception {
- // Stage file name should be : staged_locales_<user_id_int>.xml
- File stageFile = new File(STAGED_LOCALES_DIR, "xyz.xml");
- assertTrue(stageFile.createNewFile());
- assertTrue(stageFile.isFile());
-
- // Putting valid xml data in file.
- FileOutputStream out = new FileOutputStream(stageFile);
- writeTestPayload(out, DEFAULT_PACKAGE_LOCALES_MAP, /* forStage= */
- true, /* creationTimeMillis= */ 0);
- out.flush();
- out.close();
-
- verifyStageFileContent(DEFAULT_PACKAGE_LOCALES_MAP,
- new AtomicFile(stageFile), /* creationTimeMillis= */ 0);
-
- mBackupHelper = new LocaleManagerBackupHelper(mMockContext, mMockLocaleManagerService,
- mMockPackageManagerInternal, STAGED_LOCALES_DIR, mClock);
- assertFalse(stageFile.isFile());
- }
-
- @Test
- public void testLoadStageFiles_userIdNotParseable_stageFileDeleted() throws Exception {
- // Stage file name should be : staged_locales_<user_id_int>.xml
- File stageFile = new File(STAGED_LOCALES_DIR, "staged_locales_abc.xml");
- assertTrue(stageFile.createNewFile());
- assertTrue(stageFile.isFile());
-
- // Putting valid xml data in file.
- FileOutputStream out = new FileOutputStream(stageFile);
- writeTestPayload(out, DEFAULT_PACKAGE_LOCALES_MAP, /* forStage= */
- true, /* creationTimeMillis= */ 0);
- out.flush();
- out.close();
-
- verifyStageFileContent(DEFAULT_PACKAGE_LOCALES_MAP,
- new AtomicFile(stageFile), /* creationTimeMillis= */ 0);
-
- mBackupHelper = new LocaleManagerBackupHelper(mMockContext, mMockLocaleManagerService,
- mMockPackageManagerInternal, STAGED_LOCALES_DIR, mClock);
- assertFalse(stageFile.isFile());
- }
-
- @Test
- public void testLoadStageFiles_invalidContent_stageFileDeleted() throws Exception {
- File stageFile = new File(STAGED_LOCALES_DIR, "staged_locales_0.xml");
- assertTrue(stageFile.createNewFile());
- assertTrue(stageFile.isFile());
-
- FileOutputStream out = new FileOutputStream(stageFile);
- out.write("some_non_xml_string".getBytes());
- out.close();
-
- mBackupHelper = new LocaleManagerBackupHelper(mMockContext, mMockLocaleManagerService,
- mMockPackageManagerInternal, STAGED_LOCALES_DIR, mClock);
- assertFalse(stageFile.isFile());
- }
-
- @Test
- public void testLoadStageFiles_validContent_doesLazyRestore() throws Exception {
- File stageFile = new File(STAGED_LOCALES_DIR, "staged_locales_0.xml");
- assertTrue(stageFile.createNewFile());
- assertTrue(stageFile.isFile());
-
- // Putting valid xml data in file.
- FileOutputStream out = new FileOutputStream(stageFile);
- writeTestPayload(out, DEFAULT_PACKAGE_LOCALES_MAP, /* forStage= */
- true, DEFAULT_CREATION_TIME_MILLIS);
- out.flush();
- out.close();
-
- verifyStageFileContent(DEFAULT_PACKAGE_LOCALES_MAP,
- new AtomicFile(stageFile), DEFAULT_CREATION_TIME_MILLIS);
-
- mBackupHelper = new LocaleManagerBackupHelper(mMockContext, mMockLocaleManagerService,
- mMockPackageManagerInternal, STAGED_LOCALES_DIR, mClock);
- mPackageMonitor = mBackupHelper.getPackageMonitor();
-
- // Stage file still exists.
- assertTrue(stageFile.isFile());
-
- // App is installed later.
- setUpPackageInstalled(DEFAULT_PACKAGE_NAME);
- setUpLocalesForPackage(DEFAULT_PACKAGE_NAME, LocaleList.getEmptyLocaleList());
-
- mPackageMonitor.onPackageAdded(DEFAULT_PACKAGE_NAME, DEFAULT_UID);
-
- verify(mMockLocaleManagerService, times(1)).setApplicationLocales(DEFAULT_PACKAGE_NAME,
- DEFAULT_USER_ID, DEFAULT_LOCALES);
-
- // Stage file gets deleted here because all staged locales have been applied.
- assertFalse(stageFile.isFile());
+ // Stage data should be removed only for DEFAULT_USER_ID.
+ checkStageDataDoesNotExist(DEFAULT_USER_ID);
+ verifyStageDataForUser(pkgLocalesMapWorkProfile,
+ DEFAULT_CREATION_TIME_MILLIS, WORK_PROFILE_USER_ID);
}
private void setUpPackageInstalled(String packageName) throws Exception {
@@ -633,27 +581,15 @@ public class LocaleManagerBackupRestoreTest {
any());
}
-
private static void verifyPayloadForAppLocales(Map<String, String> expectedPkgLocalesMap,
byte[] payload)
throws IOException, XmlPullParserException {
- verifyPayloadForAppLocales(expectedPkgLocalesMap, payload, /* forStage= */ false, -1);
- }
-
- private static void verifyPayloadForAppLocales(Map<String, String> expectedPkgLocalesMap,
- byte[] payload, boolean forStage, long expectedCreationTime)
- throws IOException, XmlPullParserException {
final ByteArrayInputStream stream = new ByteArrayInputStream(payload);
final TypedXmlPullParser parser = Xml.newFastPullParser();
parser.setInput(stream, StandardCharsets.UTF_8.name());
Map<String, String> backupDataMap = new HashMap<>();
XmlUtils.beginDocument(parser, TEST_LOCALES_XML_TAG);
- if (forStage) {
- long actualCreationTime = parser.getAttributeLong(/* namespace= */ null,
- "creationTimeMillis");
- assertEquals(expectedCreationTime, actualCreationTime);
- }
int depth = parser.getDepth();
while (XmlUtils.nextElementWithin(parser, depth)) {
if (parser.getName().equals("package")) {
@@ -668,13 +604,6 @@ public class LocaleManagerBackupRestoreTest {
private static void writeTestPayload(OutputStream stream, Map<String, String> pkgLocalesMap)
throws IOException {
- writeTestPayload(stream, pkgLocalesMap, /* forStage= */ false, /* creationTimeMillis= */
- -1);
- }
-
- private static void writeTestPayload(OutputStream stream, Map<String, String> pkgLocalesMap,
- boolean forStage, long creationTimeMillis)
- throws IOException {
if (pkgLocalesMap.isEmpty()) {
return;
}
@@ -684,11 +613,6 @@ public class LocaleManagerBackupRestoreTest {
out.startDocument(/* encoding= */ null, /* standalone= */ true);
out.startTag(/* namespace= */ null, TEST_LOCALES_XML_TAG);
- if (forStage) {
- out.attribute(/* namespace= */ null, "creationTimeMillis",
- Long.toString(creationTimeMillis));
- }
-
for (String pkg : pkgLocalesMap.keySet()) {
out.startTag(/* namespace= */ null, "package");
out.attribute(/* namespace= */ null, "name", pkg);
@@ -700,41 +624,19 @@ public class LocaleManagerBackupRestoreTest {
out.endDocument();
}
- private static void verifyStageFileContent(Map<String, String> expectedPkgLocalesMap,
- AtomicFile stageFile,
- long creationTimeMillis)
- throws Exception {
- assertNotNull(stageFile);
- try (InputStream stagedDataInputStream = stageFile.openRead()) {
- verifyPayloadForAppLocales(expectedPkgLocalesMap, stagedDataInputStream.readAllBytes(),
- /* forStage= */ true, creationTimeMillis);
- } catch (IOException | XmlPullParserException e) {
- throw e;
- }
- }
-
- private static void checkStageFileDoesNotExist(int userId) {
- assertNull(getStageFileIfExists(userId));
+ private void verifyStageDataForUser(Map<String, String> expectedPkgLocalesMap,
+ long expectedCreationTimeMillis, int userId) {
+ LocaleManagerBackupHelper.StagedData stagedDataForUser = STAGE_DATA.get(userId);
+ assertNotNull(stagedDataForUser);
+ assertEquals(expectedCreationTimeMillis, stagedDataForUser.mCreationTimeMillis);
+ assertEquals(expectedPkgLocalesMap, stagedDataForUser.mPackageStates);
}
- private static void checkStageFileExists(int userId) {
- assertNotNull(getStageFileIfExists(userId));
+ private static void checkStageDataExists(int userId) {
+ assertNotNull(STAGE_DATA.get(userId));
}
- private static AtomicFile getStageFileIfExists(int userId) {
- File file = new File(STAGED_LOCALES_DIR, String.format("staged_locales_%d.xml", userId));
- if (file.isFile()) {
- return new AtomicFile(file);
- }
- return null;
- }
-
- private static void cleanStagedFiles() {
- File[] files = STAGED_LOCALES_DIR.listFiles();
- if (files != null) {
- for (File f : files) {
- f.delete();
- }
- }
+ private static void checkStageDataDoesNotExist(int userId) {
+ assertNull(STAGE_DATA.get(userId));
}
}
diff --git a/services/tests/servicestests/src/com/android/server/locales/ShadowLocaleManagerBackupHelper.java b/services/tests/servicestests/src/com/android/server/locales/ShadowLocaleManagerBackupHelper.java
index 93972c32a50f..b0fc6363b701 100644
--- a/services/tests/servicestests/src/com/android/server/locales/ShadowLocaleManagerBackupHelper.java
+++ b/services/tests/servicestests/src/com/android/server/locales/ShadowLocaleManagerBackupHelper.java
@@ -18,8 +18,8 @@ package com.android.server.locales;
import android.content.Context;
import android.content.pm.PackageManagerInternal;
+import android.util.SparseArray;
-import java.io.File;
import java.time.Clock;
/**
@@ -30,7 +30,8 @@ import java.time.Clock;
public class ShadowLocaleManagerBackupHelper extends LocaleManagerBackupHelper {
ShadowLocaleManagerBackupHelper(Context context,
LocaleManagerService localeManagerService,
- PackageManagerInternal pmInternal, File stagedLocalesDir, Clock clock) {
- super(context, localeManagerService, pmInternal, stagedLocalesDir, clock);
+ PackageManagerInternal pmInternal, Clock clock,
+ SparseArray<LocaleManagerBackupHelper.StagedData> stagedData) {
+ super(context, localeManagerService, pmInternal, clock, stagedData);
}
}