Merge "Replace DateView with VariableDateView (1/2)" into sc-v2-dev

This commit is contained in:
TreeHugger Robot
2021-08-12 20:55:59 +00:00
committed by Android (Google) Code Review
10 changed files with 564 additions and 16 deletions

View File

@@ -21,6 +21,9 @@
<!-- Skeleton string format for displaying the date when an alarm is set. --> <!-- Skeleton string format for displaying the date when an alarm is set. -->
<string name="abbrev_wday_month_day_no_year_alarm">EEEMMMd</string> <string name="abbrev_wday_month_day_no_year_alarm">EEEMMMd</string>
<!-- Skeleton string format for displaying the date shorter. -->
<string name="abbrev_month_day_no_year">MMMd</string>
<!-- Skeleton string format for displaying the time in 12-hour format. --> <!-- Skeleton string format for displaying the time in 12-hour format. -->
<string name="clock_12hr_format">hm</string> <string name="clock_12hr_format">hm</string>

View File

@@ -26,17 +26,38 @@
android:focusable="true" android:focusable="true"
android:theme="@style/Theme.SystemUI.QuickSettings.Header"> android:theme="@style/Theme.SystemUI.QuickSettings.Header">
<com.android.systemui.statusbar.policy.Clock <LinearLayout
android:id="@+id/clock" android:id="@+id/clock_container"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:minWidth="48dp" android:orientation="horizontal"
android:minHeight="@dimen/qs_header_row_min_height" android:layout_gravity="center_vertical|start"
android:gravity="center_vertical|start" android:gravity="center_vertical|start"
android:paddingStart="@dimen/status_bar_left_clock_starting_padding" >
android:paddingEnd="@dimen/status_bar_left_clock_end_padding"
android:singleLine="true" <com.android.systemui.statusbar.policy.Clock
android:textAppearance="@style/TextAppearance.QS.Status" /> android:id="@+id/clock"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:minWidth="48dp"
android:minHeight="@dimen/qs_header_row_min_height"
android:gravity="center_vertical|start"
android:paddingStart="@dimen/status_bar_left_clock_starting_padding"
android:paddingEnd="@dimen/status_bar_left_clock_end_padding"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.QS.Status" />
<com.android.systemui.statusbar.policy.VariableDateView
android:id="@+id/date_clock"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical|start"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.QS.Status"
systemui:longDatePattern="@string/abbrev_wday_month_day_no_year_alarm"
systemui:shortDatePattern="@string/abbrev_month_day_no_year"
/>
</LinearLayout>
<include layout="@layout/qs_carrier_group" <include layout="@layout/qs_carrier_group"
android:id="@+id/carrier_group" android:id="@+id/carrier_group"

View File

@@ -36,7 +36,7 @@
android:layout_weight="1" android:layout_weight="1"
android:gravity="center_vertical|start" > android:gravity="center_vertical|start" >
<com.android.systemui.statusbar.policy.DateView <com.android.systemui.statusbar.policy.VariableDateView
android:id="@+id/date" android:id="@+id/date"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -44,7 +44,9 @@
android:gravity="center_vertical" android:gravity="center_vertical"
android:singleLine="true" android:singleLine="true"
android:textAppearance="@style/TextAppearance.QS.Status" android:textAppearance="@style/TextAppearance.QS.Status"
systemui:datePattern="@string/abbrev_wday_month_day_no_year_alarm" /> systemui:longDatePattern="@string/abbrev_wday_month_day_no_year_alarm"
systemui:shortDatePattern="@string/abbrev_month_day_no_year"
/>
</FrameLayout> </FrameLayout>
<android.widget.Space <android.widget.Space

View File

@@ -69,6 +69,10 @@
<declare-styleable name="DateView"> <declare-styleable name="DateView">
<attr name="datePattern" format="string" /> <attr name="datePattern" format="string" />
</declare-styleable> </declare-styleable>
<declare-styleable name="VariableDateView">
<attr name="longDatePattern" format="string" />
<attr name="shortDatePattern" format="string" />
</declare-styleable>
<declare-styleable name="PseudoGridView"> <declare-styleable name="PseudoGridView">
<attr name="numColumns" format="integer" /> <attr name="numColumns" format="integer" />
<attr name="verticalSpacing" format="dimension" /> <attr name="verticalSpacing" format="dimension" />

View File

