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