Utils.java revision 1abddd9f6225298066094e20a6c29061b6af4590
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 17package com.android.tv.util; 18 19import android.annotation.SuppressLint; 20import android.content.ComponentName; 21import android.content.ContentResolver; 22import android.content.ContentValues; 23import android.content.Context; 24import android.content.Intent; 25import android.content.pm.PackageManager; 26import android.content.res.ColorStateList; 27import android.content.res.Resources; 28import android.content.res.Resources.Theme; 29import android.database.Cursor; 30import android.media.tv.TvContentRating; 31import android.media.tv.TvContract; 32import android.media.tv.TvContract.Channels; 33import android.media.tv.TvInputInfo; 34import android.media.tv.TvTrackInfo; 35import android.net.Uri; 36import android.os.Build; 37import android.preference.PreferenceManager; 38import android.support.annotation.Nullable; 39import android.support.annotation.VisibleForTesting; 40import android.support.annotation.WorkerThread; 41import android.text.TextUtils; 42import android.text.format.DateUtils; 43import android.util.Log; 44import android.view.View; 45 46import com.android.tv.Features; 47import com.android.tv.R; 48import com.android.tv.TvApplication; 49import com.android.tv.data.Channel; 50import com.android.tv.data.Program; 51import com.android.tv.data.StreamInfo; 52import com.android.usbtuner.tvinput.UsbTunerTvInputService; 53import java.text.SimpleDateFormat; 54import java.util.ArrayList; 55import java.util.Calendar; 56import java.util.Collection; 57import java.util.Date; 58import java.util.HashSet; 59import java.util.List; 60import java.util.Locale; 61import java.util.Set; 62import java.util.TimeZone; 63import java.util.concurrent.TimeUnit; 64 65/** 66 * A class that includes convenience methods for accessing TvProvider database. 67 */ 68public class Utils { 69 private static final String TAG = "Utils"; 70 private static final boolean DEBUG = false; 71 72 private static final SimpleDateFormat ISO_8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); 73 74 public static final String EXTRA_KEY_KEYCODE = "keycode"; 75 public static final String EXTRA_KEY_ACTION = "action"; 76 public static final String EXTRA_ACTION_SHOW_TV_INPUT ="show_tv_input"; 77 public static final String EXTRA_KEY_FROM_LAUNCHER = "from_launcher"; 78 79 // Query parameter in the intent of starting MainActivity. 80 public static final String PARAM_SOURCE = "source"; 81 82 private static final String PATH_CHANNEL = "channel"; 83 private static final String PATH_PROGRAM = "program"; 84 85 private static final String PREF_KEY_LAST_WATCHED_CHANNEL_ID = "last_watched_channel_id"; 86 private static final String PREF_KEY_LAST_WATCHED_CHANNEL_ID_FOR_INPUT = 87 "last_watched_channel_id_for_input_"; 88 private static final String PREF_KEY_LAST_WATCHED_CHANNEL_URI = "last_watched_channel_uri"; 89 90 private static final int VIDEO_SD_WIDTH = 704; 91 private static final int VIDEO_SD_HEIGHT = 480; 92 private static final int VIDEO_HD_WIDTH = 1280; 93 private static final int VIDEO_HD_HEIGHT = 720; 94 private static final int VIDEO_FULL_HD_WIDTH = 1920; 95 private static final int VIDEO_FULL_HD_HEIGHT = 1080; 96 private static final int VIDEO_ULTRA_HD_WIDTH = 2048; 97 private static final int VIDEO_ULTRA_HD_HEIGHT = 1536; 98 99 private static final int AUDIO_CHANNEL_NONE = 0; 100 private static final int AUDIO_CHANNEL_MONO = 1; 101 private static final int AUDIO_CHANNEL_STEREO = 2; 102 private static final int AUDIO_CHANNEL_SURROUND_6 = 6; 103 private static final int AUDIO_CHANNEL_SURROUND_8 = 8; 104 105 private enum AspectRatio { 106 ASPECT_RATIO_4_3(4, 3), 107 ASPECT_RATIO_16_9(16, 9), 108 ASPECT_RATIO_21_9(21, 9); 109 110 final int width; 111 final int height; 112 113 AspectRatio(int width, int height) { 114 this.width = width; 115 this.height = height; 116 } 117 118 @Override 119 @SuppressLint("DefaultLocale") 120 public String toString() { 121 return String.format("%d:%d", width, height); 122 } 123 } 124 125 private Utils() { 126 } 127 128 public static String buildSelectionForIds(String idName, List<Long> ids) { 129 StringBuilder sb = new StringBuilder(); 130 sb.append(idName).append(" in (") 131 .append(ids.get(0)); 132 for (int i = 1; i < ids.size(); ++i) { 133 sb.append(",").append(ids.get(i)); 134 } 135 sb.append(")"); 136 return sb.toString(); 137 } 138 139 @WorkerThread 140 public static String getInputIdForChannel(Context context, long channelId) { 141 if (channelId == Channel.INVALID_ID) { 142 return null; 143 } 144 Uri channelUri = TvContract.buildChannelUri(channelId); 145 String[] projection = {TvContract.Channels.COLUMN_INPUT_ID}; 146 try (Cursor cursor = context.getContentResolver() 147 .query(channelUri, projection, null, null, null)) { 148 if (cursor != null && cursor.moveToNext()) { 149 return Utils.intern(cursor.getString(0)); 150 } 151 } 152 return null; 153 } 154 155 public static void setLastWatchedChannel(Context context, Channel channel) { 156 if (channel == null) { 157 Log.e(TAG, "setLastWatchedChannel: channel cannot be null"); 158 return; 159 } 160 PreferenceManager.getDefaultSharedPreferences(context).edit() 161 .putString(PREF_KEY_LAST_WATCHED_CHANNEL_URI, channel.getUri().toString()).apply(); 162 if (!channel.isPassthrough()) { 163 long channelId = channel.getId(); 164 if (channel.getId() < 0) { 165 throw new IllegalArgumentException("channelId should be equal to or larger than 0"); 166 } 167 PreferenceManager.getDefaultSharedPreferences(context).edit() 168 .putLong(PREF_KEY_LAST_WATCHED_CHANNEL_ID, channelId).apply(); 169 PreferenceManager.getDefaultSharedPreferences(context).edit() 170 .putLong(PREF_KEY_LAST_WATCHED_CHANNEL_ID_FOR_INPUT + channel.getInputId(), 171 channelId).apply(); 172 } 173 } 174 175 public static long getLastWatchedChannelId(Context context) { 176 return PreferenceManager.getDefaultSharedPreferences(context) 177 .getLong(PREF_KEY_LAST_WATCHED_CHANNEL_ID, Channel.INVALID_ID); 178 } 179 180 public static long getLastWatchedChannelIdForInput(Context context, String inputId) { 181 return PreferenceManager.getDefaultSharedPreferences(context) 182 .getLong(PREF_KEY_LAST_WATCHED_CHANNEL_ID_FOR_INPUT + inputId, Channel.INVALID_ID); 183 } 184 185 public static String getLastWatchedChannelUri(Context context) { 186 return PreferenceManager.getDefaultSharedPreferences(context) 187 .getString(PREF_KEY_LAST_WATCHED_CHANNEL_URI, null); 188 } 189 190 /** 191 * Returns {@code true}, if {@code uri} specifies an input, which is usually generated 192 * from {@link TvContract#buildChannelsUriForInput}. 193 */ 194 public static boolean isChannelUriForInput(Uri uri) { 195 return isTvUri(uri) && PATH_CHANNEL.equals(uri.getPathSegments().get(0)) 196 && !TextUtils.isEmpty(uri.getQueryParameter("input")); 197 } 198 199 /** 200 * Returns {@code true}, if {@code uri} is a channel URI for a specific channel. It is copied 201 * from the hidden method TvContract.isChannelUri. 202 */ 203 public static boolean isChannelUriForOneChannel(Uri uri) { 204 return isChannelUriForTunerInput(uri) || TvContract.isChannelUriForPassthroughInput(uri); 205 } 206 207 /** 208 * Returns {@code true}, if {@code uri} is a channel URI for a tuner input. It is copied from 209 * the hidden method TvContract.isChannelUriForTunerInput. 210 */ 211 public static boolean isChannelUriForTunerInput(Uri uri) { 212 return isTvUri(uri) && isTwoSegmentUriStartingWith(uri, PATH_CHANNEL); 213 } 214 215 private static boolean isTvUri(Uri uri) { 216 return uri != null && ContentResolver.SCHEME_CONTENT.equals(uri.getScheme()) 217 && TvContract.AUTHORITY.equals(uri.getAuthority()); 218 } 219 220 private static boolean isTwoSegmentUriStartingWith(Uri uri, String pathSegment) { 221 List<String> pathSegments = uri.getPathSegments(); 222 return pathSegments.size() == 2 && pathSegment.equals(pathSegments.get(0)); 223 } 224 225 /** 226 * Returns {@code true}, if {@code uri} is a programs URI. 227 */ 228 public static boolean isProgramsUri(Uri uri) { 229 return isTvUri(uri) && PATH_PROGRAM.equals(uri.getPathSegments().get(0)); 230 } 231 232 /** 233 * Gets the info of the program on particular time. 234 */ 235 @WorkerThread 236 public static Program getProgramAt(Context context, long channelId, long timeMs) { 237 if (channelId == Channel.INVALID_ID) { 238 Log.e(TAG, "getCurrentProgramAt - channelId is invalid"); 239 return null; 240 } 241 if (context.getMainLooper().getThread().equals(Thread.currentThread())) { 242 String message = "getCurrentProgramAt called on main thread"; 243 if (DEBUG) { 244 // Generating a stack trace can be expensive, only do it in debug mode. 245 Log.w(TAG, message, new IllegalStateException(message)); 246 } else { 247 Log.w(TAG, message); 248 } 249 } 250 Uri uri = TvContract.buildProgramsUriForChannel(TvContract.buildChannelUri(channelId), 251 timeMs, timeMs); 252 try (Cursor cursor = context.getContentResolver().query(uri, Program.PROJECTION, 253 null, null, null)) { 254 if (cursor != null && cursor.moveToNext()) { 255 return Program.fromCursor(cursor); 256 } 257 } 258 return null; 259 } 260 261 /** 262 * Gets the info of the current program. 263 */ 264 @WorkerThread 265 public static Program getCurrentProgram(Context context, long channelId) { 266 return getProgramAt(context, channelId, System.currentTimeMillis()); 267 } 268 269 /** 270 * Returns duration string according to the date & time format. 271 * If {@code startUtcMillis} and {@code endUtcMills} are equal, 272 * formatted time will be returned instead. 273 * 274 * @param startUtcMillis start of duration in millis. Should be less than {code endUtcMillis}. 275 * @param endUtcMillis end of duration in millis. Should be larger than {@code startUtcMillis}. 276 * @param useShortFormat {@code true} if abbreviation is needed to save space. 277 * In that case, date will be omitted if duration starts from today 278 * and is less than a day. If it's necessary, 279 * {@link DateUtils#FORMAT_NUMERIC_DATE} is used otherwise. 280 */ 281 public static String getDurationString( 282 Context context, long startUtcMillis, long endUtcMillis, boolean useShortFormat) { 283 return getDurationString(context, System.currentTimeMillis(), startUtcMillis, endUtcMillis, 284 useShortFormat, 0); 285 } 286 287 @VisibleForTesting 288 static String getDurationString(Context context, long baseMillis, 289 long startUtcMillis, long endUtcMillis, boolean useShortFormat, int flag) { 290 flag |= DateUtils.FORMAT_ABBREV_MONTH | DateUtils.FORMAT_SHOW_TIME 291 | ((useShortFormat) ? DateUtils.FORMAT_NUMERIC_DATE : 0); 292 if (!isInGivenDay(baseMillis, startUtcMillis)) { 293 flag |= DateUtils.FORMAT_SHOW_DATE; 294 } 295 if (startUtcMillis != endUtcMillis && useShortFormat) { 296 // Do special handling for 12:00 AM when checking if it's in the given day. 297 // If it's start, it's considered as beginning of the day. (e.g. 12:00 AM - 12:30 AM) 298 // If it's end, it's considered as end of the day (e.g. 11:00 PM - 12:00 AM) 299 if (!isInGivenDay(startUtcMillis, endUtcMillis - 1) 300 && endUtcMillis - startUtcMillis < TimeUnit.HOURS.toMillis(11)) { 301 // Do not show date for short format. 302 // Extracting a day is needed because {@link DateUtils@formatDateRange} 303 // adds date if the duration covers multiple days. 304 return DateUtils.formatDateRange(context, 305 startUtcMillis, endUtcMillis - TimeUnit.DAYS.toMillis(1), flag); 306 } 307 } 308 return DateUtils.formatDateRange(context, startUtcMillis, endUtcMillis, flag); 309 } 310 311 @VisibleForTesting 312 public static boolean isInGivenDay(long dayToMatchInMillis, long subjectTimeInMillis) { 313 final long DAY_IN_MS = TimeUnit.DAYS.toMillis(1); 314 TimeZone timeZone = Calendar.getInstance().getTimeZone(); 315 long offset = timeZone.getRawOffset(); 316 if (timeZone.inDaylightTime(new Date(dayToMatchInMillis))) { 317 offset += timeZone.getDSTSavings(); 318 } 319 return Utils.floorTime(dayToMatchInMillis + offset, DAY_IN_MS) 320 == Utils.floorTime(subjectTimeInMillis + offset, DAY_IN_MS); 321 } 322 323 public static String getAspectRatioString(int width, int height) { 324 if (width == 0 || height == 0) { 325 return ""; 326 } 327 328 for (AspectRatio ratio: AspectRatio.values()) { 329 if (Math.abs((float) ratio.height / ratio.width - (float) height / width) < 0.05f) { 330 return ratio.toString(); 331 } 332 } 333 return ""; 334 } 335 336 public static int getVideoDefinitionLevelFromSize(int width, int height) { 337 if (width >= VIDEO_ULTRA_HD_WIDTH && height >= VIDEO_ULTRA_HD_HEIGHT) { 338 return StreamInfo.VIDEO_DEFINITION_LEVEL_ULTRA_HD; 339 } else if (width >= VIDEO_FULL_HD_WIDTH && height >= VIDEO_FULL_HD_HEIGHT) { 340 return StreamInfo.VIDEO_DEFINITION_LEVEL_FULL_HD; 341 } else if (width >= VIDEO_HD_WIDTH && height >= VIDEO_HD_HEIGHT) { 342 return StreamInfo.VIDEO_DEFINITION_LEVEL_HD; 343 } else if (width >= VIDEO_SD_WIDTH && height >= VIDEO_SD_HEIGHT) { 344 return StreamInfo.VIDEO_DEFINITION_LEVEL_SD; 345 } 346 return StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN; 347 } 348 349 public static String getVideoDefinitionLevelString(Context context, int videoFormat) { 350 switch (videoFormat) { 351 case StreamInfo.VIDEO_DEFINITION_LEVEL_ULTRA_HD: 352 return context.getResources().getString( 353 R.string.video_definition_level_ultra_hd); 354 case StreamInfo.VIDEO_DEFINITION_LEVEL_FULL_HD: 355 return context.getResources().getString( 356 R.string.video_definition_level_full_hd); 357 case StreamInfo.VIDEO_DEFINITION_LEVEL_HD: 358 return context.getResources().getString(R.string.video_definition_level_hd); 359 case StreamInfo.VIDEO_DEFINITION_LEVEL_SD: 360 return context.getResources().getString(R.string.video_definition_level_sd); 361 } 362 return ""; 363 } 364 365 public static String getAudioChannelString(Context context, int channelCount) { 366 switch (channelCount) { 367 case 1: 368 return context.getResources().getString(R.string.audio_channel_mono); 369 case 2: 370 return context.getResources().getString(R.string.audio_channel_stereo); 371 case 6: 372 return context.getResources().getString(R.string.audio_channel_5_1); 373 case 8: 374 return context.getResources().getString(R.string.audio_channel_7_1); 375 } 376 return ""; 377 } 378 379 public static boolean needToShowSampleRate(Context context, List<TvTrackInfo> tracks) { 380 Set<String> multiAudioStrings = new HashSet<>(); 381 for (TvTrackInfo track : tracks) { 382 String multiAudioString = getMultiAudioString(context, track, false); 383 if (multiAudioStrings.contains(multiAudioString)) { 384 return true; 385 } 386 multiAudioStrings.add(multiAudioString); 387 } 388 return false; 389 } 390 391 public static String getMultiAudioString(Context context, TvTrackInfo track, 392 boolean showSampleRate) { 393 if (track.getType() != TvTrackInfo.TYPE_AUDIO) { 394 throw new IllegalArgumentException("Not an audio track: " + track); 395 } 396 String language = context.getString(R.string.default_language); 397 if (!TextUtils.isEmpty(track.getLanguage())) { 398 language = new Locale(track.getLanguage()).getDisplayName(); 399 } else { 400 Log.d(TAG, "No language information found for the audio track: " + track); 401 } 402 403 StringBuilder metadata = new StringBuilder(); 404 switch (track.getAudioChannelCount()) { 405 case AUDIO_CHANNEL_NONE: 406 break; 407 case AUDIO_CHANNEL_MONO: 408 metadata.append(context.getString(R.string.multi_audio_channel_mono)); 409 break; 410 case AUDIO_CHANNEL_STEREO: 411 metadata.append(context.getString(R.string.multi_audio_channel_stereo)); 412 break; 413 case AUDIO_CHANNEL_SURROUND_6: 414 metadata.append(context.getString(R.string.multi_audio_channel_surround_6)); 415 break; 416 case AUDIO_CHANNEL_SURROUND_8: 417 metadata.append(context.getString(R.string.multi_audio_channel_surround_8)); 418 break; 419 default: 420 if (track.getAudioChannelCount() > 0) { 421 metadata.append(context.getString(R.string.multi_audio_channel_suffix, 422 track.getAudioChannelCount())); 423 } else { 424 Log.d(TAG, "Invalid audio channel count (" + track.getAudioChannelCount() 425 + ") found for the audio track: " + track); 426 } 427 break; 428 } 429 if (showSampleRate) { 430 int sampleRate = track.getAudioSampleRate(); 431 if (sampleRate > 0) { 432 if (metadata.length() > 0) { 433 metadata.append(", "); 434 } 435 int integerPart = sampleRate / 1000; 436 int tenths = (sampleRate % 1000) / 100; 437 metadata.append(integerPart); 438 if (tenths != 0) { 439 metadata.append("."); 440 metadata.append(tenths); 441 } 442 metadata.append("kHz"); 443 } 444 } 445 446 if (metadata.length() == 0) { 447 return language; 448 } 449 return context.getString(R.string.multi_audio_display_string_with_channel, language, 450 metadata.toString()); 451 } 452 453 public static TvContentRating[] stringToContentRatings(String commaSeparatedRatings) { 454 if (TextUtils.isEmpty(commaSeparatedRatings)) { 455 return null; 456 } 457 String[] ratings = commaSeparatedRatings.split("\\s*,\\s*"); 458 List<TvContentRating> contentRatings = new ArrayList<>(); 459 for (String rating : ratings) { 460 try { 461 contentRatings.add(TvContentRating.unflattenFromString(rating)); 462 } catch (IllegalArgumentException e) { 463 Log.e(TAG, "Can't parse the content rating: '" + rating + "'", e); 464 } 465 } 466 return contentRatings.size() == 0 ? 467 null : contentRatings.toArray(new TvContentRating[contentRatings.size()]); 468 } 469 470 public static String contentRatingsToString(TvContentRating[] contentRatings) { 471 if (contentRatings == null || contentRatings.length == 0) { 472 return null; 473 } 474 final String DELIMITER = ","; 475 StringBuilder ratings = new StringBuilder(contentRatings[0].flattenToString()); 476 for (int i = 1; i < contentRatings.length; ++i) { 477 ratings.append(DELIMITER); 478 ratings.append(contentRatings[i].flattenToString()); 479 } 480 return ratings.toString(); 481 } 482 483 public static boolean isEqualLanguage(String lang1, String lang2) { 484 if (lang1 == null) { 485 return lang2 == null; 486 } else if (lang2 == null) { 487 return false; 488 } 489 try { 490 return TextUtils.equals( 491 new Locale(lang1).getISO3Language(), new Locale(lang2).getISO3Language()); 492 } catch (Exception ignored) { 493 } 494 return false; 495 } 496 497 public static boolean isIntentAvailable(Context context, Intent intent) { 498 return context.getPackageManager().queryIntentActivities( 499 intent, PackageManager.MATCH_DEFAULT_ONLY).size() > 0; 500 } 501 502 /** 503 * Returns the label for a given input. Returns the custom label, if any. 504 */ 505 public static String loadLabel(Context context, TvInputInfo input) { 506 if (input == null) { 507 return null; 508 } 509 CharSequence customLabel = input.loadCustomLabel(context); 510 String label = (customLabel == null) ? null : customLabel.toString(); 511 if (TextUtils.isEmpty(label)) { 512 label = input.loadLabel(context).toString(); 513 } 514 return label; 515 } 516 517 /** 518 * Enable all channels synchronously. 519 */ 520 @WorkerThread 521 public static void enableAllChannels(Context context) { 522 ContentValues values = new ContentValues(); 523 values.put(Channels.COLUMN_BROWSABLE, 1); 524 context.getContentResolver().update(Channels.CONTENT_URI, values, null, null); 525 } 526 527 /** 528 * Converts time in milliseconds to a String. 529 */ 530 public static String toTimeString(long timeMillis) { 531 return new Date(timeMillis).toString(); 532 } 533 534 /** 535 * Converts time in milliseconds to a ISO 8061 string. 536 */ 537 public static String toIsoDateTimeString(long timeMillis) { 538 return ISO_8601.format(new Date(timeMillis)); 539 } 540 541 /** 542 * Returns a {@link String} object which contains the layout information of the {@code view}. 543 */ 544 public static String toRectString(View view) { 545 return "{" 546 + "l=" + view.getLeft() 547 + ",r=" + view.getRight() 548 + ",t=" + view.getTop() 549 + ",b=" + view.getBottom() 550 + ",w=" + view.getWidth() 551 + ",h=" + view.getHeight() + "}"; 552 } 553 554 /** 555 * Floors time to the given {@code timeUnit}. For example, if time is 5:32:11 and timeUnit is 556 * one hour (60 * 60 * 1000), then the output will be 5:00:00. 557 */ 558 public static long floorTime(long timeMs, long timeUnit) { 559 return timeMs - (timeMs % timeUnit); 560 } 561 562 /** 563 * Ceils time to the given {@code timeUnit}. For example, if time is 5:32:11 and timeUnit is 564 * one hour (60 * 60 * 1000), then the output will be 6:00:00. 565 */ 566 public static long ceilTime(long timeMs, long timeUnit) { 567 return timeMs + timeUnit - (timeMs % timeUnit); 568 } 569 570 /** 571 * Returns an {@link String#intern() interned} string or null if the input is null. 572 */ 573 @Nullable 574 public static String intern(@Nullable String string) { 575 return string == null ? null : string.intern(); 576 } 577 578 /** 579 * Check if the index is valid for the collection, 580 * @param collection the collection 581 * @param index the index position to test 582 * @return index >= 0 && index < collection.size(). 583 */ 584 public static boolean isIndexValid(@Nullable Collection<?> collection, int index) { 585 return collection == null ? false : index >= 0 && index < collection.size(); 586 } 587 588 /** 589 * Returns a color integer associated with a particular resource ID. 590 * 591 * @see #getColor(android.content.res.Resources,int,Theme) 592 */ 593 public static int getColor(Resources res, int id) { 594 return getColor(res, id, null); 595 } 596 597 /** 598 * Returns a color integer associated with a particular resource ID. 599 * 600 * <p>In M version, {@link android.content.res.Resources#getColor(int)} was deprecated and 601 * {@link android.content.res.Resources#getColor(int,Theme)} was newly added. 602 * 603 * @see android.content.res.Resources#getColor(int) 604 */ 605 public static int getColor(Resources res, int id, @Nullable Theme theme) { 606 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 607 return res.getColor(id, theme); 608 } else { 609 return res.getColor(id); 610 } 611 } 612 613 /** 614 * Returns a color state list associated with a particular resource ID. 615 * 616 * @see #getColorStateList(android.content.res.Resources,int,Theme) 617 */ 618 public static ColorStateList getColorStateList(Resources res, int id) { 619 return getColorStateList(res, id, null); 620 } 621 622 /** 623 * Returns a color state list associated with a particular resource ID. 624 * 625 * <p>In M version, {@link android.content.res.Resources#getColorStateList(int)} was deprecated 626 * and {@link android.content.res.Resources#getColorStateList(int,Theme)} was newly added. 627 * 628 * @see android.content.res.Resources#getColorStateList(int) 629 */ 630 public static ColorStateList getColorStateList(Resources res, int id, @Nullable Theme theme) { 631 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 632 return res.getColorStateList(id, theme); 633 } else { 634 return res.getColorStateList(id); 635 } 636 } 637 638 /** 639 * Returns input ID of {@link UsbTunerTvInputService}. 640 */ 641 @Nullable 642 public static String getUsbTunerInputId(Context context) { 643 if (!Features.USB_TUNER.isEnabled(context)) { 644 return null; 645 } 646 return TvContract.buildInputId(new ComponentName(context.getPackageName(), 647 UsbTunerTvInputService.class.getName())); 648 } 649 650 /** 651 * Returns {@link TvInputInfo} object of {@link UsbTunerTvInputService}. 652 */ 653 @Nullable 654 public static TvInputInfo getUsbTunerInputInfo(Context context) { 655 if (!Features.USB_TUNER.isEnabled(context)) { 656 return null; 657 } 658 TvInputManagerHelper helper = TvApplication.getSingletons(context) 659 .getTvInputManagerHelper(); 660 return helper.getTvInputInfo(getUsbTunerInputId(context)); 661 } 662 663 private static final class SyncRunnable implements Runnable { 664 private final Runnable mTarget; 665 private boolean mComplete; 666 667 public SyncRunnable(Runnable target) { 668 mTarget = target; 669 } 670 671 @Override 672 public void run() { 673 try { 674 mTarget.run(); 675 } finally { 676 synchronized (this) { 677 mComplete = true; 678 notifyAll(); 679 } 680 } 681 } 682 683 public void waitForComplete() { 684 boolean interrupted = false; 685 synchronized (this) { 686 try { 687 while (!mComplete) { 688 try { 689 wait(); 690 } catch (InterruptedException e) { 691 interrupted = true; 692 } 693 } 694 } finally { 695 if (interrupted) { 696 Thread.currentThread().interrupt(); 697 } 698 } 699 } 700 } 701 } 702} 703