/* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.android.alarmclock; import android.annotation.SuppressLint; import android.app.AlarmManager; import android.app.PendingIntent; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProvider; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; import android.support.annotation.NonNull; import android.text.TextUtils; import android.text.format.DateFormat; import android.util.ArraySet; import android.view.LayoutInflater; import android.view.View; import android.widget.RemoteViews; import android.widget.TextClock; import android.widget.TextView; import com.android.deskclock.DeskClock; import com.android.deskclock.LogUtils; import com.android.deskclock.R; import com.android.deskclock.Utils; import com.android.deskclock.data.City; import com.android.deskclock.data.DataModel; import com.android.deskclock.uidata.UiDataModel; import com.android.deskclock.worldclock.CitySelectionActivity; import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.TimeZone; import static android.app.AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED; import static android.app.PendingIntent.FLAG_NO_CREATE; import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT; import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH; import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT; import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH; import static android.content.Intent.ACTION_DATE_CHANGED; import static android.content.Intent.ACTION_LOCALE_CHANGED; import static android.content.Intent.ACTION_SCREEN_ON; import static android.content.Intent.ACTION_TIMEZONE_CHANGED; import static android.content.Intent.ACTION_TIME_CHANGED; import static android.util.TypedValue.COMPLEX_UNIT_PX; import static android.view.View.GONE; import static android.view.View.MeasureSpec.UNSPECIFIED; import static android.view.View.VISIBLE; import static com.android.deskclock.alarms.AlarmStateManager.ACTION_ALARM_CHANGED; import static com.android.deskclock.data.DataModel.ACTION_WORLD_CITIES_CHANGED; import static java.lang.Math.max; import static java.lang.Math.round; /** *
This provider produces a widget resembling one of the formats below.
* * If an alarm is scheduled to ring in the future: ** 12:59 AM * WED, FEB 3 ⏰ THU 9:30 AM ** * If no alarm is scheduled to ring in the future: *
* 12:59 AM * WED, FEB 3 ** * This widget is scaling the font sizes to fit within the widget bounds chosen by the user without * any clipping. To do so it measures layouts offscreen using a range of font sizes in order to * choose optimal values. */ public class DigitalAppWidgetProvider extends AppWidgetProvider { private static final LogUtils.Logger LOGGER = new LogUtils.Logger("DigitalWidgetProvider"); /** * Intent action used for refreshing a world city display when any of them changes days or when * the default TimeZone changes days. This affects the widget display because the day-of-week is * only visible when the world city day-of-week differs from the default TimeZone's day-of-week. */ private static final String ACTION_ON_DAY_CHANGE = "com.android.deskclock.ON_DAY_CHANGE"; /** Intent used to deliver the {@link #ACTION_ON_DAY_CHANGE} callback. */ private static final Intent DAY_CHANGE_INTENT = new Intent(ACTION_ON_DAY_CHANGE); @Override public void onEnabled(Context context) { super.onEnabled(context); // Schedule the day-change callback if necessary. updateDayChangeCallback(context); } @Override public void onDisabled(Context context) { super.onDisabled(context); // Remove any scheduled day-change callback. removeDayChangeCallback(context); } @Override public void onReceive(@NonNull Context context, @NonNull Intent intent) { LOGGER.i("onReceive: " + intent); super.onReceive(context, intent); final AppWidgetManager wm = AppWidgetManager.getInstance(context); if (wm == null) { return; } final ComponentName provider = new ComponentName(context, getClass()); final int[] widgetIds = wm.getAppWidgetIds(provider); final String action = intent.getAction(); switch (action) { case ACTION_NEXT_ALARM_CLOCK_CHANGED: case ACTION_DATE_CHANGED: case ACTION_LOCALE_CHANGED: case ACTION_SCREEN_ON: case ACTION_TIME_CHANGED: case ACTION_TIMEZONE_CHANGED: case ACTION_ALARM_CHANGED: case ACTION_ON_DAY_CHANGE: case ACTION_WORLD_CITIES_CHANGED: for (int widgetId : widgetIds) { relayoutWidget(context, wm, widgetId, wm.getAppWidgetOptions(widgetId)); } } final DataModel dm = DataModel.getDataModel(); dm.updateWidgetCount(getClass(), widgetIds.length, R.string.category_digital_widget); if (widgetIds.length > 0) { updateDayChangeCallback(context); } } /** * Called when widgets must provide remote views. */ @Override public void onUpdate(Context context, AppWidgetManager wm, int[] widgetIds) { super.onUpdate(context, wm, widgetIds); for (int widgetId : widgetIds) { relayoutWidget(context, wm, widgetId, wm.getAppWidgetOptions(widgetId)); } } /** * Called when the app widget changes sizes. */ @Override public void onAppWidgetOptionsChanged(Context context, AppWidgetManager wm, int widgetId, Bundle options) { super.onAppWidgetOptionsChanged(context, wm, widgetId, options); // scale the fonts of the clock to fit inside the new size relayoutWidget(context, AppWidgetManager.getInstance(context), widgetId, options); } /** * Compute optimal font and icon sizes offscreen for both portrait and landscape orientations * using the last known widget size and apply them to the widget. */ private static void relayoutWidget(Context context, AppWidgetManager wm, int widgetId, Bundle options) { final RemoteViews portrait = relayoutWidget(context, wm, widgetId, options, true); final RemoteViews landscape = relayoutWidget(context, wm, widgetId, options, false); final RemoteViews widget = new RemoteViews(landscape, portrait); wm.updateAppWidget(widgetId, widget); wm.notifyAppWidgetViewDataChanged(widgetId, R.id.world_city_list); } /** * Compute optimal font and icon sizes offscreen for the given orientation. */ private static RemoteViews relayoutWidget(Context context, AppWidgetManager wm, int widgetId, Bundle options, boolean portrait) { // Create a remote view for the digital clock. final String packageName = context.getPackageName(); final RemoteViews rv = new RemoteViews(packageName, R.layout.digital_widget); // Tapping on the widget opens the app (if not on the lock screen). if (Utils.isWidgetClickable(wm, widgetId)) { final Intent openApp = new Intent(context, DeskClock.class); final PendingIntent pi = PendingIntent.getActivity(context, 0, openApp, 0); rv.setOnClickPendingIntent(R.id.digital_widget, pi); } // Configure child views of the remote view. final CharSequence dateFormat = getDateFormat(context); rv.setCharSequence(R.id.date, "setFormat12Hour", dateFormat); rv.setCharSequence(R.id.date, "setFormat24Hour", dateFormat); final String nextAlarmTime = Utils.getNextAlarm(context); if (TextUtils.isEmpty(nextAlarmTime)) { rv.setViewVisibility(R.id.nextAlarm, GONE); rv.setViewVisibility(R.id.nextAlarmIcon, GONE); } else { rv.setTextViewText(R.id.nextAlarm, nextAlarmTime); rv.setViewVisibility(R.id.nextAlarm, VISIBLE); rv.setViewVisibility(R.id.nextAlarmIcon, VISIBLE); } if (options == null) { options = wm.getAppWidgetOptions(widgetId); } // Fetch the widget size selected by the user. final Resources resources = context.getResources(); final float density = resources.getDisplayMetrics().density; final int minWidthPx = (int) (density * options.getInt(OPTION_APPWIDGET_MIN_WIDTH)); final int minHeightPx = (int) (density * options.getInt(OPTION_APPWIDGET_MIN_HEIGHT)); final int maxWidthPx = (int) (density * options.getInt(OPTION_APPWIDGET_MAX_WIDTH)); final int maxHeightPx = (int) (density * options.getInt(OPTION_APPWIDGET_MAX_HEIGHT)); final int targetWidthPx = portrait ? minWidthPx : maxWidthPx; final int targetHeightPx = portrait ? maxHeightPx : minHeightPx; final int largestClockFontSizePx = resources.getDimensionPixelSize(R.dimen.widget_max_clock_font_size); // Create a size template that describes the widget bounds. final Sizes template = new Sizes(targetWidthPx, targetHeightPx, largestClockFontSizePx); // Compute optimal font sizes and icon sizes to fit within the widget bounds. final Sizes sizes = optimizeSizes(context, template, nextAlarmTime); if (LOGGER.isVerboseLoggable()) { LOGGER.v(sizes.toString()); } // Apply the computed sizes to the remote views. rv.setImageViewBitmap(R.id.nextAlarmIcon, sizes.mIconBitmap); rv.setTextViewTextSize(R.id.date, COMPLEX_UNIT_PX, sizes.mFontSizePx); rv.setTextViewTextSize(R.id.nextAlarm, COMPLEX_UNIT_PX, sizes.mFontSizePx); rv.setTextViewTextSize(R.id.clock, COMPLEX_UNIT_PX, sizes.mClockFontSizePx); final int smallestWorldCityListSizePx = resources.getDimensionPixelSize(R.dimen.widget_min_world_city_list_size); if (sizes.getListHeight() <= smallestWorldCityListSizePx) { // Insufficient space; hide the world city list. rv.setViewVisibility(R.id.world_city_list, GONE); } else { // Set an adapter on the world city list. That adapter connects to a Service via intent. final Intent intent = new Intent(context, DigitalAppWidgetCityService.class); intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId); intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME))); rv.setRemoteAdapter(R.id.world_city_list, intent); rv.setViewVisibility(R.id.world_city_list, VISIBLE); // Tapping on the widget opens the city selection activity (if not on the lock screen). if (Utils.isWidgetClickable(wm, widgetId)) { final Intent selectCity = new Intent(context, CitySelectionActivity.class); final PendingIntent pi = PendingIntent.getActivity(context, 0, selectCity, 0); rv.setPendingIntentTemplate(R.id.world_city_list, pi); } } return rv; } /** * Inflate an offscreen copy of the widget views. Binary search through the range of sizes until * the optimal sizes that fit within the widget bounds are located. */ private static Sizes optimizeSizes(Context context, Sizes template, String nextAlarmTime) { // Inflate a test layout to compute sizes at different font sizes. final LayoutInflater inflater = LayoutInflater.from(context); @SuppressLint("InflateParams") final View sizer = inflater.inflate(R.layout.digital_widget_sizer, null /* root */); // Configure the date to display the current date string. final CharSequence dateFormat = getDateFormat(context); final TextClock date = (TextClock) sizer.findViewById(R.id.date); date.setFormat12Hour(dateFormat); date.setFormat24Hour(dateFormat); // Configure the next alarm views to display the next alarm time or be gone. final TextView nextAlarmIcon = (TextView) sizer.findViewById(R.id.nextAlarmIcon); final TextView nextAlarm = (TextView) sizer.findViewById(R.id.nextAlarm); if (TextUtils.isEmpty(nextAlarmTime)) { nextAlarm.setVisibility(GONE); nextAlarmIcon.setVisibility(GONE); } else { nextAlarm.setText(nextAlarmTime); nextAlarm.setVisibility(VISIBLE); nextAlarmIcon.setVisibility(VISIBLE); nextAlarmIcon.setTypeface(UiDataModel.getUiDataModel().getAlarmIconTypeface()); } // Measure the widget at the largest possible size. Sizes high = measure(template, template.getLargestClockFontSizePx(), sizer); if (!high.hasViolations()) { return high; } // Measure the widget at the smallest possible size. Sizes low = measure(template, template.getSmallestClockFontSizePx(), sizer); if (low.hasViolations()) { return low; } // Binary search between the smallest and largest sizes until an optimum size is found. while (low.getClockFontSizePx() != high.getClockFontSizePx()) { final int midFontSize = (low.getClockFontSizePx() + high.getClockFontSizePx()) / 2; if (midFontSize == low.getClockFontSizePx()) { return low; } final Sizes midSize = measure(template, midFontSize, sizer); if (midSize.hasViolations()) { high = midSize; } else { low = midSize; } } return low; } /** * Remove the existing day-change callback if it is not needed (no selected cities exist). * Add the day-change callback if it is needed (selected cities exist). */ private void updateDayChangeCallback(Context context) { final DataModel dm = DataModel.getDataModel(); final List