diff --git a/services/core/java/com/android/server/locales/LocaleManagerService.java b/services/core/java/com/android/server/locales/LocaleManagerService.java index c5352c934f894..6aabdb5b05df2 100644 --- a/services/core/java/com/android/server/locales/LocaleManagerService.java +++ b/services/core/java/com/android/server/locales/LocaleManagerService.java @@ -77,15 +77,15 @@ public class LocaleManagerService extends SystemService { @VisibleForTesting LocaleManagerService(Context context, ActivityTaskManagerInternal activityTaskManagerInternal, ActivityManagerInternal activityManagerInternal, - PackageManagerInternal packageManagerInternal) { + PackageManagerInternal packageManagerInternal, + LocaleManagerBackupHelper localeManagerBackupHelper) { super(context); mContext = context; mBinderService = new LocaleManagerBinderService(); mActivityTaskManagerInternal = activityTaskManagerInternal; mActivityManagerInternal = activityManagerInternal; mPackageManagerInternal = packageManagerInternal; - mBackupHelper = new LocaleManagerBackupHelper(this, - mPackageManagerInternal); + mBackupHelper = localeManagerBackupHelper; } @Override @@ -143,11 +143,6 @@ public class LocaleManagerService extends SystemService { } - @VisibleForTesting - LocaleManagerBackupHelper getBackupHelper() { - return mBackupHelper; - } - /** * Sets the current UI locales for a specified app. */ diff --git a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java new file mode 100644 index 0000000000000..9cc59a1070d7a --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java @@ -0,0 +1,199 @@ +/* + * 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 com.android.server.locales; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNull; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManagerInternal; +import android.os.Environment; +import android.os.LocaleList; +import android.os.RemoteException; +import android.os.SimpleClock; +import android.util.TypedXmlPullParser; +import android.util.Xml; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.internal.util.XmlUtils; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.time.ZoneOffset; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Unit tests for the {@link LocaleManagerInternal}. + */ +@RunWith(AndroidJUnit4.class) +public class LocaleManagerBackupRestoreTest { + private static final String DEFAULT_PACKAGE_NAME = "com.android.myapp"; + private static final String DEFAULT_LOCALE_TAGS = "en-XC,ar-XB"; + private static final String STAGE_LOCALES_XML_TAG = "locales"; + private static final int DEFAULT_USER_ID = 0; + private static final int INVALID_UID = -1; + private static final LocaleList DEFAULT_LOCALES = + LocaleList.forLanguageTags(DEFAULT_LOCALE_TAGS); + private static final Map DEFAULT_PACKAGE_LOCALES_MAP = Map.of( + DEFAULT_PACKAGE_NAME, DEFAULT_LOCALE_TAGS); + + private LocaleManagerBackupHelper mBackupHelper; + private long mCurrentTimeMillis; + + @Mock + private Context mMockContext; + @Mock + private PackageManagerInternal mMockPackageManagerInternal; + @Mock + private LocaleManagerService mMockLocaleManagerService; + + private final Clock mClock = new SimpleClock(ZoneOffset.UTC) { + @Override + public long millis() { + return currentTimeMillis(); + } + }; + + private long currentTimeMillis() { + return mCurrentTimeMillis; + } + + private void setCurrentTimeMillis(long currentTimeMillis) { + mCurrentTimeMillis = currentTimeMillis; + } + + @Before + public void setUp() throws Exception { + mMockContext = mock(Context.class); + mMockPackageManagerInternal = mock(PackageManagerInternal.class); + + mMockLocaleManagerService = mock(LocaleManagerService.class); + + mBackupHelper = new LocaleManagerBackupHelper(mMockContext, mMockLocaleManagerService, + mMockPackageManagerInternal, + new File(Environment.getExternalStorageDirectory(), "lmsUnitTests"), + mClock); + } + + @Test + public void testBackupPayload_noAppsInstalled_returnsNull() throws Exception { + doReturn(List.of()).when(mMockPackageManagerInternal) + .getInstalledApplications(anyLong(), anyInt(), anyInt()); + + assertNull(mBackupHelper.getBackupPayload(DEFAULT_USER_ID)); + } + + @Test + public void testBackupPayload_noAppLocalesSet_returnsNull() throws Exception { + setUpLocalesForPackage(DEFAULT_PACKAGE_NAME, LocaleList.getEmptyLocaleList()); + setUpDummyAppForPackageManager(DEFAULT_PACKAGE_NAME); + + assertNull(mBackupHelper.getBackupPayload(DEFAULT_USER_ID)); + } + + @Test + public void testBackupPayload_appLocalesSet_returnsNonNullBlob() throws Exception { + setUpLocalesForPackage(DEFAULT_PACKAGE_NAME, DEFAULT_LOCALES); + setUpDummyAppForPackageManager(DEFAULT_PACKAGE_NAME); + + byte[] payload = mBackupHelper.getBackupPayload(DEFAULT_USER_ID); + verifyPayloadForAppLocales(DEFAULT_PACKAGE_LOCALES_MAP, payload); + } + + @Test + public void testBackupPayload_exceptionInGetLocalesAllPackages_returnsNull() throws Exception { + setUpDummyAppForPackageManager(DEFAULT_PACKAGE_NAME); + doThrow(new RemoteException("mock")).when(mMockLocaleManagerService).getApplicationLocales( + anyString(), anyInt()); + + assertNull(mBackupHelper.getBackupPayload(DEFAULT_USER_ID)); + } + + @Test + public void testBackupPayload_exceptionInGetLocalesSomePackages_appsWithExceptionNotBackedUp() + throws Exception { + // Set up two apps. + ApplicationInfo defaultAppInfo = new ApplicationInfo(); + ApplicationInfo anotherAppInfo = new ApplicationInfo(); + defaultAppInfo.packageName = DEFAULT_PACKAGE_NAME; + anotherAppInfo.packageName = "com.android.anotherapp"; + doReturn(List.of(defaultAppInfo, anotherAppInfo)).when(mMockPackageManagerInternal) + .getInstalledApplications(anyLong(), anyInt(), anyInt()); + + setUpLocalesForPackage(DEFAULT_PACKAGE_NAME, DEFAULT_LOCALES); + // Exception when getting locales for anotherApp. + doThrow(new RemoteException("mock")).when(mMockLocaleManagerService).getApplicationLocales( + eq(anotherAppInfo.packageName), anyInt()); + + byte[] payload = mBackupHelper.getBackupPayload(DEFAULT_USER_ID); + verifyPayloadForAppLocales(DEFAULT_PACKAGE_LOCALES_MAP, payload); + } + + private void setUpLocalesForPackage(String packageName, LocaleList locales) throws Exception { + doReturn(locales).when(mMockLocaleManagerService).getApplicationLocales( + eq(packageName), anyInt()); + } + + private void setUpDummyAppForPackageManager(String packageName) { + ApplicationInfo dummyApp = new ApplicationInfo(); + dummyApp.packageName = packageName; + doReturn(List.of(dummyApp)).when(mMockPackageManagerInternal) + .getInstalledApplications(anyLong(), anyInt(), anyInt()); + } + + private void verifyPayloadForAppLocales(Map expectedPkgLocalesMap, + byte[] payload) + throws IOException, XmlPullParserException { + final ByteArrayInputStream stream = new ByteArrayInputStream(payload); + final TypedXmlPullParser parser = Xml.newFastPullParser(); + parser.setInput(stream, StandardCharsets.UTF_8.name()); + + Map backupDataMap = new HashMap<>(); + XmlUtils.beginDocument(parser, STAGE_LOCALES_XML_TAG); + int depth = parser.getDepth(); + while (XmlUtils.nextElementWithin(parser, depth)) { + if (parser.getName().equals("package")) { + String packageName = parser.getAttributeValue(null, "name"); + String languageTags = parser.getAttributeValue(null, "locales"); + backupDataMap.put(packageName, languageTags); + } + } + + assertEquals(expectedPkgLocalesMap, backupDataMap); + } +} diff --git a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java index ddc58b23f6352..658f8d52d74b4 100644 --- a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java @@ -30,6 +30,7 @@ import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.Manifest; @@ -60,7 +61,7 @@ public class LocaleManagerServiceTest { private static final int DEFAULT_USER_ID = 0; private static final int DEFAULT_UID = Binder.getCallingUid() + 100; private static final int INVALID_UID = -1; - private static final String DEFAULT_LOCALE_TAGS = "en-XC, ar-XB"; + private static final String DEFAULT_LOCALE_TAGS = "en-XC,ar-XB"; private static final LocaleList DEFAULT_LOCALES = LocaleList.forLanguageTags(DEFAULT_LOCALE_TAGS); private static final InstallSourceInfo DEFAULT_INSTALL_SOURCE_INFO = new InstallSourceInfo( @@ -68,6 +69,7 @@ public class LocaleManagerServiceTest { /* originatingPackageName = */ null, /* installingPackageName = */ null); private LocaleManagerService mLocaleManagerService; + private LocaleManagerBackupHelper mMockBackupHelper; @Mock private Context mMockContext; @@ -104,8 +106,9 @@ public class LocaleManagerServiceTest { .handleIncomingUser(anyInt(), anyInt(), eq(DEFAULT_USER_ID), anyBoolean(), anyInt(), anyString(), anyString()); + mMockBackupHelper = mock(ShadowLocaleManagerBackupHelper.class); mLocaleManagerService = new LocaleManagerService(mMockContext, mMockActivityTaskManager, - mMockActivityManager, mMockPackageManagerInternal); + mMockActivityManager, mMockPackageManagerInternal, mMockBackupHelper); } @Test(expected = SecurityException.class) @@ -122,6 +125,7 @@ public class LocaleManagerServiceTest { verify(mMockContext).enforceCallingOrSelfPermission( eq(android.Manifest.permission.CHANGE_CONFIGURATION), anyString()); + verify(mMockBackupHelper, times(0)).notifyBackupManager(); assertNoLocalesStored(mFakePackageConfigurationUpdater.getStoredLocales()); } } @@ -133,6 +137,7 @@ public class LocaleManagerServiceTest { DEFAULT_USER_ID, LocaleList.getEmptyLocaleList()); fail("Expected NullPointerException"); } finally { + verify(mMockBackupHelper, times(0)).notifyBackupManager(); assertNoLocalesStored(mFakePackageConfigurationUpdater.getStoredLocales()); } } @@ -146,6 +151,7 @@ public class LocaleManagerServiceTest { /* locales = */ null); fail("Expected NullPointerException"); } finally { + verify(mMockBackupHelper, times(0)).notifyBackupManager(); assertNoLocalesStored(mFakePackageConfigurationUpdater.getStoredLocales()); } } @@ -163,6 +169,7 @@ public class LocaleManagerServiceTest { DEFAULT_LOCALES); assertEquals(DEFAULT_LOCALES, mFakePackageConfigurationUpdater.getStoredLocales()); + verify(mMockBackupHelper, times(1)).notifyBackupManager(); } @@ -175,6 +182,7 @@ public class LocaleManagerServiceTest { DEFAULT_LOCALES); assertEquals(DEFAULT_LOCALES, mFakePackageConfigurationUpdater.getStoredLocales()); + verify(mMockBackupHelper, times(1)).notifyBackupManager(); } @Test(expected = IllegalArgumentException.class) @@ -187,6 +195,7 @@ public class LocaleManagerServiceTest { fail("Expected IllegalArgumentException"); } finally { assertNoLocalesStored(mFakePackageConfigurationUpdater.getStoredLocales()); + verify(mMockBackupHelper, times(0)).notifyBackupManager(); } } @@ -217,7 +226,7 @@ public class LocaleManagerServiceTest { .when(mMockActivityTaskManager).getApplicationConfig(anyString(), anyInt()); LocaleList locales = mLocaleManagerService.getApplicationLocales( - DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID); + DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID); assertEquals(LocaleList.getEmptyLocaleList(), locales); } diff --git a/services/tests/servicestests/src/com/android/server/locales/ShadowLocaleManagerBackupHelper.java b/services/tests/servicestests/src/com/android/server/locales/ShadowLocaleManagerBackupHelper.java new file mode 100644 index 0000000000000..93972c32a50f0 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/locales/ShadowLocaleManagerBackupHelper.java @@ -0,0 +1,36 @@ +/* + * 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 com.android.server.locales; + +import android.content.Context; +import android.content.pm.PackageManagerInternal; + +import java.io.File; +import java.time.Clock; + +/** + * Shadow for {@link LocaleManagerBackupHelper} to enable mocking it for tests. + * + *

{@link LocaleManagerBackupHelper} is a package private class and hence not mockable directly. + */ +public class ShadowLocaleManagerBackupHelper extends LocaleManagerBackupHelper { + ShadowLocaleManagerBackupHelper(Context context, + LocaleManagerService localeManagerService, + PackageManagerInternal pmInternal, File stagedLocalesDir, Clock clock) { + super(context, localeManagerService, pmInternal, stagedLocalesDir, clock); + } +}