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
17package com.android.systemui.keyguard;
18
19import android.app.ActivityManager;
20import android.app.AlarmManager;
21import android.app.NotificationManager;
22import android.app.PendingIntent;
23import android.content.BroadcastReceiver;
24import android.content.ContentResolver;
25import android.content.Context;
26import android.content.Intent;
27import android.content.IntentFilter;
28import android.graphics.drawable.Icon;
29import android.icu.text.DateFormat;
30import android.icu.text.DisplayContext;
31import android.net.Uri;
32import android.os.Handler;
33import android.provider.Settings;
34import android.service.notification.ZenModeConfig;
35import android.text.TextUtils;
36
37import com.android.internal.annotations.VisibleForTesting;
38import com.android.systemui.R;
39import com.android.systemui.statusbar.policy.NextAlarmController;
40import com.android.systemui.statusbar.policy.NextAlarmControllerImpl;
41import com.android.systemui.statusbar.policy.ZenModeController;
42import com.android.systemui.statusbar.policy.ZenModeControllerImpl;
43
44import java.util.Date;
45import java.util.Locale;
46import java.util.concurrent.TimeUnit;
47
48import androidx.slice.Slice;
49import androidx.slice.SliceProvider;
50import androidx.slice.builders.ListBuilder;
51import androidx.slice.builders.ListBuilder.RowBuilder;
52import androidx.slice.builders.SliceAction;
53
54/**
55 * Simple Slice provider that shows the current date.
56 */
57public 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
115    public KeyguardSliceProvider() {
116        this(new Handler());
117    }
118
119    @VisibleForTesting
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
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
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
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     */
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     */
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
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
202    public void onZenChanged(int zen) {
203        mContentResolver.notifyChange(mSliceUri, null /* observer */);
204    }
205
206    @Override
207    public void onConfigChanged(ZenModeConfig config) {
208        mContentResolver.notifyChange(mSliceUri, null /* observer */);
209    }
210
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
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     */
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
252    boolean isRegistered() {
253        return mRegistered;
254    }
255
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
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
276    void cleanDateFormat() {
277        mDateFormat = null;
278    }
279
280    @Override
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