Show next alarm on ambient display

Next alarm will be visible 12h before triggering.

Test: Set alarm that will ring in 8h
Test: Set alarm that will ring in 14h
Test: Set alarm that will ring in 11:59, wait one minute
Test: atest packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java
Change-Id: Icd4253771efcdf5afb4e9e52329fa410d7fd1cc1
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java
index b54d09a..2adb286 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java
@@ -167,7 +167,8 @@
             }
             mClickActions.put(button, pendingIntent);
 
-            button.setText(rc.getTitleItem().getText());
+            final SliceItem titleItem = rc.getTitleItem();
+            button.setText(titleItem == null ? null : titleItem.getText());
 
             Drawable iconDrawable = null;
             SliceItem icon = SliceQuery.find(item.getSlice(),
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java
index c7d276c..26618bf 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java
@@ -19,6 +19,7 @@
 import android.app.ActivityManager;
 import android.app.AlarmManager;
 import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
@@ -27,6 +28,7 @@
 import android.icu.text.DisplayContext;
 import android.net.Uri;
 import android.os.Handler;
+import android.os.SystemClock;
 import android.text.TextUtils;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -36,6 +38,7 @@
 
 import java.util.Date;
 import java.util.Locale;
+import java.util.concurrent.TimeUnit;
 
 import androidx.app.slice.Slice;
 import androidx.app.slice.SliceProvider;
@@ -53,6 +56,12 @@
     public static final String KEYGUARD_NEXT_ALARM_URI =
             "content://com.android.systemui.keyguard/alarm";
 
+    /**
+     * Only show alarms that will ring within N hours.
+     */
+    @VisibleForTesting
+    static final int ALARM_VISIBILITY_HOURS = 12;
+
     private final Date mCurrentTime = new Date();
     protected final Uri mSliceUri;
     protected final Uri mDateUri;
@@ -65,6 +74,10 @@
     private boolean mRegisteredEveryMinute;
     private String mNextAlarm;
     private NextAlarmController mNextAlarmController;
+    protected AlarmManager mAlarmManager;
+    protected ContentResolver mContentResolver;
+    private AlarmManager.AlarmClockInfo mNextAlarmInfo;
+    private final AlarmManager.OnAlarmListener mUpdateNextAlarm = this::updateNextAlarm;
 
     /**
      * Receiver responsible for time ticking and updating the date format.
@@ -105,17 +118,26 @@
     public Slice onBindSlice(Uri sliceUri) {
         ListBuilder builder = new ListBuilder(getContext(), mSliceUri);
         builder.addRow(new RowBuilder(builder, mDateUri).setTitle(mLastText));
-        if (!TextUtils.isEmpty(mNextAlarm)) {
-            Icon icon = Icon.createWithResource(getContext(), R.drawable.ic_access_alarms_big);
-            builder.addRow(new RowBuilder(builder, mAlarmUri)
-                    .setTitle(mNextAlarm).addEndItem(icon));
+        addNextAlarm(builder);
+        return builder.build();
+    }
+
+    protected void addNextAlarm(ListBuilder builder) {
+        if (TextUtils.isEmpty(mNextAlarm)) {
+            return;
         }
 
-        return builder.build();
+        Icon alarmIcon = Icon.createWithResource(getContext(), R.drawable.ic_access_alarms_big);
+        RowBuilder alarmRowBuilder = new RowBuilder(builder, mAlarmUri)
+                .setTitle(mNextAlarm)
+                .addEndItem(alarmIcon);
+        builder.addRow(alarmRowBuilder);
     }
 
     @Override
     public boolean onCreateSliceProvider() {
+        mAlarmManager = getContext().getSystemService(AlarmManager.class);
+        mContentResolver = getContext().getContentResolver();
         mNextAlarmController = new NextAlarmControllerImpl(getContext());
         mNextAlarmController.addCallback(this);
         mDatePattern = getContext().getString(R.string.system_ui_aod_date_pattern);
@@ -124,15 +146,25 @@
         return true;
     }
 
-    public static String formatNextAlarm(Context context, AlarmManager.AlarmClockInfo info) {
-        if (info == null) {
-            return "";
+    private void updateNextAlarm() {
+        if (withinNHours(mNextAlarmInfo, ALARM_VISIBILITY_HOURS)) {
+            String pattern = android.text.format.DateFormat.is24HourFormat(getContext(),
+                    ActivityManager.getCurrentUser()) ? "H:mm" : "h:mm";
+            mNextAlarm = android.text.format.DateFormat.format(pattern,
+                    mNextAlarmInfo.getTriggerTime()).toString();
+        } else {
+            mNextAlarm = "";
         }
-        String skeleton = android.text.format.DateFormat
-                .is24HourFormat(context, ActivityManager.getCurrentUser()) ? "EHm" : "Ehma";
-        String pattern = android.text.format.DateFormat
-                .getBestDateTimePattern(Locale.getDefault(), skeleton);
-        return android.text.format.DateFormat.format(pattern, info.getTriggerTime()).toString();
+        mContentResolver.notifyChange(mSliceUri, null /* observer */);
+    }
+
+    private boolean withinNHours(AlarmManager.AlarmClockInfo alarmClockInfo, int hours) {
+        if (alarmClockInfo == null) {
+            return false;
+        }
+
+        long limit = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(hours);
+        return mNextAlarmInfo.getTriggerTime() <= limit;
     }
 
     /**
@@ -181,7 +213,7 @@
         final String text = getFormattedDate();
         if (!text.equals(mLastText)) {
             mLastText = text;
-            getContext().getContentResolver().notifyChange(mSliceUri, null /* observer */);
+            mContentResolver.notifyChange(mSliceUri, null /* observer */);
         }
     }
 
