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

Commit cf88ea1e authored by Brandon Dayauon's avatar Brandon Dayauon
Browse files

Support two line text in AllApps/OnDeviceSearch w/ feature flag

Made separate feature flag for on device search
Add unit test to test twoLine string
- Unit tests for testing newStringThatShouldSupportTwoLineText() in BubbleTextView.java. This class tests a couple of strings
and uses the getLineCount() to determine if the test passes. Verifying with getLineCount() is sufficient since BubbleTextView can only be in one line or two lines,
and this is enough to ensure whether the string should be specifically wrapped onto the second line and to ensure truncation.

bug: 201388851
test: presubmit, ran locally on big and small device, before: https://screenshot.googleplex.com/3Q6pwveFDZqxDXL (ORIGINAL TWO LINE TEXT)
after:  https://screenshot.googleplex.com/7pkwUto6HGzMYoT

Change-Id: I93e6ed179e1081d5cdffc6db9c7ae34de8021c24
parent c523de6d
Loading
Loading
Loading
Loading
+0 −1
Original line number Diff line number Diff line
@@ -19,7 +19,6 @@
    android:id="@+id/icon"
    android:singleLine="false"
    android:lines="2"
    android:inputType="textMultiLine"
    launcher:iconDisplay="all_apps"
    launcher:centerVertically="true" />
+118 −3
Original line number Diff line number Diff line
@@ -52,8 +52,10 @@ import android.widget.TextView;

import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.annotation.VisibleForTesting;

