diff --git a/res/layout/all_apps_icon_twoline.xml b/res/layout/all_apps_icon_twoline.xml index 54c714734c..b0d02c15de 100644 --- a/res/layout/all_apps_icon_twoline.xml +++ b/res/layout/all_apps_icon_twoline.xml @@ -19,7 +19,6 @@ android:id="@+id/icon" android:singleLine="false" android:lines="2" - android:inputType="textMultiLine" launcher:iconDisplay="all_apps" launcher:centerVertically="true" /> diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index 3eb03edf13..352163e921 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -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 DOT_SCALE_PROPERTY = new Property(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(); diff --git a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java index 7040de5075..8fa42765b0 100644 --- a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java +++ b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java @@ -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 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 apps, SearchAdapterProvider adapterProvider) { @@ -152,7 +153,8 @@ public abstract class BaseAllAppsAdapter 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 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: diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java index 6bb2a0f0a8..d1aaef1aca 100644 --- a/src/com/android/launcher3/config/FeatureFlags.java +++ b/src/com/android/launcher3/config/FeatureFlags.java @@ -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"); diff --git a/src/com/android/launcher3/search/StringMatcherUtility.java b/src/com/android/launcher3/search/StringMatcherUtility.java index c66f3a19e4..28fc4f002b 100644 --- a/src/com/android/launcher3/search/StringMatcherUtility.java +++ b/src/com/android/launcher3/search/StringMatcherUtility.java @@ -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 diff --git a/tests/src/com/android/launcher3/search/StringMatcherUtilityTest.java b/tests/src/com/android/launcher3/search/StringMatcherUtilityTest.java index 3b53255550..0ba46f4f55 100644 --- a/tests/src/com/android/launcher3/search/StringMatcherUtilityTest.java +++ b/tests/src/com/android/launcher3/search/StringMatcherUtilityTest.java @@ -15,8 +15,10 @@ */ package com.android.launcher3.search; +import static com.android.launcher3.search.StringMatcherUtility.getListOfBreakpoints; import static com.android.launcher3.search.StringMatcherUtility.matches; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -25,6 +27,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.launcher3.search.StringMatcherUtility.StringMatcher; import com.android.launcher3.search.StringMatcherUtility.StringMatcherSpace; +import com.android.launcher3.util.IntArray; import org.junit.Test; import org.junit.runner.RunWith; @@ -138,4 +141,96 @@ public class StringMatcherUtilityTest { assertFalse(matches("phant", "elephant", MATCHER_SPACE)); assertFalse(matches("elephants", "elephant", MATCHER_SPACE)); } + + @Test + public void testStringWithProperBreaks() { + // empty string + assertEquals(IntArray.wrap(), getListOfBreakpoints("", MATCHER)); + + // should be "D Dz" that's why breakpoint is at 0 + assertEquals(IntArray.wrap(0), getListOfBreakpoints("DDz", MATCHER)); + + // test all caps and all lower-case + assertEquals(IntArray.wrap(), getListOfBreakpoints("SNKRS", MATCHER)); + assertEquals(IntArray.wrap(), getListOfBreakpoints("flutterappflorafy", MATCHER)); + assertEquals(IntArray.wrap(), getListOfBreakpoints("LEGO®", MATCHER)); + + // test camel case + // breakpoint at 9 to be "flutterapp Florafy" + assertEquals(IntArray.wrap(9), getListOfBreakpoints("flutterappFlorafy", MATCHER)); + // breakpoint at 4 to be "Metro Zone" + assertEquals(IntArray.wrap(4), getListOfBreakpoints("MetroZone", MATCHER)); + // breakpoint at 4,5 to be "metro X Zone" + assertEquals(IntArray.wrap(4,5), getListOfBreakpoints("metroXZone", MATCHER)); + // breakpoint at 0 to be "G Pay" + assertEquals(IntArray.wrap(0), getListOfBreakpoints("GPay", MATCHER)); + // breakpoint at 4 to be "Whats App" + assertEquals(IntArray.wrap(4), getListOfBreakpoints("WhatsApp", MATCHER)); + // breakpoint at 2 to be "aaa A" + assertEquals(IntArray.wrap(2), getListOfBreakpoints("aaaA", MATCHER)); + // breakpoint at 4,12,16 to be "Other Launcher Test App" + assertEquals(IntArray.wrap(4,12,16), + getListOfBreakpoints("OtherLauncherTestApp", MATCHER)); + + // test with TITLECASE_LETTER + // should be "DDz" that's why there are no break points + assertEquals(IntArray.wrap(), getListOfBreakpoints("DDz", MATCHER)); + // breakpoint at 0 to be "D DDž" + assertEquals(IntArray.wrap(0), getListOfBreakpoints("DDDž", MATCHER)); + // breakpoint at 0 because there is a space to be "Dž DD" + assertEquals(IntArray.wrap(0), getListOfBreakpoints("Dž DD", MATCHER)); + // breakpoint at 1 to be "Dw Dz" + assertEquals(IntArray.wrap(1), getListOfBreakpoints("DwDz", MATCHER)); + // breakpoint at 0,2 to be "Dw Dz" + assertEquals(IntArray.wrap(0,2), getListOfBreakpoints("wDwDz", MATCHER)); + // breakpoint at 1,3 to be "ᾋw Dw Dz" + assertEquals(IntArray.wrap(1,3), getListOfBreakpoints("ᾋwDwDz", MATCHER)); + // breakpoint at 0,2,4 to be "ᾋ ᾋw Dw Dz" + assertEquals(IntArray.wrap(0,2,4), getListOfBreakpoints("ᾋᾋwDwDz", MATCHER)); + // breakpoint at 0,2,4 to be "ᾋ ᾋw Dw Dz®" + assertEquals(IntArray.wrap(0,2,4), getListOfBreakpoints("ᾋᾋwDwDz®", MATCHER)); + + // test with numbers and symbols + // breakpoint at 3,11 to be "Test Activity 13" + assertEquals(IntArray.wrap(3,11), getListOfBreakpoints("TestActivity13", MATCHER)); + // breakpoint at 3, 4, 12, 13 as the breakpoints are at the dashes + assertEquals(IntArray.wrap(3,4,12,13), + getListOfBreakpoints("Test-Activity-12", MATCHER)); + // breakpoint at 1 to be "AA 2" + assertEquals(IntArray.wrap(1), getListOfBreakpoints("AA2", MATCHER)); + // breakpoint at 1 to be "AAA 2" + assertEquals(IntArray.wrap(2), getListOfBreakpoints("AAA2", MATCHER)); + // breakpoint at 1 to be "ab 2" + assertEquals(IntArray.wrap(1), getListOfBreakpoints("ab2", MATCHER)); + // breakpoint at 1,2 to be "el 3 suhwee" + assertEquals(IntArray.wrap(1,2), getListOfBreakpoints("el3suhwee", MATCHER)); + // breakpoint at 0,1 as the breakpoints are at '-' + assertEquals(IntArray.wrap(0,1), getListOfBreakpoints("t-mobile", MATCHER)); + assertEquals(IntArray.wrap(0,1), getListOfBreakpoints("t-Mobile", MATCHER)); + // breakpoint at 0,1,2 as the breakpoints are at '-' + assertEquals(IntArray.wrap(0,1,2), getListOfBreakpoints("t--Mobile", MATCHER)); + // breakpoint at 1,2,3 as the breakpoints are at '-' + assertEquals(IntArray.wrap(1,2,3), getListOfBreakpoints("tr--Mobile", MATCHER)); + // breakpoint at 3,4 as the breakpoints are at '.' + assertEquals(IntArray.wrap(3,4), getListOfBreakpoints("Agar.io", MATCHER)); + assertEquals(IntArray.wrap(3,4), getListOfBreakpoints("Hole.Io", MATCHER)); + + // breakpoint at 0 to be "µ Torrent®" + assertEquals(IntArray.wrap(0), getListOfBreakpoints("µTorrent®", MATCHER)); + // breakpoint at 4 to be "LEGO® Builder" + assertEquals(IntArray.wrap(4), getListOfBreakpoints("LEGO®Builder", MATCHER)); + // breakpoint at 4 to be "LEGO® builder" + assertEquals(IntArray.wrap(4), getListOfBreakpoints("LEGO®builder", MATCHER)); + // breakpoint at 4 to be "lego® builder" + assertEquals(IntArray.wrap(4), getListOfBreakpoints("lego®builder", MATCHER)); + + // test string with spaces - where the breakpoints are right before where the spaces are at + assertEquals(IntArray.wrap(3,8), getListOfBreakpoints("HEAD BALL 2", MATCHER)); + assertEquals(IntArray.wrap(2,8), + getListOfBreakpoints("OFL Agent Application", MATCHER)); + assertEquals(IntArray.wrap(0,2), getListOfBreakpoints("D D z", MATCHER)); + assertEquals(IntArray.wrap(6), getListOfBreakpoints("Battery Stats", MATCHER)); + assertEquals(IntArray.wrap(5,9,15), + getListOfBreakpoints("System UWB Field Test", MATCHER)); + } } diff --git a/tests/src/com/android/launcher3/ui/BubbleTextViewTest.java b/tests/src/com/android/launcher3/ui/BubbleTextViewTest.java new file mode 100644 index 0000000000..528f7ac57d --- /dev/null +++ b/tests/src/com/android/launcher3/ui/BubbleTextViewTest.java @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2023 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.launcher3.ui; + +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; + +import static com.android.launcher3.config.FeatureFlags.ENABLE_TWOLINE_ALLAPPS; + +import static org.junit.Assert.assertEquals; + +import android.content.Context; +import android.graphics.Typeface; +import android.view.ViewGroup; + +import com.android.launcher3.BubbleTextView; +import com.android.launcher3.Utilities; +import com.android.launcher3.model.data.ItemInfoWithIcon; +import com.android.launcher3.search.StringMatcherUtility; +import com.android.launcher3.util.ActivityContextWrapper; +import com.android.launcher3.util.IntArray; +import com.android.launcher3.util.TestUtil; +import com.android.launcher3.views.BaseDragLayer; + +import org.junit.Before; +import org.junit.Test; + +/** + * Unit tests for testing modifyTitleToSupportMultiLine() in BubbleTextView.java + * This class tests a couple of strings and uses the getMaxLines() to determine if the test passes. + * Verifying with getMaxLines() 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. + */ +public class BubbleTextViewTest { + + private static final StringMatcherUtility.StringMatcher + MATCHER = StringMatcherUtility.StringMatcher.getInstance(); + private static final int ONE_LINE = 1; + private static final int TWO_LINE = 2; + private static final String TEST_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT = "Battery Stats"; + private static final String TEST_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT_RESULT = + "Battery\nStats"; + private static final String TEST_LONG_STRING_NO_SPACE_LONGER_THAN_CHAR_LIMIT = + "flutterappflorafy"; + private static final String TEST_LONG_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT = + "System UWB Field Test"; + private static final String TEST_LONG_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT_RESULT = + "System\nUWB Field Test"; + private static final String TEST_LONG_STRING_SYMBOL_LONGER_THAN_CHAR_LIMIT = + "LEGO®Builder"; + private static final String TEST_LONG_STRING_SYMBOL_LONGER_THAN_CHAR_LIMIT_RESULT = + "LEGO®\nBuilder"; + private static final String EMPTY_STRING = ""; + private static final int CHAR_CNT = 7; + + private BubbleTextView mBubbleTextView; + private ItemInfoWithIcon mItemInfoWithIcon; + private Context mContext; + private int mLimitedWidth; + + @Before + public void setUp() throws Exception { + Utilities.enableRunningInTestHarnessForTests(); + mContext = new ActivityContextWrapper(getApplicationContext()); + mBubbleTextView = new BubbleTextView(mContext); + mBubbleTextView.reset(); + mBubbleTextView.setDisplayAllApps(); + assertEquals(ONE_LINE, mBubbleTextView.getMaxLines()); + + BubbleTextView testView = new BubbleTextView(mContext); + testView.setTypeface(Typeface.MONOSPACE); + testView.setText("B"); + // calculate the maxWidth of the textView by calculating the width of one monospace + // character * CHAR_CNT + mLimitedWidth = + (int) (testView.getPaint().measureText(testView.getText().toString()) * CHAR_CNT); + // needed otherwise there is a NPE during setText() on checkForRelayout() + mBubbleTextView.setLayoutParams( + new ViewGroup.LayoutParams(mLimitedWidth, + BaseDragLayer.LayoutParams.WRAP_CONTENT)); + mItemInfoWithIcon = new ItemInfoWithIcon() { + @Override + public ItemInfoWithIcon clone() { + return null; + } + }; + } + + @Test + public void testEmptyString_flagOn() { + TestUtil.overrideFlag(ENABLE_TWOLINE_ALLAPPS, true); + mItemInfoWithIcon.title = EMPTY_STRING; + mBubbleTextView.applyLabel(mItemInfoWithIcon); + mBubbleTextView.setTypeface(Typeface.MONOSPACE); + mBubbleTextView.measure(mLimitedWidth, 0); + mBubbleTextView.onPreDraw(); + assertEquals(ONE_LINE, mBubbleTextView.getMaxLines()); + } + + @Test + public void testEmptyString_flagOff() { + TestUtil.overrideFlag(ENABLE_TWOLINE_ALLAPPS, false); + mItemInfoWithIcon.title = EMPTY_STRING; + mBubbleTextView.applyLabel(mItemInfoWithIcon); + mBubbleTextView.setTypeface(Typeface.MONOSPACE); + mBubbleTextView.measure(mLimitedWidth, 0); + mBubbleTextView.onPreDraw(); + assertEquals(ONE_LINE, mBubbleTextView.getLineCount()); + } + + @Test + public void testStringWithSpaceLongerThanCharLimit_flagOn() { + TestUtil.overrideFlag(ENABLE_TWOLINE_ALLAPPS, true); + // test string: "Battery Stats" + mItemInfoWithIcon.title = TEST_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT; + mBubbleTextView.applyLabel(mItemInfoWithIcon); + mBubbleTextView.setTypeface(Typeface.MONOSPACE); + mBubbleTextView.measure(mLimitedWidth, 0); + mBubbleTextView.onPreDraw(); + assertEquals(TWO_LINE, mBubbleTextView.getLineCount()); + } + + @Test + public void testStringWithSpaceLongerThanCharLimit_flagOff() { + TestUtil.overrideFlag(ENABLE_TWOLINE_ALLAPPS, false); + // test string: "Battery Stats" + mItemInfoWithIcon.title = TEST_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT; + mBubbleTextView.applyLabel(mItemInfoWithIcon); + mBubbleTextView.setTypeface(Typeface.MONOSPACE); + mBubbleTextView.measure(mLimitedWidth, 0); + mBubbleTextView.onPreDraw(); + assertEquals(ONE_LINE, mBubbleTextView.getLineCount()); + } + + @Test + public void testLongStringNoSpaceLongerThanCharLimit_flagOn() { + TestUtil.overrideFlag(ENABLE_TWOLINE_ALLAPPS, true); + // test string: "flutterappflorafy" + mItemInfoWithIcon.title = TEST_LONG_STRING_NO_SPACE_LONGER_THAN_CHAR_LIMIT; + mBubbleTextView.applyLabel(mItemInfoWithIcon); + mBubbleTextView.setTypeface(Typeface.MONOSPACE); + mBubbleTextView.measure(mLimitedWidth, 0); + mBubbleTextView.onPreDraw(); + assertEquals(ONE_LINE, mBubbleTextView.getLineCount()); + } + + @Test + public void testLongStringNoSpaceLongerThanCharLimit_flagOff() { + TestUtil.overrideFlag(ENABLE_TWOLINE_ALLAPPS, false); + // test string: "flutterappflorafy" + mItemInfoWithIcon.title = TEST_LONG_STRING_NO_SPACE_LONGER_THAN_CHAR_LIMIT; + mBubbleTextView.applyLabel(mItemInfoWithIcon); + mBubbleTextView.setTypeface(Typeface.MONOSPACE); + mBubbleTextView.measure(mLimitedWidth, 0); + mBubbleTextView.onPreDraw(); + assertEquals(ONE_LINE, mBubbleTextView.getLineCount()); + } + + @Test + public void testLongStringWithSpaceLongerThanCharLimit_flagOn() { + TestUtil.overrideFlag(ENABLE_TWOLINE_ALLAPPS, true); + // test string: "System UWB Field Test" + mItemInfoWithIcon.title = TEST_LONG_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT; + mBubbleTextView.applyLabel(mItemInfoWithIcon); + mBubbleTextView.setTypeface(Typeface.MONOSPACE); + mBubbleTextView.measure(mLimitedWidth, 0); + mBubbleTextView.onPreDraw(); + assertEquals(TWO_LINE, mBubbleTextView.getLineCount()); + } + + @Test + public void testLongStringWithSpaceLongerThanCharLimit_flagOff() { + TestUtil.overrideFlag(ENABLE_TWOLINE_ALLAPPS, false); + // test string: "System UWB Field Test" + mItemInfoWithIcon.title = TEST_LONG_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT; + mBubbleTextView.applyLabel(mItemInfoWithIcon); + mBubbleTextView.setTypeface(Typeface.MONOSPACE); + mBubbleTextView.measure(mLimitedWidth, 0); + mBubbleTextView.onPreDraw(); + assertEquals(ONE_LINE, mBubbleTextView.getLineCount()); + } + + @Test + public void testLongStringSymbolLongerThanCharLimit_flagOn() { + TestUtil.overrideFlag(ENABLE_TWOLINE_ALLAPPS, true); + // test string: "LEGO®Builder" + mItemInfoWithIcon.title = TEST_LONG_STRING_SYMBOL_LONGER_THAN_CHAR_LIMIT; + mBubbleTextView.applyLabel(mItemInfoWithIcon); + mBubbleTextView.setTypeface(Typeface.MONOSPACE); + mBubbleTextView.measure(mLimitedWidth, 0); + mBubbleTextView.onPreDraw(); + assertEquals(TWO_LINE, mBubbleTextView.getLineCount()); + } + + @Test + public void testLongStringSymbolLongerThanCharLimit_flagOff() { + TestUtil.overrideFlag(ENABLE_TWOLINE_ALLAPPS, false); + // test string: "LEGO®Builder" + mItemInfoWithIcon.title = TEST_LONG_STRING_SYMBOL_LONGER_THAN_CHAR_LIMIT; + mBubbleTextView.applyLabel(mItemInfoWithIcon); + mBubbleTextView.setTypeface(Typeface.MONOSPACE); + mBubbleTextView.measure(mLimitedWidth, 0); + mBubbleTextView.onPreDraw(); + assertEquals(ONE_LINE, mBubbleTextView.getLineCount()); + } + + @Test + public void modifyTitleToSupportMultiLine_TEST_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT() { + // test string: "Battery Stats" + IntArray breakPoints = StringMatcherUtility.getListOfBreakpoints( + TEST_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT, MATCHER); + CharSequence newString = BubbleTextView.modifyTitleToSupportMultiLine(mLimitedWidth, + TEST_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT, mBubbleTextView.getPaint(), + breakPoints); + assertEquals(TEST_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT_RESULT, newString); + } + + @Test + public void modifyTitleToSupportMultiLine_TEST_LONG_STRING_NO_SPACE_LONGER_THAN_CHAR_LIMIT() { + // test string: "flutterappflorafy" + IntArray breakPoints = StringMatcherUtility.getListOfBreakpoints( + TEST_LONG_STRING_NO_SPACE_LONGER_THAN_CHAR_LIMIT, MATCHER); + CharSequence newString = BubbleTextView.modifyTitleToSupportMultiLine(mLimitedWidth, + TEST_LONG_STRING_NO_SPACE_LONGER_THAN_CHAR_LIMIT, mBubbleTextView.getPaint(), + breakPoints); + assertEquals(TEST_LONG_STRING_NO_SPACE_LONGER_THAN_CHAR_LIMIT, newString); + } + + @Test + public void modifyTitleToSupportMultiLine_TEST_LONG_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT() { + // test string: "System UWB Field Test" + IntArray breakPoints = StringMatcherUtility.getListOfBreakpoints( + TEST_LONG_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT, MATCHER); + CharSequence newString = BubbleTextView.modifyTitleToSupportMultiLine(mLimitedWidth, + TEST_LONG_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT, mBubbleTextView.getPaint(), + breakPoints); + assertEquals(TEST_LONG_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT_RESULT, newString); + } + + @Test + public void modifyTitleToSupportMultiLine_TEST_LONG_STRING_SYMBOL_LONGER_THAN_CHAR_LIMIT() { + // test string: "LEGO®Builder" + IntArray breakPoints = StringMatcherUtility.getListOfBreakpoints( + TEST_LONG_STRING_SYMBOL_LONGER_THAN_CHAR_LIMIT, MATCHER); + CharSequence newString = BubbleTextView.modifyTitleToSupportMultiLine(mLimitedWidth, + TEST_LONG_STRING_SYMBOL_LONGER_THAN_CHAR_LIMIT, mBubbleTextView.getPaint(), + breakPoints); + assertEquals(TEST_LONG_STRING_SYMBOL_LONGER_THAN_CHAR_LIMIT_RESULT, newString); + } +}