[Panlingual] Expose the API for the current IME

Give the current IME the ability to fetch the specific app’s locales in order to enable the corresponding keyboard

Bug: 248406206
Test: atest LocaleManagerServiceTest
      atest LocaleManagerTests
Change-Id: I34f59136ede15d094447ce5b637e51c7248635be
This commit is contained in:
Josh Hou
2022-10-13 14:39:42 +08:00
parent d4c74dd729
commit 4645a257c7
4 changed files with 75 additions and 9 deletions

View File

@@ -127,6 +127,7 @@ public class LocaleManager {
* <p>This API can be used by an app's installer
* (per {@link android.content.pm.InstallSourceInfo#getInstallingPackageName}) to retrieve
* the app's locales.
* <p>This API can be used by the current input method to retrieve locales of another packages.
* All other cases require {@code android.Manifest.permission#READ_APP_SPECIFIC_LOCALES}.
* Apps should generally retrieve their own locales via their in-process LocaleLists,
* or by calling {@link #getApplicationLocales()}.

View File

@@ -25,6 +25,7 @@ import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.ILocaleManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
@@ -38,6 +39,8 @@ import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ShellCallback;
import android.os.UserHandle;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
@@ -357,17 +360,20 @@ public class LocaleManagerService extends SystemService {
false /* allowAll */, ActivityManagerInternal.ALLOW_NON_FULL,
"getApplicationLocales", /* callerPackage= */ null);
// This function handles three types of query operations:
// This function handles four types of query operations:
// 1.) A normal, non-privileged app querying its own locale.
// 2.) The installer of the given app querying locales of a package installed
// by said installer.
// 3.) A privileged system service querying locales of another package.
// The least privileged case is a normal app performing a query, so check that first and
// get locales if the package name is owned by the app. Next check if the calling app
// is the installer of the given app and get locales. If neither conditions matched,
// check if the caller has the necessary permission and fetch locales.
// 2.) The installer of the given app querying locales of a package installed by said
// installer.
// 3.) The current input method querying locales of another package.
// 4.) A privileged system service querying locales of another package.
// The least privileged case is a normal app performing a query, so check that first and get
// locales if the package name is owned by the app. Next check if the calling app is the
// installer of the given app and get locales. Finally check if the calling app is the
// current input method. If neither conditions matched, check if the caller has the
// necessary permission and fetch locales.
if (!isPackageOwnedByCaller(appPackageName, userId)
&& !isCallerInstaller(appPackageName, userId)) {
&& !isCallerInstaller(appPackageName, userId)
&& !isCallerFromCurrentInputMethod(userId)) {
enforceReadAppSpecificLocalesPermission();
}
final long token = Binder.clearCallingIdentity();
@@ -412,6 +418,26 @@ public class LocaleManagerService extends SystemService {
return false;
}
/**
* Checks if the calling app is the current input method.
*/
private boolean isCallerFromCurrentInputMethod(int userId) {
String currentInputMethod = Settings.Secure.getStringForUser(
mContext.getContentResolver(),
Settings.Secure.DEFAULT_INPUT_METHOD,
userId);
if (!TextUtils.isEmpty(currentInputMethod)) {
String inputMethodPkgName = ComponentName
.unflattenFromString(currentInputMethod)
.getPackageName();
int inputMethodUid = getPackageUid(inputMethodPkgName, userId);
return inputMethodUid >= 0 && UserHandle.isSameApp(Binder.getCallingUid(),
inputMethodUid);
}
return false;
}
private void enforceReadAppSpecificLocalesPermission() {
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.READ_APP_SPECIFIC_LOCALES,

View File

@@ -16,6 +16,8 @@
package com.android.server.locales;
import static com.google.common.truth.Truth.assertThat;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.fail;
@@ -35,13 +37,16 @@ import static org.mockito.Mockito.verify;
import android.Manifest;
import android.app.ActivityManagerInternal;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.InstallSourceInfo;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.os.Binder;
import android.os.LocaleList;
import android.provider.Settings;
import androidx.test.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.android.internal.content.PackageMonitor;
@@ -111,6 +116,8 @@ public class LocaleManagerServiceTest {
doReturn(DEFAULT_USER_ID).when(mMockActivityManager)
.handleIncomingUser(anyInt(), anyInt(), eq(DEFAULT_USER_ID), anyBoolean(), anyInt(),
anyString(), anyString());
doReturn(InstrumentationRegistry.getContext().getContentResolver())
.when(mMockContext).getContentResolver();
mMockBackupHelper = mock(ShadowLocaleManagerBackupHelper.class);
mLocaleManagerService = new LocaleManagerService(mMockContext, mMockActivityTaskManager,
@@ -299,6 +306,25 @@ public class LocaleManagerServiceTest {
assertEquals(DEFAULT_LOCALES, locales);
}
@Test
public void testGetApplicationLocales_callerIsCurrentInputMethod_returnsLocales()
throws Exception {
doReturn(DEFAULT_UID).when(mMockPackageManager)
.getPackageUidAsUser(anyString(), any(), anyInt());
doReturn(new PackageConfig(/* nightMode = */ 0, DEFAULT_LOCALES))
.when(mMockActivityTaskManager).getApplicationConfig(anyString(), anyInt());
String imPkgName = getCurrentInputMethodPackageName();
doReturn(Binder.getCallingUid()).when(mMockPackageManager)
.getPackageUidAsUser(eq(imPkgName), any(), anyInt());
LocaleList locales =
mLocaleManagerService.getApplicationLocales(
DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID);
verify(mMockContext, never()).enforceCallingOrSelfPermission(any(), any());
assertEquals(DEFAULT_LOCALES, locales);
}
private static void assertNoLocalesStored(LocaleList locales) {
assertNull(locales);
}
@@ -311,4 +337,13 @@ public class LocaleManagerServiceTest {
private void setUpPassingPermissionCheckFor(String permission) {
doNothing().when(mMockContext).enforceCallingOrSelfPermission(eq(permission), any());
}
private String getCurrentInputMethodPackageName() {
String im = Settings.Secure.getString(
InstrumentationRegistry.getContext().getContentResolver(),
Settings.Secure.DEFAULT_INPUT_METHOD);
ComponentName cn = ComponentName.unflattenFromString(im);
assertThat(cn).isNotNull();
return cn.getPackageName();
}
}

View File

@@ -47,6 +47,8 @@ import android.util.AtomicFile;
import android.util.TypedXmlPullParser;
import android.util.Xml;
import androidx.test.InstrumentationRegistry;
import com.android.internal.content.PackageMonitor;
import com.android.internal.util.XmlUtils;
import com.android.server.wm.ActivityTaskManagerInternal;
@@ -124,6 +126,8 @@ public class SystemAppUpdateTrackerTest {
doReturn(DEFAULT_INSTALL_SOURCE_INFO).when(mMockPackageManager)
.getInstallSourceInfo(anyString());
doReturn(mMockPackageManager).when(mMockContext).getPackageManager();
doReturn(InstrumentationRegistry.getContext().getContentResolver())
.when(mMockContext).getContentResolver();
mStoragefile = new AtomicFile(new File(
Environment.getExternalStorageDirectory(), "systemUpdateUnitTests.xml"));