Merge "Add new Next Alarm tile" into sc-dev

This commit is contained in:
Fabian Kozynski
2021-03-10 15:28:46 +00:00
committed by Android (Google) Code Review
7 changed files with 281 additions and 2 deletions

View File

@@ -107,7 +107,7 @@
<!-- Tiles native to System UI. Order should match "quick_settings_tiles_default" --> <!-- Tiles native to System UI. Order should match "quick_settings_tiles_default" -->
<string name="quick_settings_tiles_stock" translatable="false"> <string name="quick_settings_tiles_stock" translatable="false">
wifi,cell,battery,dnd,flashlight,rotation,bt,airplane,location,hotspot,inversion,saver,dark,work,cast,night,screenrecord,reverse,reduce_brightness,cameratoggle,mictoggle,controls wifi,cell,battery,dnd,flashlight,rotation,bt,airplane,location,hotspot,inversion,saver,dark,work,cast,night,screenrecord,reverse,reduce_brightness,cameratoggle,mictoggle,controls,alarm
</string> </string>
<!-- The tiles to display in QuickSettings --> <!-- The tiles to display in QuickSettings -->

View File

@@ -46,4 +46,6 @@
<bool name="flag_navigation_bar_overlay">false</bool> <bool name="flag_navigation_bar_overlay">false</bool>
<bool name="flag_pm_lite">false</bool> <bool name="flag_pm_lite">false</bool>
<bool name="flag_alarm_tile">false</bool>
</resources> </resources>

View File

@@ -2861,4 +2861,7 @@
<string name="qs_remove_labels" translatable="false"></string> <string name="qs_remove_labels" translatable="false"></string>
<string name="qs_tile_label_fontFamily" translatable="false">@*android:string/config_headlineFontFamily</string> <string name="qs_tile_label_fontFamily" translatable="false">@*android:string/config_headlineFontFamily</string>
<!-- Secondary label for alarm tile when there is no next alarm information [CHAR LIMIT=20] -->
<string name="qs_alarm_tile_no_alarm">No alarm set</string>
</resources> </resources>

View File

