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 
17 package com.android.alarmclock;
18 
19 import android.app.AlarmManager;
20 import android.app.PendingIntent;
21 import android.appwidget.AppWidgetManager;
22 import android.appwidget.AppWidgetProvider;
23 import android.appwidget.AppWidgetProviderInfo;
24 import android.content.ComponentName;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.net.Uri;
28 import android.os.Bundle;
29 import android.support.annotation.NonNull;
30 import android.text.TextUtils;
31 import android.text.format.DateFormat;
32 import android.util.Log;
33 import android.util.TypedValue;
34 import android.view.View;
35 import android.widget.RemoteViews;
36 
37 import com.android.deskclock.HandleDeskClockApiCalls;
38 import com.android.deskclock.R;
39 import com.android.deskclock.Utils;
40 import com.android.deskclock.alarms.AlarmStateManager;
41 import com.android.deskclock.data.DataModel;
42 import com.android.deskclock.worldclock.CitySelectionActivity;
43 
44 import java.util.Locale;
45 
46 public 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 
DigitalAppWidgetProvider()60     public DigitalAppWidgetProvider() {
61     }
62 
63     @Override
onEnabled(Context context)64     public void onEnabled(Context context) {
65         super.onEnabled(context);
66         startAlarmOnQuarterHour(context);
67     }
68 
69     @Override
onDisabled(Context context)70     public void onDisabled(Context context) {
71         super.onDisabled(context);
72         cancelAlarmOnQuarterHour(context);
73     }
74 
75     @Override
onReceive(@onNull Context context, @NonNull Intent intent)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
onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds)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
onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions)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      */
isNextAlarmChangedAction(String action)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 
updateClock( Context context, AppWidgetManager appWidgetManager, int appWidgetId, float ratio)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 
refreshDate(Context context, RemoteViews widget, float ratio)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 
refreshAlarm(Context context, RemoteViews widget, float ratio)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      */
startAlarmOnQuarterHour(Context context)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      */
cancelAlarmOnQuarterHour(Context context)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      */
getOnQuarterHourPendingIntent(Context context)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      */
getComponentName(Context context)308     private ComponentName getComponentName(Context context) {
309         if (mComponentName == null) {
310             mComponentName = new ComponentName(context, getClass());
311         }
312         return mComponentName;
313     }
314 }
315