diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index b75a0bc1525a9..da58a3442e6f6 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -107,7 +107,7 @@ - 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 diff --git a/packages/SystemUI/res/values/flags.xml b/packages/SystemUI/res/values/flags.xml index e5518928c98cc..8cd5757247e21 100644 --- a/packages/SystemUI/res/values/flags.xml +++ b/packages/SystemUI/res/values/flags.xml @@ -46,4 +46,6 @@ false false + + false diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index cc5b44b680bf2..fcda80dd5f15b 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2861,4 +2861,7 @@ @*android:string/config_headlineFontFamily + + + No alarm set diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java index 6983b38489f66..9e5fe732cd0cb 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java @@ -30,6 +30,7 @@ import com.android.systemui.plugins.qs.QSTileView; import com.android.systemui.qs.QSHost; import com.android.systemui.qs.external.CustomTile; 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.BluetoothTile; import com.android.systemui.qs.tiles.CameraToggleTile; @@ -91,6 +92,7 @@ public class QSFactoryImpl implements QSFactory { private final Provider mCameraToggleTileProvider; private final Provider mMicrophoneToggleTileProvider; private final Provider mDeviceControlsTileProvider; + private final Provider mAlarmTileProvider; private final Lazy mQsHostLazy; private final Provider mCustomTileBuilderProvider; @@ -126,7 +128,8 @@ public class QSFactoryImpl implements QSFactory { Provider reduceBrightColorsTileProvider, Provider cameraToggleTileProvider, Provider microphoneToggleTileProvider, - Provider deviceControlsTileProvider) { + Provider deviceControlsTileProvider, + Provider alarmTileProvider) { mQsHostLazy = qsHostLazy; mCustomTileBuilderProvider = customTileBuilderProvider; @@ -157,6 +160,7 @@ public class QSFactoryImpl implements QSFactory { mCameraToggleTileProvider = cameraToggleTileProvider; mMicrophoneToggleTileProvider = microphoneToggleTileProvider; mDeviceControlsTileProvider = deviceControlsTileProvider; + mAlarmTileProvider = alarmTileProvider; } public QSTile createTile(String tileSpec) { @@ -218,6 +222,8 @@ public class QSFactoryImpl implements QSFactory { return mMicrophoneToggleTileProvider.get(); case "controls": return mDeviceControlsTileProvider.get(); + case "alarm": + return mAlarmTileProvider.get(); } // Custom tiles diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/AlarmTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/AlarmTile.kt new file mode 100644 index 0000000000000..dede627cd4dcb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/AlarmTile.kt @@ -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( + 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 + } +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/FeatureFlags.java b/packages/SystemUI/src/com/android/systemui/statusbar/FeatureFlags.java index 1d59257c9c4f6..c8e0d60ce304a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/FeatureFlags.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/FeatureFlags.java @@ -90,4 +90,8 @@ public class FeatureFlags { public boolean isPMLiteEnabled() { return mFlagReader.isEnabled(R.bool.flag_pm_lite); } + + public boolean isAlarmTileAvailable() { + return mFlagReader.isEnabled(R.bool.flag_alarm_tile); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/AlarmTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/AlarmTileTest.kt new file mode 100644 index 0000000000000..a9d10e9e68728 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/AlarmTileTest.kt @@ -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 + + 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) + } +} \ No newline at end of file