@@ -30,6 +30,7 @@ import com.android.systemui.plugins.qs.QSTileView;
import com.android.systemui.qs.QSHost; import com.android.systemui.qs.QSHost;
import com.android.systemui.qs.external.CustomTile; import com.android.systemui.qs.external.CustomTile;
import com.android.systemui.qs.tiles.AirplaneModeTile; import com.android.systemui.qs.tiles.AirplaneModeTile;
import com.android.systemui.qs.tiles.AlarmTile;
import com.android.systemui.qs.tiles.BatterySaverTile; import com.android.systemui.qs.tiles.BatterySaverTile;
import com.android.systemui.qs.tiles.BluetoothTile; import com.android.systemui.qs.tiles.BluetoothTile;
import com.android.systemui.qs.tiles.CameraToggleTile; import com.android.systemui.qs.tiles.CameraToggleTile;
@@ -91,6 +92,7 @@ public class QSFactoryImpl implements QSFactory {
private final Provider<CameraToggleTile> mCameraToggleTileProvider; private final Provider<CameraToggleTile> mCameraToggleTileProvider;
private final Provider<MicrophoneToggleTile> mMicrophoneToggleTileProvider; private final Provider<MicrophoneToggleTile> mMicrophoneToggleTileProvider;
private final Provider<DeviceControlsTile> mDeviceControlsTileProvider; private final Provider<DeviceControlsTile> mDeviceControlsTileProvider;
private final Provider<AlarmTile> mAlarmTileProvider;
private final Lazy<QSHost> mQsHostLazy; private final Lazy<QSHost> mQsHostLazy;
private final Provider<CustomTile.Builder> mCustomTileBuilderProvider; private final Provider<CustomTile.Builder> mCustomTileBuilderProvider;
@@ -126,7 +128,8 @@ public class QSFactoryImpl implements QSFactory {
Provider<ReduceBrightColorsTile> reduceBrightColorsTileProvider, Provider<ReduceBrightColorsTile> reduceBrightColorsTileProvider,
Provider<CameraToggleTile> cameraToggleTileProvider, Provider<CameraToggleTile> cameraToggleTileProvider,
Provider<MicrophoneToggleTile> microphoneToggleTileProvider, Provider<MicrophoneToggleTile> microphoneToggleTileProvider,
Provider<DeviceControlsTile> deviceControlsTileProvider) { Provider<DeviceControlsTile> deviceControlsTileProvider,
Provider<AlarmTile> alarmTileProvider) {
mQsHostLazy = qsHostLazy; mQsHostLazy = qsHostLazy;
mCustomTileBuilderProvider = customTileBuilderProvider; mCustomTileBuilderProvider = customTileBuilderProvider;
@@ -157,6 +160,7 @@ public class QSFactoryImpl implements QSFactory {
mCameraToggleTileProvider = cameraToggleTileProvider; mCameraToggleTileProvider = cameraToggleTileProvider;
mMicrophoneToggleTileProvider = microphoneToggleTileProvider; mMicrophoneToggleTileProvider = microphoneToggleTileProvider;
mDeviceControlsTileProvider = deviceControlsTileProvider; mDeviceControlsTileProvider = deviceControlsTileProvider;
mAlarmTileProvider = alarmTileProvider;
} }
public QSTile createTile(String tileSpec) { public QSTile createTile(String tileSpec) {
@@ -218,6 +222,8 @@ public class QSFactoryImpl implements QSFactory {
return mMicrophoneToggleTileProvider.get(); return mMicrophoneToggleTileProvider.get();
case "controls": case "controls":
return mDeviceControlsTileProvider.get(); return mDeviceControlsTileProvider.get();
case "alarm":
return mAlarmTileProvider.get();
} }
// Custom tiles // Custom tiles

View File

@@ -0,0 +1,117 @@
package com.android.systemui.qs.tiles
import android.app.AlarmManager
import android.app.AlarmManager.AlarmClockInfo
import android.content.Intent
import android.os.Handler
import android.os.Looper
import android.provider.AlarmClock
import android.service.quicksettings.Tile
import android.text.TextUtils
import android.text.format.DateFormat
import androidx.annotation.VisibleForTesting
import com.android.internal.logging.MetricsLogger
import com.android.systemui.R
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.qs.QSHost
import com.android.systemui.qs.logging.QSLogger
import com.android.systemui.qs.tileimpl.QSTileImpl
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.FeatureFlags
import com.android.systemui.statusbar.policy.NextAlarmController
import java.util.Locale
import javax.inject.Inject
class AlarmTile @Inject constructor(
host: QSHost,
@Background backgroundLooper: Looper,
@Main mainHandler: Handler,
metricsLogger: MetricsLogger,
statusBarStateController: StatusBarStateController,
activityStarter: ActivityStarter,
qsLogger: QSLogger,
private val featureFlags: FeatureFlags,
private val userTracker: UserTracker,
nextAlarmController: NextAlarmController
) : QSTileImpl<QSTile.State>(
host,
backgroundLooper,
mainHandler,
metricsLogger,
statusBarStateController,
activityStarter,
qsLogger
) {
private var lastAlarmInfo: AlarmManager.AlarmClockInfo? = null
private val icon = ResourceIcon.get(R.drawable.ic_alarm)
@VisibleForTesting
internal val defaultIntent = Intent(AlarmClock.ACTION_SET_ALARM)
private val callback = NextAlarmController.NextAlarmChangeCallback { nextAlarm ->
lastAlarmInfo = nextAlarm
refreshState()
}
init {
nextAlarmController.observe(this, callback)
}
override fun isAvailable(): Boolean {
return featureFlags.isAlarmTileAvailable
}
override fun newTileState(): QSTile.State {
return QSTile.State().apply {
handlesLongClick = false
}
}
private fun startDefaultSetAlarm() {
mActivityStarter.postStartActivityDismissingKeyguard(defaultIntent, 0)
}
override fun handleClick() {
lastAlarmInfo?.showIntent?.let {
mActivityStarter.postStartActivityDismissingKeyguard(it)
} ?: startDefaultSetAlarm()
}
override fun handleUpdateState(state: QSTile.State, arg: Any?) {
state.icon = icon
state.label = tileLabel
lastAlarmInfo?.let {
state.secondaryLabel = formatNextAlarm(it)
state.state = Tile.STATE_ACTIVE
} ?: run {
state.secondaryLabel = mContext.getString(R.string.qs_alarm_tile_no_alarm)
state.state = Tile.STATE_INACTIVE
}
state.contentDescription = TextUtils.concat(state.label, ", ", state.secondaryLabel)
}
override fun getTileLabel(): CharSequence {
return mContext.getString(R.string.status_bar_alarm)
}
private fun formatNextAlarm(info: AlarmClockInfo): String {
val skeleton = if (use24HourFormat()) "EHm" else "Ehma"
val pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton)
return DateFormat.format(pattern, info.triggerTime).toString()
}
private fun use24HourFormat(): Boolean {
return DateFormat.is24HourFormat(mContext, userTracker.userId)
}
override fun getMetricsCategory(): Int {
return 0
}
override fun getLongClickIntent(): Intent? {
return null
}
}

View File

@@ -90,4 +90,8 @@ public class FeatureFlags {
public boolean isPMLiteEnabled() { public boolean isPMLiteEnabled() {
return mFlagReader.isEnabled(R.bool.flag_pm_lite); return mFlagReader.isEnabled(R.bool.flag_pm_lite);
} }
public boolean isAlarmTileAvailable() {
return mFlagReader.isEnabled(R.bool.flag_alarm_tile);
}
} }

