1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * 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, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package com.android.alarmclock;
18
19import android.app.AlarmManager;
20import android.app.PendingIntent;
21import android.appwidget.AppWidgetManager;
22import android.appwidget.AppWidgetProvider;
23import android.appwidget.AppWidgetProviderInfo;
24import android.content.ComponentName;
25import android.content.Context;
26import android.content.Intent;
27import android.net.Uri;
28import android.os.Bundle;
29import android.support.annotation.NonNull;
30import android.text.TextUtils;
31import android.text.format.DateFormat;
32import android.util.Log;
33import android.util.TypedValue;
34import android.view.View;
35import android.widget.RemoteViews;
36
37import com.android.deskclock.HandleDeskClockApiCalls;
38import com.android.deskclock.R;
39import com.android.deskclock.Utils;
40import com.android.deskclock.alarms.AlarmStateManager;
41import com.android.deskclock.data.DataModel;
42import com.android.deskclock.worldclock.CitySelectionActivity;
43
44import java.util.Locale;
45
46public class DigitalAppWidgetProvider extends AppWidgetProvider {
47    private static final String TAG = "DigAppWidgetProvider";
48
49    /**
50     * Intent to be used for checking if a world clock's date has changed. Must be every fifteen
51     * minutes because not all time zones are hour-locked.
52     **/
53    public static final String ACTION_ON_QUARTER_HOUR = "com.android.deskclock.ON_QUARTER_HOUR";
54
55    // Lazily creating this intent to use with the AlarmManager
56    private PendingIntent mPendingIntent;
57    // Lazily creating this name to use with the AppWidgetManager
58    private ComponentName mComponentName;
59
60    public DigitalAppWidgetProvider() {
61    }
62
63    @Override
64    public void onEnabled(Context context) {
65        super.onEnabled(context);
66        startAlarmOnQuarterHour(context);
67    }
68
69    @Override
70    public void onDisabled(Context context) {
71        super.onDisabled(context);
72        cancelAlarmOnQuarterHour(context);
73    }
74
75    @Override
76    public void onReceive(@NonNull Context context, @NonNull Intent intent) {
77        String action = intent.getAction();
78        if (DigitalAppWidgetService.LOGGING) {
79            Log.i(TAG, "onReceive: " + action);
80        }
81        super.onReceive(context, intent);
82
83        if (ACTION_ON_QUARTER_HOUR.equals(action)
84                || Intent.ACTION_DATE_CHANGED.equals(action)
85                || Intent.ACTION_TIMEZONE_CHANGED.equals(action)
86                || Intent.ACTION_TIME_CHANGED.equals(action)
87                || Intent.ACTION_LOCALE_CHANGED.equals(action)) {
88            AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
89            if (appWidgetManager != null) {
90                int[] appWidgetIds = appWidgetManager.getAppWidgetIds(getComponentName(context));
91                for (int appWidgetId : appWidgetIds) {
92                    appWidgetManager.
93                            notifyAppWidgetViewDataChanged(appWidgetId,
94                                    R.id.digital_appwidget_listview);
95                    RemoteViews widget = new RemoteViews(context.getPackageName(),
96                            R.layout.digital_appwidget);
97                    float ratio = WidgetUtils.getScaleRatio(context, null, appWidgetId);
98                    WidgetUtils.setTimeFormat(context, widget, false /* showAmPm */,
99                            R.id.the_clock);
100                    WidgetUtils.setClockSize(context, widget, ratio);
101                    refreshAlarm(context, widget, ratio);
102                    appWidgetManager.partiallyUpdateAppWidget(appWidgetId, widget);
103                }
104            }
105            if(!ACTION_ON_QUARTER_HOUR.equals(action)) {
106                cancelAlarmOnQuarterHour(context);
107            }
108            startAlarmOnQuarterHour(context);
109        } else if (isNextAlarmChangedAction(action)
110                || Intent.ACTION_SCREEN_ON.equals(action)
111                || DataModel.ACTION_DIGITAL_WIDGET_CHANGED.equals(action)) {
112            AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
113            if (appWidgetManager != null) {
114                int[] appWidgetIds = appWidgetManager.getAppWidgetIds(getComponentName(context));
115                for (int appWidgetId : appWidgetIds) {
116                    final float ratio = WidgetUtils.getScaleRatio(context, null, appWidgetId);
117                    updateClock(context, appWidgetManager, appWidgetId, ratio);
118                }
119            }
120        }
121    }
122
123    @Override
124    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
125        if (DigitalAppWidgetService.LOGGING) {
126            Log.i(TAG, "onUpdate");
127        }
128        for (int appWidgetId : appWidgetIds) {
129            float ratio = WidgetUtils.getScaleRatio(context, null, appWidgetId);
130            updateClock(context, appWidgetManager, appWidgetId, ratio);
131        }
132        startAlarmOnQuarterHour(context);
133        super.onUpdate(context, appWidgetManager, appWidgetIds);
134    }
135
136    @Override
137    public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager,
138            int appWidgetId, Bundle newOptions) {
139        // scale the fonts of the clock to fit inside the new size
140        float ratio = WidgetUtils.getScaleRatio(context, newOptions, appWidgetId);
141        AppWidgetManager widgetManager = AppWidgetManager.getInstance(context);
142        updateClock(context, widgetManager, appWidgetId, ratio);
143    }
144
145    /**
146     * Determine whether action received corresponds to a "next alarm" changed action depending
147     * on the SDK version.
148     */
149    private boolean isNextAlarmChangedAction(String action) {
150        final String nextAlarmIntentAction;
151        if (Utils.isLOrLater()) {
152            nextAlarmIntentAction = AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED;
153        } else {
154            nextAlarmIntentAction = AlarmStateManager.SYSTEM_ALARM_CHANGE_ACTION;
155        }
156        return nextAlarmIntentAction.equals(action);
157    }
158
159    private void updateClock(
160            Context context, AppWidgetManager appWidgetManager, int appWidgetId, float ratio) {
161        RemoteViews widget = new RemoteViews(context.getPackageName(), R.layout.digital_appwidget);
162
163        // Launch clock when clicking on the time in the widget only if not a lock screen widget
164        Bundle newOptions = appWidgetManager.getAppWidgetOptions(appWidgetId);
165        if (newOptions != null &&
166                newOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY, -1)
167                != AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD) {
168            final Intent showClock = new Intent(HandleDeskClockApiCalls.ACTION_SHOW_CLOCK)
169                    .putExtra(HandleDeskClockApiCalls.EXTRA_FROM_WIDGET, true);
170            final PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, showClock, 0);
171            widget.setOnClickPendingIntent(R.id.digital_appwidget, pendingIntent);
172        }
173
174        // Setup formats and font sizes
175        refreshDate(context, widget, ratio);
176        refreshAlarm(context, widget, ratio);
177        WidgetUtils.setTimeFormat(context, widget, false /* showAmPm */, R.id.the_clock);
178        WidgetUtils.setClockSize(context, widget, ratio);
179
180        // Set up R.id.digital_appwidget_listview to use a remote views adapter
181        // That remote views adapter connects to a RemoteViewsService through intent.
182        final Intent intent = new Intent(context, DigitalAppWidgetService.class);
183        intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
184        intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
185        widget.setRemoteAdapter(R.id.digital_appwidget_listview, intent);
186
187        // Set up the click on any world clock to start the Cities Activity
188        //TODO: Should this be in the options guard above?
189        final Intent selectCitiesIntent = new Intent(context, CitySelectionActivity.class);
190        widget.setPendingIntentTemplate(R.id.digital_appwidget_listview,
191                PendingIntent.getActivity(context, 0, selectCitiesIntent, 0));
192
193        // Refresh the widget
194        appWidgetManager.notifyAppWidgetViewDataChanged(
195                appWidgetId, R.id.digital_appwidget_listview);
196        appWidgetManager.updateAppWidget(appWidgetId, widget);
197    }
198
199    private void refreshDate(Context context, RemoteViews widget, float ratio) {
200        if (ratio < 1) {
201            // The time text normally has a negative bottom margin to reduce the space between the
202            // time and the date. When we scale down they overlap, so give the date a positive
203            // top padding.
204            final float padding = (1 - ratio) *
205                    -context.getResources().getDimension(R.dimen.bottom_text_spacing_digital);
206            widget.setViewPadding(R.id.date_and_alarm, 0, (int) padding, 0, 0);
207        }
208
209        // Set today's date format
210        final Locale locale = Locale.getDefault();
211        final String skeleton = context.getString(R.string.abbrev_wday_abbrev_month_day_no_year);
212        final CharSequence timeFormat = DateFormat.getBestDateTimePattern(locale, skeleton);
213        widget.setCharSequence(R.id.date, "setFormat12Hour", timeFormat);
214        widget.setCharSequence(R.id.date, "setFormat24Hour", timeFormat);
215        final float fontSize = context.getResources().getDimension(R.dimen.widget_label_font_size);
216        widget.setTextViewTextSize(R.id.date, TypedValue.COMPLEX_UNIT_PX, fontSize * ratio);
217    }
218
219    protected void refreshAlarm(Context context, RemoteViews widget, float ratio) {
220        final String nextAlarm = Utils.getNextAlarm(context);
221        if (!TextUtils.isEmpty(nextAlarm)) {
222            final float fontSize =
223                    context.getResources().getDimension(R.dimen.widget_label_font_size);
224            widget.setTextViewTextSize(
225                    R.id.nextAlarm, TypedValue.COMPLEX_UNIT_PX, fontSize * ratio);
226
227            int alarmDrawableResId;
228            if (ratio < .72f) {
229                alarmDrawableResId = R.drawable.ic_alarm_small_12dp;
230            }
231            else if (ratio < .95f) {
232                alarmDrawableResId = R.drawable.ic_alarm_small_18dp;
233            }
234            else {
235                alarmDrawableResId = R.drawable.ic_alarm_small_24dp;
236            }
237            widget.setTextViewCompoundDrawablesRelative(
238                    R.id.nextAlarm, alarmDrawableResId, 0, 0, 0);
239
240            widget.setTextViewText(R.id.nextAlarm, nextAlarm);
241            widget.setViewVisibility(R.id.nextAlarm, View.VISIBLE);
242            if (DigitalAppWidgetService.LOGGING) {
243                Log.v(TAG, "DigitalWidget sets next alarm string to " + nextAlarm);
244            }
245        } else  {
246            widget.setViewVisibility(R.id.nextAlarm, View.GONE);
247            if (DigitalAppWidgetService.LOGGING) {
248                Log.v(TAG, "DigitalWidget sets next alarm string to null");
249            }
250        }
251    }
252
253    /**
254     * Start an alarm that fires on the next quarter hour to update the world clock city
255     * day when the local time or the world city crosses midnight.
256     *
257     * @param context The context in which the PendingIntent should perform the broadcast.
258     */
259    private void startAlarmOnQuarterHour(Context context) {
260        if (context != null) {
261            final long onQuarterHour = Utils.getAlarmOnQuarterHour();
262            final PendingIntent quarterlyIntent = getOnQuarterHourPendingIntent(context);
263            final AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
264            am.setExact(AlarmManager.RTC, onQuarterHour, quarterlyIntent);
265            if (DigitalAppWidgetService.LOGGING) {
266                Log.v(TAG, "startAlarmOnQuarterHour " + context.toString());
267            }
268        }
269    }
270
271
272    /**
273     * Remove the alarm for the quarter hour update.
274     *
275     * @param context The context in which the PendingIntent was started to perform the broadcast.
276     */
277    public void cancelAlarmOnQuarterHour(Context context) {
278        if (context != null) {
279            PendingIntent quarterlyIntent = getOnQuarterHourPendingIntent(context);
280            if (DigitalAppWidgetService.LOGGING) {
281                Log.v(TAG, "cancelAlarmOnQuarterHour " + context.toString());
282            }
283            ((AlarmManager) context.getSystemService(Context.ALARM_SERVICE)).cancel(
284                    quarterlyIntent);
285        }
286    }
287
288    /**
289     * Create the pending intent that is broadcast on the quarter hour.
290     *
291     * @param context The Context in which this PendingIntent should perform the broadcast.
292     * @return a pending intent with an intent unique to DigitalAppWidgetProvider
293     */
294    private PendingIntent getOnQuarterHourPendingIntent(Context context) {
295        if (mPendingIntent == null) {
296            mPendingIntent = PendingIntent.getBroadcast(context, 0,
297                new Intent(ACTION_ON_QUARTER_HOUR), PendingIntent.FLAG_CANCEL_CURRENT);
298        }
299        return mPendingIntent;
300    }
301
302    /**
303     * Create the component name for this class
304     *
305     * @param context The Context in which the widgets for this component are created
306     * @return the ComponentName unique to DigitalAppWidgetProvider
307     */
308    private ComponentName getComponentName(Context context) {
309        if (mComponentName == null) {
310            mComponentName = new ComponentName(context, getClass());
311        }
312        return mComponentName;
313    }
314}
315