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