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