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

Commit c659053c authored by Yohei Yukawa's avatar Yohei Yukawa Committed by Android (Google) Code Review
Browse files

Merge "Implement dynamic IME rotation based on user action"

parents bdbb0f0e a9bda774
Loading
Loading
Loading
Loading
+92 −4
Original line number Diff line number Diff line
@@ -271,13 +271,87 @@ public class InputMethodSubtypeSwitchingController {
        }
    }

    private static class DynamicRotationList {
        private static final String TAG = DynamicRotationList.class.getSimpleName();
        private final List<ImeSubtypeListItem> mImeSubtypeList;
        private final int[] mUsageHistoryOfSubtypeListItemIndex;

        public DynamicRotationList(final List<ImeSubtypeListItem> imeSubtypeListItems) {
            mImeSubtypeList = imeSubtypeListItems;
            mUsageHistoryOfSubtypeListItemIndex = new int[mImeSubtypeList.size()];
            final int N = mImeSubtypeList.size();
            for (int i = 0; i < N; i++) {
                mUsageHistoryOfSubtypeListItemIndex[i] = i;
            }
        }

        /**
         * Returns the index of the specified object in
         * {@link #mUsageHistoryOfSubtypeListItemIndex}.
         * <p>We call the index of {@link #mUsageHistoryOfSubtypeListItemIndex} as "Usage Rank"
         * so as not to be confused with the index in {@link #mImeSubtypeList}.
         * @return -1 when the specified item doesn't belong to {@link #mImeSubtypeList} actually.
         */
        private int getUsageRank(final InputMethodInfo imi, InputMethodSubtype subtype) {
            final int currentSubtypeId = calculateSubtypeId(imi, subtype);
            final int N = mUsageHistoryOfSubtypeListItemIndex.length;
            for (int usageRank = 0; usageRank < N; usageRank++) {
                final int subtypeListItemIndex = mUsageHistoryOfSubtypeListItemIndex[usageRank];
                final ImeSubtypeListItem subtypeListItem =
                        mImeSubtypeList.get(subtypeListItemIndex);
                if (subtypeListItem.mImi.equals(imi) &&
                        subtypeListItem.mSubtypeId == currentSubtypeId) {
                    return usageRank;
                }
            }
            // Not found in the known IME/Subtype list.
            return -1;
        }

        public void onUserAction(InputMethodInfo imi, InputMethodSubtype subtype) {
            final int currentUsageRank = getUsageRank(imi, subtype);
            // Do nothing if currentUsageRank == -1 (not found), or currentUsageRank == 0
            if (currentUsageRank <= 0) {
                return;
            }
            final int currentItemIndex = mUsageHistoryOfSubtypeListItemIndex[currentUsageRank];
            System.arraycopy(mUsageHistoryOfSubtypeListItemIndex, 0,
                    mUsageHistoryOfSubtypeListItemIndex, 1, currentUsageRank);
            mUsageHistoryOfSubtypeListItemIndex[0] = currentItemIndex;
        }

        public ImeSubtypeListItem getNextInputMethodLocked(boolean onlyCurrentIme,
                InputMethodInfo imi, InputMethodSubtype subtype) {
            int currentUsageRank = getUsageRank(imi, subtype);
            if (currentUsageRank < 0) {
                if (DEBUG) {
                    Slog.d(TAG, "IME/subtype is not found: " + imi.getId() + ", " + subtype);
                }
                return null;
            }
            final int N = mUsageHistoryOfSubtypeListItemIndex.length;
            for (int i = 1; i < N; i++) {
                final int subtypeListItemRank = (currentUsageRank + i) % N;
                final int subtypeListItemIndex =
                        mUsageHistoryOfSubtypeListItemIndex[subtypeListItemRank];
                final ImeSubtypeListItem subtypeListItem =
                        mImeSubtypeList.get(subtypeListItemIndex);
                if (onlyCurrentIme && !imi.equals(subtypeListItem.mImi)) {
                    continue;
                }
                return subtypeListItem;
            }
            return null;
        }
    }

    @VisibleForTesting
    public static class ControllerImpl {
        private final StaticRotationList mSwitchingAwareSubtypeList;
        private final DynamicRotationList mSwitchingAwareSubtypeList;
        private final StaticRotationList mSwitchingUnawareSubtypeList;

        public ControllerImpl(final List<ImeSubtypeListItem> sortedItems) {
            mSwitchingAwareSubtypeList = new StaticRotationList(filterImeSubtypeList(sortedItems,
            mSwitchingAwareSubtypeList = new DynamicRotationList(filterImeSubtypeList(sortedItems,
                    true /* supportsSwitchingToNextInputMethod */));
            mSwitchingUnawareSubtypeList = new StaticRotationList(filterImeSubtypeList(sortedItems,
                    false /* supportsSwitchingToNextInputMethod */));
@@ -297,6 +371,15 @@ public class InputMethodSubtypeSwitchingController {
            }
        }

        public void onUserActionLocked(InputMethodInfo imi, InputMethodSubtype subtype) {
            if (imi == null) {
                return;
            }
            if (imi.supportsSwitchingToNextInputMethod()) {
                mSwitchingAwareSubtypeList.onUserAction(imi, subtype);
            }
        }

        private static List<ImeSubtypeListItem> filterImeSubtypeList(
                final List<ImeSubtypeListItem> items,
                final boolean supportsSwitchingToNextInputMethod) {
@@ -327,9 +410,14 @@ public class InputMethodSubtypeSwitchingController {
        return new InputMethodSubtypeSwitchingController(settings, context);
    }

    // TODO: write unit tests for this method and the logic that determines the next subtype
    public void onCommitTextLocked(InputMethodInfo imi, InputMethodSubtype subtype) {
        // TODO: Implement this.
        if (mController == null) {
            if (DEBUG) {
                Log.e(TAG, "mController shouldn't be null.");
            }
            return;
        }
        mController.onUserActionLocked(imi, subtype);
    }

    public void resetCircularListLocked(Context context) {
+90 −21
Original line number Diff line number Diff line
@@ -132,6 +132,29 @@ public class InputMethodSubtypeSwitchingControllerTest extends InstrumentationTe
        assertEquals(nextItem, nextIme);
    }

    private void assertRotationOrder(final ControllerImpl controller,
            final boolean onlyCurrentIme,
            final ImeSubtypeListItem... expectedRotationOrderOfImeSubtypeList) {
        final int N = expectedRotationOrderOfImeSubtypeList.length;
        for (int i = 0; i < N; i++) {
            final int currentIndex = i;
            final int nextIndex = (currentIndex + 1) % N;
            final ImeSubtypeListItem currentItem =
                    expectedRotationOrderOfImeSubtypeList[currentIndex];
            final ImeSubtypeListItem nextItem = expectedRotationOrderOfImeSubtypeList[nextIndex];
            assertNextInputMethod(controller, onlyCurrentIme, currentItem, nextItem);
        }
    }

    private void onUserAction(final ControllerImpl controller,
            final ImeSubtypeListItem subtypeListItem) {
        InputMethodSubtype subtype = null;
        if (subtypeListItem.mSubtypeName != null) {
            subtype = createDummySubtype(subtypeListItem.mSubtypeName.toString());
        }
        controller.onUserActionLocked(subtypeListItem.mImi, subtype);
    }

    @SmallTest
    public void testControllerImpl() throws Exception {
        final List<ImeSubtypeListItem> disabledItems = createDisabledImeSubtypes();
@@ -152,32 +175,19 @@ public class InputMethodSubtypeSwitchingControllerTest extends InstrumentationTe
        final ControllerImpl controller = new ControllerImpl(enabledItems);

        // switching-aware loop
        assertNextInputMethod(controller, false /* onlyCurrentIme */,
                latinIme_en_US, latinIme_fr);
        assertNextInputMethod(controller, false /* onlyCurrentIme */,
                latinIme_fr, japaneseIme_ja_JP);
        assertNextInputMethod(controller, false /* onlyCurrentIme */,
                japaneseIme_ja_JP, latinIme_en_US);
        assertRotationOrder(controller, false /* onlyCurrentIme */,
                latinIme_en_US, latinIme_fr, japaneseIme_ja_JP);

        // switching-unaware loop
        assertNextInputMethod(controller, false /* onlyCurrentIme */,
                switchingUnawarelatinIme_en_UK, switchingUnawarelatinIme_hi);
        assertNextInputMethod(controller, false /* onlyCurrentIme */,
                switchingUnawarelatinIme_hi, subtypeUnawareIme);
        assertNextInputMethod(controller, false /* onlyCurrentIme */,
                subtypeUnawareIme, switchUnawareJapaneseIme_ja_JP);
        assertNextInputMethod(controller, false /* onlyCurrentIme */,
                switchUnawareJapaneseIme_ja_JP, switchingUnawarelatinIme_en_UK);
        assertRotationOrder(controller, false /* onlyCurrentIme */,
                switchingUnawarelatinIme_en_UK, switchingUnawarelatinIme_hi, subtypeUnawareIme,
                switchUnawareJapaneseIme_ja_JP);

        // test onlyCurrentIme == true
        assertNextInputMethod(controller, true /* onlyCurrentIme */,
        assertRotationOrder(controller, true /* onlyCurrentIme */,
                latinIme_en_US, latinIme_fr);
        assertNextInputMethod(controller, true /* onlyCurrentIme */,
                latinIme_fr, latinIme_en_US);
        assertNextInputMethod(controller, true /* onlyCurrentIme */,
        assertRotationOrder(controller, true /* onlyCurrentIme */,
                switchingUnawarelatinIme_en_UK, switchingUnawarelatinIme_hi);
        assertNextInputMethod(controller, true /* onlyCurrentIme */,
                switchingUnawarelatinIme_hi, switchingUnawarelatinIme_en_UK);
        assertNextInputMethod(controller, true /* onlyCurrentIme */,
                subtypeUnawareIme, null);
        assertNextInputMethod(controller, true /* onlyCurrentIme */,
@@ -203,4 +213,63 @@ public class InputMethodSubtypeSwitchingControllerTest extends InstrumentationTe
        assertNextInputMethod(controller, true /* onlyCurrentIme */,
                disabledSubtypeUnawareIme, null);
    }

    @SmallTest
    public void testControllerImplWithUserAction() throws Exception {
        final List<ImeSubtypeListItem> enabledItems = createEnabledImeSubtypes();
        final ImeSubtypeListItem latinIme_en_US = enabledItems.get(0);
        final ImeSubtypeListItem latinIme_fr = enabledItems.get(1);
        final ImeSubtypeListItem switchingUnawarelatinIme_en_UK = enabledItems.get(2);
        final ImeSubtypeListItem switchingUnawarelatinIme_hi = enabledItems.get(3);
        final ImeSubtypeListItem subtypeUnawareIme = enabledItems.get(4);
        final ImeSubtypeListItem japaneseIme_ja_JP = enabledItems.get(5);
        final ImeSubtypeListItem switchUnawareJapaneseIme_ja_JP = enabledItems.get(6);

        final ControllerImpl controller = new ControllerImpl(enabledItems);

        // === switching-aware loop ===
        assertRotationOrder(controller, false /* onlyCurrentIme */,
                latinIme_en_US, latinIme_fr, japaneseIme_ja_JP);
        // Then notify that a user did something for latinIme_fr.
        onUserAction(controller, latinIme_fr);
        assertRotationOrder(controller, false /* onlyCurrentIme */,
                latinIme_fr, latinIme_en_US, japaneseIme_ja_JP);
        // Then notify that a user did something for latinIme_fr again.
        onUserAction(controller, latinIme_fr);
        assertRotationOrder(controller, false /* onlyCurrentIme */,
                latinIme_fr, latinIme_en_US, japaneseIme_ja_JP);
        // Then notify that a user did something for japaneseIme_ja_JP.
        onUserAction(controller, latinIme_fr);
        assertRotationOrder(controller, false /* onlyCurrentIme */,
                japaneseIme_ja_JP, latinIme_fr, latinIme_en_US);
        // Check onlyCurrentIme == true.
        assertNextInputMethod(controller, true /* onlyCurrentIme */,
                japaneseIme_ja_JP, null);
        assertRotationOrder(controller, true /* onlyCurrentIme */,
                latinIme_fr, latinIme_en_US);
        assertRotationOrder(controller, true /* onlyCurrentIme */,
                latinIme_en_US, latinIme_fr);

        // === switching-unaware loop ===
        assertRotationOrder(controller, false /* onlyCurrentIme */,
                switchingUnawarelatinIme_en_UK, switchingUnawarelatinIme_hi, subtypeUnawareIme,
                switchUnawareJapaneseIme_ja_JP);
        // User action should be ignored for switching unaware IMEs.
        onUserAction(controller, switchingUnawarelatinIme_hi);
        assertRotationOrder(controller, false /* onlyCurrentIme */,
                switchingUnawarelatinIme_en_UK, switchingUnawarelatinIme_hi, subtypeUnawareIme,
                switchUnawareJapaneseIme_ja_JP);
        // User action should be ignored for switching unaware IMEs.
        onUserAction(controller, switchUnawareJapaneseIme_ja_JP);
        assertRotationOrder(controller, false /* onlyCurrentIme */,
                switchingUnawarelatinIme_en_UK, switchingUnawarelatinIme_hi, subtypeUnawareIme,
                switchUnawareJapaneseIme_ja_JP);
        // Check onlyCurrentIme == true.
        assertRotationOrder(controller, true /* onlyCurrentIme */,
                switchingUnawarelatinIme_en_UK, switchingUnawarelatinIme_hi);
        assertNextInputMethod(controller, true /* onlyCurrentIme */,
                subtypeUnawareIme, null);
        assertNextInputMethod(controller, true /* onlyCurrentIme */,
                switchUnawareJapaneseIme_ja_JP, null);
    }
}