1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of 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, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.deskclock; 18 19 import android.annotation.SuppressLint; 20 import android.annotation.TargetApi; 21 import android.app.AlarmManager; 22 import android.app.AlarmManager.AlarmClockInfo; 23 import android.app.PendingIntent; 24 import android.appwidget.AppWidgetManager; 25 import android.content.ContentResolver; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.graphics.Bitmap; 29 import android.graphics.Canvas; 30 import android.graphics.Color; 31 import android.graphics.Paint; 32 import android.graphics.PorterDuff; 33 import android.graphics.PorterDuffColorFilter; 34 import android.graphics.Typeface; 35 import android.net.Uri; 36 import android.os.Build; 37 import android.os.Bundle; 38 import android.os.Looper; 39 import android.provider.Settings; 40 import androidx.annotation.AnyRes; 41 import androidx.annotation.DrawableRes; 42 import androidx.annotation.StringRes; 43 import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat; 44 import androidx.core.os.BuildCompat; 45 import androidx.core.view.AccessibilityDelegateCompat; 46 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; 47 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat; 48 import android.text.Spannable; 49 import android.text.SpannableString; 50 import android.text.TextUtils; 51 import android.text.format.DateFormat; 52 import android.text.format.DateUtils; 53 import android.text.style.RelativeSizeSpan; 54 import android.text.style.StyleSpan; 55 import android.text.style.TypefaceSpan; 56 import android.util.ArraySet; 57 import android.view.View; 58 import android.widget.TextClock; 59 import android.widget.TextView; 60 61 import com.android.deskclock.data.DataModel; 62 import com.android.deskclock.provider.AlarmInstance; 63 import com.android.deskclock.uidata.UiDataModel; 64 65 import java.text.NumberFormat; 66 import java.text.SimpleDateFormat; 67 import java.util.Calendar; 68 import java.util.Collection; 69 import java.util.Date; 70 import java.util.Locale; 71 import java.util.TimeZone; 72 73 import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; 74 import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY; 75 import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD; 76 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; 77 import static android.content.res.Configuration.ORIENTATION_PORTRAIT; 78 import static android.graphics.Bitmap.Config.ARGB_8888; 79 80 public class Utils { 81 82 /** 83 * {@link Uri} signifying the "silent" ringtone. 84 */ 85 public static final Uri RINGTONE_SILENT = Uri.EMPTY; 86 enforceMainLooper()87 public static void enforceMainLooper() { 88 if (Looper.getMainLooper() != Looper.myLooper()) { 89 throw new IllegalAccessError("May only call from main thread."); 90 } 91 } 92 enforceNotMainLooper()93 public static void enforceNotMainLooper() { 94 if (Looper.getMainLooper() == Looper.myLooper()) { 95 throw new IllegalAccessError("May not call from main thread."); 96 } 97 } 98 indexOf(Object[] array, Object item)99 public static int indexOf(Object[] array, Object item) { 100 for (int i = 0; i < array.length; i++) { 101 if (array[i].equals(item)) { 102 return i; 103 } 104 } 105 return -1; 106 } 107 108 /** 109 * @return {@code true} if the device is prior to {@link Build.VERSION_CODES#LOLLIPOP} 110 */ isPreL()111 public static boolean isPreL() { 112 return Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP; 113 } 114 115 /** 116 * @return {@code true} if the device is {@link Build.VERSION_CODES#LOLLIPOP} or 117 * {@link Build.VERSION_CODES#LOLLIPOP_MR1} 118 */ isLOrLMR1()119 public static boolean isLOrLMR1() { 120 final int sdkInt = Build.VERSION.SDK_INT; 121 return sdkInt == Build.VERSION_CODES.LOLLIPOP || sdkInt == Build.VERSION_CODES.LOLLIPOP_MR1; 122 } 123 124 /** 125 * @return {@code true} if the device is {@link Build.VERSION_CODES#LOLLIPOP} or later 126 */ isLOrLater()127 public static boolean isLOrLater() { 128 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; 129 } 130 131 /** 132 * @return {@code true} if the device is {@link Build.VERSION_CODES#LOLLIPOP_MR1} or later 133 */ isLMR1OrLater()134 public static boolean isLMR1OrLater() { 135 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1; 136 } 137 138 /** 139 * @return {@code true} if the device is {@link Build.VERSION_CODES#M} or later 140 */ isMOrLater()141 public static boolean isMOrLater() { 142 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; 143 } 144 145 /** 146 * @return {@code true} if the device is {@link Build.VERSION_CODES#N} or later 147 */ isNOrLater()148 public static boolean isNOrLater() { 149 return BuildCompat.isAtLeastN(); 150 } 151 152 /** 153 * @return {@code true} if the device is {@link Build.VERSION_CODES#N_MR1} or later 154 */ isNMR1OrLater()155 public static boolean isNMR1OrLater() { 156 return BuildCompat.isAtLeastNMR1(); 157 } 158 159 /** 160 * @param resourceId identifies an application resource 161 * @return the Uri by which the application resource is accessed 162 */ getResourceUri(Context context, @AnyRes int resourceId)163 public static Uri getResourceUri(Context context, @AnyRes int resourceId) { 164 return new Uri.Builder() 165 .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) 166 .authority(context.getPackageName()) 167 .path(String.valueOf(resourceId)) 168 .build(); 169 } 170 171 /** 172 * @param view the scrollable view to test 173 * @return {@code true} iff the {@code view} content is currently scrolled to the top 174 */ isScrolledToTop(View view)175 public static boolean isScrolledToTop(View view) { 176 return !view.canScrollVertically(-1); 177 } 178 179 /** 180 * Calculate the amount by which the radius of a CircleTimerView should be offset by any 181 * of the extra painted objects. 182 */ calculateRadiusOffset( float strokeSize, float dotStrokeSize, float markerStrokeSize)183 public static float calculateRadiusOffset( 184 float strokeSize, float dotStrokeSize, float markerStrokeSize) { 185 return Math.max(strokeSize, Math.max(dotStrokeSize, markerStrokeSize)); 186 } 187 188 /** 189 * Configure the clock that is visible to display seconds. The clock that is not visible never 190 * displays seconds to avoid it scheduling unnecessary ticking runnables. 191 */ setClockSecondsEnabled(TextClock digitalClock, AnalogClock analogClock)192 public static void setClockSecondsEnabled(TextClock digitalClock, AnalogClock analogClock) { 193 final boolean displaySeconds = DataModel.getDataModel().getDisplayClockSeconds(); 194 final DataModel.ClockStyle clockStyle = DataModel.getDataModel().getClockStyle(); 195 switch (clockStyle) { 196 case ANALOG: 197 setTimeFormat(digitalClock, false); 198 analogClock.enableSeconds(displaySeconds); 199 return; 200 case DIGITAL: 201 analogClock.enableSeconds(false); 202 setTimeFormat(digitalClock, displaySeconds); 203 return; 204 } 205 206 throw new IllegalStateException("unexpected clock style: " + clockStyle); 207 } 208 209 /** 210 * Set whether the digital or analog clock should be displayed in the application. 211 * Returns the view to be displayed. 212 */ setClockStyle(View digitalClock, View analogClock)213 public static View setClockStyle(View digitalClock, View analogClock) { 214 final DataModel.ClockStyle clockStyle = DataModel.getDataModel().getClockStyle(); 215 switch (clockStyle) { 216 case ANALOG: 217 digitalClock.setVisibility(View.GONE); 218 analogClock.setVisibility(View.VISIBLE); 219 return analogClock; 220 case DIGITAL: 221 digitalClock.setVisibility(View.VISIBLE); 222 analogClock.setVisibility(View.GONE); 223 return digitalClock; 224 } 225 226 throw new IllegalStateException("unexpected clock style: " + clockStyle); 227 } 228 229 /** 230 * For screensavers to set whether the digital or analog clock should be displayed. 231 * Returns the view to be displayed. 232 */ setScreensaverClockStyle(View digitalClock, View analogClock)233 public static View setScreensaverClockStyle(View digitalClock, View analogClock) { 234 final DataModel.ClockStyle clockStyle = DataModel.getDataModel().getScreensaverClockStyle(); 235 switch (clockStyle) { 236 case ANALOG: 237 digitalClock.setVisibility(View.GONE); 238 analogClock.setVisibility(View.VISIBLE); 239 return analogClock; 240 case DIGITAL: 241 digitalClock.setVisibility(View.VISIBLE); 242 analogClock.setVisibility(View.GONE); 243 return digitalClock; 244 } 245 246 throw new IllegalStateException("unexpected clock style: " + clockStyle); 247 } 248 249 /** 250 * For screensavers to dim the lights if necessary. 251 */ dimClockView(boolean dim, View clockView)252 public static void dimClockView(boolean dim, View clockView) { 253 Paint paint = new Paint(); 254 paint.setColor(Color.WHITE); 255 paint.setColorFilter(new PorterDuffColorFilter( 256 (dim ? 0x40FFFFFF : 0xC0FFFFFF), 257 PorterDuff.Mode.MULTIPLY)); 258 clockView.setLayerType(View.LAYER_TYPE_HARDWARE, paint); 259 } 260 261 /** 262 * Update and return the PendingIntent corresponding to the given {@code intent}. 263 * 264 * @param context the Context in which the PendingIntent should start the service 265 * @param intent an Intent describing the service to be started 266 * @return a PendingIntent that will start a service 267 */ pendingServiceIntent(Context context, Intent intent)268 public static PendingIntent pendingServiceIntent(Context context, Intent intent) { 269 return PendingIntent.getService(context, 0, intent, FLAG_UPDATE_CURRENT); 270 } 271 272 /** 273 * Update and return the PendingIntent corresponding to the given {@code intent}. 274 * 275 * @param context the Context in which the PendingIntent should start the activity 276 * @param intent an Intent describing the activity to be started 277 * @return a PendingIntent that will start an activity 278 */ pendingActivityIntent(Context context, Intent intent)279 public static PendingIntent pendingActivityIntent(Context context, Intent intent) { 280 return PendingIntent.getActivity(context, 0, intent, FLAG_UPDATE_CURRENT); 281 } 282 283 /** 284 * @return The next alarm from {@link AlarmManager} 285 */ getNextAlarm(Context context)286 public static String getNextAlarm(Context context) { 287 return isPreL() ? getNextAlarmPreL(context) : getNextAlarmLOrLater(context); 288 } 289 290 @SuppressWarnings("deprecation") 291 @TargetApi(Build.VERSION_CODES.KITKAT) getNextAlarmPreL(Context context)292 private static String getNextAlarmPreL(Context context) { 293 final ContentResolver cr = context.getContentResolver(); 294 return Settings.System.getString(cr, Settings.System.NEXT_ALARM_FORMATTED); 295 } 296 297 @TargetApi(Build.VERSION_CODES.LOLLIPOP) getNextAlarmLOrLater(Context context)298 private static String getNextAlarmLOrLater(Context context) { 299 final AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 300 final AlarmClockInfo info = getNextAlarmClock(am); 301 if (info != null) { 302 final long triggerTime = info.getTriggerTime(); 303 final Calendar alarmTime = Calendar.getInstance(); 304 alarmTime.setTimeInMillis(triggerTime); 305 return AlarmUtils.getFormattedTime(context, alarmTime); 306 } 307 308 return null; 309 } 310 311 @TargetApi(Build.VERSION_CODES.LOLLIPOP) getNextAlarmClock(AlarmManager am)312 private static AlarmClockInfo getNextAlarmClock(AlarmManager am) { 313 return am.getNextAlarmClock(); 314 } 315 316 @TargetApi(Build.VERSION_CODES.LOLLIPOP) updateNextAlarm(AlarmManager am, AlarmClockInfo info, PendingIntent op)317 public static void updateNextAlarm(AlarmManager am, AlarmClockInfo info, PendingIntent op) { 318 am.setAlarmClock(info, op); 319 } 320 isAlarmWithin24Hours(AlarmInstance alarmInstance)321 public static boolean isAlarmWithin24Hours(AlarmInstance alarmInstance) { 322 final Calendar nextAlarmTime = alarmInstance.getAlarmTime(); 323 final long nextAlarmTimeMillis = nextAlarmTime.getTimeInMillis(); 324 return nextAlarmTimeMillis - System.currentTimeMillis() <= DateUtils.DAY_IN_MILLIS; 325 } 326 327 /** 328 * Clock views can call this to refresh their alarm to the next upcoming value. 329 */ refreshAlarm(Context context, View clock)330 public static void refreshAlarm(Context context, View clock) { 331 final TextView nextAlarmIconView = (TextView) clock.findViewById(R.id.nextAlarmIcon); 332 final TextView nextAlarmView = (TextView) clock.findViewById(R.id.nextAlarm); 333 if (nextAlarmView == null) { 334 return; 335 } 336 337 final String alarm = getNextAlarm(context); 338 if (!TextUtils.isEmpty(alarm)) { 339 final String description = context.getString(R.string.next_alarm_description, alarm); 340 nextAlarmView.setText(alarm); 341 nextAlarmView.setContentDescription(description); 342 nextAlarmView.setVisibility(View.VISIBLE); 343 nextAlarmIconView.setVisibility(View.VISIBLE); 344 nextAlarmIconView.setContentDescription(description); 345 } else { 346 nextAlarmView.setVisibility(View.GONE); 347 nextAlarmIconView.setVisibility(View.GONE); 348 } 349 } 350 setClockIconTypeface(View clock)351 public static void setClockIconTypeface(View clock) { 352 final TextView nextAlarmIconView = (TextView) clock.findViewById(R.id.nextAlarmIcon); 353 nextAlarmIconView.setTypeface(UiDataModel.getUiDataModel().getAlarmIconTypeface()); 354 } 355 356 /** 357 * Clock views can call this to refresh their date. 358 **/ updateDate(String dateSkeleton, String descriptionSkeleton, View clock)359 public static void updateDate(String dateSkeleton, String descriptionSkeleton, View clock) { 360 final TextView dateDisplay = (TextView) clock.findViewById(R.id.date); 361 if (dateDisplay == null) { 362 return; 363 } 364 365 final Locale l = Locale.getDefault(); 366 final String datePattern = DateFormat.getBestDateTimePattern(l, dateSkeleton); 367 final String descriptionPattern = DateFormat.getBestDateTimePattern(l, descriptionSkeleton); 368 369 final Date now = new Date(); 370 dateDisplay.setText(new SimpleDateFormat(datePattern, l).format(now)); 371 dateDisplay.setVisibility(View.VISIBLE); 372 dateDisplay.setContentDescription(new SimpleDateFormat(descriptionPattern, l).format(now)); 373 } 374 375 /*** 376 * Formats the time in the TextClock according to the Locale with a special 377 * formatting treatment for the am/pm label. 378 * 379 * @param clock TextClock to format 380 * @param includeSeconds whether or not to include seconds in the clock's time 381 */ setTimeFormat(TextClock clock, boolean includeSeconds)382 public static void setTimeFormat(TextClock clock, boolean includeSeconds) { 383 if (clock != null) { 384 // Get the best format for 12 hours mode according to the locale 385 clock.setFormat12Hour(get12ModeFormat(0.4f /* amPmRatio */, includeSeconds)); 386 // Get the best format for 24 hours mode according to the locale 387 clock.setFormat24Hour(get24ModeFormat(includeSeconds)); 388 } 389 } 390 391 /** 392 * @param amPmRatio a value between 0 and 1 that is the ratio of the relative size of the 393 * am/pm string to the time string 394 * @param includeSeconds whether or not to include seconds in the time string 395 * @return format string for 12 hours mode time, not including seconds 396 */ get12ModeFormat(float amPmRatio, boolean includeSeconds)397 public static CharSequence get12ModeFormat(float amPmRatio, boolean includeSeconds) { 398 String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), 399 includeSeconds ? "hmsa" : "hma"); 400 if (amPmRatio <= 0) { 401 pattern = pattern.replaceAll("a", "").trim(); 402 } 403 404 // Replace spaces with "Hair Space" 405 pattern = pattern.replaceAll(" ", "\u200A"); 406 // Build a spannable so that the am/pm will be formatted 407 int amPmPos = pattern.indexOf('a'); 408 if (amPmPos == -1) { 409 return pattern; 410 } 411 412 final Spannable sp = new SpannableString(pattern); 413 sp.setSpan(new RelativeSizeSpan(amPmRatio), amPmPos, amPmPos + 1, 414 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 415 sp.setSpan(new StyleSpan(Typeface.NORMAL), amPmPos, amPmPos + 1, 416 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 417 sp.setSpan(new TypefaceSpan("sans-serif"), amPmPos, amPmPos + 1, 418 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 419 420 return sp; 421 } 422 get24ModeFormat(boolean includeSeconds)423 public static CharSequence get24ModeFormat(boolean includeSeconds) { 424 return DateFormat.getBestDateTimePattern(Locale.getDefault(), 425 includeSeconds ? "Hms" : "Hm"); 426 } 427 428 /** 429 * Returns string denoting the timezone hour offset (e.g. GMT -8:00) 430 * 431 * @param useShortForm Whether to return a short form of the header that rounds to the 432 * nearest hour and excludes the "GMT" prefix 433 */ getGMTHourOffset(TimeZone timezone, boolean useShortForm)434 public static String getGMTHourOffset(TimeZone timezone, boolean useShortForm) { 435 final int gmtOffset = timezone.getRawOffset(); 436 final long hour = gmtOffset / DateUtils.HOUR_IN_MILLIS; 437 final long min = (Math.abs(gmtOffset) % DateUtils.HOUR_IN_MILLIS) / 438 DateUtils.MINUTE_IN_MILLIS; 439 440 if (useShortForm) { 441 return String.format(Locale.ENGLISH, "%+d", hour); 442 } else { 443 return String.format(Locale.ENGLISH, "GMT %+d:%02d", hour, min); 444 } 445 } 446 447 /** 448 * Given a point in time, return the subsequent moment any of the time zones changes days. 449 * e.g. Given 8:00pm on 1/1/2016 and time zones in LA and NY this method would return a Date for 450 * midnight on 1/2/2016 in the NY timezone since it changes days first. 451 * 452 * @param time a point in time from which to compute midnight on the subsequent day 453 * @param zones a collection of time zones 454 * @return the nearest point in the future at which any of the time zones changes days 455 */ getNextDay(Date time, Collection<TimeZone> zones)456 public static Date getNextDay(Date time, Collection<TimeZone> zones) { 457 Calendar next = null; 458 for (TimeZone tz : zones) { 459 final Calendar c = Calendar.getInstance(tz); 460 c.setTime(time); 461 462 // Advance to the next day. 463 c.add(Calendar.DAY_OF_YEAR, 1); 464 465 // Reset the time to midnight. 466 c.set(Calendar.HOUR_OF_DAY, 0); 467 c.set(Calendar.MINUTE, 0); 468 c.set(Calendar.SECOND, 0); 469 c.set(Calendar.MILLISECOND, 0); 470 471 if (next == null || c.compareTo(next) < 0) { 472 next = c; 473 } 474 } 475 476 return next == null ? null : next.getTime(); 477 } 478 getNumberFormattedQuantityString(Context context, int id, int quantity)479 public static String getNumberFormattedQuantityString(Context context, int id, int quantity) { 480 final String localizedQuantity = NumberFormat.getInstance().format(quantity); 481 return context.getResources().getQuantityString(id, quantity, localizedQuantity); 482 } 483 484 /** 485 * @return {@code true} iff the widget is being hosted in a container where tapping is allowed 486 */ isWidgetClickable(AppWidgetManager widgetManager, int widgetId)487 public static boolean isWidgetClickable(AppWidgetManager widgetManager, int widgetId) { 488 final Bundle wo = widgetManager.getAppWidgetOptions(widgetId); 489 return wo != null 490 && wo.getInt(OPTION_APPWIDGET_HOST_CATEGORY, -1) != WIDGET_CATEGORY_KEYGUARD; 491 } 492 493 /** 494 * @return a vector-drawable inflated from the given {@code resId} 495 */ getVectorDrawable(Context context, @DrawableRes int resId)496 public static VectorDrawableCompat getVectorDrawable(Context context, @DrawableRes int resId) { 497 return VectorDrawableCompat.create(context.getResources(), resId, context.getTheme()); 498 } 499 500 /** 501 * This method assumes the given {@code view} has already been layed out. 502 * 503 * @return a Bitmap containing an image of the {@code view} at its current size 504 */ createBitmap(View view)505 public static Bitmap createBitmap(View view) { 506 final Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), ARGB_8888); 507 final Canvas canvas = new Canvas(bitmap); 508 view.draw(canvas); 509 return bitmap; 510 } 511 512 /** 513 * {@link ArraySet} is @hide prior to {@link Build.VERSION_CODES#M}. 514 */ 515 @SuppressLint("NewApi") newArraySet(Collection<E> collection)516 public static <E> ArraySet<E> newArraySet(Collection<E> collection) { 517 final ArraySet<E> arraySet = new ArraySet<>(collection.size()); 518 arraySet.addAll(collection); 519 return arraySet; 520 } 521 522 /** 523 * @param context from which to query the current device configuration 524 * @return {@code true} if the device is currently in portrait or reverse portrait orientation 525 */ isPortrait(Context context)526 public static boolean isPortrait(Context context) { 527 return context.getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT; 528 } 529 530 /** 531 * @param context from which to query the current device configuration 532 * @return {@code true} if the device is currently in landscape or reverse landscape orientation 533 */ isLandscape(Context context)534 public static boolean isLandscape(Context context) { 535 return context.getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE; 536 } 537 now()538 public static long now() { 539 return DataModel.getDataModel().elapsedRealtime(); 540 } 541 wallClock()542 public static long wallClock() { 543 return DataModel.getDataModel().currentTimeMillis(); 544 } 545 546 /** 547 * @param context to obtain strings. 548 * @param displayMinutes whether or not minutes should be included 549 * @param isAhead {@code true} if the time should be marked 'ahead', else 'behind' 550 * @param hoursDifferent the number of hours the time is ahead/behind 551 * @param minutesDifferent the number of minutes the time is ahead/behind 552 * @return String describing the hours/minutes ahead or behind 553 */ createHoursDifferentString(Context context, boolean displayMinutes, boolean isAhead, int hoursDifferent, int minutesDifferent)554 public static String createHoursDifferentString(Context context, boolean displayMinutes, 555 boolean isAhead, int hoursDifferent, int minutesDifferent) { 556 String timeString; 557 if (displayMinutes && hoursDifferent != 0) { 558 // Both minutes and hours 559 final String hoursShortQuantityString = 560 Utils.getNumberFormattedQuantityString(context, 561 R.plurals.hours_short, Math.abs(hoursDifferent)); 562 final String minsShortQuantityString = 563 Utils.getNumberFormattedQuantityString(context, 564 R.plurals.minutes_short, Math.abs(minutesDifferent)); 565 final @StringRes int stringType = isAhead 566 ? R.string.world_hours_minutes_ahead 567 : R.string.world_hours_minutes_behind; 568 timeString = context.getString(stringType, hoursShortQuantityString, 569 minsShortQuantityString); 570 } else { 571 // Minutes alone or hours alone 572 final String hoursQuantityString = Utils.getNumberFormattedQuantityString( 573 context, R.plurals.hours, Math.abs(hoursDifferent)); 574 final String minutesQuantityString = Utils.getNumberFormattedQuantityString( 575 context, R.plurals.minutes, Math.abs(minutesDifferent)); 576 final @StringRes int stringType = isAhead ? R.string.world_time_ahead 577 : R.string.world_time_behind; 578 timeString = context.getString(stringType, displayMinutes 579 ? minutesQuantityString : hoursQuantityString); 580 } 581 return timeString; 582 } 583 584 /** 585 * @param context The context from which to obtain strings 586 * @param hours Hours to display (if any) 587 * @param minutes Minutes to display (if any) 588 * @param seconds Seconds to display 589 * @return Provided time formatted as a String 590 */ getTimeString(Context context, int hours, int minutes, int seconds)591 static String getTimeString(Context context, int hours, int minutes, int seconds) { 592 if (hours != 0) { 593 return context.getString(R.string.hours_minutes_seconds, hours, minutes, seconds); 594 } 595 if (minutes != 0) { 596 return context.getString(R.string.minutes_seconds, minutes, seconds); 597 } 598 return context.getString(R.string.seconds, seconds); 599 } 600 601 public static final class ClickAccessibilityDelegate extends AccessibilityDelegateCompat { 602 603 /** The label for talkback to apply to the view */ 604 private final String mLabel; 605 606 /** Whether or not to always make the view visible to talkback */ 607 private final boolean mIsAlwaysAccessibilityVisible; 608 ClickAccessibilityDelegate(String label)609 public ClickAccessibilityDelegate(String label) { 610 this(label, false); 611 } 612 ClickAccessibilityDelegate(String label, boolean isAlwaysAccessibilityVisible)613 public ClickAccessibilityDelegate(String label, boolean isAlwaysAccessibilityVisible) { 614 mLabel = label; 615 mIsAlwaysAccessibilityVisible = isAlwaysAccessibilityVisible; 616 } 617 618 @Override onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info)619 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { 620 super.onInitializeAccessibilityNodeInfo(host, info); 621 if (mIsAlwaysAccessibilityVisible) { 622 info.setVisibleToUser(true); 623 } 624 info.addAction(new AccessibilityActionCompat( 625 AccessibilityActionCompat.ACTION_CLICK.getId(), mLabel)); 626 } 627 } 628 }