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 * @return {@code true} if the device is {@link Build.VERSION_CODES#O} or later 161 */ isOOrLater()162 public static boolean isOOrLater() { 163 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; 164 } 165 166 /** 167 * @param resourceId identifies an application resource 168 * @return the Uri by which the application resource is accessed 169 */ getResourceUri(Context context, @AnyRes int resourceId)170 public static Uri getResourceUri(Context context, @AnyRes int resourceId) { 171 return new Uri.Builder() 172 .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) 173 .authority(context.getPackageName()) 174 .path(String.valueOf(resourceId)) 175 .build(); 176 } 177 178 /** 179 * @param view the scrollable view to test 180 * @return {@code true} iff the {@code view} content is currently scrolled to the top 181 */ isScrolledToTop(View view)182 public static boolean isScrolledToTop(View view) { 183 return !view.canScrollVertically(-1); 184 } 185 186 /** 187 * Calculate the amount by which the radius of a CircleTimerView should be offset by any 188 * of the extra painted objects. 189 */ calculateRadiusOffset( float strokeSize, float dotStrokeSize, float markerStrokeSize)190 public static float calculateRadiusOffset( 191 float strokeSize, float dotStrokeSize, float markerStrokeSize) { 192 return Math.max(strokeSize, Math.max(dotStrokeSize, markerStrokeSize)); 193 } 194 195 /** 196 * Configure the clock that is visible to display seconds. The clock that is not visible never 197 * displays seconds to avoid it scheduling unnecessary ticking runnables. 198 */ setClockSecondsEnabled(TextClock digitalClock, AnalogClock analogClock)199 public static void setClockSecondsEnabled(TextClock digitalClock, AnalogClock analogClock) { 200 final boolean displaySeconds = DataModel.getDataModel().getDisplayClockSeconds(); 201 final DataModel.ClockStyle clockStyle = DataModel.getDataModel().getClockStyle(); 202 switch (clockStyle) { 203 case ANALOG: 204 setTimeFormat(digitalClock, false); 205 analogClock.enableSeconds(displaySeconds); 206 return; 207 case DIGITAL: 208 analogClock.enableSeconds(false); 209 setTimeFormat(digitalClock, displaySeconds); 210 return; 211 } 212 213 throw new IllegalStateException("unexpected clock style: " + clockStyle); 214 } 215 216 /** 217 * Set whether the digital or analog clock should be displayed in the application. 218 * Returns the view to be displayed. 219 */ setClockStyle(View digitalClock, View analogClock)220 public static View setClockStyle(View digitalClock, View analogClock) { 221 final DataModel.ClockStyle clockStyle = DataModel.getDataModel().getClockStyle(); 222 switch (clockStyle) { 223 case ANALOG: 224 digitalClock.setVisibility(View.GONE); 225 analogClock.setVisibility(View.VISIBLE); 226 return analogClock; 227 case DIGITAL: 228 digitalClock.setVisibility(View.VISIBLE); 229 analogClock.setVisibility(View.GONE); 230 return digitalClock; 231 } 232 233 throw new IllegalStateException("unexpected clock style: " + clockStyle); 234 } 235 236 /** 237 * For screensavers to set whether the digital or analog clock should be displayed. 238 * Returns the view to be displayed. 239 */ setScreensaverClockStyle(View digitalClock, View analogClock)240 public static View setScreensaverClockStyle(View digitalClock, View analogClock) { 241 final DataModel.ClockStyle clockStyle = DataModel.getDataModel().getScreensaverClockStyle(); 242 switch (clockStyle) { 243 case ANALOG: 244 digitalClock.setVisibility(View.GONE); 245 analogClock.setVisibility(View.VISIBLE); 246 return analogClock; 247 case DIGITAL: 248 digitalClock.setVisibility(View.VISIBLE); 249 analogClock.setVisibility(View.GONE); 250 return digitalClock; 251 } 252 253 throw new IllegalStateException("unexpected clock style: " + clockStyle); 254 } 255 256 /** 257 * For screensavers to dim the lights if necessary. 258 */ dimClockView(boolean dim, View clockView)259 public static void dimClockView(boolean dim, View clockView) { 260 Paint paint = new Paint(); 261 paint.setColor(Color.WHITE); 262 paint.setColorFilter(new PorterDuffColorFilter( 263 (dim ? 0x40FFFFFF : 0xC0FFFFFF), 264 PorterDuff.Mode.MULTIPLY)); 265 clockView.setLayerType(View.LAYER_TYPE_HARDWARE, paint); 266 } 267 268 /** 269 * Update and return the PendingIntent corresponding to the given {@code intent}. 270 * 271 * @param context the Context in which the PendingIntent should start the service 272 * @param intent an Intent describing the service to be started 273 * @return a PendingIntent that will start a service 274 */ pendingServiceIntent(Context context, Intent intent)275 public static PendingIntent pendingServiceIntent(Context context, Intent intent) { 276 return PendingIntent.getService(context, 0, intent, FLAG_UPDATE_CURRENT); 277 } 278 279 /** 280 * Update and return the PendingIntent corresponding to the given {@code intent}. 281 * 282 * @param context the Context in which the PendingIntent should start the activity 283 * @param intent an Intent describing the activity to be started 284 * @return a PendingIntent that will start an activity 285 */ pendingActivityIntent(Context context, Intent intent)286 public static PendingIntent pendingActivityIntent(Context context, Intent intent) { 287 return PendingIntent.getActivity(context, 0, intent, FLAG_UPDATE_CURRENT); 288 } 289 290 /** 291 * @return The next alarm from {@link AlarmManager} 292 */ getNextAlarm(Context context)293 public static String getNextAlarm(Context context) { 294 return isPreL() ? getNextAlarmPreL(context) : getNextAlarmLOrLater(context); 295 } 296 297 @SuppressWarnings("deprecation") 298 @TargetApi(Build.VERSION_CODES.KITKAT) getNextAlarmPreL(Context context)299 private static String getNextAlarmPreL(Context context) { 300 final ContentResolver cr = context.getContentResolver(); 301 return Settings.System.getString(cr, Settings.System.NEXT_ALARM_FORMATTED); 302 } 303 304 @TargetApi(Build.VERSION_CODES.LOLLIPOP) getNextAlarmLOrLater(Context context)305 private static String getNextAlarmLOrLater(Context context) { 306 final AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 307 final AlarmClockInfo info = getNextAlarmClock(am); 308 if (info != null) { 309 final long triggerTime = info.getTriggerTime(); 310 final Calendar alarmTime = Calendar.getInstance(); 311 alarmTime.setTimeInMillis(triggerTime); 312 return AlarmUtils.getFormattedTime(context, alarmTime); 313 } 314 315 return null; 316 } 317 318 @TargetApi(Build.VERSION_CODES.LOLLIPOP) getNextAlarmClock(AlarmManager am)319 private static AlarmClockInfo getNextAlarmClock(AlarmManager am) { 320 return am.getNextAlarmClock(); 321 } 322 323 @TargetApi(Build.VERSION_CODES.LOLLIPOP) updateNextAlarm(AlarmManager am, AlarmClockInfo info, PendingIntent op)324 public static void updateNextAlarm(AlarmManager am, AlarmClockInfo info, PendingIntent op) { 325 am.setAlarmClock(info, op); 326 } 327 isAlarmWithin24Hours(AlarmInstance alarmInstance)328 public static boolean isAlarmWithin24Hours(AlarmInstance alarmInstance) { 329 final Calendar nextAlarmTime = alarmInstance.getAlarmTime(); 330 final long nextAlarmTimeMillis = nextAlarmTime.getTimeInMillis(); 331 return nextAlarmTimeMillis - System.currentTimeMillis() <= DateUtils.DAY_IN_MILLIS; 332 } 333 334 /** 335 * Clock views can call this to refresh their alarm to the next upcoming value. 336 */ refreshAlarm(Context context, View clock)337 public static void refreshAlarm(Context context, View clock) { 338 final TextView nextAlarmIconView = (TextView) clock.findViewById(R.id.nextAlarmIcon); 339 final TextView nextAlarmView = (TextView) clock.findViewById(R.id.nextAlarm); 340 if (nextAlarmView == null) { 341 return; 342 } 343 344 final String alarm = getNextAlarm(context); 345 if (!TextUtils.isEmpty(alarm)) { 346 final String description = context.getString(R.string.next_alarm_description, alarm); 347 nextAlarmView.setText(alarm); 348 nextAlarmView.setContentDescription(description); 349 nextAlarmView.setVisibility(View.VISIBLE); 350 nextAlarmIconView.setVisibility(View.VISIBLE); 351 nextAlarmIconView.setContentDescription(description); 352 } else { 353 nextAlarmView.setVisibility(View.GONE); 354 nextAlarmIconView.setVisibility(View.GONE); 355 } 356 } 357 setClockIconTypeface(View clock)358 public static void setClockIconTypeface(View clock) { 359 final TextView nextAlarmIconView = (TextView) clock.findViewById(R.id.nextAlarmIcon); 360 nextAlarmIconView.setTypeface(UiDataModel.getUiDataModel().getAlarmIconTypeface()); 361 } 362 363 /** 364 * Clock views can call this to refresh their date. 365 **/ updateDate(String dateSkeleton, String descriptionSkeleton, View clock)366 public static void updateDate(String dateSkeleton, String descriptionSkeleton, View clock) { 367 final TextView dateDisplay = (TextView) clock.findViewById(R.id.date); 368 if (dateDisplay == null) { 369 return; 370 } 371 372 final Locale l = Locale.getDefault(); 373 final String datePattern = DateFormat.getBestDateTimePattern(l, dateSkeleton); 374 final String descriptionPattern = DateFormat.getBestDateTimePattern(l, descriptionSkeleton); 375 376 final Date now = new Date(); 377 dateDisplay.setText(new SimpleDateFormat(datePattern, l).format(now)); 378 dateDisplay.setVisibility(View.VISIBLE); 379 dateDisplay.setContentDescription(new SimpleDateFormat(descriptionPattern, l).format(now)); 380 } 381 382 /*** 383 * Formats the time in the TextClock according to the Locale with a special 384 * formatting treatment for the am/pm label. 385 * 386 * @param clock TextClock to format 387 * @param includeSeconds whether or not to include seconds in the clock's time 388 */ setTimeFormat(TextClock clock, boolean includeSeconds)389 public static void setTimeFormat(TextClock clock, boolean includeSeconds) { 390 if (clock != null) { 391 // Get the best format for 12 hours mode according to the locale 392 clock.setFormat12Hour(get12ModeFormat(0.4f /* amPmRatio */, includeSeconds)); 393 // Get the best format for 24 hours mode according to the locale 394 clock.setFormat24Hour(get24ModeFormat(includeSeconds)); 395 } 396 } 397 398 /** 399 * @param amPmRatio a value between 0 and 1 that is the ratio of the relative size of the 400 * am/pm string to the time string 401 * @param includeSeconds whether or not to include seconds in the time string 402 * @return format string for 12 hours mode time, not including seconds 403 */ get12ModeFormat(float amPmRatio, boolean includeSeconds)404 public static CharSequence get12ModeFormat(float amPmRatio, boolean includeSeconds) { 405 String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), 406 includeSeconds ? "hmsa" : "hma"); 407 if (amPmRatio <= 0) { 408 pattern = pattern.replaceAll("a", "").trim(); 409 } 410 411 // Replace spaces with "Hair Space" 412 pattern = pattern.replaceAll(" ", "\u200A"); 413 // Build a spannable so that the am/pm will be formatted 414 int amPmPos = pattern.indexOf('a'); 415 if (amPmPos == -1) { 416 return pattern; 417 } 418 419 final Spannable sp = new SpannableString(pattern); 420 sp.setSpan(new RelativeSizeSpan(amPmRatio), amPmPos, amPmPos + 1, 421 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 422 sp.setSpan(new StyleSpan(Typeface.NORMAL), amPmPos, amPmPos + 1, 423 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 424 sp.setSpan(new TypefaceSpan("sans-serif"), amPmPos, amPmPos + 1, 425 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 426 427 return sp; 428 } 429 get24ModeFormat(boolean includeSeconds)430 public static CharSequence get24ModeFormat(boolean includeSeconds) { 431 return DateFormat.getBestDateTimePattern(Locale.getDefault(), 432 includeSeconds ? "Hms" : "Hm"); 433 } 434 435 /** 436 * Returns string denoting the timezone hour offset (e.g. GMT -8:00) 437 * 438 * @param useShortForm Whether to return a short form of the header that rounds to the 439 * nearest hour and excludes the "GMT" prefix 440 */ getGMTHourOffset(TimeZone timezone, boolean useShortForm)441 public static String getGMTHourOffset(TimeZone timezone, boolean useShortForm) { 442 final int gmtOffset = timezone.getRawOffset(); 443 final long hour = gmtOffset / DateUtils.HOUR_IN_MILLIS; 444 final long min = (Math.abs(gmtOffset) % DateUtils.HOUR_IN_MILLIS) / 445 DateUtils.MINUTE_IN_MILLIS; 446 447 if (useShortForm) { 448 return String.format(Locale.ENGLISH, "%+d", hour); 449 } else { 450 return String.format(Locale.ENGLISH, "GMT %+d:%02d", hour, min); 451 } 452 } 453 454 /** 455 * Given a point in time, return the subsequent moment any of the time zones changes days. 456 * e.g. Given 8:00pm on 1/1/2016 and time zones in LA and NY this method would return a Date for 457 * midnight on 1/2/2016 in the NY timezone since it changes days first. 458 * 459 * @param time a point in time from which to compute midnight on the subsequent day 460 * @param zones a collection of time zones 461 * @return the nearest point in the future at which any of the time zones changes days 462 */ getNextDay(Date time, Collection<TimeZone> zones)463 public static Date getNextDay(Date time, Collection<TimeZone> zones) { 464 Calendar next = null; 465 for (TimeZone tz : zones) { 466 final Calendar c = Calendar.getInstance(tz); 467 c.setTime(time); 468 469 // Advance to the next day. 470 c.add(Calendar.DAY_OF_YEAR, 1); 471 472 // Reset the time to midnight. 473 c.set(Calendar.HOUR_OF_DAY, 0); 474 c.set(Calendar.MINUTE, 0); 475 c.set(Calendar.SECOND, 0); 476 c.set(Calendar.MILLISECOND, 0); 477 478 if (next == null || c.compareTo(next) < 0) { 479 next = c; 480 } 481 } 482 483 return next == null ? null : next.getTime(); 484 } 485 getNumberFormattedQuantityString(Context context, int id, int quantity)486 public static String getNumberFormattedQuantityString(Context context, int id, int quantity) { 487 final String localizedQuantity = NumberFormat.getInstance().format(quantity); 488 return context.getResources().getQuantityString(id, quantity, localizedQuantity); 489 } 490 491 /** 492 * @return {@code true} iff the widget is being hosted in a container where tapping is allowed 493 */ isWidgetClickable(AppWidgetManager widgetManager, int widgetId)494 public static boolean isWidgetClickable(AppWidgetManager widgetManager, int widgetId) { 495 final Bundle wo = widgetManager.getAppWidgetOptions(widgetId); 496 return wo != null 497 && wo.getInt(OPTION_APPWIDGET_HOST_CATEGORY, -1) != WIDGET_CATEGORY_KEYGUARD; 498 } 499 500 /** 501 * @return a vector-drawable inflated from the given {@code resId} 502 */ getVectorDrawable(Context context, @DrawableRes int resId)503 public static VectorDrawableCompat getVectorDrawable(Context context, @DrawableRes int resId) { 504 return VectorDrawableCompat.create(context.getResources(), resId, context.getTheme()); 505 } 506 507 /** 508 * This method assumes the given {@code view} has already been layed out. 509 * 510 * @return a Bitmap containing an image of the {@code view} at its current size 511 */ createBitmap(View view)512 public static Bitmap createBitmap(View view) { 513 final Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), ARGB_8888); 514 final Canvas canvas = new Canvas(bitmap); 515 view.draw(canvas); 516 return bitmap; 517 } 518 519 /** 520 * {@link ArraySet} is @hide prior to {@link Build.VERSION_CODES#M}. 521 */ 522 @SuppressLint("NewApi") newArraySet(Collection<E> collection)523 public static <E> ArraySet<E> newArraySet(Collection<E> collection) { 524 final ArraySet<E> arraySet = new ArraySet<>(collection.size()); 525 arraySet.addAll(collection); 526 return arraySet; 527 } 528 529 /** 530 * @param context from which to query the current device configuration 531 * @return {@code true} if the device is currently in portrait or reverse portrait orientation 532 */ isPortrait(Context context)533 public static boolean isPortrait(Context context) { 534 return context.getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT; 535 } 536 537 /** 538 * @param context from which to query the current device configuration 539 * @return {@code true} if the device is currently in landscape or reverse landscape orientation 540 */ isLandscape(Context context)541 public static boolean isLandscape(Context context) { 542 return context.getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE; 543 } 544 now()545 public static long now() { 546 return DataModel.getDataModel().elapsedRealtime(); 547 } 548 wallClock()549 public static long wallClock() { 550 return DataModel.getDataModel().currentTimeMillis(); 551 } 552 553 /** 554 * @param context to obtain strings. 555 * @param displayMinutes whether or not minutes should be included 556 * @param isAhead {@code true} if the time should be marked 'ahead', else 'behind' 557 * @param hoursDifferent the number of hours the time is ahead/behind 558 * @param minutesDifferent the number of minutes the time is ahead/behind 559 * @return String describing the hours/minutes ahead or behind 560 */ createHoursDifferentString(Context context, boolean displayMinutes, boolean isAhead, int hoursDifferent, int minutesDifferent)561 public static String createHoursDifferentString(Context context, boolean displayMinutes, 562 boolean isAhead, int hoursDifferent, int minutesDifferent) { 563 String timeString; 564 if (displayMinutes && hoursDifferent != 0) { 565 // Both minutes and hours 566 final String hoursShortQuantityString = 567 Utils.getNumberFormattedQuantityString(context, 568 R.plurals.hours_short, Math.abs(hoursDifferent)); 569 final String minsShortQuantityString = 570 Utils.getNumberFormattedQuantityString(context, 571 R.plurals.minutes_short, Math.abs(minutesDifferent)); 572 final @StringRes int stringType = isAhead 573 ? R.string.world_hours_minutes_ahead 574 : R.string.world_hours_minutes_behind; 575 timeString = context.getString(stringType, hoursShortQuantityString, 576 minsShortQuantityString); 577 } else { 578 // Minutes alone or hours alone 579 final String hoursQuantityString = Utils.getNumberFormattedQuantityString( 580 context, R.plurals.hours, Math.abs(hoursDifferent)); 581 final String minutesQuantityString = Utils.getNumberFormattedQuantityString( 582 context, R.plurals.minutes, Math.abs(minutesDifferent)); 583 final @StringRes int stringType = isAhead ? R.string.world_time_ahead 584 : R.string.world_time_behind; 585 timeString = context.getString(stringType, displayMinutes 586 ? minutesQuantityString : hoursQuantityString); 587 } 588 return timeString; 589 } 590 591 /** 592 * @param context The context from which to obtain strings 593 * @param hours Hours to display (if any) 594 * @param minutes Minutes to display (if any) 595 * @param seconds Seconds to display 596 * @return Provided time formatted as a String 597 */ getTimeString(Context context, int hours, int minutes, int seconds)598 static String getTimeString(Context context, int hours, int minutes, int seconds) { 599 if (hours != 0) { 600 return context.getString(R.string.hours_minutes_seconds, hours, minutes, seconds); 601 } 602 if (minutes != 0) { 603 return context.getString(R.string.minutes_seconds, minutes, seconds); 604 } 605 return context.getString(R.string.seconds, seconds); 606 } 607 608 public static final class ClickAccessibilityDelegate extends AccessibilityDelegateCompat { 609 610 /** The label for talkback to apply to the view */ 611 private final String mLabel; 612 613 /** Whether or not to always make the view visible to talkback */ 614 private final boolean mIsAlwaysAccessibilityVisible; 615 ClickAccessibilityDelegate(String label)616 public ClickAccessibilityDelegate(String label) { 617 this(label, false); 618 } 619 ClickAccessibilityDelegate(String label, boolean isAlwaysAccessibilityVisible)620 public ClickAccessibilityDelegate(String label, boolean isAlwaysAccessibilityVisible) { 621 mLabel = label; 622 mIsAlwaysAccessibilityVisible = isAlwaysAccessibilityVisible; 623 } 624 625 @Override onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info)626 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { 627 super.onInitializeAccessibilityNodeInfo(host, info); 628 if (mIsAlwaysAccessibilityVisible) { 629 info.setVisibleToUser(true); 630 } 631 info.addAction(new AccessibilityActionCompat( 632 AccessibilityActionCompat.ACTION_CLICK.getId(), mLabel)); 633 } 634 } 635 }