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

Commit 8e6fa02f authored by Guliz Tuncay's avatar Guliz Tuncay
Browse files

Restructure TSMS

Remove TextServicesSettings, methods that deal with Settings and methods
that deal with TextServicesData (dump etc.) into TextServicesData. This
is a purely refactoring CL and it should not introduce any behavior
change.

Bug: 63041121
Test: None
Change-Id: I721fe464157c5850bd15d1d18fddb8c818c82683
parent 31fe1bd7
Loading
Loading
Loading
Loading
+182 −236
Original line number Original line Diff line number Diff line
@@ -75,12 +75,9 @@ public class TextServicesManagerService extends ITextServicesManager.Stub {
    private final Context mContext;
    private final Context mContext;
    private final TextServicesMonitor mMonitor;
    private final TextServicesMonitor mMonitor;
    private final SparseArray<TextServicesData> mUserData = new SparseArray<>();
    private final SparseArray<TextServicesData> mUserData = new SparseArray<>();
    private final TextServicesSettings mSettings;
    @NonNull
    @NonNull
    private final UserManager mUserManager;
    private final UserManager mUserManager;
    private final Object mLock = new Object();
    private final Object mLock = new Object();
    @GuardedBy("mLock")
    private int mSpellCheckerMapUpdateCount = 0;


    private static class TextServicesData {
    private static class TextServicesData {
        @UserIdInt
        @UserIdInt
@@ -88,12 +85,180 @@ public class TextServicesManagerService extends ITextServicesManager.Stub {
        private final HashMap<String, SpellCheckerInfo> mSpellCheckerMap;
        private final HashMap<String, SpellCheckerInfo> mSpellCheckerMap;
        private final ArrayList<SpellCheckerInfo> mSpellCheckerList;
        private final ArrayList<SpellCheckerInfo> mSpellCheckerList;
        private final HashMap<String, SpellCheckerBindGroup> mSpellCheckerBindGroups;
        private final HashMap<String, SpellCheckerBindGroup> mSpellCheckerBindGroups;
        private final Context mContext;
        private final ContentResolver mResolver;
        public int mUpdateCount = 0;


        public TextServicesData(@UserIdInt int userId) {
        public TextServicesData(@UserIdInt int userId, @NonNull Context context) {
            mUserId = userId;
            mUserId = userId;
            mSpellCheckerMap = new HashMap<>();
            mSpellCheckerMap = new HashMap<>();
            mSpellCheckerList = new ArrayList<>();
            mSpellCheckerList = new ArrayList<>();
            mSpellCheckerBindGroups = new HashMap<>();
            mSpellCheckerBindGroups = new HashMap<>();
            mContext = context;
            mResolver = context.getContentResolver();
        }

        private void putString(final String key, final String str) {
            Settings.Secure.putStringForUser(mResolver, key, str, mUserId);
        }

        @Nullable
        private String getString(@NonNull final String key, @Nullable final String defaultValue) {
            final String result;
            result = Settings.Secure.getStringForUser(mResolver, key, mUserId);
            return result != null ? result : defaultValue;
        }

        private void putInt(final String key, final int value) {
            Settings.Secure.putIntForUser(mResolver, key, value, mUserId);
        }

        private int getInt(final String key, final int defaultValue) {
            return Settings.Secure.getIntForUser(mResolver, key, defaultValue, mUserId);
        }

        private boolean getBoolean(final String key, final boolean defaultValue) {
            return getInt(key, defaultValue ? 1 : 0) == 1;
        }

        private void putSelectedSpellChecker(@Nullable String sciId) {
            if (TextUtils.isEmpty(sciId)) {
                // OK to coalesce to null, since getSelectedSpellChecker() can take care of the
                // empty data scenario.
                putString(Settings.Secure.SELECTED_SPELL_CHECKER, null);
            } else {
                putString(Settings.Secure.SELECTED_SPELL_CHECKER, sciId);
            }
        }

        private void putSelectedSpellCheckerSubtype(int hashCode) {
            putInt(Settings.Secure.SELECTED_SPELL_CHECKER_SUBTYPE, hashCode);
        }

        @NonNull
        private String getSelectedSpellChecker() {
            return getString(Settings.Secure.SELECTED_SPELL_CHECKER, "");
        }

        public int getSelectedSpellCheckerSubtype(final int defaultValue) {
            return getInt(Settings.Secure.SELECTED_SPELL_CHECKER_SUBTYPE, defaultValue);
        }

        public boolean isSpellCheckerEnabled() {
            return getBoolean(Settings.Secure.SPELL_CHECKER_ENABLED, true);
        }

        @Nullable
        public SpellCheckerInfo getCurrentSpellChecker() {
            final String curSpellCheckerId = getSelectedSpellChecker();
            if (TextUtils.isEmpty(curSpellCheckerId)) {
                return null;
            }
            return mSpellCheckerMap.get(curSpellCheckerId);
        }

        public void setCurrentSpellChecker(SpellCheckerInfo sci) {
            putSelectedSpellChecker(sci.getId());
            putSelectedSpellCheckerSubtype(SpellCheckerSubtype.SUBTYPE_ID_NONE);
        }

        private void initializeTextServicesData() {
            if (DBG) {
                Slog.d(TAG, "initializeTextServicesData for user: " + mUserId);
            }
            mSpellCheckerList.clear();
            mSpellCheckerMap.clear();
            mUpdateCount++;
            final PackageManager pm = mContext.getPackageManager();
            // Note: We do not specify PackageManager.MATCH_ENCRYPTION_* flags here because the
            // default behavior of PackageManager is exactly what we want.  It by default picks up
            // appropriate services depending on the unlock state for the specified user.
            final List<ResolveInfo> services = pm.queryIntentServicesAsUser(
                    new Intent(SpellCheckerService.SERVICE_INTERFACE), PackageManager.GET_META_DATA,
                    mUserId);
            final int N = services.size();
            for (int i = 0; i < N; ++i) {
                final ResolveInfo ri = services.get(i);
                final ServiceInfo si = ri.serviceInfo;
                final ComponentName compName = new ComponentName(si.packageName, si.name);
                if (!android.Manifest.permission.BIND_TEXT_SERVICE.equals(si.permission)) {
                    Slog.w(TAG, "Skipping text service " + compName
                            + ": it does not require the permission "
                            + android.Manifest.permission.BIND_TEXT_SERVICE);
                    continue;
                }
                if (DBG) Slog.d(TAG, "Add: " + compName + " for user: " + mUserId);
                try {
                    final SpellCheckerInfo sci = new SpellCheckerInfo(mContext, ri);
                    if (sci.getSubtypeCount() <= 0) {
                        Slog.w(TAG, "Skipping text service " + compName
                                + ": it does not contain subtypes.");
                        continue;
                    }
                    mSpellCheckerList.add(sci);
                    mSpellCheckerMap.put(sci.getId(), sci);
                } catch (XmlPullParserException e) {
                    Slog.w(TAG, "Unable to load the spell checker " + compName, e);
                } catch (IOException e) {
                    Slog.w(TAG, "Unable to load the spell checker " + compName, e);
                }
            }
            if (DBG) {
                Slog.d(TAG, "initializeSpellCheckerMap: " + mSpellCheckerList.size() + ","
                        + mSpellCheckerMap.size());
            }
        }

        private void dump(PrintWriter pw) {
            int spellCheckerIndex = 0;
            pw.println("  User #" + mUserId);
            pw.println("  Spell Checkers:");
            pw.println("  Spell Checkers: " +  "mUpdateCount=" + mUpdateCount);
            for (final SpellCheckerInfo info : mSpellCheckerMap.values()) {
                pw.println("  Spell Checker #" + spellCheckerIndex);
                info.dump(pw, "    ");
                ++spellCheckerIndex;
            }

            pw.println("");
            pw.println("  Spell Checker Bind Groups:");
            HashMap<String, SpellCheckerBindGroup> spellCheckerBindGroups = mSpellCheckerBindGroups;
            for (final Map.Entry<String, SpellCheckerBindGroup> ent
                    : spellCheckerBindGroups.entrySet()) {
                final SpellCheckerBindGroup grp = ent.getValue();
                pw.println("    " + ent.getKey() + " " + grp + ":");
                pw.println("      " + "mInternalConnection=" + grp.mInternalConnection);
                pw.println("      " + "mSpellChecker=" + grp.mSpellChecker);
                pw.println("      " + "mUnbindCalled=" + grp.mUnbindCalled);
                pw.println("      " + "mConnected=" + grp.mConnected);
                final int numPendingSessionRequests = grp.mPendingSessionRequests.size();
                for (int j = 0; j < numPendingSessionRequests; j++) {
                    final SessionRequest req = grp.mPendingSessionRequests.get(j);
                    pw.println("      " + "Pending Request #" + j + ":");
                    pw.println("        " + "mTsListener=" + req.mTsListener);
                    pw.println("        " + "mScListener=" + req.mScListener);
                    pw.println(
                            "        " + "mScLocale=" + req.mLocale + " mUid=" + req.mUserId);
                }
                final int numOnGoingSessionRequests = grp.mOnGoingSessionRequests.size();
                for (int j = 0; j < numOnGoingSessionRequests; j++) {
                    final SessionRequest req = grp.mOnGoingSessionRequests.get(j);
                    pw.println("      " + "On going Request #" + j + ":");
                    ++j;
                    pw.println("        " + "mTsListener=" + req.mTsListener);
                    pw.println("        " + "mScListener=" + req.mScListener);
                    pw.println(
                            "        " + "mScLocale=" + req.mLocale + " mUid=" + req.mUserId);
                }
                final int N = grp.mListeners.getRegisteredCallbackCount();
                for (int j = 0; j < N; j++) {
                    final ISpellCheckerSessionListener mScListener =
                            grp.mListeners.getRegisteredCallbackItem(j);
                    pw.println("      " + "Listener #" + j + ":");
                    pw.println("        " + "mScListener=" + mScListener);
                    pw.println("        " + "mGroup=" + grp);
                }
            }
        }
        }
    }
    }


@@ -153,18 +318,17 @@ public class TextServicesManagerService extends ITextServicesManager.Stub {


        mMonitor = new TextServicesMonitor();
        mMonitor = new TextServicesMonitor();
        mMonitor.register(context, null, UserHandle.ALL, true);
        mMonitor.register(context, null, UserHandle.ALL, true);
        mSettings = new TextServicesSettings(context.getContentResolver());
    }
    }


    private void initializeInternalStateLocked(@UserIdInt int userId) {
    private void initializeInternalStateLocked(@UserIdInt int userId) {
        TextServicesData tsd = mUserData.get(userId);
        TextServicesData tsd = mUserData.get(userId);
        if (tsd == null) {
        if (tsd == null) {
            tsd = new TextServicesData(userId);
            tsd = new TextServicesData(userId, mContext);
            mUserData.put(userId, tsd);
            mUserData.put(userId, tsd);
        }
        }


        updateTextServiceDataLocked(tsd);
        tsd.initializeTextServicesData();
        SpellCheckerInfo sci = getCurrentSpellCheckerInternalLocked(tsd);
        SpellCheckerInfo sci = tsd.getCurrentSpellChecker();
        if (sci == null) {
        if (sci == null) {
            sci = findAvailSpellCheckerLocked(null, tsd);
            sci = findAvailSpellCheckerLocked(null, tsd);
            if (sci != null) {
            if (sci != null) {
@@ -176,14 +340,6 @@ public class TextServicesManagerService extends ITextServicesManager.Stub {
        }
        }
    }
    }


    private void updateTextServiceDataLocked(TextServicesData tsd) {
        if (DBG) {
            Slog.d(TAG, "updateTextServiceDataLocked for user: " + tsd.mUserId);
        }
        buildSpellCheckerMapLocked(mContext, tsd.mSpellCheckerList, tsd.mSpellCheckerMap,
                tsd.mUserId);
    }

    private final class TextServicesMonitor extends PackageMonitor {
    private final class TextServicesMonitor extends PackageMonitor {
        @Override
        @Override
        public void onSomePackagesChanged() {
        public void onSomePackagesChanged() {
@@ -197,8 +353,8 @@ public class TextServicesManagerService extends ITextServicesManager.Stub {
                if (tsd == null) return;
                if (tsd == null) return;


                // TODO: Update for each locale
                // TODO: Update for each locale
                SpellCheckerInfo sci = getCurrentSpellCheckerInternalLocked(tsd);
                SpellCheckerInfo sci = tsd.getCurrentSpellChecker();
                updateTextServiceDataLocked(tsd);
                tsd.initializeTextServicesData();
                // If no spell checker is enabled, just return. The user should explicitly
                // If no spell checker is enabled, just return. The user should explicitly
                // enable the spell checker.
                // enable the spell checker.
                if (sci == null) return;
                if (sci == null) return;
@@ -219,51 +375,6 @@ public class TextServicesManagerService extends ITextServicesManager.Stub {
        }
        }
    }
    }


    private void buildSpellCheckerMapLocked(Context context,
            ArrayList<SpellCheckerInfo> list, HashMap<String, SpellCheckerInfo> map,
            @UserIdInt int userId) {
        list.clear();
        map.clear();
        mSpellCheckerMapUpdateCount++;
        final PackageManager pm = context.getPackageManager();
        // Note: We do not specify PackageManager.MATCH_ENCRYPTION_* flags here because the default
        // behavior of PackageManager is exactly what we want.  It by default picks up appropriate
        // services depending on the unlock state for the specified user.
        final List<ResolveInfo> services = pm.queryIntentServicesAsUser(
                new Intent(SpellCheckerService.SERVICE_INTERFACE), PackageManager.GET_META_DATA,
                userId);
        final int N = services.size();
        for (int i = 0; i < N; ++i) {
            final ResolveInfo ri = services.get(i);
            final ServiceInfo si = ri.serviceInfo;
            final ComponentName compName = new ComponentName(si.packageName, si.name);
            if (!android.Manifest.permission.BIND_TEXT_SERVICE.equals(si.permission)) {
                Slog.w(TAG, "Skipping text service " + compName
                        + ": it does not require the permission "
                        + android.Manifest.permission.BIND_TEXT_SERVICE);
                continue;
            }
            if (DBG) Slog.d(TAG, "Add: " + compName + " for user: " + userId);
            try {
                final SpellCheckerInfo sci = new SpellCheckerInfo(context, ri);
                if (sci.getSubtypeCount() <= 0) {
                    Slog.w(TAG, "Skipping text service " + compName
                            + ": it does not contain subtypes.");
                    continue;
                }
                list.add(sci);
                map.put(sci.getId(), sci);
            } catch (XmlPullParserException e) {
                Slog.w(TAG, "Unable to load the spell checker " + compName, e);
            } catch (IOException e) {
                Slog.w(TAG, "Unable to load the spell checker " + compName, e);
            }
        }
        if (DBG) {
            Slog.d(TAG, "buildSpellCheckerMapLocked: " + list.size() + "," + map.size());
        }
    }

    private boolean bindCurrentSpellCheckerService(
    private boolean bindCurrentSpellCheckerService(
            Intent service, ServiceConnection conn, int flags, @UserIdInt int userId) {
            Intent service, ServiceConnection conn, int flags, @UserIdInt int userId) {
        if (service == null || conn == null) {
        if (service == null || conn == null) {
@@ -348,19 +459,8 @@ public class TextServicesManagerService extends ITextServicesManager.Stub {
            TextServicesData tsd = mUserData.get(userId);
            TextServicesData tsd = mUserData.get(userId);
            if (tsd == null) return null;
            if (tsd == null) return null;


            return getCurrentSpellCheckerInternalLocked(tsd);
            return tsd.getCurrentSpellChecker();
        }
    }

    private SpellCheckerInfo getCurrentSpellCheckerInternalLocked(TextServicesData tsd) {
        final String curSpellCheckerId = mSettings.getSelectedSpellChecker(tsd.mUserId);
        if (DBG) {
            Slog.w(TAG, "getCurrentSpellChecker: " + curSpellCheckerId);
        }
        if (TextUtils.isEmpty(curSpellCheckerId)) {
            return null;
        }
        }
        return tsd.mSpellCheckerMap.get(curSpellCheckerId);
    }
    }


    // TODO: Respect allowImplicitlySelectedSubtype
    // TODO: Respect allowImplicitlySelectedSubtype
@@ -378,12 +478,11 @@ public class TextServicesManagerService extends ITextServicesManager.Stub {
            if (tsd == null) return null;
            if (tsd == null) return null;


            subtypeHashCode =
            subtypeHashCode =
                    mSettings.getSelectedSpellCheckerSubtype(SpellCheckerSubtype.SUBTYPE_ID_NONE,
                    tsd.getSelectedSpellCheckerSubtype(SpellCheckerSubtype.SUBTYPE_ID_NONE);
                            userId);
            if (DBG) {
            if (DBG) {
                Slog.w(TAG, "getCurrentSpellCheckerSubtype: " + subtypeHashCode);
                Slog.w(TAG, "getCurrentSpellCheckerSubtype: " + subtypeHashCode);
            }
            }
            sci = getCurrentSpellCheckerInternalLocked(tsd);
            sci = tsd.getCurrentSpellChecker();
            systemLocale = mContext.getResources().getConfiguration().locale;
            systemLocale = mContext.getResources().getConfiguration().locale;
        }
        }
        if (sci == null || sci.getSubtypeCount() == 0) {
        if (sci == null || sci.getSubtypeCount() == 0) {
@@ -496,7 +595,7 @@ public class TextServicesManagerService extends ITextServicesManager.Stub {
            TextServicesData tsd = mUserData.get(userId);
            TextServicesData tsd = mUserData.get(userId);
            if (tsd == null) return false;
            if (tsd == null) return false;


            return isSpellCheckerEnabledLocked(userId);
            return tsd.isSpellCheckerEnabled();
        }
        }
    }
    }


@@ -578,42 +677,7 @@ public class TextServicesManagerService extends ITextServicesManager.Stub {
        }
        }
        final long ident = Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();
        try {
        try {
            mSettings.putSelectedSpellChecker(sciId, tsd.mUserId);
            tsd.setCurrentSpellChecker(sci);
            setCurrentSpellCheckerSubtypeLocked(0, tsd);
        } finally {
            Binder.restoreCallingIdentity(ident);
        }
    }

    private void setCurrentSpellCheckerSubtypeLocked(int hashCode, TextServicesData tsd) {
        if (DBG) {
            Slog.w(TAG, "setCurrentSpellCheckerSubtype: " + hashCode);
        }

        final SpellCheckerInfo sci = getCurrentSpellCheckerInternalLocked(tsd);
        int tempHashCode = 0;
        for (int i = 0; sci != null && i < sci.getSubtypeCount(); ++i) {
            if(sci.getSubtypeAt(i).hashCode() == hashCode) {
                tempHashCode = hashCode;
                break;
            }
        }
        final long ident = Binder.clearCallingIdentity();
        try {
            mSettings.putSelectedSpellCheckerSubtype(tempHashCode, tsd.mUserId);
        } finally {
            Binder.restoreCallingIdentity(ident);
        }
    }

    private boolean isSpellCheckerEnabledLocked(@UserIdInt int userId) {
        final long ident = Binder.clearCallingIdentity();
        try {
            final boolean retval = mSettings.isSpellCheckerEnabled(userId);
            if (DBG) {
                Slog.w(TAG, "getSpellCheckerEnabled: " + retval);
            }
            return retval;
        } finally {
        } finally {
            Binder.restoreCallingIdentity(ident);
            Binder.restoreCallingIdentity(ident);
        }
        }
@@ -626,14 +690,11 @@ public class TextServicesManagerService extends ITextServicesManager.Stub {
        if (args.length == 0) {  // Dump all users' data
        if (args.length == 0) {  // Dump all users' data
            synchronized (mLock) {
            synchronized (mLock) {
                pw.println("Current Text Services Manager state:");
                pw.println("Current Text Services Manager state:");
                pw.println("  Text Services: mSpellCheckerMapUpdateCount="
                        + mSpellCheckerMapUpdateCount);
                pw.println("  Users:");
                pw.println("  Users:");
                final int numOfUsers = mUserData.size();
                final int numOfUsers = mUserData.size();
                for (int i = 0; i < numOfUsers; i++) {
                for (int i = 0; i < numOfUsers; i++) {
                    int userId = mUserData.keyAt(i);
                    TextServicesData tsd = mUserData.valueAt(i);
                    pw.println("  User #" + userId);
                    tsd.dump(pw);
                    dumpUserDataLocked(pw, userId);
                }
                }
            }
            }
        } else {  // Dump a given user's data
        } else {  // Dump a given user's data
@@ -654,64 +715,9 @@ public class TextServicesManagerService extends ITextServicesManager.Stub {
                }
                }
                synchronized (mLock) {
                synchronized (mLock) {
                    pw.println("Current Text Services Manager state:");
                    pw.println("Current Text Services Manager state:");
                    pw.println("  Text Services: mSpellCheckerMapUpdateCount="
                            + mSpellCheckerMapUpdateCount);
                    pw.println("  User " + userId + ":");
                    pw.println("  User " + userId + ":");
                    dumpUserDataLocked(pw, userId);
                    tsd.dump(pw);
                }
            }
        }
    }

    private void dumpUserDataLocked(PrintWriter pw, @UserIdInt int userId) {
        TextServicesData tsd = mUserData.get(userId);
        HashMap<String, SpellCheckerInfo> spellCheckerMap = tsd.mSpellCheckerMap;
        int spellCheckerIndex = 0;
        pw.println("  Spell Checkers:");
        for (final SpellCheckerInfo info : spellCheckerMap.values()) {
            pw.println("  Spell Checker #" + spellCheckerIndex);
            info.dump(pw, "    ");
            ++spellCheckerIndex;
        }

        pw.println("");
        pw.println("  Spell Checker Bind Groups:");
        HashMap<String, SpellCheckerBindGroup> spellCheckerBindGroups =
                tsd.mSpellCheckerBindGroups;
        for (final Map.Entry<String, SpellCheckerBindGroup> ent
                : spellCheckerBindGroups.entrySet()) {
            final SpellCheckerBindGroup grp = ent.getValue();
            pw.println("    " + ent.getKey() + " " + grp + ":");
            pw.println("      " + "mInternalConnection=" + grp.mInternalConnection);
            pw.println("      " + "mSpellChecker=" + grp.mSpellChecker);
            pw.println("      " + "mUnbindCalled=" + grp.mUnbindCalled);
            pw.println("      " + "mConnected=" + grp.mConnected);
            final int numPendingSessionRequests = grp.mPendingSessionRequests.size();
            for (int j = 0; j < numPendingSessionRequests; j++) {
                final SessionRequest req = grp.mPendingSessionRequests.get(j);
                pw.println("      " + "Pending Request #" + j + ":");
                pw.println("        " + "mTsListener=" + req.mTsListener);
                pw.println("        " + "mScListener=" + req.mScListener);
                pw.println(
                        "        " + "mScLocale=" + req.mLocale + " mUid=" + req.mUserId);
                }
                }
            final int numOnGoingSessionRequests = grp.mOnGoingSessionRequests.size();
            for (int j = 0; j < numOnGoingSessionRequests; j++) {
                final SessionRequest req = grp.mOnGoingSessionRequests.get(j);
                pw.println("      " + "On going Request #" + j + ":");
                ++j;
                pw.println("        " + "mTsListener=" + req.mTsListener);
                pw.println("        " + "mScListener=" + req.mScListener);
                pw.println(
                        "        " + "mScLocale=" + req.mLocale + " mUid=" + req.mUserId);
            }
            final int N = grp.mListeners.getRegisteredCallbackCount();
            for (int j = 0; j < N; j++) {
                final ISpellCheckerSessionListener mScListener =
                        grp.mListeners.getRegisteredCallbackItem(j);
                pw.println("      " + "Listener #" + j + ":");
                pw.println("        " + "mScListener=" + mScListener);
                pw.println("        " + "mGroup=" + grp);
            }
            }
        }
        }
    }
    }
