Merge "Simplify app-locales B&R stage data retention."

This commit is contained in:
Nishant Singh
2022-01-13 07:10:07 +00:00
committed by Android (Google) Code Review
3 changed files with 155 additions and 381 deletions

View File

@@ -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);
@@ -157,67 +140,6 @@ class LocaleManagerBackupHelper {
return mPackageMonitor;
}
/**
* 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)
*/
@@ -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.

View File

@@ -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 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 checkStageFileDoesNotExist(int userId) {
assertNull(getStageFileIfExists(userId));
private static void checkStageDataExists(int userId) {
assertNotNull(STAGE_DATA.get(userId));
}
private static void checkStageFileExists(int userId) {
assertNotNull(getStageFileIfExists(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));
}
}

View File

@@ -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);
}
}