diff options
5 files changed, 237 insertions, 6 deletions
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 569a0db768aa..4612820d62bc 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -84,6 +84,7 @@ import android.util.ArraySet; 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 @@ public final class Settings { 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 f368c74a17b5..21ec42b1d557 100644 --- a/core/java/android/view/textservice/TextServicesManager.java +++ b/core/java/android/view/textservice/TextServicesManager.java @@ -66,6 +66,12 @@ public final class TextServicesManager { 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 000000000000..ca9936c5fcda --- /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 000000000000..351852710773 --- /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 965714dc279a..26a8cf74d245 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.ISpellCheckerSession; 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 @@ public class TextServicesManagerService extends ITextServicesManager.Stub { 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 @@ public class TextServicesManagerService extends ITextServicesManager.Stub { 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 class TextServicesManagerService extends ITextServicesManager.Stub { 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 class TextServicesManagerService extends ITextServicesManager.Stub { 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 @@ public class TextServicesManagerService extends ITextServicesManager.Stub { 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 @@ public class TextServicesManagerService extends ITextServicesManager.Stub { 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 @@ public class TextServicesManagerService extends ITextServicesManager.Stub { 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 @@ public class TextServicesManagerService extends ITextServicesManager.Stub { 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 @@ public class TextServicesManagerService extends ITextServicesManager.Stub { 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 @@ public class TextServicesManagerService extends ITextServicesManager.Stub { } } + /** + * @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 |