@@ -42,6 +42,7 @@ import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconMa
import com.android.systemui.statusbar.phone.StatusBarWindowView; import com.android.systemui.statusbar.phone.StatusBarWindowView;
import com.android.systemui.statusbar.phone.StatusIconContainer; import com.android.systemui.statusbar.phone.StatusIconContainer;
import com.android.systemui.statusbar.policy.Clock; import com.android.systemui.statusbar.policy.Clock;
import com.android.systemui.statusbar.policy.VariableDateView;
import java.util.List; import java.util.List;
@@ -61,11 +62,15 @@ public class QuickStatusBarHeader extends FrameLayout {
protected QuickQSPanel mHeaderQsPanel; protected QuickQSPanel mHeaderQsPanel;
private View mDatePrivacyView; private View mDatePrivacyView;
private View mDateView;
// DateView next to clock. Visible on QQS
private VariableDateView mClockDateView;
private View mSecurityHeaderView; private View mSecurityHeaderView;
private View mStatusIconsView; private View mStatusIconsView;
private View mContainer; private View mContainer;
private View mQSCarriers; private View mQSCarriers;
private ViewGroup mClockContainer;
private Clock mClockView; private Clock mClockView;
private Space mDatePrivacySeparator; private Space mDatePrivacySeparator;
private View mClockIconsSeparator; private View mClockIconsSeparator;
@@ -120,12 +125,15 @@ public class QuickStatusBarHeader extends FrameLayout {
mContainer = findViewById(R.id.qs_container); mContainer = findViewById(R.id.qs_container);
mIconContainer = findViewById(R.id.statusIcons); mIconContainer = findViewById(R.id.statusIcons);
mPrivacyChip = findViewById(R.id.privacy_chip); mPrivacyChip = findViewById(R.id.privacy_chip);
mDateView = findViewById(R.id.date);
mClockDateView = findViewById(R.id.date_clock);
mSecurityHeaderView = findViewById(R.id.header_text_container); mSecurityHeaderView = findViewById(R.id.header_text_container);
mClockIconsSeparator = findViewById(R.id.separator); mClockIconsSeparator = findViewById(R.id.separator);
mRightLayout = findViewById(R.id.rightLayout); mRightLayout = findViewById(R.id.rightLayout);
mDateContainer = findViewById(R.id.date_container); mDateContainer = findViewById(R.id.date_container);
mPrivacyContainer = findViewById(R.id.privacy_container); mPrivacyContainer = findViewById(R.id.privacy_container);
mClockContainer = findViewById(R.id.clock_container);
mClockView = findViewById(R.id.clock); mClockView = findViewById(R.id.clock);
mDatePrivacySeparator = findViewById(R.id.space); mDatePrivacySeparator = findViewById(R.id.space);
// Tint for the battery icons are handled in setupHost() // Tint for the battery icons are handled in setupHost()
@@ -172,7 +180,7 @@ public class QuickStatusBarHeader extends FrameLayout {
super.onMeasure(widthMeasureSpec, heightMeasureSpec); super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mDatePrivacyView.getMeasuredHeight() != mTopViewMeasureHeight) { if (mDatePrivacyView.getMeasuredHeight() != mTopViewMeasureHeight) {
mTopViewMeasureHeight = mDatePrivacyView.getMeasuredHeight(); mTopViewMeasureHeight = mDatePrivacyView.getMeasuredHeight();
updateAnimators(); post(this::updateAnimators);
} }
} }
@@ -280,7 +288,8 @@ public class QuickStatusBarHeader extends FrameLayout {
TouchAnimator.Builder builder = new TouchAnimator.Builder() TouchAnimator.Builder builder = new TouchAnimator.Builder()
.addFloat(mSecurityHeaderView, "alpha", 0, 1) .addFloat(mSecurityHeaderView, "alpha", 0, 1)
// These views appear on expanding down // These views appear on expanding down
.addFloat(mClockView, "alpha", 0, 1) .addFloat(mDateView, "alpha", 0, 0, 1)
.addFloat(mClockDateView, "alpha", 1, 0, 0)
.addFloat(mQSCarriers, "alpha", 0, 1) .addFloat(mQSCarriers, "alpha", 0, 1)
.setListener(new TouchAnimator.ListenerAdapter() { .setListener(new TouchAnimator.ListenerAdapter() {
@Override @Override
@@ -289,10 +298,14 @@ public class QuickStatusBarHeader extends FrameLayout {
if (!mIsSingleCarrier) { if (!mIsSingleCarrier) {
mIconContainer.addIgnoredSlots(mRssiIgnoredSlots); mIconContainer.addIgnoredSlots(mRssiIgnoredSlots);
} }
// Make it gone so there's enough room for carrier names
mClockDateView.setVisibility(View.GONE);
} }
@Override @Override
public void onAnimationStarted() { public void onAnimationStarted() {
mClockDateView.setVisibility(View.VISIBLE);
mClockDateView.setFreezeSwitching(true);
setSeparatorVisibility(false); setSeparatorVisibility(false);
if (!mIsSingleCarrier) { if (!mIsSingleCarrier) {
mIconContainer.addIgnoredSlots(mRssiIgnoredSlots); mIconContainer.addIgnoredSlots(mRssiIgnoredSlots);
@@ -302,6 +315,7 @@ public class QuickStatusBarHeader extends FrameLayout {
@Override @Override
public void onAnimationAtStart() { public void onAnimationAtStart() {
super.onAnimationAtStart(); super.onAnimationAtStart();
mClockDateView.setFreezeSwitching(false);
setSeparatorVisibility(mShowClockIconsSeparator); setSeparatorVisibility(mShowClockIconsSeparator);
// In QQS we never ignore RSSI. // In QQS we never ignore RSSI.
mIconContainer.removeIgnoredSlots(mRssiIgnoredSlots); mIconContainer.removeIgnoredSlots(mRssiIgnoredSlots);
@@ -434,10 +448,11 @@ public class QuickStatusBarHeader extends FrameLayout {
mClockIconsSeparator.setVisibility(visible ? View.VISIBLE : View.GONE); mClockIconsSeparator.setVisibility(visible ? View.VISIBLE : View.GONE);
mQSCarriers.setVisibility(visible ? View.GONE : View.VISIBLE); mQSCarriers.setVisibility(visible ? View.GONE : View.VISIBLE);
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mClockView.getLayoutParams(); LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) mClockContainer.getLayoutParams();
lp.width = visible ? 0 : WRAP_CONTENT; lp.width = visible ? 0 : WRAP_CONTENT;
lp.weight = visible ? 1f : 0f; lp.weight = visible ? 1f : 0f;
mClockView.setLayoutParams(lp); mClockContainer.setLayoutParams(lp);
lp = (LinearLayout.LayoutParams) mRightLayout.getLayoutParams(); lp = (LinearLayout.LayoutParams) mRightLayout.getLayoutParams();
lp.width = visible ? 0 : WRAP_CONTENT; lp.width = visible ? 0 : WRAP_CONTENT;

View File

@@ -42,6 +42,7 @@ import com.android.systemui.qs.dagger.QSScope;
import com.android.systemui.statusbar.phone.StatusBarIconController; import com.android.systemui.statusbar.phone.StatusBarIconController;
import com.android.systemui.statusbar.phone.StatusIconContainer; import com.android.systemui.statusbar.phone.StatusIconContainer;
import com.android.systemui.statusbar.policy.Clock; import com.android.systemui.statusbar.policy.Clock;
import com.android.systemui.statusbar.policy.VariableDateViewController;
import com.android.systemui.util.ViewController; import com.android.systemui.util.ViewController;
import java.util.List; import java.util.List;
@@ -73,6 +74,9 @@ class QuickStatusBarHeaderController extends ViewController<QuickStatusBarHeader
private final BatteryMeterViewController mBatteryMeterViewController; private final BatteryMeterViewController mBatteryMeterViewController;
private final FeatureFlags mFeatureFlags; private final FeatureFlags mFeatureFlags;
private final VariableDateViewController mVariableDateViewControllerDateView;
private final VariableDateViewController mVariableDateViewControllerClockDateView;
private boolean mListening; private boolean mListening;
private boolean mMicCameraIndicatorsEnabled; private boolean mMicCameraIndicatorsEnabled;
private boolean mLocationIndicatorsEnabled; private boolean mLocationIndicatorsEnabled;
@@ -137,7 +141,8 @@ class QuickStatusBarHeaderController extends ViewController<QuickStatusBarHeader
PrivacyDialogController privacyDialogController, PrivacyDialogController privacyDialogController,
QSExpansionPathInterpolator qsExpansionPathInterpolator, QSExpansionPathInterpolator qsExpansionPathInterpolator,
BatteryMeterViewController batteryMeterViewController, BatteryMeterViewController batteryMeterViewController,
FeatureFlags featureFlags) { FeatureFlags featureFlags,
VariableDateViewController.Factory variableDateViewControllerFactory) {
super(view); super(view);
mPrivacyItemController = privacyItemController; mPrivacyItemController = privacyItemController;
mActivityStarter = activityStarter; mActivityStarter = activityStarter;
@@ -158,6 +163,12 @@ class QuickStatusBarHeaderController extends ViewController<QuickStatusBarHeader
mPrivacyChip = mView.findViewById(R.id.privacy_chip); mPrivacyChip = mView.findViewById(R.id.privacy_chip);
mClockView = mView.findViewById(R.id.clock); mClockView = mView.findViewById(R.id.clock);
mIconContainer = mView.findViewById(R.id.statusIcons); mIconContainer = mView.findViewById(R.id.statusIcons);
mVariableDateViewControllerDateView = variableDateViewControllerFactory.create(
mView.requireViewById(R.id.date)
);
mVariableDateViewControllerClockDateView = variableDateViewControllerFactory.create(
mView.requireViewById(R.id.date_clock)
);
mIconManager = new StatusBarIconController.TintedIconManager(mIconContainer, featureFlags); mIconManager = new StatusBarIconController.TintedIconManager(mIconContainer, featureFlags);
mDemoModeReceiver = new ClockDemoModeReceiver(mClockView); mDemoModeReceiver = new ClockDemoModeReceiver(mClockView);
@@ -215,6 +226,9 @@ class QuickStatusBarHeaderController extends ViewController<QuickStatusBarHeader
mView.onAttach(mIconManager, mQSExpansionPathInterpolator, rssiIgnoredSlots); mView.onAttach(mIconManager, mQSExpansionPathInterpolator, rssiIgnoredSlots);
mDemoModeController.addCallback(mDemoModeReceiver); mDemoModeController.addCallback(mDemoModeReceiver);
mVariableDateViewControllerDateView.init();
mVariableDateViewControllerClockDateView.init();
} }
@Override @Override

View File

@@ -0,0 +1,79 @@
/*
* 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.policy
import android.content.Context
import android.text.StaticLayout
import android.util.AttributeSet
import android.widget.TextView
import com.android.systemui.R
/**
* View for showing a date that can toggle between two different formats depending on size.
*
* If no pattern can fit, it will display empty.
*
* @see R.styleable.VariableDateView_longDatePattern
* @see R.styleable.VariableDateView_shortDatePattern
*/
class VariableDateView(context: Context, attrs: AttributeSet) : TextView(context, attrs) {
val longerPattern: String
val shorterPattern: String
init {
val a = context.theme.obtainStyledAttributes(
attrs,
R.styleable.VariableDateView,
0, 0)
longerPattern = a.getString(R.styleable.VariableDateView_longDatePattern)
?: context.getString(R.string.system_ui_date_pattern)
shorterPattern = a.getString(R.styleable.VariableDateView_shortDatePattern)
?: context.getString(R.string.abbrev_month_day_no_year)
a.recycle()
}
/**
* Freeze the pattern switching
*
* Use during animations if the container will change its size but this view should not change
*/
var freezeSwitching = false
private var onMeasureListener: OnMeasureListener? = null
fun onAttach(listener: OnMeasureListener?) {
onMeasureListener = listener
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val availableWidth = MeasureSpec.getSize(widthMeasureSpec) - paddingStart - paddingEnd
if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED && !freezeSwitching) {
onMeasureListener?.onMeasureAction(availableWidth)
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
fun getDesiredWidthForText(text: CharSequence): Float {
return StaticLayout.getDesiredWidth(text, paint)
}
interface OnMeasureListener {
fun onMeasureAction(availableWidth: Int)
}
}

View File

@@ -0,0 +1,221 @@
/*
* 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.policy
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.icu.text.DateFormat
import android.icu.text.DisplayContext
import android.icu.util.Calendar
import android.os.Handler
import android.os.HandlerExecutor
import android.os.UserHandle
import android.text.TextUtils
import android.util.Log
import androidx.annotation.VisibleForTesting
import com.android.systemui.Dependency
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.util.ViewController
import com.android.systemui.util.time.SystemClock
import java.text.FieldPosition
import java.text.ParsePosition
import java.util.Date
import java.util.Locale
import javax.inject.Inject
import javax.inject.Named
@VisibleForTesting
internal fun getTextForFormat(date: Date?, format: DateFormat): String {
return if (format === EMPTY_FORMAT) { // Check if same object
""
} else format.format(date)
}
@VisibleForTesting
internal fun getFormatFromPattern(pattern: String?): DateFormat {
if (TextUtils.equals(pattern, "")) {
return EMPTY_FORMAT
}
val l = Locale.getDefault()
val format = DateFormat.getInstanceForSkeleton(pattern, l)
format.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE)
return format
}
private val EMPTY_FORMAT: DateFormat = object : DateFormat() {
override fun format(
cal: Calendar,
toAppendTo: StringBuffer,
fieldPosition: FieldPosition
): StringBuffer? {
return null
}
override fun parse(text: String, cal: Calendar, pos: ParsePosition) {}
}
private const val DEBUG = false
private const val TAG = "VariableDateViewController"
class VariableDateViewController(
private val systemClock: SystemClock,
private val broadcastDispatcher: BroadcastDispatcher,
private val timeTickHandler: Handler,
view: VariableDateView
) : ViewController<VariableDateView>(view) {
private var dateFormat: DateFormat? = null
private var datePattern = view.longerPattern
set(value) {
if (field == value) return
field = value
dateFormat = null
if (isAttachedToWindow) {
post(::updateClock)
}
}
private var lastWidth = Integer.MAX_VALUE
private var lastText = ""
private var currentTime = Date()
// View class easy accessors
private val longerPattern: String
get() = mView.longerPattern
private val shorterPattern: String
get() = mView.shorterPattern
private fun post(block: () -> Unit) = mView.handler?.post(block)
private val intentReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// If the handler is null, it means we received a broadcast while the view has not
// finished being attached or in the process of being detached.
// In that case, do not post anything.
val handler = mView.handler ?: return
val action = intent.action
if (
Intent.ACTION_TIME_TICK == action ||
Intent.ACTION_TIME_CHANGED == action ||
Intent.ACTION_TIMEZONE_CHANGED == action ||
Intent.ACTION_LOCALE_CHANGED == action
) {
if (
Intent.ACTION_LOCALE_CHANGED == action ||
Intent.ACTION_TIMEZONE_CHANGED == action
) {
// need to get a fresh date format
handler.post { dateFormat = null }
}
handler.post(::updateClock)
}
}
}
private val onMeasureListener = object : VariableDateView.OnMeasureListener {
override fun onMeasureAction(availableWidth: Int) {
if (availableWidth != lastWidth) {
// maybeChangeFormat will post if the pattern needs to change.
maybeChangeFormat(availableWidth)
lastWidth = availableWidth
}
}
}
override fun onViewAttached() {
val filter = IntentFilter().apply {
addAction(Intent.ACTION_TIME_TICK)
addAction(Intent.ACTION_TIME_CHANGED)
addAction(Intent.ACTION_TIMEZONE_CHANGED)
addAction(Intent.ACTION_LOCALE_CHANGED)
}
broadcastDispatcher.registerReceiver(intentReceiver, filter,
HandlerExecutor(timeTickHandler), UserHandle.SYSTEM)
post(::updateClock)
mView.onAttach(onMeasureListener)
}
override fun onViewDetached() {
dateFormat = null
mView.onAttach(null)
broadcastDispatcher.unregisterReceiver(intentReceiver)
}
private fun updateClock() {
if (dateFormat == null) {
dateFormat = getFormatFromPattern(datePattern)
}
currentTime.time = systemClock.currentTimeMillis()
val text = getTextForFormat(currentTime, dateFormat!!)
if (text != lastText) {
mView.setText(text)
lastText = text
}
}
private fun maybeChangeFormat(availableWidth: Int) {
if (mView.freezeSwitching ||
availableWidth > lastWidth && datePattern == longerPattern ||
availableWidth < lastWidth && datePattern == ""
) {
// Nothing to do
return
}
if (DEBUG) Log.d(TAG, "Width changed. Maybe changing pattern")
// Start with longer pattern and see what fits
var text = getTextForFormat(currentTime, getFormatFromPattern(longerPattern))
var length = mView.getDesiredWidthForText(text)
if (length <= availableWidth) {
changePattern(longerPattern)
return
}
text = getTextForFormat(currentTime, getFormatFromPattern(shorterPattern))
length = mView.getDesiredWidthForText(text)
if (length <= availableWidth) {
changePattern(shorterPattern)
return
}
changePattern("")
}
private fun changePattern(newPattern: String) {
if (newPattern.equals(datePattern)) return
if (DEBUG) Log.d(TAG, "Changing pattern to $newPattern")
datePattern = newPattern
}
class Factory @Inject constructor(
private val systemClock: SystemClock,
private val broadcastDispatcher: BroadcastDispatcher,
@Named(Dependency.TIME_TICK_HANDLER_NAME) private val handler: Handler
) {
fun create(view: VariableDateView): VariableDateViewController {
return VariableDateViewController(
systemClock,
broadcastDispatcher,
handler,
view
)
}
}
}

View File

@@ -37,6 +37,8 @@ import com.android.systemui.qs.carrier.QSCarrierGroupController
import com.android.systemui.statusbar.phone.StatusBarIconController import com.android.systemui.statusbar.phone.StatusBarIconController
import com.android.systemui.statusbar.phone.StatusIconContainer import com.android.systemui.statusbar.phone.StatusIconContainer
import com.android.systemui.statusbar.policy.Clock import com.android.systemui.statusbar.policy.Clock
import com.android.systemui.statusbar.policy.VariableDateView
import com.android.systemui.statusbar.policy.VariableDateViewController
import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.capture
@@ -88,10 +90,16 @@ class QuickStatusBarHeaderControllerTest : SysuiTestCase() {
@Mock @Mock
private lateinit var privacyDialogController: PrivacyDialogController private lateinit var privacyDialogController: PrivacyDialogController
@Mock @Mock
private lateinit var variableDateViewControllerFactory: VariableDateViewController.Factory
@Mock
private lateinit var variableDateViewController: VariableDateViewController
@Mock
private lateinit var batteryMeterViewController: BatteryMeterViewController private lateinit var batteryMeterViewController: BatteryMeterViewController
@Mock @Mock
private lateinit var clock: Clock private lateinit var clock: Clock
@Mock @Mock
private lateinit var variableDateView: VariableDateView
@Mock
private lateinit var mockView: View private lateinit var mockView: View
@Mock(answer = Answers.RETURNS_DEEP_STUBS) @Mock(answer = Answers.RETURNS_DEEP_STUBS)
private lateinit var context: Context private lateinit var context: Context
@@ -112,6 +120,8 @@ class QuickStatusBarHeaderControllerTest : SysuiTestCase() {
stubViews() stubViews()
`when`(iconContainer.context).thenReturn(context) `when`(iconContainer.context).thenReturn(context)
`when`(qsCarrierGroupControllerBuilder.build()).thenReturn(qsCarrierGroupController) `when`(qsCarrierGroupControllerBuilder.build()).thenReturn(qsCarrierGroupController)
`when`(variableDateViewControllerFactory.create(any()))
.thenReturn(variableDateViewController)
`when`(view.resources).thenReturn(mContext.resources) `when`(view.resources).thenReturn(mContext.resources)
`when`(view.isAttachedToWindow).thenReturn(true) `when`(view.isAttachedToWindow).thenReturn(true)
`when`(view.context).thenReturn(context) `when`(view.context).thenReturn(context)
@@ -137,7 +147,8 @@ class QuickStatusBarHeaderControllerTest : SysuiTestCase() {
privacyDialogController, privacyDialogController,
qsExpansionPathInterpolator, qsExpansionPathInterpolator,
batteryMeterViewController, batteryMeterViewController,
featureFlags featureFlags,
variableDateViewControllerFactory
) )
} }
@@ -278,6 +289,8 @@ class QuickStatusBarHeaderControllerTest : SysuiTestCase() {
`when`(view.findViewById<StatusIconContainer>(R.id.statusIcons)).thenReturn(iconContainer) `when`(view.findViewById<StatusIconContainer>(R.id.statusIcons)).thenReturn(iconContainer)
`when`(view.findViewById<OngoingPrivacyChip>(R.id.privacy_chip)).thenReturn(privacyChip) `when`(view.findViewById<OngoingPrivacyChip>(R.id.privacy_chip)).thenReturn(privacyChip)
`when`(view.findViewById<Clock>(R.id.clock)).thenReturn(clock) `when`(view.findViewById<Clock>(R.id.clock)).thenReturn(clock)
`when`(view.requireViewById<VariableDateView>(R.id.date)).thenReturn(variableDateView)
`when`(view.requireViewById<VariableDateView>(R.id.date_clock)).thenReturn(variableDateView)
} }
private fun setPrivacyController(micCamera: Boolean, location: Boolean) { private fun setPrivacyController(micCamera: Boolean, location: Boolean) {

View File

@@ -0,0 +1,176 @@
/*
* 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.policy
import android.os.Handler
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.Mockito.anyString
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import java.util.Date
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper
@SmallTest
class VariableDateViewControllerTest : SysuiTestCase() {
companion object {
private const val TIME_STAMP = 1_500_000_000_000
private const val LONG_PATTERN = "EEEMMMd"
private const val SHORT_PATTERN = "MMMd"
private const val CHAR_WIDTH = 10f
}
@Mock
private lateinit var broadcastDispatcher: BroadcastDispatcher
@Mock
private lateinit var view: VariableDateView
@Captor
private lateinit var onMeasureListenerCaptor: ArgumentCaptor<VariableDateView.OnMeasureListener>
private var lastText: String? = null
private lateinit var systemClock: FakeSystemClock
private lateinit var testableLooper: TestableLooper
private lateinit var testableHandler: Handler
private lateinit var controller: VariableDateViewController
private lateinit var longText: String
private lateinit var shortText: String
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
testableLooper = TestableLooper.get(this)
testableHandler = Handler(testableLooper.looper)
systemClock = FakeSystemClock()
systemClock.setCurrentTimeMillis(TIME_STAMP)
`when`(view.longerPattern).thenReturn(LONG_PATTERN)
`when`(view.shorterPattern).thenReturn(SHORT_PATTERN)
`when`(view.handler).thenReturn(testableHandler)
`when`(view.setText(anyString())).thenAnswer {
lastText = it.arguments[0] as? String
Unit
}
`when`(view.isAttachedToWindow).thenReturn(true)
val date = Date(TIME_STAMP)
longText = getTextForFormat(date, getFormatFromPattern(LONG_PATTERN))
shortText = getTextForFormat(date, getFormatFromPattern(SHORT_PATTERN))
// Assume some sizes for the text, the controller doesn't need to know if these sizes are
// the true ones
`when`(view.getDesiredWidthForText(any())).thenAnswer {
getTextLength(it.arguments[0] as CharSequence)
}
controller = VariableDateViewController(
systemClock,
broadcastDispatcher,
testableHandler,
view
)
controller.init()
testableLooper.processAllMessages()
verify(view).onAttach(capture(onMeasureListenerCaptor))
}
@Test
fun testViewStartsWithLongText() {
assertThat(lastText).isEqualTo(longText)
}
@Test
fun testListenerNotNull() {
assertThat(onMeasureListenerCaptor.value).isNotNull()
}
@Test
fun testLotsOfSpaceUseLongText() {
onMeasureListenerCaptor.value.onMeasureAction(10000)
testableLooper.processAllMessages()
assertThat(lastText).isEqualTo(longText)
}
@Test
fun testSmallSpaceUseEmpty() {
onMeasureListenerCaptor.value.onMeasureAction(1)
testableLooper.processAllMessages()
assertThat(lastText).isEmpty()
}
@Test
fun testSpaceInBetweenUseShortText() {
val average = ((getTextLength(longText) + getTextLength(shortText)) / 2).toInt()
onMeasureListenerCaptor.value.onMeasureAction(average)
testableLooper.processAllMessages()
assertThat(lastText).isEqualTo(shortText)
}
@Test
fun testSwitchBackToLonger() {
onMeasureListenerCaptor.value.onMeasureAction(1)
testableLooper.processAllMessages()
onMeasureListenerCaptor.value.onMeasureAction(10000)
testableLooper.processAllMessages()
assertThat(lastText).isEqualTo(longText)
}
@Test
fun testNoSwitchingWhenFrozen() {
`when`(view.freezeSwitching).thenReturn(true)
val average = ((getTextLength(longText) + getTextLength(shortText)) / 2).toInt()
onMeasureListenerCaptor.value.onMeasureAction(average)
testableLooper.processAllMessages()
assertThat(lastText).isEqualTo(longText)
onMeasureListenerCaptor.value.onMeasureAction(1)
testableLooper.processAllMessages()
assertThat(lastText).isEqualTo(longText)
}
private fun getTextLength(text: CharSequence): Float {
return text.length * CHAR_WIDTH
}
}