@@ -971,64 +977,4 @@ public class TextServicesManagerService extends ITextServicesManager.Stub {
            mBindGroup.onSessionCreated(newSession, mRequest);
            mBindGroup.onSessionCreated(newSession, mRequest);
        }
        }
    }
    }

    private static final class TextServicesSettings {
        private final ContentResolver mResolver;

        public TextServicesSettings(ContentResolver resolver) {
            mResolver = resolver;
        }

        private void putString(final String key, final String str, @UserIdInt int userId) {
            Settings.Secure.putStringForUser(mResolver, key, str, userId);
        }

        @Nullable
        private String getString(@NonNull final String key, @Nullable final String defaultValue,
                @UserIdInt int userId) {
            final String result;
            result = Settings.Secure.getStringForUser(mResolver, key, userId);
            return result != null ? result : defaultValue;
        }

        private void putInt(final String key, final int value, @UserIdInt int userId) {
            Settings.Secure.putIntForUser(mResolver, key, value, userId);
        }

        private int getInt(final String key, final int defaultValue, @UserIdInt int userId) {
            return Settings.Secure.getIntForUser(mResolver, key, defaultValue, userId);
        }

        private boolean getBoolean(final String key, final boolean defaultValue,
                @UserIdInt int userId) {
            return getInt(key, defaultValue ? 1 : 0, userId) == 1;
        }

        public void putSelectedSpellChecker(@Nullable String sciId, @UserIdInt int userId) {
            if (TextUtils.isEmpty(sciId)) {
                // OK to coalesce to null, since getSelectedSpellChecker() can take care of the
                // empty data scenario.
                putString(Settings.Secure.SELECTED_SPELL_CHECKER, null, userId);
            } else {
                putString(Settings.Secure.SELECTED_SPELL_CHECKER, sciId, userId);
            }
        }

        public void putSelectedSpellCheckerSubtype(int hashCode, @UserIdInt int userId) {
            putInt(Settings.Secure.SELECTED_SPELL_CHECKER_SUBTYPE, hashCode, userId);
        }

        @NonNull
        public String getSelectedSpellChecker(@UserIdInt int userId) {
            return getString(Settings.Secure.SELECTED_SPELL_CHECKER, "", userId);
        }

        public int getSelectedSpellCheckerSubtype(final int defaultValue, @UserIdInt int userId) {
            return getInt(Settings.Secure.SELECTED_SPELL_CHECKER_SUBTYPE, defaultValue, userId);
        }

        public boolean isSpellCheckerEnabled(@UserIdInt int userId) {
            return getBoolean(Settings.Secure.SPELL_CHECKER_ENABLED, true, userId);
        }
    }
}
}