Merge "Re-enable cross-profile use of spell checker APIs"
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 8fc6901..6fe249a 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -84,6 +84,7 @@
import android.util.Log;
import android.util.MemoryIntArray;
import android.util.StatsLog;
+import android.view.textservice.TextServicesManager;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.widget.ILockSettings;
@@ -7970,6 +7971,10 @@
CLONE_TO_MANAGED_PROFILE.add(LOCATION_MODE);
CLONE_TO_MANAGED_PROFILE.add(LOCATION_PROVIDERS_ALLOWED);
CLONE_TO_MANAGED_PROFILE.add(SELECTED_INPUT_METHOD_SUBTYPE);
+ if (TextServicesManager.DISABLE_PER_PROFILE_SPELL_CHECKER) {
+ CLONE_TO_MANAGED_PROFILE.add(SELECTED_SPELL_CHECKER);
+ CLONE_TO_MANAGED_PROFILE.add(SELECTED_SPELL_CHECKER_SUBTYPE);
+ }
}
/** @hide */
diff --git a/core/java/android/view/textservice/TextServicesManager.java b/core/java/android/view/textservice/TextServicesManager.java
index f368c74..21ec42b 100644
--- a/core/java/android/view/textservice/TextServicesManager.java
+++ b/core/java/android/view/textservice/TextServicesManager.java
@@ -66,6 +66,12 @@
private static final String TAG = TextServicesManager.class.getSimpleName();
private static final boolean DBG = false;
+ /**
+ * A compile time switch to control per-profile spell checker, which is not yet ready.
+ * @hide
+ */
+ public static final boolean DISABLE_PER_PROFILE_SPELL_CHECKER = true;
+
private static TextServicesManager sInstance;
private final ITextServicesManager mService;
diff --git a/core/java/com/android/internal/textservice/LazyIntToIntMap.java b/core/java/com/android/internal/textservice/LazyIntToIntMap.java
new file mode 100644
index 0000000..ca9936c
--- /dev/null
+++ b/core/java/com/android/internal/textservice/LazyIntToIntMap.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2018 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.internal.textservice;
+
+import android.annotation.NonNull;
+import android.util.SparseIntArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.function.IntUnaryOperator;
+
+/**
+ * Simple int-to-int key-value-store that is to be lazily initialized with the given
+ * {@link IntUnaryOperator}.
+ */
+@VisibleForTesting
+public final class LazyIntToIntMap {
+
+ private final SparseIntArray mMap = new SparseIntArray();
+
+ @NonNull
+ private final IntUnaryOperator mMappingFunction;
+
+ /**
+ * @param mappingFunction int to int mapping rules to be (lazily) evaluated
+ */
+ public LazyIntToIntMap(@NonNull IntUnaryOperator mappingFunction) {
+ mMappingFunction = mappingFunction;
+ }
+
+ /**
+ * Deletes {@code key} and associated value.
+ * @param key key to be deleted
+ */
+ public void delete(int key) {
+ mMap.delete(key);
+ }
+
+ /**
+ * @param key key associated with the value
+ * @return value associated with the {@code key}. If this is the first time to access
+ * {@code key}, then {@code mappingFunction} passed to the constructor will be evaluated
+ */
+ public int get(int key) {
+ final int index = mMap.indexOfKey(key);
+ if (index >= 0) {
+ return mMap.valueAt(index);
+ }
+ final int value = mMappingFunction.applyAsInt(key);
+ mMap.append(key, value);
+ return value;
+ }
+}
diff --git a/core/tests/coretests/src/com/android/internal/textservice/LazyIntToIntMapTest.java b/core/tests/coretests/src/com/android/internal/textservice/LazyIntToIntMapTest.java
new file mode 100644
index 0000000..3518527
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/textservice/LazyIntToIntMapTest.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2018 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.internal.textservice;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.IntUnaryOperator;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class LazyIntToIntMapTest {
+ @Test
+ public void testLaziness() {
+ final IntUnaryOperator func = mock(IntUnaryOperator.class);
+ when(func.applyAsInt(eq(1))).thenReturn(11);
+ when(func.applyAsInt(eq(2))).thenReturn(22);
+
+ final LazyIntToIntMap map = new LazyIntToIntMap(func);
+
+ verify(func, never()).applyAsInt(anyInt());
+
+ assertEquals(22, map.get(2));
+ verify(func, times(0)).applyAsInt(eq(1));
+ verify(func, times(1)).applyAsInt(eq(2));
+
+ // Accessing to the same key does not evaluate the function again.
+ assertEquals(22, map.get(2));
+ verify(func, times(0)).applyAsInt(eq(1));
+ verify(func, times(1)).applyAsInt(eq(2));
+ }
+
+ @Test
+ public void testDelete() {
+ final IntUnaryOperator func1 = mock(IntUnaryOperator.class);
+ when(func1.applyAsInt(eq(1))).thenReturn(11);
+ when(func1.applyAsInt(eq(2))).thenReturn(22);
+
+ final IntUnaryOperator func2 = mock(IntUnaryOperator.class);
+ when(func2.applyAsInt(eq(1))).thenReturn(111);
+ when(func2.applyAsInt(eq(2))).thenReturn(222);
+
+ final AtomicReference<IntUnaryOperator> funcRef = new AtomicReference<>(func1);
+ final LazyIntToIntMap map = new LazyIntToIntMap(i -> funcRef.get().applyAsInt(i));
+
+ verify(func1, never()).applyAsInt(anyInt());
+ verify(func2, never()).applyAsInt(anyInt());
+
+ assertEquals(22, map.get(2));
+ verify(func1, times(1)).applyAsInt(eq(2));
+ verify(func2, times(0)).applyAsInt(eq(2));
+
+ // Swap func1 with func2 then invalidate the key=2
+ funcRef.set(func2);
+ map.delete(2);
+
+ // Calling get(2) again should re-evaluate the value.
+ assertEquals(222, map.get(2));
+ verify(func1, times(1)).applyAsInt(eq(2));
+ verify(func2, times(1)).applyAsInt(eq(2));
+
+ // Trying to delete non-existing keys does nothing.
+ map.delete(1);
+ }
+}
diff --git a/services/core/java/com/android/server/TextServicesManagerService.java b/services/core/java/com/android/server/TextServicesManagerService.java
index 965714d..26a8cf7 100644
--- a/services/core/java/com/android/server/TextServicesManagerService.java
+++ b/services/core/java/com/android/server/TextServicesManagerService.java
@@ -16,6 +16,9 @@
package com.android.server;
+import static android.view.textservice.TextServicesManager.DISABLE_PER_PROFILE_SPELL_CHECKER;
+
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.content.PackageMonitor;
import com.android.internal.inputmethod.InputMethodUtils;
import com.android.internal.textservice.ISpellCheckerService;
@@ -24,6 +27,7 @@
import com.android.internal.textservice.ISpellCheckerSessionListener;
import com.android.internal.textservice.ITextServicesManager;
import com.android.internal.textservice.ITextServicesSessionListener;
+import com.android.internal.textservice.LazyIntToIntMap;
import com.android.internal.util.DumpUtils;
import org.xmlpull.v1.XmlPullParserException;
@@ -79,6 +83,10 @@
private final UserManager mUserManager;
private final Object mLock = new Object();
+ @NonNull
+ @GuardedBy("mLock")
+ private final LazyIntToIntMap mSpellCheckerOwnerUserIdMap;
+
private static class TextServicesData {
@UserIdInt
private final int mUserId;
@@ -294,6 +302,9 @@
void onStopUser(@UserIdInt int userId) {
synchronized (mLock) {
+ // Clear user ID mapping table.
+ mSpellCheckerOwnerUserIdMap.delete(userId);
+
// Clean per-user data
TextServicesData tsd = mUserData.get(userId);
if (tsd == null) return;
@@ -313,12 +324,32 @@
public TextServicesManagerService(Context context) {
mContext = context;
mUserManager = mContext.getSystemService(UserManager.class);
+ mSpellCheckerOwnerUserIdMap = new LazyIntToIntMap(callingUserId -> {
+ if (DISABLE_PER_PROFILE_SPELL_CHECKER) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ final UserInfo parent = mUserManager.getProfileParent(callingUserId);
+ return (parent != null) ? parent.id : callingUserId;
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ } else {
+ return callingUserId;
+ }
+ });
mMonitor = new TextServicesMonitor();
mMonitor.register(context, null, UserHandle.ALL, true);
}
private void initializeInternalStateLocked(@UserIdInt int userId) {
+ // When DISABLE_PER_PROFILE_SPELL_CHECKER is true, we make sure here that work profile users
+ // will never have non-null TextServicesData for their user ID.
+ if (DISABLE_PER_PROFILE_SPELL_CHECKER
+ && userId != mSpellCheckerOwnerUserIdMap.get(userId)) {
+ return;
+ }
+
TextServicesData tsd = mUserData.get(userId);
if (tsd == null) {
tsd = new TextServicesData(userId, mContext);
@@ -470,7 +501,7 @@
public SpellCheckerInfo getCurrentSpellChecker(String locale) {
int userId = UserHandle.getCallingUserId();
synchronized (mLock) {
- TextServicesData tsd = mUserData.get(userId);
+ final TextServicesData tsd = getDataFromCallingUserIdLocked(userId);
if (tsd == null) return null;
return tsd.getCurrentSpellChecker();
@@ -488,7 +519,7 @@
final int userId = UserHandle.getCallingUserId();
synchronized (mLock) {
- TextServicesData tsd = mUserData.get(userId);
+ final TextServicesData tsd = getDataFromCallingUserIdLocked(userId);
if (tsd == null) return null;
subtypeHashCode =
@@ -569,7 +600,7 @@
int callingUserId = UserHandle.getCallingUserId();
synchronized (mLock) {
- TextServicesData tsd = mUserData.get(callingUserId);
+ final TextServicesData tsd = getDataFromCallingUserIdLocked(callingUserId);
if (tsd == null) return;
HashMap<String, SpellCheckerInfo> spellCheckerMap = tsd.mSpellCheckerMap;
@@ -606,7 +637,7 @@
int userId = UserHandle.getCallingUserId();
synchronized (mLock) {
- TextServicesData tsd = mUserData.get(userId);
+ final TextServicesData tsd = getDataFromCallingUserIdLocked(userId);
if (tsd == null) return false;
return tsd.isSpellCheckerEnabled();
@@ -643,7 +674,7 @@
int callingUserId = UserHandle.getCallingUserId();
synchronized (mLock) {
- TextServicesData tsd = mUserData.get(callingUserId);
+ final TextServicesData tsd = getDataFromCallingUserIdLocked(callingUserId);
if (tsd == null) return null;
ArrayList<SpellCheckerInfo> spellCheckerList = tsd.mSpellCheckerList;
@@ -666,7 +697,7 @@
int userId = UserHandle.getCallingUserId();
synchronized (mLock) {
- TextServicesData tsd = mUserData.get(userId);
+ final TextServicesData tsd = getDataFromCallingUserIdLocked(userId);
if (tsd == null) return;
final ArrayList<SpellCheckerBindGroup> removeList = new ArrayList<>();
@@ -737,6 +768,36 @@
}
}
+ /**
+ * @param callingUserId user ID of the calling process
+ * @return {@link TextServicesData} for the given user. {@code null} if spell checker is not
+ * temporarily / permanently available for the specified user
+ */
+ @Nullable
+ private TextServicesData getDataFromCallingUserIdLocked(@UserIdInt int callingUserId) {
+ final int spellCheckerOwnerUserId = mSpellCheckerOwnerUserIdMap.get(callingUserId);
+ final TextServicesData data = mUserData.get(spellCheckerOwnerUserId);
+ if (DISABLE_PER_PROFILE_SPELL_CHECKER) {
+ if (spellCheckerOwnerUserId != callingUserId) {
+ // Calling process is running under child profile.
+ if (data == null) {
+ return null;
+ }
+ final SpellCheckerInfo info = data.getCurrentSpellChecker();
+ if (info == null) {
+ return null;
+ }
+ final ServiceInfo serviceInfo = info.getServiceInfo();
+ if ((serviceInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
+ // To be conservative, non pre-installed spell checker services are not allowed
+ // to be used for child profiles.
+ return null;
+ }
+ }
+ }
+ return data;
+ }
+
private static final class SessionRequest {
public final int mUid;
@Nullable