import com.android.launcher3.accessibility.BaseAccessibilityDelegate;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.dot.DotInfo;
import com.android.launcher3.dragndrop.DragOptions.PreDragCondition;
import com.android.launcher3.dragndrop.DraggableView;
@@ -71,6 +73,8 @@ import com.android.launcher3.model.data.ItemInfoWithIcon;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.popup.PopupContainerWithArrow;
import com.android.launcher3.util.MultiTranslateDelegate;
import com.android.launcher3.search.StringMatcherUtility;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.SafeCloseable;
import com.android.launcher3.util.ShortcutUtil;
import com.android.launcher3.views.ActivityContext;
@@ -97,11 +101,19 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,

    private static final float MIN_LETTER_SPACING = -0.05f;
    private static final int MAX_SEARCH_LOOP_COUNT = 20;
    private static final Character NEW_LINE = '\n';
    private static final String EMPTY = "";
    private static final StringMatcherUtility.StringMatcher MATCHER =
            StringMatcherUtility.StringMatcher.getInstance();

    private static final int[] STATE_PRESSED = new int[]{android.R.attr.state_pressed};

    private float mScaleForReorderBounce = 1f;

    private IntArray mBreakPointsIntArray;
    private CharSequence mLastOriginalText;
    private CharSequence mLastModifiedText;

    private static final Property<BubbleTextView, Float> DOT_SCALE_PROPERTY
            = new Property<BubbleTextView, Float>(Float.TYPE, "dotScale") {
        @Override
@@ -134,7 +146,7 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
    private FastBitmapDrawable mIcon;
    private boolean mCenterVertically;

    protected final int mDisplay;
    protected int mDisplay;

    private final CheckLongPressHelper mLongPressHelper;

@@ -255,6 +267,8 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
        mDotParams.scale = 0f;
        mForceHideDot = false;
        setBackground(null);
        setSingleLine(true);
        setMaxLines(1);

        setTag(null);
        if (mIconLoadRequest != null) {
@@ -382,8 +396,15 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
    }

    @UiThread
    private void applyLabel(ItemInfoWithIcon info) {
        setText(info.title);
    @VisibleForTesting
    public void applyLabel(ItemInfoWithIcon info) {
        CharSequence label = info.title;
        if (label != null) {
            mLastOriginalText = label;
            mLastModifiedText = mLastOriginalText;
            mBreakPointsIntArray = StringMatcherUtility.getListOfBreakpoints(label, MATCHER);
            setText(label);
        }
        if (info.contentDescription != null) {
            setContentDescription(info.isDisabled()
                    ? getContext().getString(R.string.disabled_app_label, info.contentDescription)
@@ -391,6 +412,12 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
        }
    }

    /** This is used for testing to forcefully set the display to ALL_APPS */
    @VisibleForTesting
    public void setDisplayAllApps() {
        mDisplay = DISPLAY_ALL_APPS;
    }

    /**
     * Overrides the default long press timeout.
     */
@@ -637,6 +664,27 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
            setPadding(getPaddingLeft(), (height - cellHeightPx) / 2, getPaddingRight(),
                    getPaddingBottom());
        }
        // only apply two line for all_apps
        if (FeatureFlags.ENABLE_TWOLINE_ALLAPPS.get() && (mLastOriginalText != null)
                && (mDisplay == DISPLAY_ALL_APPS)) {
            CharSequence modifiedString = modifyTitleToSupportMultiLine(
                    MeasureSpec.getSize(widthMeasureSpec) - getCompoundPaddingLeft()
                            - getCompoundPaddingRight(),
                    mLastOriginalText,
                    getPaint(), mBreakPointsIntArray);
            if (!TextUtils.equals(modifiedString, mLastModifiedText)) {
                mLastModifiedText = modifiedString;
                setText(modifiedString);
                // if text contains NEW_LINE, set max lines to 2
                if (TextUtils.indexOf(modifiedString, NEW_LINE) != -1) {
                    setSingleLine(false);
                    setMaxLines(2);
                } else {
                    setSingleLine(true);
                    setMaxLines(1);
                }
            }
        }
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

@@ -697,6 +745,73 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
        return ObjectAnimator.ofFloat(this, TEXT_ALPHA_PROPERTY, toAlpha);
    }

    /**
     * Generate a new string that will support two line text depending on the current string.
     * This method calculates the limited width of a text view and creates a string to fit as
     * many words as it can until the limit is reached. Once the limit is reached, we decide to
     * either return the original title or continue on a new line. How to get the new string is by
     * iterating through the list of break points and determining if the strings between the break
     * points can fit within the line it is in.
     *  Example assuming each character takes up one spot:
     *  title = "Battery Stats", breakpoint = [6], stringPtr = 0, limitedWidth = 7
     *  We get the current word -> from sublist(0, breakpoint[i]+1) so sublist (0,7) -> Battery,
     *  now stringPtr = 7 then from sublist(7) the current string is " Stats" and the runningWidth
     *  at this point exceeds limitedWidth and so we put " Stats" onto the next line (after checking
     *  if the first char is a SPACE, we trim to append "Stats". So resulting string would be
     *  "Battery\nStats"
     */
    public static CharSequence modifyTitleToSupportMultiLine(int limitedWidth, CharSequence title,
            TextPaint paint, IntArray breakPoints) {
        // current title is less than the width allowed so we can just skip
        if (title == null || paint.measureText(title, 0, title.length()) <= limitedWidth) {
            return title;
        }
        float currentWordWidth, runningWidth = 0;
        CharSequence currentWord;
        StringBuilder newString = new StringBuilder();
        int stringPtr = 0;
        for (int i = 0; i < breakPoints.size()+1; i++) {
            if (i < breakPoints.size()) {
                currentWord = title.subSequence(stringPtr, breakPoints.get(i)+1);
            } else {
                // last word from recent breakpoint until the end of the string
                currentWord = title.subSequence(stringPtr, title.length());
            }
            currentWordWidth = paint.measureText(currentWord,0, currentWord.length());
            runningWidth += currentWordWidth;
            if (runningWidth <= limitedWidth) {
                newString.append(currentWord);
            } else {
                // there is no more space
                if (i == 0) {
                    // if the first words exceeds width, just return as the first line will ellipse
                    return title;
                } else {
                    // If putting word onto a new line, make sure there is no space or new line
                    // character in the beginning of the current word and just put in the rest of
                    // the characters.
                    CharSequence lastCharacters = title.subSequence(stringPtr, title.length());
                    int beginningLetterType =
                            Character.getType(Character.codePointAt(lastCharacters,0));
                    if (beginningLetterType == Character.SPACE_SEPARATOR
                            || beginningLetterType == Character.LINE_SEPARATOR) {
                        lastCharacters = lastCharacters.length() > 1
                                ? lastCharacters.subSequence(1, lastCharacters.length())
                                : EMPTY;
                    }
                    newString.append(NEW_LINE).append(lastCharacters);
                    return newString.toString();
                }
            }
            if (i >= breakPoints.size()) {
                // no need to look forward into the string if we've already finished processing
                break;
            }
            stringPtr = breakPoints.get(i)+1;
        }
        return newString.toString();
    }

    @Override
    public void cancelLongPress() {
        super.cancelLongPress();
+5 −3
Original line number Diff line number Diff line
@@ -33,6 +33,7 @@ import androidx.recyclerview.widget.RecyclerView;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.R;
import com.android.launcher3.allapps.search.SearchAdapterProvider;
import com.android.launcher3.Utilities;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.views.ActivityContext;
@@ -140,7 +141,7 @@ public abstract class BaseAllAppsAdapter<T extends Context & ActivityContext> ex
    protected final OnClickListener mOnIconClickListener;
    protected OnLongClickListener mOnIconLongClickListener = INSTANCE_ALL_APPS;
    protected OnFocusChangeListener mIconFocusListener;
    private final int mExtraHeight;
    private final int mExtraTextHeight;

    public BaseAllAppsAdapter(T activityContext, LayoutInflater inflater,
            AlphabeticalAppsList<T> apps, SearchAdapterProvider<?> adapterProvider) {
@@ -152,7 +153,8 @@ public abstract class BaseAllAppsAdapter<T extends Context & ActivityContext> ex
        mOnIconClickListener = mActivityContext.getItemOnClickListener();

        mAdapterProvider = adapterProvider;
        mExtraHeight = res.getDimensionPixelSize(R.dimen.all_apps_height_extra);
        mExtraTextHeight = Utilities.calculateTextHeight(
                mActivityContext.getDeviceProfile().allAppsIconTextSizePx);
    }

    /**
@@ -197,7 +199,7 @@ public abstract class BaseAllAppsAdapter<T extends Context & ActivityContext> ex
                icon.getLayoutParams().height =
                        mActivityContext.getDeviceProfile().allAppsCellHeightPx;
                if (FeatureFlags.ENABLE_TWOLINE_ALLAPPS.get()) {
                    icon.getLayoutParams().height += mExtraHeight;
                    icon.getLayoutParams().height += mExtraTextHeight;
                }
                return new ViewHolder(icon);
            case VIEW_TYPE_EMPTY_SEARCH:
+4 −0
Original line number Diff line number Diff line
@@ -113,6 +113,10 @@ public final class FeatureFlags {
    public static final BooleanFlag ENABLE_TWOLINE_ALLAPPS = getDebugFlag(270390937,
            "ENABLE_TWOLINE_ALLAPPS", false, "Enables two line label inside all apps.");

    public static final BooleanFlag ENABLE_TWOLINE_DEVICESEARCH = getDebugFlag(201388851,
            "ENABLE_TWOLINE_DEVICESEARCH", false,
            "Enable two line label for icons with labels on device search.");

    public static final BooleanFlag ENABLE_DEVICE_SEARCH_PERFORMANCE_LOGGING = getReleaseFlag(
            270391397, "ENABLE_DEVICE_SEARCH_PERFORMANCE_LOGGING", false,
            "Allows on device search in all apps logging");
+47 −1
Original line number Diff line number Diff line
@@ -16,13 +16,20 @@

package com.android.launcher3.search;

import android.text.TextUtils;

import com.android.launcher3.util.IntArray;

import java.text.Collator;
import java.util.stream.IntStream;

/**
 * Utilities for matching query string to target string.
 */
public class StringMatcherUtility {

    private static final Character SPACE = ' ';

    /**
     * Returns {@code true} if {@code query} is a prefix of a substring in {@code target}. How to
     * break target to valid substring is defined in the given {@code matcher}.
@@ -58,6 +65,41 @@ public class StringMatcherUtility {
        return false;
    }

    /**
     * Returns a list of breakpoints wherever the string contains a break. For example:
     * "t-mobile" would have breakpoints at [0, 1]
     * "Agar.io" would have breakpoints at [3, 4]
     * "LEGO®Builder" would have a breakpoint at [4]
     */
    public static IntArray getListOfBreakpoints(CharSequence input, StringMatcher matcher) {
        int inputLength = input.length();
        if ((inputLength <= 2) || TextUtils.indexOf(input, SPACE) != -1) {
            // when there is a space in the string, return a list where the elements are the
            // position of the spaces - 1. This is to make the logic consistent where breakpoints
            // are placed
            return IntArray.wrap(IntStream.range(0, inputLength)
                    .filter(i -> input.charAt(i) == SPACE)
                    .map(i -> i - 1)
                    .toArray());
        }
        IntArray listOfBreakPoints = new IntArray();
        int prevType;
        int thisType = Character.getType(Character.codePointAt(input, 0));
        int nextType = Character.getType(Character.codePointAt(input, 1));
        for (int i = 1; i < inputLength; i++) {
            prevType = thisType;
            thisType = nextType;
            nextType = i < (inputLength - 1)
                    ? Character.getType(Character.codePointAt(input, i + 1))
                    : Character.UNASSIGNED;
            if (matcher.isBreak(thisType, prevType, nextType)) {
                // breakpoint is at previous
                listOfBreakPoints.add(i-1);
            }
        }
        return listOfBreakPoints;
    }

    /**
     * Performs locale sensitive string comparison using {@link Collator}.
     */
@@ -118,7 +160,11 @@ public class StringMatcherUtility {
            }
            switch (thisType) {
                case Character.UPPERCASE_LETTER:
                    if (nextType == Character.UPPERCASE_LETTER) {
                    // takes care of the case where there are consistent uppercase letters as well
                    // as a special symbol following the capitalize letters for example: LEGO®
                    if (nextType != Character.UPPERCASE_LETTER && nextType != Character.OTHER_SYMBOL
                            && nextType != Character.DECIMAL_DIGIT_NUMBER
                            && nextType != Character.UNASSIGNED) {
                        return true;
                    }
                    // Follow through
Loading