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.Configuration;
28import android.content.res.Resources;
29import android.content.res.Resources.Theme;
30import android.database.Cursor;
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;
45import android.widget.Toast;
46
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;
52
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    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
114        AspectRatio(int width, int height) {
115            this.width = width;
116            this.height = height;
117        }
118
119        @Override
120        @SuppressLint("DefaultLocale")
121        public String toString() {
122            return String.format("%d:%d", width, height);
123        }
124    }
125
126    private Utils() {
127    }
128
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
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
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
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
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
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     */
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     */
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     */
212    public static boolean isChannelUriForTunerInput(Uri uri) {
213        return isTvUri(uri) && isTwoSegmentUriStartingWith(uri, PATH_CHANNEL);
214    }
215
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
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     */
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
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
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     */
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
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
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
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
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
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
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
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
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
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
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
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     */
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
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     */
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     */
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     */
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     */
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     */
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
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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