View File

@@ -0,0 +1,147 @@
package com.android.systemui.qs.tiles
import android.app.AlarmManager
import android.app.PendingIntent
import android.os.Handler
import android.service.quicksettings.Tile
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import androidx.test.filters.SmallTest
import com.android.internal.logging.MetricsLogger
import com.android.internal.logging.UiEventLogger
import com.android.systemui.SysuiTestCase
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.qs.QSHost
import com.android.systemui.qs.logging.QSLogger
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.FeatureFlags
import com.android.systemui.statusbar.policy.NextAlarmController
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq
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.verify
import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
class AlarmTileTest : SysuiTestCase() {
@Mock
private lateinit var qsHost: QSHost
@Mock
private lateinit var metricsLogger: MetricsLogger
@Mock
private lateinit var statusBarStateController: StatusBarStateController
@Mock
private lateinit var activityStarter: ActivityStarter
@Mock
private lateinit var qsLogger: QSLogger
@Mock
private lateinit var featureFlags: FeatureFlags
@Mock
private lateinit var userTracker: UserTracker
@Mock
private lateinit var nextAlarmController: NextAlarmController
@Mock
private lateinit var uiEventLogger: UiEventLogger
@Mock
private lateinit var pendingIntent: PendingIntent
@Captor
private lateinit var callbackCaptor: ArgumentCaptor<NextAlarmController.NextAlarmChangeCallback>
private lateinit var testableLooper: TestableLooper
private lateinit var tile: AlarmTile
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
testableLooper = TestableLooper.get(this)
`when`(qsHost.context).thenReturn(mContext)
`when`(qsHost.uiEventLogger).thenReturn(uiEventLogger)
tile = AlarmTile(
qsHost,
testableLooper.looper,
Handler(testableLooper.looper),
metricsLogger,
statusBarStateController,
activityStarter,
qsLogger,
featureFlags,
userTracker,
nextAlarmController
)
verify(nextAlarmController).observe(eq(tile), capture(callbackCaptor))
tile.refreshState()
testableLooper.processAllMessages()
}
@Test
fun testNotAvailableFeatureFlag() {
`when`(featureFlags.isAlarmTileAvailable).thenReturn(false)
assertThat(tile.isAvailable).isFalse()
}
@Test
fun testAvailableFeatureFlag() {
`when`(featureFlags.isAlarmTileAvailable).thenReturn(true)
assertThat(tile.isAvailable).isTrue()
}
@Test
fun testDoesntHandleLongClick() {
assertThat(tile.state.handlesLongClick).isFalse()
}
@Test
fun testInactiveByDefault() {
assertThat(tile.state.state).isEqualTo(Tile.STATE_INACTIVE)
}
@Test
fun testInactiveAfterNullNextAlarm() {
callbackCaptor.value.onNextAlarmChanged(null)
testableLooper.processAllMessages()
assertThat(tile.state.state).isEqualTo(Tile.STATE_INACTIVE)
}
@Test
fun testActivityStartedWhenNullNextAlarm() {
callbackCaptor.value.onNextAlarmChanged(null)
tile.click()
testableLooper.processAllMessages()
verify(activityStarter).postStartActivityDismissingKeyguard(tile.defaultIntent, 0)
}
@Test
fun testActiveAfterNextAlarm() {
val alarmInfo = AlarmManager.AlarmClockInfo(1L, pendingIntent)
callbackCaptor.value.onNextAlarmChanged(alarmInfo)
testableLooper.processAllMessages()
assertThat(tile.state.state).isEqualTo(Tile.STATE_ACTIVE)
}
@Test
fun testActivityStartedWhenNextAlarm() {
val alarmInfo = AlarmManager.AlarmClockInfo(1L, pendingIntent)
callbackCaptor.value.onNextAlarmChanged(alarmInfo)
tile.click()
testableLooper.processAllMessages()
verify(activityStarter).postStartActivityDismissingKeyguard(pendingIntent)
}
}