summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/res/layout/ongoing_call_chip.xml7
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallChronometer.kt87
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallChronometerTest.kt161
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
+ }
+}