Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 3f8c5688 authored by Yohei Yukawa's avatar Yohei Yukawa
Browse files

Re-enable cross-profile use of spell checker APIs

Recently we successfully removed the restriction that up to one
SpellCheckerService can be active at the same time [1].  This still
makes much sense at high level, but at the ecosystem level there are
still some products / components that depend on the previous behavior
that child profile users can use parent profile's spell checker
service, which was originally introduced as a stopgap solution for
Android N MR1 [2].

Our decision for Android P for now is to revert back to the previous
behavior only when the calling process is running under work
profile.

At the implementation level, we can summarize the new behavior as
follows:
 * When TextServicesManager APIs are called from work-profile
   processes, those API calls will be evaluated with parent-profile's
   user ID to match the previous behavior [2].
   * If the currently selected spell checker is not a pre-installed
     one, then API calls from work profile will fail to match the
     previous behavior [2].
 * When TextServicesManager APIs are called from non work-profile
   processes, those API calls will continue being evaluated with
   calling user ID, as we planned for Android P [1].
 * TextServicesData will not be created for child profile users.

 [1]: I06c27ef834203a21cc445dc126602c799384527b
      06a26240
 [2]: Iae9045ba5baccd04ed68906e7afb9160677ec4a5
      095fa371

Bug: 63041121
Bug: 64718412
Bug: 70922751
Bug: 73609140
Fix: 73862883
Test: atest FrameworksCoreTests:com.android.internal.textservice.LazyIntToIntMapTest
Test: Manually tested with Test DPC as follows:
      * When AOSP Spell Checker is pre-installed and the current spell
        checker, both main profile and work profile can use AOSP spell
        checker.
      * When SampleSpellCheckerService is side-loaded and the current
        spell checker, only main profile can use
        SampleSpellCheckerService.
Change-Id: Ic046f832f203115106409a53418a5746eb6d4939
parent 30982fac
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -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 */
+6 −0
Original line number Diff line number Diff line
@@ -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;
+67 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
+92 −0
Original line number Diff line number Diff line
/*
 * 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);
    }
}
+67 −6
Original line number Diff line number Diff line
@@ -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