1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package com.android.systemui.keyguard;
18 
19 import android.app.ActivityManager;
20 import android.app.AlarmManager;
21 import android.app.NotificationManager;
22 import android.app.PendingIntent;
23 import android.content.BroadcastReceiver;
24 import android.content.ContentResolver;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.IntentFilter;
28 import android.graphics.drawable.Icon;
29 import android.icu.text.DateFormat;
30 import android.icu.text.DisplayContext;
31 import android.net.Uri;
32 import android.os.Handler;
33 import android.provider.Settings;
34 import android.service.notification.ZenModeConfig;
35 import android.text.TextUtils;
36 
37 import com.android.internal.annotations.VisibleForTesting;
38 import com.android.systemui.R;
39 import com.android.systemui.statusbar.policy.NextAlarmController;
40 import com.android.systemui.statusbar.policy.NextAlarmControllerImpl;
41 import com.android.systemui.statusbar.policy.ZenModeController;
42 import com.android.systemui.statusbar.policy.ZenModeControllerImpl;
43 
44 import java.util.Date;
45 import java.util.Locale;
46 import java.util.concurrent.TimeUnit;
47 
48 import androidx.slice.Slice;
49 import androidx.slice.SliceProvider;
50 import androidx.slice.builders.ListBuilder;
51 import androidx.slice.builders.ListBuilder.RowBuilder;
52 import androidx.slice.builders.SliceAction;
53 
54 /**
55  * Simple Slice provider that shows the current date.
56  */
57 public class KeyguardSliceProvider extends SliceProvider implements
58         NextAlarmController.NextAlarmChangeCallback, ZenModeController.Callback {
59 
60     public static final String KEYGUARD_SLICE_URI = "content://com.android.systemui.keyguard/main";
61     public static final String KEYGUARD_DATE_URI = "content://com.android.systemui.keyguard/date";
62     public static final String KEYGUARD_NEXT_ALARM_URI =
63             "content://com.android.systemui.keyguard/alarm";
64     public static final String KEYGUARD_DND_URI = "content://com.android.systemui.keyguard/dnd";
65     public static final String KEYGUARD_ACTION_URI =
66             "content://com.android.systemui.keyguard/action";
67 
68     /**
69      * Only show alarms that will ring within N hours.
70      */
71     @VisibleForTesting
72     static final int ALARM_VISIBILITY_HOURS = 12;
73 
74     protected final Uri mSliceUri;
75     protected final Uri mDateUri;
76     protected final Uri mAlarmUri;
77     protected final Uri mDndUri;
78     private final Date mCurrentTime = new Date();
79     private final Handler mHandler;
80     private final AlarmManager.OnAlarmListener mUpdateNextAlarm = this::updateNextAlarm;
81     private ZenModeController mZenModeController;
82     private String mDatePattern;
83     private DateFormat mDateFormat;
84     private String mLastText;
85     private boolean mRegistered;
86     private String mNextAlarm;
87     private NextAlarmController mNextAlarmController;
88     protected AlarmManager mAlarmManager;
89     protected ContentResolver mContentResolver;
90     private AlarmManager.AlarmClockInfo mNextAlarmInfo;
91 
92     /**
93      * Receiver responsible for time ticking and updating the date format.
94      */
95     @VisibleForTesting
96     final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
97         @Override
98         public void onReceive(Context context, Intent intent) {
99             final String action = intent.getAction();
100             if (Intent.ACTION_TIME_TICK.equals(action)
101                     || Intent.ACTION_DATE_CHANGED.equals(action)
102                     || Intent.ACTION_TIME_CHANGED.equals(action)
103                     || Intent.ACTION_TIMEZONE_CHANGED.equals(action)
104                     || Intent.ACTION_LOCALE_CHANGED.equals(action)) {
105                 if (Intent.ACTION_LOCALE_CHANGED.equals(action)
106                         || Intent.ACTION_TIMEZONE_CHANGED.equals(action)) {
107                     // need to get a fresh date format
108                     mHandler.post(KeyguardSliceProvider.this::cleanDateFormat);
109                 }
110                 mHandler.post(KeyguardSliceProvider.this::updateClock);
111             }
112         }
113     };
114 
KeyguardSliceProvider()115     public KeyguardSliceProvider() {
116         this(new Handler());
117     }
118 
119     @VisibleForTesting
KeyguardSliceProvider(Handler handler)120     KeyguardSliceProvider(Handler handler) {
121         mHandler = handler;
122         mSliceUri = Uri.parse(KEYGUARD_SLICE_URI);
123         mDateUri = Uri.parse(KEYGUARD_DATE_URI);
124         mAlarmUri = Uri.parse(KEYGUARD_NEXT_ALARM_URI);
125         mDndUri = Uri.parse(KEYGUARD_DND_URI);
126     }
127 
128     @Override
onBindSlice(Uri sliceUri)129     public Slice onBindSlice(Uri sliceUri) {
130         ListBuilder builder = new ListBuilder(getContext(), mSliceUri);
131         builder.addRow(new RowBuilder(builder, mDateUri).setTitle(mLastText));
132         addNextAlarm(builder);
133         addZenMode(builder);
134         addPrimaryAction(builder);
135         return builder.build();
136     }
137 
addPrimaryAction(ListBuilder builder)138     protected void addPrimaryAction(ListBuilder builder) {
139         // Add simple action because API requires it; Keyguard handles presenting
140         // its own slices so this action + icon are actually never used.
141         PendingIntent pi = PendingIntent.getActivity(getContext(), 0, new Intent(), 0);
142         Icon icon = Icon.createWithResource(getContext(), R.drawable.ic_access_alarms_big);
143         SliceAction action = new SliceAction(pi, icon, mLastText);
144 
145         RowBuilder primaryActionRow = new RowBuilder(builder, Uri.parse(KEYGUARD_ACTION_URI))
146             .setPrimaryAction(action);
147         builder.addRow(primaryActionRow);
148     }
149 
addNextAlarm(ListBuilder builder)150     protected void addNextAlarm(ListBuilder builder) {
151         if (TextUtils.isEmpty(mNextAlarm)) {
152             return;
153         }
154 
155         Icon alarmIcon = Icon.createWithResource(getContext(), R.drawable.ic_access_alarms_big);
156         RowBuilder alarmRowBuilder = new RowBuilder(builder, mAlarmUri)
157                 .setTitle(mNextAlarm)
158                 .addEndItem(alarmIcon);
159         builder.addRow(alarmRowBuilder);
160     }
161 
162     /**
163      * Add zen mode (DND) icon to slice if it's enabled.
164      * @param builder The slice builder.
165      */
addZenMode(ListBuilder builder)166     protected void addZenMode(ListBuilder builder) {
167         if (!isDndSuppressingNotifications()) {
168             return;
169         }
170         RowBuilder dndBuilder = new RowBuilder(builder, mDndUri)
171                 .setContentDescription(getContext().getResources()
172                         .getString(R.string.accessibility_quick_settings_dnd))
173                 .addEndItem(Icon.createWithResource(getContext(), R.drawable.stat_sys_dnd));
174         builder.addRow(dndBuilder);
175     }
176 
177     /**
178      * Return true if DND is enabled suppressing notifications.
179      */
isDndSuppressingNotifications()180     protected boolean isDndSuppressingNotifications() {
181         boolean suppressingNotifications = (mZenModeController.getConfig().suppressedVisualEffects
182                 & NotificationManager.Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST) != 0;
183         return mZenModeController.getZen() != Settings.Global.ZEN_MODE_OFF
184                 && suppressingNotifications;
185     }
186 
187     @Override
onCreateSliceProvider()188     public boolean onCreateSliceProvider() {
189         mAlarmManager = getContext().getSystemService(AlarmManager.class);
190         mContentResolver = getContext().getContentResolver();
191         mNextAlarmController = new NextAlarmControllerImpl(getContext());
192         mNextAlarmController.addCallback(this);
193         mZenModeController = new ZenModeControllerImpl(getContext(), mHandler);
194         mZenModeController.addCallback(this);
195         mDatePattern = getContext().getString(R.string.system_ui_aod_date_pattern);
196         registerClockUpdate();
197         updateClock();
198         return true;
199     }
200 
201     @Override
onZenChanged(int zen)202     public void onZenChanged(int zen) {
203         mContentResolver.notifyChange(mSliceUri, null /* observer */);
204     }
205 
206     @Override
onConfigChanged(ZenModeConfig config)207     public void onConfigChanged(ZenModeConfig config) {
208         mContentResolver.notifyChange(mSliceUri, null /* observer */);
209     }
210 
updateNextAlarm()211     private void updateNextAlarm() {
212         if (withinNHours(mNextAlarmInfo, ALARM_VISIBILITY_HOURS)) {
213             String pattern = android.text.format.DateFormat.is24HourFormat(getContext(),
214                     ActivityManager.getCurrentUser()) ? "H:mm" : "h:mm";
215             mNextAlarm = android.text.format.DateFormat.format(pattern,
216                     mNextAlarmInfo.getTriggerTime()).toString();
217         } else {
218             mNextAlarm = "";
219         }
220         mContentResolver.notifyChange(mSliceUri, null /* observer */);
221     }
222 
withinNHours(AlarmManager.AlarmClockInfo alarmClockInfo, int hours)223     private boolean withinNHours(AlarmManager.AlarmClockInfo alarmClockInfo, int hours) {
224         if (alarmClockInfo == null) {
225             return false;
226         }
227 
228         long limit = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(hours);
229         return mNextAlarmInfo.getTriggerTime() <= limit;
230     }
231 
232     /**
233      * Registers a broadcast receiver for clock updates, include date, time zone and manually
234      * changing the date/time via the settings app.
235      */
registerClockUpdate()236     private void registerClockUpdate() {
237         if (mRegistered) {
238             return;
239         }
240 
241         IntentFilter filter = new IntentFilter();
242         filter.addAction(Intent.ACTION_DATE_CHANGED);
243         filter.addAction(Intent.ACTION_TIME_CHANGED);
244         filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
245         filter.addAction(Intent.ACTION_LOCALE_CHANGED);
246         getContext().registerReceiver(mIntentReceiver, filter, null /* permission*/,
247                 null /* scheduler */);
248         mRegistered = true;
249     }
250 
251     @VisibleForTesting
isRegistered()252     boolean isRegistered() {
253         return mRegistered;
254     }
255 
updateClock()256     protected void updateClock() {
257         final String text = getFormattedDate();
258         if (!text.equals(mLastText)) {
259             mLastText = text;
260             mContentResolver.notifyChange(mSliceUri, null /* observer */);
261         }
262     }
263 
getFormattedDate()264     protected String getFormattedDate() {
265         if (mDateFormat == null) {
266             final Locale l = Locale.getDefault();
267             DateFormat format = DateFormat.getInstanceForSkeleton(mDatePattern, l);
268             format.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE);
269             mDateFormat = format;
270         }
271         mCurrentTime.setTime(System.currentTimeMillis());
272         return mDateFormat.format(mCurrentTime);
273     }
274 
275     @VisibleForTesting
cleanDateFormat()276     void cleanDateFormat() {
277         mDateFormat = null;
278     }
279 
280     @Override
onNextAlarmChanged(AlarmManager.AlarmClockInfo nextAlarm)281     public void onNextAlarmChanged(AlarmManager.AlarmClockInfo nextAlarm) {
282         mNextAlarmInfo = nextAlarm;
283         mAlarmManager.cancel(mUpdateNextAlarm);
284 
285         long triggerAt = mNextAlarmInfo == null ? -1 : mNextAlarmInfo.getTriggerTime()
286                 - TimeUnit.HOURS.toMillis(ALARM_VISIBILITY_HOURS);
287         if (triggerAt > 0) {
288             mAlarmManager.setExact(AlarmManager.RTC, triggerAt, "lock_screen_next_alarm",
289                     mUpdateNextAlarm, mHandler);
290         }
291         updateNextAlarm();
292     }
293 }
294