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