@@ -203,7 +235,15 @@
 
     @Override
     public void onNextAlarmChanged(AlarmManager.AlarmClockInfo nextAlarm) {
-        mNextAlarm = formatNextAlarm(getContext(), nextAlarm);
-        getContext().getContentResolver().notifyChange(mSliceUri, null /* observer */);
+        mNextAlarmInfo = nextAlarm;
+        mAlarmManager.cancel(mUpdateNextAlarm);
+
+        long triggerAt = mNextAlarmInfo == null ? -1 : mNextAlarmInfo.getTriggerTime()
+                - TimeUnit.HOURS.toMillis(ALARM_VISIBILITY_HOURS);
+        if (triggerAt > 0) {
+            mAlarmManager.setExact(AlarmManager.RTC, triggerAt, "lock_screen_next_alarm",
+                    mUpdateNextAlarm, mHandler);
+        }
+        updateNextAlarm();
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
index 78481d3..2151436 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
@@ -15,10 +15,10 @@
 package com.android.systemui.qs;
 
 import static android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS;
-import static com.android.systemui.keyguard.KeyguardSliceProvider.formatNextAlarm;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
+import android.app.ActivityManager;
 import android.app.AlarmManager;
 import android.content.Context;
 import android.content.Intent;
@@ -51,6 +51,8 @@
 import com.android.systemui.statusbar.policy.DarkIconDispatcher.DarkReceiver;
 import com.android.systemui.statusbar.policy.NextAlarmController;
 
+import java.util.Locale;
+
 /**
  * View that contains the top-most bits of the screen (primarily the status bar with date, time, and
  * battery) and also contains the {@link QuickQSPanel} along with some of the panel's inner
@@ -289,7 +291,7 @@
 
     @Override
     public void onNextAlarmChanged(AlarmManager.AlarmClockInfo nextAlarm) {
-        mNextAlarmText = nextAlarm != null ? formatNextAlarm(mContext, nextAlarm) : null;
+        mNextAlarmText = nextAlarm != null ? formatNextAlarm(nextAlarm) : null;
         if (mNextAlarmText != null) {
             hideLongPressTooltip(true /* shouldFadeInAlarmText */);
         } else {
@@ -430,4 +432,15 @@
     public void setCallback(Callback qsPanelCallback) {
         mHeaderQsPanel.setCallback(qsPanelCallback);
     }
+
+    private String formatNextAlarm(AlarmManager.AlarmClockInfo info) {
+        if (info == null) {
+            return "";
+        }
+        String skeleton = android.text.format.DateFormat
+                .is24HourFormat(mContext, ActivityManager.getCurrentUser()) ? "EHm" : "Ehma";
+        String pattern = android.text.format.DateFormat
+                .getBestDateTimePattern(Locale.getDefault(), skeleton);
+        return android.text.format.DateFormat.format(pattern, info.getTriggerTime()).toString();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java
index cd409d8..b6116e0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java
@@ -16,10 +16,17 @@
 
 package com.android.systemui.keyguard;
 
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+
 import androidx.app.slice.Slice;
+
+import android.app.AlarmManager;
+import android.content.ContentResolver;
 import android.content.Intent;
 import android.net.Uri;
-import android.os.Debug;
 import android.os.Handler;
 import android.support.test.filters.SmallTest;
 import android.testing.AndroidTestingRunner;
@@ -32,24 +39,31 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
 
 import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
 
 import androidx.app.slice.SliceItem;
 import androidx.app.slice.SliceProvider;
 import androidx.app.slice.SliceSpecs;
 import androidx.app.slice.core.SliceQuery;
-import androidx.app.slice.widget.SliceLiveData;
 
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @RunWithLooper(setAsMainLooper = true)
 public class KeyguardSliceProviderTest extends SysuiTestCase {
 
+    @Mock
+    private ContentResolver mContentResolver;
+    @Mock
+    private AlarmManager mAlarmManager;
     private TestableKeyguardSliceProvider mProvider;
 
     @Before
     public void setup() {
+        MockitoAnnotations.initMocks(this);
         mProvider = new TestableKeyguardSliceProvider();
         mProvider.attachInfo(getContext(), null);
         SliceProvider.setSpecs(Arrays.asList(SliceSpecs.LIST));
@@ -70,7 +84,7 @@
 
     @Test
     public void returnsValidSlice() {
-        Slice slice = mProvider.onBindSlice(Uri.parse(KeyguardSliceProvider.KEYGUARD_SLICE_URI));
+        Slice slice = mProvider.onBindSlice(mProvider.getUri());
         SliceItem text = SliceQuery.find(slice, android.app.slice.SliceItem.FORMAT_TEXT,
                 android.app.slice.Slice.HINT_TITLE,
                 null /* nonHints */);
@@ -87,21 +101,52 @@
 
     @Test
     public void updatesClock() {
-        mProvider.mUpdateClockInvokations = 0;
         mProvider.mIntentReceiver.onReceive(getContext(), new Intent(Intent.ACTION_TIME_TICK));
         TestableLooper.get(this).processAllMessages();
-        Assert.assertEquals("Clock should have been updated.", 1 /* expected */,
-                mProvider.mUpdateClockInvokations);
+        verify(mContentResolver).notifyChange(eq(mProvider.getUri()), eq(null));
+    }
+
+    @Test
+    public void schedulesAlarm12hBefore() {
+        long in16Hours = System.currentTimeMillis() + TimeUnit.HOURS.toHours(16);
+        AlarmManager.AlarmClockInfo alarmClockInfo = new AlarmManager.AlarmClockInfo(in16Hours, null);
+        mProvider.onNextAlarmChanged(alarmClockInfo);
+
+        long twelveHours = TimeUnit.HOURS.toMillis(KeyguardSliceProvider.ALARM_VISIBILITY_HOURS);
+        long triggerAt = in16Hours - twelveHours;
+        verify(mAlarmManager).setExact(eq(AlarmManager.RTC), eq(triggerAt), anyString(), any(),
+                any());
+    }
+
+    @Test
+    public void updatingNextAlarmInvalidatesSlice() {
+        long in16Hours = System.currentTimeMillis() + TimeUnit.HOURS.toHours(8);
+        AlarmManager.AlarmClockInfo alarmClockInfo = new AlarmManager.AlarmClockInfo(in16Hours, null);
+        mProvider.onNextAlarmChanged(alarmClockInfo);
+
+        verify(mContentResolver).notifyChange(eq(mProvider.getUri()), eq(null));
     }
 
     private class TestableKeyguardSliceProvider extends KeyguardSliceProvider {
         int mCleanDateFormatInvokations;
-        int mUpdateClockInvokations;
+        private int mCounter;
 
         TestableKeyguardSliceProvider() {
             super(new Handler(TestableLooper.get(KeyguardSliceProviderTest.this).getLooper()));
         }
 
+        Uri getUri() {
+            return mSliceUri;
+        }
+
+        @Override
+        public boolean onCreateSliceProvider() {
+            super.onCreateSliceProvider();
+            mAlarmManager = KeyguardSliceProviderTest.this.mAlarmManager;
+            mContentResolver = KeyguardSliceProviderTest.this.mContentResolver;
+            return true;
+        }
+
         @Override
         void cleanDateFormat() {
             super.cleanDateFormat();
@@ -109,9 +154,8 @@
         }
 
         @Override
-        protected void updateClock() {
-            super.updateClock();
-            mUpdateClockInvokations++;
+        protected String getFormattedDate() {
+            return super.getFormattedDate() + mCounter++;
         }
     }