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