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

Commit df7105a9 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Re-enable cross-profile use of spell checker APIs"

parents f6876df3 3f8c5688
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