Merge "Replace DateView with VariableDateView (1/2)" into sc-v2-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
c68117c2fb
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user