Home | History | Annotate | Download | only in alarmclock
      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.annotation.SuppressLint;
     20 import android.app.AlarmManager;
     21 import android.app.PendingIntent;
     22 import android.appwidget.AppWidgetManager;
     23 import android.appwidget.AppWidgetProvider;
     24 import android.content.ComponentName;
     25 import android.content.Context;
     26 import android.content.Intent;
     27 import android.content.res.Resources;
     28 import android.graphics.Bitmap;
     29 import android.net.Uri;
     30 import android.os.Bundle;
     31 import android.support.annotation.NonNull;
     32 import android.text.TextUtils;
     33 import android.text.format.DateFormat;
     34 import android.util.ArraySet;
     35 import android.view.LayoutInflater;
     36 import android.view.View;
     37 import android.widget.RemoteViews;
     38 import android.widget.TextClock;
     39 import android.widget.TextView;
     40 
     41 import com.android.deskclock.DeskClock;
     42 import com.android.deskclock.LogUtils;
     43 import com.android.deskclock.R;
     44 import com.android.deskclock.Utils;
     45 import com.android.deskclock.data.City;
     46 import com.android.deskclock.data.DataModel;
     47 import com.android.deskclock.uidata.UiDataModel;
     48 import com.android.deskclock.worldclock.CitySelectionActivity;
     49 
     50 import java.util.Calendar;
     51 import java.util.Date;
     52 import java.util.List;
     53 import java.util.Locale;
     54 import java.util.Set;
     55 import java.util.TimeZone;
     56 
     57 import static android.app.AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED;
     58 import static android.app.PendingIntent.FLAG_NO_CREATE;
     59 import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
     60 import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT;
     61 import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH;
     62 import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT;
     63 import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH;
     64 import static android.content.Intent.ACTION_DATE_CHANGED;
     65 import static android.content.Intent.ACTION_LOCALE_CHANGED;
     66 import static android.content.Intent.ACTION_SCREEN_ON;
     67 import static android.content.Intent.ACTION_TIMEZONE_CHANGED;
     68 import static android.content.Intent.ACTION_TIME_CHANGED;
     69 import static android.util.TypedValue.COMPLEX_UNIT_PX;
     70 import static android.view.View.GONE;
     71 import static android.view.View.MeasureSpec.UNSPECIFIED;
     72 import static android.view.View.VISIBLE;
     73 import static com.android.deskclock.alarms.AlarmStateManager.ACTION_ALARM_CHANGED;
     74 import static com.android.deskclock.data.DataModel.ACTION_WORLD_CITIES_CHANGED;
     75 import static java.lang.Math.max;
     76 import static java.lang.Math.round;
     77 
     78 /**
     79  * <p>This provider produces a widget resembling one of the formats below.</p>
     80  *
     81  * If an alarm is scheduled to ring in the future:
     82  * <pre>
     83  *         12:59 AM
     84  * WED, FEB 3  THU 9:30 AM
     85  * </pre>
     86  *
     87  * If no alarm is scheduled to ring in the future:
     88  * <pre>
     89  *         12:59 AM
     90  *        WED, FEB 3
     91  * </pre>
     92  *
     93  * This widget is scaling the font sizes to fit within the widget bounds chosen by the user without
     94  * any clipping. To do so it measures layouts offscreen using a range of font sizes in order to
     95  * choose optimal values.
     96  */
     97 public class DigitalAppWidgetProvider extends AppWidgetProvider {
     98 
     99     private static final LogUtils.Logger LOGGER = new LogUtils.Logger("DigitalWidgetProvider");
    100 
    101     /**
    102      * Intent action used for refreshing a world city display when any of them changes days or when
    103      * the default TimeZone changes days. This affects the widget display because the day-of-week is
    104      * only visible when the world city day-of-week differs from the default TimeZone's day-of-week.
    105      */
    106     private static final String ACTION_ON_DAY_CHANGE = "com.android.deskclock.ON_DAY_CHANGE";
    107 
    108     /** Intent used to deliver the {@link #ACTION_ON_DAY_CHANGE} callback. */
    109     private static final Intent DAY_CHANGE_INTENT = new Intent(ACTION_ON_DAY_CHANGE);
    110 
    111     @Override
    112     public void onEnabled(Context context) {
    113         super.onEnabled(context);
    114 
    115         // Schedule the day-change callback if necessary.
    116         updateDayChangeCallback(context);
    117     }
    118 
    119     @Override
    120     public void onDisabled(Context context) {
    121         super.onDisabled(context);
    122 
    123         // Remove any scheduled day-change callback.
    124         removeDayChangeCallback(context);
    125     }
    126 
    127     @Override
    128     public void onReceive(@NonNull Context context, @NonNull Intent intent) {
    129         LOGGER.i("onReceive: " + intent);
    130         super.onReceive(context, intent);
    131 
    132         final AppWidgetManager wm = AppWidgetManager.getInstance(context);
    133         if (wm == null) {
    134             return;
    135         }
    136 
    137         final ComponentName provider = new ComponentName(context, getClass());
    138         final int[] widgetIds = wm.getAppWidgetIds(provider);
    139 
    140         final String action = intent.getAction();
    141         switch (action) {
    142             case ACTION_NEXT_ALARM_CLOCK_CHANGED:
    143             case ACTION_DATE_CHANGED:
    144             case ACTION_LOCALE_CHANGED:
    145             case ACTION_SCREEN_ON:
    146             case ACTION_TIME_CHANGED:
    147             case ACTION_TIMEZONE_CHANGED:
    148             case ACTION_ALARM_CHANGED:
    149             case ACTION_ON_DAY_CHANGE:
    150             case ACTION_WORLD_CITIES_CHANGED:
    151                 for (int widgetId : widgetIds) {
    152                     relayoutWidget(context, wm, widgetId, wm.getAppWidgetOptions(widgetId));
    153                 }
    154         }
    155 
    156         final DataModel dm = DataModel.getDataModel();
    157         dm.updateWidgetCount(getClass(), widgetIds.length, R.string.category_digital_widget);
    158 
    159         if (widgetIds.length > 0) {
    160             updateDayChangeCallback(context);
    161         }
    162     }
    163 
    164     /**
    165      * Called when widgets must provide remote views.
    166      */
    167     @Override
    168     public void onUpdate(Context context, AppWidgetManager wm, int[] widgetIds) {
    169         super.onUpdate(context, wm, widgetIds);
    170 
    171         for (int widgetId : widgetIds) {
    172             relayoutWidget(context, wm, widgetId, wm.getAppWidgetOptions(widgetId));
    173         }
    174     }
    175 
    176     /**
    177      * Called when the app widget changes sizes.
    178      */
    179     @Override
    180     public void onAppWidgetOptionsChanged(Context context, AppWidgetManager wm, int widgetId,
    181             Bundle options) {
    182         super.onAppWidgetOptionsChanged(context, wm, widgetId, options);
    183 
    184         // scale the fonts of the clock to fit inside the new size
    185         relayoutWidget(context, AppWidgetManager.getInstance(context), widgetId, options);
    186     }
    187 
    188     /**
    189      * Compute optimal font and icon sizes offscreen for both portrait and landscape orientations
    190      * using the last known widget size and apply them to the widget.
    191      */
    192     private static void relayoutWidget(Context context, AppWidgetManager wm, int widgetId,
    193             Bundle options) {
    194         final RemoteViews portrait = relayoutWidget(context, wm, widgetId, options, true);
    195         final RemoteViews landscape = relayoutWidget(context, wm, widgetId, options, false);
    196         final RemoteViews widget = new RemoteViews(landscape, portrait);
    197         wm.updateAppWidget(widgetId, widget);
    198         wm.notifyAppWidgetViewDataChanged(widgetId, R.id.world_city_list);
    199     }
    200 
    201     /**
    202      * Compute optimal font and icon sizes offscreen for the given orientation.
    203      */
    204     private static RemoteViews relayoutWidget(Context context, AppWidgetManager wm, int widgetId,
    205             Bundle options, boolean portrait) {
    206         // Create a remote view for the digital clock.
    207         final String packageName = context.getPackageName();
    208         final RemoteViews rv = new RemoteViews(packageName, R.layout.digital_widget);
    209 
    210         // Tapping on the widget opens the app (if not on the lock screen).
    211         if (Utils.isWidgetClickable(wm, widgetId)) {
    212             final Intent openApp = new Intent(context, DeskClock.class);
    213             final PendingIntent pi = PendingIntent.getActivity(context, 0, openApp, 0);
    214             rv.setOnClickPendingIntent(R.id.digital_widget, pi);
    215         }
    216 
    217         // Configure child views of the remote view.
    218         final CharSequence dateFormat = getDateFormat(context);
    219         rv.setCharSequence(R.id.date, "setFormat12Hour", dateFormat);
    220         rv.setCharSequence(R.id.date, "setFormat24Hour", dateFormat);
    221 
    222         final String nextAlarmTime = Utils.getNextAlarm(context);
    223         if (TextUtils.isEmpty(nextAlarmTime)) {
    224             rv.setViewVisibility(R.id.nextAlarm, GONE);
    225             rv.setViewVisibility(R.id.nextAlarmIcon, GONE);
    226         } else  {
    227             rv.setTextViewText(R.id.nextAlarm, nextAlarmTime);
    228             rv.setViewVisibility(R.id.nextAlarm, VISIBLE);
    229             rv.setViewVisibility(R.id.nextAlarmIcon, VISIBLE);
    230         }
    231 
    232         if (options == null) {
    233             options = wm.getAppWidgetOptions(widgetId);
    234         }
    235 
    236         // Fetch the widget size selected by the user.
    237         final Resources resources = context.getResources();
    238         final float density = resources.getDisplayMetrics().density;
    239         final int minWidthPx = (int) (density * options.getInt(OPTION_APPWIDGET_MIN_WIDTH));
    240         final int minHeightPx = (int) (density * options.getInt(OPTION_APPWIDGET_MIN_HEIGHT));
    241         final int maxWidthPx = (int) (density * options.getInt(OPTION_APPWIDGET_MAX_WIDTH));
    242         final int maxHeightPx = (int) (density * options.getInt(OPTION_APPWIDGET_MAX_HEIGHT));
    243         final int targetWidthPx = portrait ? minWidthPx : maxWidthPx;
    244         final int targetHeightPx = portrait ? maxHeightPx : minHeightPx;
    245         final int largestClockFontSizePx =
    246                 resources.getDimensionPixelSize(R.dimen.widget_max_clock_font_size);
    247 
    248         // Create a size template that describes the widget bounds.
    249         final Sizes template = new Sizes(targetWidthPx, targetHeightPx, largestClockFontSizePx);
    250 
    251         // Compute optimal font sizes and icon sizes to fit within the widget bounds.
    252         final Sizes sizes = optimizeSizes(context, template, nextAlarmTime);
    253         if (LOGGER.isVerboseLoggable()) {
    254             LOGGER.v(sizes.toString());
    255         }
    256 
    257         // Apply the computed sizes to the remote views.
    258         rv.setImageViewBitmap(R.id.nextAlarmIcon, sizes.mIconBitmap);
    259         rv.setTextViewTextSize(R.id.date, COMPLEX_UNIT_PX, sizes.mFontSizePx);
    260         rv.setTextViewTextSize(R.id.nextAlarm, COMPLEX_UNIT_PX, sizes.mFontSizePx);
    261         rv.setTextViewTextSize(R.id.clock, COMPLEX_UNIT_PX, sizes.mClockFontSizePx);
    262 
    263         final int smallestWorldCityListSizePx =
    264                 resources.getDimensionPixelSize(R.dimen.widget_min_world_city_list_size);
    265         if (sizes.getListHeight() <= smallestWorldCityListSizePx) {
    266             // Insufficient space; hide the world city list.
    267             rv.setViewVisibility(R.id.world_city_list, GONE);
    268         } else {
    269             // Set an adapter on the world city list. That adapter connects to a Service via intent.
    270             final Intent intent = new Intent(context, DigitalAppWidgetCityService.class);
    271             intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId);
    272             intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
    273             rv.setRemoteAdapter(R.id.world_city_list, intent);
    274             rv.setViewVisibility(R.id.world_city_list, VISIBLE);
    275 
    276             // Tapping on the widget opens the city selection activity (if not on the lock screen).
    277             if (Utils.isWidgetClickable(wm, widgetId)) {
    278                 final Intent selectCity = new Intent(context, CitySelectionActivity.class);
    279                 final PendingIntent pi = PendingIntent.getActivity(context, 0, selectCity, 0);
    280                 rv.setPendingIntentTemplate(R.id.world_city_list, pi);
    281             }
    282         }
    283 
    284         return rv;
    285     }
    286 
    287     /**
    288      * Inflate an offscreen copy of the widget views. Binary search through the range of sizes until
    289      * the optimal sizes that fit within the widget bounds are located.
    290      */
    291     private static Sizes optimizeSizes(Context context, Sizes template, String nextAlarmTime) {
    292         // Inflate a test layout to compute sizes at different font sizes.
    293         final LayoutInflater inflater = LayoutInflater.from(context);
    294         @SuppressLint("InflateParams")
    295         final View sizer = inflater.inflate(R.layout.digital_widget_sizer, null /* root */);
    296 
    297         // Configure the date to display the current date string.
    298         final CharSequence dateFormat = getDateFormat(context);
    299         final TextClock date = (TextClock) sizer.findViewById(R.id.date);
    300         date.setFormat12Hour(dateFormat);
    301         date.setFormat24Hour(dateFormat);
    302 
    303         // Configure the next alarm views to display the next alarm time or be gone.
    304         final TextView nextAlarmIcon = (TextView) sizer.findViewById(R.id.nextAlarmIcon);
    305         final TextView nextAlarm = (TextView) sizer.findViewById(R.id.nextAlarm);
    306         if (TextUtils.isEmpty(nextAlarmTime)) {
    307             nextAlarm.setVisibility(GONE);
    308             nextAlarmIcon.setVisibility(GONE);
    309         } else  {
    310             nextAlarm.setText(nextAlarmTime);
    311             nextAlarm.setVisibility(VISIBLE);
    312             nextAlarmIcon.setVisibility(VISIBLE);
    313             nextAlarmIcon.setTypeface(UiDataModel.getUiDataModel().getAlarmIconTypeface());
    314         }
    315 
    316         // Measure the widget at the largest possible size.
    317         Sizes high = measure(template, template.getLargestClockFontSizePx(), sizer);
    318         if (!high.hasViolations()) {
    319             return high;
    320         }
    321 
    322         // Measure the widget at the smallest possible size.
    323         Sizes low = measure(template, template.getSmallestClockFontSizePx(), sizer);
    324         if (low.hasViolations()) {
    325             return low;
    326         }
    327 
    328         // Binary search between the smallest and largest sizes until an optimum size is found.
    329         while (low.getClockFontSizePx() != high.getClockFontSizePx()) {
    330             final int midFontSize = (low.getClockFontSizePx() + high.getClockFontSizePx()) / 2;
    331             if (midFontSize == low.getClockFontSizePx()) {
    332                 return low;
    333             }
    334 
    335             final Sizes midSize = measure(template, midFontSize, sizer);
    336             if (midSize.hasViolations()) {
    337                 high = midSize;
    338             } else {
    339                 low = midSize;
    340             }
    341         }
    342 
    343         return low;
    344     }
    345 
    346     /**
    347      * Remove the existing day-change callback if it is not needed (no selected cities exist).
    348      * Add the day-change callback if it is needed (selected cities exist).
    349      */
    350     private void updateDayChangeCallback(Context context) {
    351         final DataModel dm = DataModel.getDataModel();
    352         final List<City> selectedCities = dm.getSelectedCities();
    353         final boolean showHomeClock = dm.getShowHomeClock();
    354         if (selectedCities.isEmpty() && !showHomeClock) {
    355             // Remove the existing day-change callback.
    356             removeDayChangeCallback(context);
    357             return;
    358         }
    359 
    360         // Look up the time at which the next day change occurs across all timezones.
    361         final Set<TimeZone> zones = new ArraySet<>(selectedCities.size() + 2);
    362         zones.add(TimeZone.getDefault());
    363         if (showHomeClock) {
    364             zones.add(dm.getHomeCity().getTimeZone());
    365         }
    366         for (City city : selectedCities) {
    367             zones.add(city.getTimeZone());
    368         }
    369         final Date nextDay = Utils.getNextDay(new Date(), zones);
    370 
    371         // Schedule the next day-change callback; at least one city is displayed.
    372         final PendingIntent pi =
    373                 PendingIntent.getBroadcast(context, 0, DAY_CHANGE_INTENT, FLAG_UPDATE_CURRENT);
    374         getAlarmManager(context).setExact(AlarmManager.RTC, nextDay.getTime(), pi);
    375     }
    376 
    377     /**
    378      * Remove the existing day-change callback.
    379      */
    380     private void removeDayChangeCallback(Context context) {
    381         final PendingIntent pi =
    382                 PendingIntent.getBroadcast(context, 0, DAY_CHANGE_INTENT, FLAG_NO_CREATE);
    383         if (pi != null) {
    384             getAlarmManager(context).cancel(pi);
    385             pi.cancel();
    386         }
    387     }
    388 
    389     private static AlarmManager getAlarmManager(Context context) {
    390         return (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
    391     }
    392 
    393     /**
    394      * Compute all font and icon sizes based on the given {@code clockFontSize} and apply them to
    395      * the offscreen {@code sizer} view. Measure the {@code sizer} view and return the resulting
    396      * size measurements.
    397      */
    398     private static Sizes measure(Sizes template, int clockFontSize, View sizer) {
    399         // Create a copy of the given template sizes.
    400         final Sizes measuredSizes = template.newSize();
    401 
    402         // Configure the clock to display the widest time string.
    403         final TextClock date = (TextClock) sizer.findViewById(R.id.date);
    404         final TextClock clock = (TextClock) sizer.findViewById(R.id.clock);
    405         final TextView nextAlarm = (TextView) sizer.findViewById(R.id.nextAlarm);
    406         final TextView nextAlarmIcon = (TextView) sizer.findViewById(R.id.nextAlarmIcon);
    407 
    408         // Adjust the font sizes.
    409         measuredSizes.setClockFontSizePx(clockFontSize);
    410         clock.setText(getLongestTimeString(clock));
    411         clock.setTextSize(COMPLEX_UNIT_PX, measuredSizes.mClockFontSizePx);
    412         date.setTextSize(COMPLEX_UNIT_PX, measuredSizes.mFontSizePx);
    413         nextAlarm.setTextSize(COMPLEX_UNIT_PX, measuredSizes.mFontSizePx);
    414         nextAlarmIcon.setTextSize(COMPLEX_UNIT_PX, measuredSizes.mIconFontSizePx);
    415         nextAlarmIcon.setPadding(measuredSizes.mIconPaddingPx, 0, measuredSizes.mIconPaddingPx, 0);
    416 
    417         // Measure and layout the sizer.
    418         final int widthSize = View.MeasureSpec.getSize(measuredSizes.mTargetWidthPx);
    419         final int heightSize = View.MeasureSpec.getSize(measuredSizes.mTargetHeightPx);
    420         final int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(widthSize, UNSPECIFIED);
    421         final int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(heightSize, UNSPECIFIED);
    422         sizer.measure(widthMeasureSpec, heightMeasureSpec);
    423         sizer.layout(0, 0, sizer.getMeasuredWidth(), sizer.getMeasuredHeight());
    424 
    425         // Copy the measurements into the result object.
    426         measuredSizes.mMeasuredWidthPx = sizer.getMeasuredWidth();
    427         measuredSizes.mMeasuredHeightPx = sizer.getMeasuredHeight();
    428         measuredSizes.mMeasuredTextClockWidthPx = clock.getMeasuredWidth();
    429         measuredSizes.mMeasuredTextClockHeightPx = clock.getMeasuredHeight();
    430 
    431         // If an alarm icon is required, generate one from the TextView with the special font.
    432         if (nextAlarmIcon.getVisibility() == VISIBLE) {
    433             measuredSizes.mIconBitmap = Utils.createBitmap(nextAlarmIcon);
    434         }
    435 
    436         return measuredSizes;
    437     }
    438 
    439     /**
    440      * @return "11:59" or "23:59" in the current locale
    441      */
    442     private static CharSequence getLongestTimeString(TextClock clock) {
    443         final CharSequence format = clock.is24HourModeEnabled()
    444                 ? clock.getFormat24Hour()
    445                 : clock.getFormat12Hour();
    446         final Calendar longestPMTime = Calendar.getInstance();
    447         longestPMTime.set(0, 0, 0, 23, 59);
    448         return DateFormat.format(format, longestPMTime);
    449     }
    450 
    451     /**
    452      * @return the locale-specific date pattern
    453      */
    454     private static String getDateFormat(Context context) {
    455         final Locale locale = Locale.getDefault();
    456         final String skeleton = context.getString(R.string.abbrev_wday_month_day_no_year);
    457         return DateFormat.getBestDateTimePattern(locale, skeleton);
    458     }
    459 
    460     /**
    461      * This class stores the target size of the widget as well as the measured size using a given
    462      * clock font size. All other fonts and icons are scaled proportional to the clock font.
    463      */
    464     private static final class Sizes {
    465 
    466         private final int mTargetWidthPx;
    467         private final int mTargetHeightPx;
    468         private final int mLargestClockFontSizePx;
    469         private final int mSmallestClockFontSizePx;
    470         private Bitmap mIconBitmap;
    471 
    472         private int mMeasuredWidthPx;
    473         private int mMeasuredHeightPx;
    474         private int mMeasuredTextClockWidthPx;
    475         private int mMeasuredTextClockHeightPx;
    476 
    477         /** The size of the font to use on the date / next alarm time fields. */
    478         private int mFontSizePx;
    479 
    480         /** The size of the font to use on the clock field. */
    481         private int mClockFontSizePx;
    482 
    483         private int mIconFontSizePx;
    484         private int mIconPaddingPx;
    485 
    486         private Sizes(int targetWidthPx, int targetHeightPx, int largestClockFontSizePx) {
    487             mTargetWidthPx = targetWidthPx;
    488             mTargetHeightPx = targetHeightPx;
    489             mLargestClockFontSizePx = largestClockFontSizePx;
    490             mSmallestClockFontSizePx = 1;
    491         }
    492 
    493         private int getLargestClockFontSizePx() { return mLargestClockFontSizePx; }
    494         private int getSmallestClockFontSizePx() { return mSmallestClockFontSizePx; }
    495         private int getClockFontSizePx() { return mClockFontSizePx; }
    496         private void setClockFontSizePx(int clockFontSizePx) {
    497             mClockFontSizePx = clockFontSizePx;
    498             mFontSizePx = max(1, round(clockFontSizePx / 7.5f));
    499             mIconFontSizePx = (int) (mFontSizePx * 1.4f);
    500             mIconPaddingPx = mFontSizePx / 3;
    501         }
    502 
    503         /**
    504          * @return the amount of widget height available to the world cities list
    505          */
    506         private int getListHeight() {
    507             return mTargetHeightPx - mMeasuredHeightPx;
    508         }
    509 
    510         private boolean hasViolations() {
    511             return mMeasuredWidthPx > mTargetWidthPx || mMeasuredHeightPx > mTargetHeightPx;
    512         }
    513 
    514         private Sizes newSize() {
    515             return new Sizes(mTargetWidthPx, mTargetHeightPx, mLargestClockFontSizePx);
    516         }
    517 
    518         @Override
    519         public String toString() {
    520             final StringBuilder builder = new StringBuilder(1000);
    521             builder.append("\n");
    522             append(builder, "Target dimensions: %dpx x %dpx\n", mTargetWidthPx, mTargetHeightPx);
    523             append(builder, "Last valid widget container measurement: %dpx x %dpx\n",
    524                     mMeasuredWidthPx, mMeasuredHeightPx);
    525             append(builder, "Last text clock measurement: %dpx x %dpx\n",
    526                     mMeasuredTextClockWidthPx, mMeasuredTextClockHeightPx);
    527             if (mMeasuredWidthPx > mTargetWidthPx) {
    528                 append(builder, "Measured width %dpx exceeded widget width %dpx\n",
    529                         mMeasuredWidthPx, mTargetWidthPx);
    530             }
    531             if (mMeasuredHeightPx > mTargetHeightPx) {
    532                 append(builder, "Measured height %dpx exceeded widget height %dpx\n",
    533                         mMeasuredHeightPx, mTargetHeightPx);
    534             }
    535             append(builder, "Clock font: %dpx\n", mClockFontSizePx);
    536             return builder.toString();
    537         }
    538 
    539         private static void append(StringBuilder builder, String format, Object... args) {
    540             builder.append(String.format(Locale.ENGLISH, format, args));
    541         }
    542     }
    543 }
    544