diff options
3 files changed, 251 insertions, 4 deletions
diff --git a/packages/SystemUI/res/layout/ongoing_call_chip.xml b/packages/SystemUI/res/layout/ongoing_call_chip.xml index a5e7f5d4cfe6..a146547d0083 100644 --- a/packages/SystemUI/res/layout/ongoing_call_chip.xml +++ b/packages/SystemUI/res/layout/ongoing_call_chip.xml @@ -29,18 +29,17 @@ android:src="@*android:drawable/ic_phone" android:layout_width="@dimen/ongoing_call_chip_icon_size" android:layout_height="@dimen/ongoing_call_chip_icon_size" - android:paddingEnd="@dimen/ongoing_call_chip_icon_text_padding" android:tint="?android:attr/colorPrimary" /> <!-- TODO(b/183229367): The text in this view isn't quite centered within the chip. --> - <!-- TODO(b/183229367): This text view's width shouldn't change as the time increases. --> - <Chronometer + <com.android.systemui.statusbar.phone.ongoingcall.OngoingCallChronometer android:id="@+id/ongoing_call_chip_time" android:layout_width="wrap_content" android:layout_height="wrap_content" android:singleLine="true" - android:gravity="center" + android:gravity="center|start" + android:paddingStart="@dimen/ongoing_call_chip_icon_text_padding" android:textAppearance="@android:style/TextAppearance.Material.Small" android:fontFamily="@*android:string/config_headlineFontFamily" android:textColor="?android:attr/colorPrimary" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallChronometer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallChronometer.kt new file mode 100644 index 000000000000..1fe77fd441bc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallChronometer.kt @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2021 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.systemui.statusbar.phone.ongoingcall + +import android.content.Context +import android.util.AttributeSet + +import android.widget.Chronometer + +/** + * A [Chronometer] specifically for the ongoing call chip in the status bar. + * + * This class handles: + * 1) Setting the text width. If we used a basic WRAP_CONTENT for width, the chip width would + * change slightly each second because the width of each number is slightly different. + * + * Instead, we save the largest number width seen so far and ensure that the chip is at least + * that wide. This means the chip may get larger over time (e.g. in the transition from 59:59 + * to 1:00:00), but never smaller. + * + * 2) Hiding the text if the time gets too long for the space available. Once the text has been + * hidden, it remains hidden for the duration of the call. + * + * Note that if the text was too big in portrait mode, resulting in the text being hidden, then the + * text will also be hidden in landscape (even if there is enough space for it in landscape). + */ +class OngoingCallChronometer @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0 +) : Chronometer(context, attrs, defStyle) { + + // Minimum width that the text view can be. Corresponds with the largest number width seen so + // far. + var minimumTextWidth: Int = 0 + + // True if the text is too long for the space available, so the text should be hidden. + var shouldHideText: Boolean = false + + override fun setBase(base: Long) { + // These variables may have changed during the previous call, so re-set them before the new + // call starts. + minimumTextWidth = 0 + shouldHideText = false + super.setBase(base) + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + if (shouldHideText) { + setMeasuredDimension(0, 0) + return + } + + // Evaluate how wide the text *wants* to be if it had unlimited space. + super.onMeasure( + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), + heightMeasureSpec) + val desiredTextWidth = measuredWidth + + // Evaluate how wide the text *can* be based on the enforced constraints + val enforcedTextWidth = resolveSize(desiredTextWidth, widthMeasureSpec) + + if (desiredTextWidth > enforcedTextWidth) { + shouldHideText = true + setMeasuredDimension(0, 0) + } else { + // It's possible that the current text could fit in a smaller width, but we don't want + // the chip to change size every second. Instead, keep it at the minimum required width. + minimumTextWidth = desiredTextWidth.coerceAtLeast(minimumTextWidth) + setMeasuredDimension(minimumTextWidth, MeasureSpec.getSize(heightMeasureSpec)) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallChronometerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallChronometerTest.kt new file mode 100644 index 000000000000..0e77bb36d68f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallChronometerTest.kt @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2021 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.systemui.statusbar.phone.ongoingcall + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.view.LayoutInflater +import android.view.View +import android.widget.LinearLayout +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +private const val TEXT_VIEW_MAX_WIDTH = 400 + +// When a [Chronometer] is created, it starts off with "00:00" as its text. +private const val INITIAL_TEXT = "00:00" +private const val LARGE_TEXT = "00:000" +private const val XL_TEXT = "00:0000" + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class OngoingCallChronometerTest : SysuiTestCase() { + + private lateinit var textView: OngoingCallChronometer + private lateinit var doesNotFitText: String + + @Before + fun setUp() { + allowTestableLooperAsMainThread() + TestableLooper.get(this).runWithLooper { + val chipView = LayoutInflater.from(mContext) + .inflate(R.layout.ongoing_call_chip, null) as LinearLayout + textView = chipView.findViewById(R.id.ongoing_call_chip_time)!! + measureTextView() + calculateDoesNotFixText() + } + } + + @Test + fun verifyTextSizes() { + val initialTextLength = textView.paint.measureText(INITIAL_TEXT) + val largeTextLength = textView.paint.measureText(LARGE_TEXT) + val xlTextLength = textView.paint.measureText(XL_TEXT) + + // Assert that our test text sizes do what we expect them to do in the rest of the tests. + assertThat(initialTextLength).isLessThan(TEXT_VIEW_MAX_WIDTH) + assertThat(largeTextLength).isLessThan(TEXT_VIEW_MAX_WIDTH) + assertThat(xlTextLength).isLessThan(TEXT_VIEW_MAX_WIDTH) + assertThat(textView.paint.measureText(doesNotFitText)).isGreaterThan(TEXT_VIEW_MAX_WIDTH) + + assertThat(largeTextLength).isGreaterThan(initialTextLength) + assertThat(xlTextLength).isGreaterThan(largeTextLength) + } + + @Test + fun onMeasure_initialTextFitsInSpace_textDisplayed() { + assertThat(textView.measuredWidth).isGreaterThan(0) + } + + @Test + fun onMeasure_newTextLargerThanPreviousText_widthGetsLarger() { + val initialTextLength = textView.measuredWidth + + setTextAndMeasure(LARGE_TEXT) + + assertThat(textView.measuredWidth).isGreaterThan(initialTextLength) + } + + @Test + fun onMeasure_newTextSmallerThanPreviousText_widthDoesNotGetSmaller() { + setTextAndMeasure(XL_TEXT) + val xlWidth = textView.measuredWidth + + setTextAndMeasure(LARGE_TEXT) + + assertThat(textView.measuredWidth).isEqualTo(xlWidth) + } + + @Test + fun onMeasure_textDoesNotFit_textHidden() { + setTextAndMeasure(doesNotFitText) + + assertThat(textView.measuredWidth).isEqualTo(0) + } + + @Test + fun onMeasure_newTextFitsButPreviousTextDidNot_textHidden() { + setTextAndMeasure(doesNotFitText) + + setTextAndMeasure(LARGE_TEXT) + + assertThat(textView.measuredWidth).isEqualTo(0) + } + + @Test + fun resetBase_hadLongerTextThenSetBaseThenShorterText_widthIsShort() { + setTextAndMeasure(XL_TEXT) + val xlWidth = textView.measuredWidth + + textView.base = 0L + setTextAndMeasure(INITIAL_TEXT) + + assertThat(textView.measuredWidth).isLessThan(xlWidth) + assertThat(textView.measuredWidth).isGreaterThan(0) + } + + @Test + fun setBase_wasHidingTextThenSetBaseThenShorterText_textShown() { + setTextAndMeasure(doesNotFitText) + + textView.base = 0L + setTextAndMeasure(INITIAL_TEXT) + + assertThat(textView.measuredWidth).isGreaterThan(0) + } + + private fun setTextAndMeasure(text: String) { + textView.text = text + measureTextView() + } + + private fun measureTextView() { + textView.measure( + View.MeasureSpec.makeMeasureSpec(TEXT_VIEW_MAX_WIDTH, View.MeasureSpec.AT_MOST), + View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) + ) + } + + /** + * Calculates what [doesNotFitText] should be. Needs to be done dynamically because different + * devices have different densities, which means the textView can fit different amounts of + * characters. + */ + private fun calculateDoesNotFixText() { + var currentText = XL_TEXT + "0" + while (textView.paint.measureText(currentText) <= TEXT_VIEW_MAX_WIDTH) { + currentText += "0" + } + doesNotFitText = currentText + } +} |