blob: aab41595553a00e79662a85292ab794b0cfdf2f2 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.backup.cts;
import static android.app.time.cts.shell.DeviceConfigKeys.NAMESPACE_SYSTEM_TIME;
import static android.app.time.cts.shell.DeviceConfigShellHelper.SYNC_DISABLED_MODE_UNTIL_REBOOT;
import static com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity;
import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
import android.Manifest;
import android.app.Instrumentation;
import android.app.LocaleManager;
import android.app.time.ExternalTimeSuggestion;
import android.app.time.TimeManager;
import android.app.time.cts.shell.DeviceConfigKeys;
import android.app.time.cts.shell.DeviceConfigShellHelper;
import android.app.time.cts.shell.DeviceShellCommandExecutor;
import android.app.time.cts.shell.device.InstrumentationShellCommandExecutor;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.LocaleList;
import android.os.SystemClock;
import android.platform.test.annotations.AppModeFull;
import androidx.test.InstrumentationRegistry;
import com.android.compatibility.common.util.AmUtils;
import com.android.compatibility.common.util.ShellUtils;
import org.junit.After;
import org.junit.Before;
import java.time.Duration;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@AppModeFull
public class AppLocalesBackupTest extends BaseBackupCtsTest {
private static final String APK_PATH = "/data/local/tmp/cts/backup/";
private static final String TEST_APP_APK_1 = APK_PATH + "CtsAppLocalesBackupApp1.apk";
private static final String TEST_APP_PACKAGE_1 =
"android.cts.backup.applocalesbackupapp1";
private static final String TEST_APP_APK_2 = APK_PATH + "CtsAppLocalesBackupApp2.apk";
private static final String TEST_APP_PACKAGE_2 =
"android.cts.backup.applocalesbackupapp2";
private static final String SYSTEM_PACKAGE = "android";
private static final LocaleList DEFAULT_LOCALES_1 = LocaleList.forLanguageTags("hi-IN,de-DE");
private static final LocaleList DEFAULT_LOCALES_2 = LocaleList.forLanguageTags("fr-CA");
private static final LocaleList EMPTY_LOCALES = LocaleList.getEmptyLocaleList();
// An identifier for the backup dataset. Since we're using localtransport, it's set to "1".
private static final String RESTORE_TOKEN = "1";
private static final Duration RETENTION_PERIOD = Duration.ofDays(3);
private Context mContext;
private LocaleManager mLocaleManager;
@Before
@Override
public void setUp() throws Exception {
super.setUp();
mContext = InstrumentationRegistry.getTargetContext();
mLocaleManager = mContext.getSystemService(LocaleManager.class);
install(TEST_APP_APK_1);
install(TEST_APP_APK_2);
}
@After
public void tearDown() throws Exception {
uninstall(TEST_APP_PACKAGE_1);
uninstall(TEST_APP_PACKAGE_2);
}
/**
* Tests the scenario where all apps are installed on the device when restore is triggered.
*
* <p>In this case, all the apps should have their locales restored as soon as the restore
* operation finishes. The only condition is that the apps should not have the locales set
* already before restore.
*/
public void testBackupRestore_allAppsInstalledNoAppLocalesSet_restoresImmediately()
throws Exception {
if (!isBackupSupported()) {
return;
}
setAndBackupDefaultAppLocales();
resetAppLocales();
getBackupUtils().restoreAndAssertSuccess(RESTORE_TOKEN, SYSTEM_PACKAGE);
assertLocalesForApp(TEST_APP_PACKAGE_1, DEFAULT_LOCALES_1);
assertLocalesForApp(TEST_APP_PACKAGE_2, DEFAULT_LOCALES_2);
}
/**
* Tests the scenario where the user sets the app-locales before the restore could be applied.
*
* <p>The locales from the backup data should be ignored in this case.
*/
public void testBackupRestore_localeAlreadySet_doesNotRestore() throws Exception {
if (!isBackupSupported()) {
return;
}
setAndBackupDefaultAppLocales();
LocaleList newLocales = LocaleList.forLanguageTags("zh,hi");
setApplicationLocalesAndVerify(TEST_APP_PACKAGE_1, newLocales);
setApplicationLocalesAndVerify(TEST_APP_PACKAGE_2, EMPTY_LOCALES);
getBackupUtils().restoreAndAssertSuccess(RESTORE_TOKEN, SYSTEM_PACKAGE);
// Should restore only for app_2.
assertLocalesForApp(TEST_APP_PACKAGE_1, newLocales);
assertLocalesForApp(TEST_APP_PACKAGE_2, DEFAULT_LOCALES_2);
}
/**
* Tests the scenario when some apps are installed after the restore finishes.
*
* <p>More specifically, this tests the lazy restore where the locales are fetched and
* restored from the stage file if the app is installed within a certain amount of time after
* the initial restore.
*/
public void testBackupRestore_appInstalledAfterRestore_doesLazyRestore() throws Exception {
if (!isBackupSupported()) {
return;
}
setAndBackupDefaultAppLocales();
resetAppLocales();
uninstall(TEST_APP_PACKAGE_2);
getBackupUtils().restoreAndAssertSuccess(RESTORE_TOKEN, SYSTEM_PACKAGE);
// Locales for App1 should be restored immediately since that's present already.
assertLocalesForApp(TEST_APP_PACKAGE_1, DEFAULT_LOCALES_1);
// This is to ensure there are no lingering broadcasts (could be from the setUp method
// where we are calling setApplicationLocales).
AmUtils.waitForBroadcastIdle();
BlockingBroadcastReceiver appSpecificLocaleBroadcastReceiver =
new BlockingBroadcastReceiver();
mContext.registerReceiver(appSpecificLocaleBroadcastReceiver,
new IntentFilter(Intent.ACTION_APPLICATION_LOCALE_CHANGED));
// Hold Manifest.permission.READ_APP_SPECIFIC_LOCALES while the broadcast is sent,
// so that we receive it.
runWithShellPermissionIdentity(() -> {
// Installation will trigger lazy restore, which internally calls setApplicationLocales
// which sends out the ACTION_APPLICATION_LOCALE_CHANGED broadcast.
install(TEST_APP_APK_2);
appSpecificLocaleBroadcastReceiver.await();
}, Manifest.permission.READ_APP_SPECIFIC_LOCALES);
appSpecificLocaleBroadcastReceiver.assertOneBroadcastReceived();
appSpecificLocaleBroadcastReceiver.assertReceivedBroadcastContains(TEST_APP_PACKAGE_2,
DEFAULT_LOCALES_2);
// Verify that lazy restore occurred upon package install.
assertLocalesForApp(TEST_APP_PACKAGE_2, DEFAULT_LOCALES_2);
// APP2's entry is removed from the stage file after restore so nothing should be restored
// when APP2 is installed for the second time.
uninstall(TEST_APP_PACKAGE_2);
install(TEST_APP_APK_2);
assertLocalesForApp(TEST_APP_PACKAGE_2, EMPTY_LOCALES);
}
/**
* Tests the scenario when an application is removed from the device.
*
* <p>The data for the uninstalled app should be removed from the next backup pass.
*/
public void testBackupRestore_uninstallApp_deletesDataFromBackup() throws Exception {
if (!isBackupSupported()) {
return;
}
setAndBackupDefaultAppLocales();
// Uninstall an app and run the backup pass. The locales for the uninstalled app should
// be removed from the backup.
uninstall(TEST_APP_PACKAGE_2);
setApplicationLocalesAndVerify(TEST_APP_PACKAGE_1, DEFAULT_LOCALES_1);
getBackupUtils().backupNowAndAssertSuccess(SYSTEM_PACKAGE);
install(TEST_APP_APK_2);
// Remove app1's locales so that it can be restored.
setApplicationLocalesAndVerify(TEST_APP_PACKAGE_1, EMPTY_LOCALES);
getBackupUtils().restoreAndAssertSuccess(RESTORE_TOKEN, SYSTEM_PACKAGE);
// Restores only app1's locales because app2's data is no longer present in the backup.
assertLocalesForApp(TEST_APP_PACKAGE_1, DEFAULT_LOCALES_1);
assertLocalesForApp(TEST_APP_PACKAGE_2, EMPTY_LOCALES);
}
/**
* Tests the scenario when backup pass is run after retention period has expired.
*
* <p>Stage data should be removed since retention period has expired.
* <p><b>Note:</b>Manipulates device's system clock directly to simulate the passage of time.
*/
public void testRetentionPeriod_backupPassAfterRetentionPeriod_removesStagedData()
throws Exception {
if (!isBackupSupported()) {
return;
}
setAndBackupDefaultAppLocales();
uninstall(TEST_APP_PACKAGE_2);
getBackupUtils().restoreAndAssertSuccess(RESTORE_TOKEN, SYSTEM_PACKAGE);
// Locales for App1 should be restored immediately since that's present already.
assertLocalesForApp(TEST_APP_PACKAGE_1, DEFAULT_LOCALES_1);
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
DeviceShellCommandExecutor shellCommandExecutor = new InstrumentationShellCommandExecutor(
instrumentation.getUiAutomation());
DeviceConfigShellHelper deviceConfigShellHelper = new DeviceConfigShellHelper(
shellCommandExecutor);
// This anticipates a future state where a generally applied target preparer may disable
// device_config sync for all CTS tests: only suspend syncing if it isn't already suspended,
// and only resume it if this test suspended it.
DeviceConfigShellHelper.PreTestState deviceConfigPreTestState =
deviceConfigShellHelper.setSyncModeForTest(
SYNC_DISABLED_MODE_UNTIL_REBOOT, NAMESPACE_SYSTEM_TIME);
TimeManager timeManager = mContext.getSystemService(TimeManager.class);
assertNotNull(timeManager);
// Capture clock values so that the system clock can be set back after the test.
long startCurrentTimeMillis = System.currentTimeMillis();
long elapsedRealtimeMillis = SystemClock.elapsedRealtime();
try {
// Set the time detector to only use ORIGIN_EXTERNAL.
deviceConfigShellHelper.put(NAMESPACE_SYSTEM_TIME,
DeviceConfigKeys.TimeDetector.KEY_TIME_DETECTOR_ORIGIN_PRIORITIES_OVERRIDE,
DeviceConfigKeys.TimeDetector.ORIGIN_EXTERNAL);
sleepForAsyncOperation();
// 1 second elapses after retention period.
long afterRetentionPeriodMillis = Duration.ofMillis(startCurrentTimeMillis).plusMillis(
RETENTION_PERIOD.plusSeconds(1).toMillis()).toMillis();
ExternalTimeSuggestion futureTimeSuggestion =
new ExternalTimeSuggestion(elapsedRealtimeMillis, afterRetentionPeriodMillis);
runWithShellPermissionIdentity(() -> {
timeManager.suggestExternalTime(futureTimeSuggestion);
}, Manifest.permission.SUGGEST_EXTERNAL_TIME);
sleepForAsyncOperation();
// The suggestion should have been accepted so the system clock should have advanced.
assertTrue(System.currentTimeMillis() >= afterRetentionPeriodMillis);
// Run the backup pass now.
getBackupUtils().backupNowAndAssertSuccess(SYSTEM_PACKAGE);
} finally {
// Now do our best to return the device to its original state.
ExternalTimeSuggestion originalTimeSuggestion =
new ExternalTimeSuggestion(elapsedRealtimeMillis, startCurrentTimeMillis);
runWithShellPermissionIdentity(() -> {
timeManager.suggestExternalTime(originalTimeSuggestion);
}, Manifest.permission.SUGGEST_EXTERNAL_TIME);
sleepForAsyncOperation();
deviceConfigShellHelper.restoreDeviceConfigStateForTest(deviceConfigPreTestState);
}
// We install the app after restoring the device time so that retention check during lazy
// restore doesn't try to delete the stage data. Hence, ensuring that stage data is deleted
// during the backup pass.
BlockingBroadcastReceiver appSpecificLocaleBroadcastReceiver =
new BlockingBroadcastReceiver();
mContext.registerReceiver(appSpecificLocaleBroadcastReceiver,
new IntentFilter(Intent.ACTION_APPLICATION_LOCALE_CHANGED));
// Hold Manifest.permission.READ_APP_SPECIFIC_LOCALES while the broadcast is sent,
// so that we receive it.
runWithShellPermissionIdentity(() -> {
// Installation will trigger lazy restore, which internally calls setApplicationLocales
// which sends out the ACTION_APPLICATION_LOCALE_CHANGED broadcast.
install(TEST_APP_APK_2);
appSpecificLocaleBroadcastReceiver.await();
}, Manifest.permission.READ_APP_SPECIFIC_LOCALES);
appSpecificLocaleBroadcastReceiver.assertNoBroadcastReceived();
// Does not restore the locales on package install.
assertLocalesForApp(TEST_APP_PACKAGE_2, EMPTY_LOCALES);
}
/**
* Tests the scenario when lazy restore happens after retention period has expired.
*
* <p>Stage data should be removed since retention period has expired.
* <p><b>Note:</b>Manipulates device's system clock directly to simulate the passage of time.
*/
public void testRetentionPeriod_lazyRestoreAfterRetentionPeriod_removesStagedData()
throws Exception {
if (!isBackupSupported()) {
return;
}
setAndBackupDefaultAppLocales();
uninstall(TEST_APP_PACKAGE_1);
uninstall(TEST_APP_PACKAGE_2);
getBackupUtils().restoreAndAssertSuccess(RESTORE_TOKEN, SYSTEM_PACKAGE);
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
DeviceShellCommandExecutor shellCommandExecutor = new InstrumentationShellCommandExecutor(
instrumentation.getUiAutomation());
DeviceConfigShellHelper deviceConfigShellHelper = new DeviceConfigShellHelper(
shellCommandExecutor);
// This anticipates a future state where a generally applied target preparer may disable
// device_config sync for all CTS tests: only suspend syncing if it isn't already suspended,
// and only resume it if this test suspended it.
DeviceConfigShellHelper.PreTestState deviceConfigPreTestState =
deviceConfigShellHelper.setSyncModeForTest(
SYNC_DISABLED_MODE_UNTIL_REBOOT, NAMESPACE_SYSTEM_TIME);
TimeManager timeManager = mContext.getSystemService(TimeManager.class);
assertNotNull(timeManager);
// Capture clock values so that the system clock can be set back after the test.
long startCurrentTimeMillis = System.currentTimeMillis();
long elapsedRealtimeMillis = SystemClock.elapsedRealtime();
try {
BlockingBroadcastReceiver appSpecificLocaleBroadcastReceiver =
new BlockingBroadcastReceiver();
mContext.registerReceiver(appSpecificLocaleBroadcastReceiver,
new IntentFilter(Intent.ACTION_APPLICATION_LOCALE_CHANGED));
// Hold Manifest.permission.READ_APP_SPECIFIC_LOCALES while the broadcast is sent,
// so that we receive it.
runWithShellPermissionIdentity(() -> {
// Installation will trigger lazy restore, which internally calls
// setApplicationLocales
// which sends out the ACTION_APPLICATION_LOCALE_CHANGED broadcast.
install(TEST_APP_APK_1);
appSpecificLocaleBroadcastReceiver.await();
}, Manifest.permission.READ_APP_SPECIFIC_LOCALES);
appSpecificLocaleBroadcastReceiver.assertOneBroadcastReceived();
appSpecificLocaleBroadcastReceiver.assertReceivedBroadcastContains(TEST_APP_PACKAGE_1,
DEFAULT_LOCALES_1);
assertLocalesForApp(TEST_APP_PACKAGE_1, DEFAULT_LOCALES_1);
appSpecificLocaleBroadcastReceiver.reset();
// Set the time detector to only use ORIGIN_EXTERNAL.
deviceConfigShellHelper.put(NAMESPACE_SYSTEM_TIME,
DeviceConfigKeys.TimeDetector.KEY_TIME_DETECTOR_ORIGIN_PRIORITIES_OVERRIDE,
DeviceConfigKeys.TimeDetector.ORIGIN_EXTERNAL);
sleepForAsyncOperation();
// 1 second elapses after retention period.
long afterRetentionPeriodMillis = Duration.ofMillis(startCurrentTimeMillis).plusMillis(
RETENTION_PERIOD.plusSeconds(1).toMillis()).toMillis();
ExternalTimeSuggestion futureTimeSuggestion =
new ExternalTimeSuggestion(elapsedRealtimeMillis, afterRetentionPeriodMillis);
runWithShellPermissionIdentity(() -> {
timeManager.suggestExternalTime(futureTimeSuggestion);
}, Manifest.permission.SUGGEST_EXTERNAL_TIME);
sleepForAsyncOperation();
// The suggestion should have been accepted so the system clock should have advanced.
assertTrue(System.currentTimeMillis() >= afterRetentionPeriodMillis);
// Hold Manifest.permission.READ_APP_SPECIFIC_LOCALES while the broadcast is sent,
// so that we receive it.
runWithShellPermissionIdentity(() -> {
// Installation will trigger lazy restore, which internally calls
// setApplicationLocales
// which sends out the ACTION_APPLICATION_LOCALE_CHANGED broadcast.
install(TEST_APP_APK_2);
appSpecificLocaleBroadcastReceiver.await();
}, Manifest.permission.READ_APP_SPECIFIC_LOCALES);
appSpecificLocaleBroadcastReceiver.assertNoBroadcastReceived();
// Does not restore the locales on package install.
assertLocalesForApp(TEST_APP_PACKAGE_2, EMPTY_LOCALES);
} finally {
// Now do our best to return the device to its original state.
ExternalTimeSuggestion originalTimeSuggestion =
new ExternalTimeSuggestion(elapsedRealtimeMillis, startCurrentTimeMillis);
runWithShellPermissionIdentity(() -> {
timeManager.suggestExternalTime(originalTimeSuggestion);
}, Manifest.permission.SUGGEST_EXTERNAL_TIME);
sleepForAsyncOperation();
deviceConfigShellHelper.restoreDeviceConfigStateForTest(deviceConfigPreTestState);
}
}
/**
* Sleeps for a length of time sufficient to allow async operations to complete. Many time
* manager APIs are or could be asynchronous and deal with time, so there are no practical
* alternatives.
*/
private static void sleepForAsyncOperation() throws Exception {
Thread.sleep(5_000);
}
// TODO(b/210593602): Add a test to check staged data removal after the retention period.
private void setApplicationLocalesAndVerify(String packageName, LocaleList locales)
throws Exception {
runWithShellPermissionIdentity(() ->
mLocaleManager.setApplicationLocales(packageName, locales),
Manifest.permission.CHANGE_CONFIGURATION);
assertLocalesForApp(packageName, locales);
}
/**
* Verifies that the locales are correctly set for another package
* by fetching locales of the app with a binder call.
*/
private void assertLocalesForApp(String packageName,
LocaleList expectedLocales) throws Exception {
assertEquals(expectedLocales, getApplicationLocales(packageName));
}
private LocaleList getApplicationLocales(String packageName) throws Exception {
return callWithShellPermissionIdentity(() ->
mLocaleManager.getApplicationLocales(packageName),
Manifest.permission.READ_APP_SPECIFIC_LOCALES);
}
private void install(String apk) {
ShellUtils.runShellCommand("pm install -r " + apk);
}
private void uninstall(String packageName) {
ShellUtils.runShellCommand("pm uninstall " + packageName);
}
private void setAndBackupDefaultAppLocales() throws Exception {
setApplicationLocalesAndVerify(TEST_APP_PACKAGE_1, DEFAULT_LOCALES_1);
setApplicationLocalesAndVerify(TEST_APP_PACKAGE_2, DEFAULT_LOCALES_2);
// Backup the data for SYSTEM_PACKAGE which includes app-locales.
getBackupUtils().backupNowAndAssertSuccess(SYSTEM_PACKAGE);
}
private void resetAppLocales() throws Exception {
setApplicationLocalesAndVerify(TEST_APP_PACKAGE_1, EMPTY_LOCALES);
setApplicationLocalesAndVerify(TEST_APP_PACKAGE_2, EMPTY_LOCALES);
}
private static final class BlockingBroadcastReceiver extends BroadcastReceiver {
private CountDownLatch mLatch = new CountDownLatch(1);
private String mPackageName;
private LocaleList mLocales;
private int mCalls;
@Override
public void onReceive(Context context, Intent intent) {
if (intent.hasExtra(Intent.EXTRA_PACKAGE_NAME)) {
mPackageName = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME);
}
if (intent.hasExtra(Intent.EXTRA_LOCALE_LIST)) {
mLocales = intent.getParcelableExtra(Intent.EXTRA_LOCALE_LIST);
}
mCalls += 1;
mLatch.countDown();
}
public void await() throws Exception {
mLatch.await(/* timeout= */ 5, TimeUnit.SECONDS);
}
public void reset() {
mLatch = new CountDownLatch(1);
mCalls = 0;
mPackageName = null;
mLocales = null;
}
public void assertOneBroadcastReceived() {
assertEquals(1, mCalls);
}
public void assertNoBroadcastReceived() {
assertEquals(0, mCalls);
}
/**
* Verifies that the broadcast received in the relevant apps have the correct information
* in the intent extras. It verifies the below extras:
* <ul>
* <li> {@link Intent#EXTRA_PACKAGE_NAME}
* <li> {@link Intent#EXTRA_LOCALE_LIST}
* </ul>
*/
public void assertReceivedBroadcastContains(String expectedPackageName,
LocaleList expectedLocales) {
assertEquals(expectedPackageName, mPackageName);
assertEquals(expectedLocales, mLocales);
}
}
}