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 androidx.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
onEnabled(Context context)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
onDisabled(Context context)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
onReceive(@onNull Context context, @NonNull Intent intent)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
onUpdate(Context context, AppWidgetManager wm, int[] widgetIds)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
onAppWidgetOptionsChanged(Context context, AppWidgetManager wm, int widgetId, Bundle options)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      */
relayoutWidget(Context context, AppWidgetManager wm, int widgetId, Bundle options)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      */
relayoutWidget(Context context, AppWidgetManager wm, int widgetId, Bundle options, boolean portrait)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      */
optimizeSizes(Context context, Sizes template, String nextAlarmTime)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      */
updateDayChangeCallback(Context context)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      */
removeDayChangeCallback(Context context)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 
getAlarmManager(Context context)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      */
measure(Sizes template, int clockFontSize, View sizer)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      */
getLongestTimeString(TextClock clock)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      */
getDateFormat(Context context)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 
Sizes(int targetWidthPx, int targetHeightPx, int largestClockFontSizePx)486         private Sizes(int targetWidthPx, int targetHeightPx, int largestClockFontSizePx) {
487             mTargetWidthPx = targetWidthPx;
488             mTargetHeightPx = targetHeightPx;
489             mLargestClockFontSizePx = largestClockFontSizePx;
490             mSmallestClockFontSizePx = 1;
491         }
492 
getLargestClockFontSizePx()493         private int getLargestClockFontSizePx() { return mLargestClockFontSizePx; }
getSmallestClockFontSizePx()494         private int getSmallestClockFontSizePx() { return mSmallestClockFontSizePx; }
getClockFontSizePx()495         private int getClockFontSizePx() { return mClockFontSizePx; }
setClockFontSizePx(int clockFontSizePx)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          */
getListHeight()506         private int getListHeight() {
507             return mTargetHeightPx - mMeasuredHeightPx;
508         }
509 
hasViolations()510         private boolean hasViolations() {
511             return mMeasuredWidthPx > mTargetWidthPx || mMeasuredHeightPx > mTargetHeightPx;
512         }
513 
newSize()514         private Sizes newSize() {
515             return new Sizes(mTargetWidthPx, mTargetHeightPx, mLargestClockFontSizePx);
516         }
517 
518         @Override
toString()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 
append(StringBuilder builder, String format, Object... args)539         private static void append(StringBuilder builder, String format, Object... args) {
540             builder.append(String.format(Locale.ENGLISH, format, args));
541         }
542     }
543 }
544