Utils.java revision 944779887775bd950cf1abf348d2df461593f6ab
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.Configuration;
27import android.database.Cursor;
28import android.media.tv.TvContract;
29import android.media.tv.TvContract.Channels;
30import android.media.tv.TvContract.Programs.Genres;
31import android.media.tv.TvInputInfo;
32import android.media.tv.TvTrackInfo;
33import android.net.Uri;
34import android.os.Looper;
35import android.preference.PreferenceManager;
36import android.support.annotation.Nullable;
37import android.support.annotation.VisibleForTesting;
38import android.support.annotation.WorkerThread;
39import android.text.TextUtils;
40import android.text.format.DateUtils;
41import android.util.Log;
42import android.view.View;
43import com.android.tv.R;
44import com.android.tv.TvSingletons;
45import com.android.tv.common.SoftPreconditions;
46import com.android.tv.data.Channel;
47import com.android.tv.data.GenreItems;
48import com.android.tv.data.Program;
49import com.android.tv.data.StreamInfo;
50import java.text.SimpleDateFormat;
51import java.util.ArrayList;
52import java.util.Arrays;
53import java.util.Calendar;
54import java.util.Collection;
55import java.util.Date;
56import java.util.HashSet;
57import java.util.List;
58import java.util.Locale;
59import java.util.Set;
60import java.util.TimeZone;
61import java.util.concurrent.ExecutionException;
62import java.util.concurrent.Future;
63import java.util.concurrent.TimeUnit;
64
65/** A class that includes convenience methods for accessing TvProvider database. */
66@SuppressWarnings("TryWithResources") // TODO(b/62143348): remove when error prone check fixed
67public class Utils {
68    private static final String TAG = "Utils";
69    private static final boolean DEBUG = false;
70
71    public static final String EXTRA_KEY_ACTION = "action";
72    public static final String EXTRA_ACTION_SHOW_TV_INPUT = "show_tv_input";
73    public static final String EXTRA_KEY_FROM_LAUNCHER = "from_launcher";
74    public static final String EXTRA_KEY_RECORDED_PROGRAM_ID = "recorded_program_id";
75    public static final String EXTRA_KEY_RECORDED_PROGRAM_SEEK_TIME = "recorded_program_seek_time";
76    public static final String EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED =
77            "recorded_program_pin_checked";
78
79    private static final String PATH_CHANNEL = "channel";
80    private static final String PATH_PROGRAM = "program";
81    private static final String PATH_RECORDED_PROGRAM = "recorded_program";
82
83    private static final String PREF_KEY_LAST_WATCHED_CHANNEL_ID = "last_watched_channel_id";
84    private static final String PREF_KEY_LAST_WATCHED_CHANNEL_ID_FOR_INPUT =
85            "last_watched_channel_id_for_input_";
86    private static final String PREF_KEY_LAST_WATCHED_CHANNEL_URI = "last_watched_channel_uri";
87    private static final String PREF_KEY_LAST_WATCHED_TUNER_INPUT_ID =
88            "last_watched_tuner_input_id";
89    private static final String PREF_KEY_RECORDING_FAILED_REASONS = "recording_failed_reasons";
90    private static final String PREF_KEY_FAILED_SCHEDULED_RECORDING_INFO_SET =
91            "failed_scheduled_recording_info_set";
92
93    private static final int VIDEO_SD_WIDTH = 704;
94    private static final int VIDEO_SD_HEIGHT = 480;
95    private static final int VIDEO_HD_WIDTH = 1280;
96    private static final int VIDEO_HD_HEIGHT = 720;
97    private static final int VIDEO_FULL_HD_WIDTH = 1920;
98    private static final int VIDEO_FULL_HD_HEIGHT = 1080;
99    private static final int VIDEO_ULTRA_HD_WIDTH = 2048;
100    private static final int VIDEO_ULTRA_HD_HEIGHT = 1536;
101
102    private static final int AUDIO_CHANNEL_NONE = 0;
103    private static final int AUDIO_CHANNEL_MONO = 1;
104    private static final int AUDIO_CHANNEL_STEREO = 2;
105    private static final int AUDIO_CHANNEL_SURROUND_6 = 6;
106    private static final int AUDIO_CHANNEL_SURROUND_8 = 8;
107
108    private static final long RECORDING_FAILED_REASON_NONE = 0;
109    private static final long HALF_MINUTE_MS = TimeUnit.SECONDS.toMillis(30);
110    private static final long ONE_DAY_MS = TimeUnit.DAYS.toMillis(1);
111
112    private enum AspectRatio {
113        ASPECT_RATIO_4_3(4, 3),
114        ASPECT_RATIO_16_9(16, 9),
115        ASPECT_RATIO_21_9(21, 9);
116
117        final int width;
118        final int height;
119
120        AspectRatio(int width, int height) {
121            this.width = width;
122            this.height = height;
123        }
124
125        @Override
126        @SuppressLint("DefaultLocale")
127        public String toString() {
128            return String.format("%d:%d", width, height);
129        }
130    }
131
132    private Utils() {}
133
134    public static String buildSelectionForIds(String idName, List<Long> ids) {
135        StringBuilder sb = new StringBuilder();
136        sb.append(idName).append(" in (").append(ids.get(0));
137        for (int i = 1; i < ids.size(); ++i) {
138            sb.append(",").append(ids.get(i));
139        }
140        sb.append(")");
141        return sb.toString();
142    }
143
144    @WorkerThread
145    public static String getInputIdForChannel(Context context, long channelId) {
146        if (channelId == Channel.INVALID_ID) {
147            return null;
148        }
149        Uri channelUri = TvContract.buildChannelUri(channelId);
150        String[] projection = {TvContract.Channels.COLUMN_INPUT_ID};
151        try (Cursor cursor =
152                context.getContentResolver().query(channelUri, projection, null, null, null)) {
153            if (cursor != null && cursor.moveToNext()) {
154                return Utils.intern(cursor.getString(0));
155            }
156        }
157        return null;
158    }
159
160    public static void setLastWatchedChannel(Context context, Channel channel) {
161        if (channel == null) {
162            Log.e(TAG, "setLastWatchedChannel: channel cannot be null");
163            return;
164        }
165        PreferenceManager.getDefaultSharedPreferences(context)
166                .edit()
167                .putString(PREF_KEY_LAST_WATCHED_CHANNEL_URI, channel.getUri().toString())
168                .apply();
169        if (!channel.isPassthrough()) {
170            long channelId = channel.getId();
171            if (channel.getId() < 0) {
172                throw new IllegalArgumentException("channelId should be equal to or larger than 0");
173            }
174            PreferenceManager.getDefaultSharedPreferences(context)
175                    .edit()
176                    .putLong(PREF_KEY_LAST_WATCHED_CHANNEL_ID, channelId)
177                    .putLong(
178                            PREF_KEY_LAST_WATCHED_CHANNEL_ID_FOR_INPUT + channel.getInputId(),
179                            channelId)
180                    .putString(PREF_KEY_LAST_WATCHED_TUNER_INPUT_ID, channel.getInputId())
181                    .apply();
182        }
183    }
184
185    /** Sets recording failed reason. */
186    public static void setRecordingFailedReason(Context context, int reason) {
187        long reasons = getRecordingFailedReasons(context) | 0x1 << reason;
188        PreferenceManager.getDefaultSharedPreferences(context)
189                .edit()
190                .putLong(PREF_KEY_RECORDING_FAILED_REASONS, reasons)
191                .apply();
192    }
193
194    /** Adds the info of failed scheduled recording. */
195    public static void addFailedScheduledRecordingInfo(
196            Context context, String scheduledRecordingInfo) {
197        Set<String> failedScheduledRecordingInfoSet = getFailedScheduledRecordingInfoSet(context);
198        failedScheduledRecordingInfoSet.add(scheduledRecordingInfo);
199        PreferenceManager.getDefaultSharedPreferences(context)
200                .edit()
201                .putStringSet(
202                        PREF_KEY_FAILED_SCHEDULED_RECORDING_INFO_SET,
203                        failedScheduledRecordingInfoSet)
204                .apply();
205    }
206
207    /** Clears the failed scheduled recording info set. */
208    public static void clearFailedScheduledRecordingInfoSet(Context context) {
209        PreferenceManager.getDefaultSharedPreferences(context)
210                .edit()
211                .remove(PREF_KEY_FAILED_SCHEDULED_RECORDING_INFO_SET)
212                .apply();
213    }
214
215    /** Clears recording failed reason. */
216    public static void clearRecordingFailedReason(Context context, int reason) {
217        long reasons = getRecordingFailedReasons(context) & ~(0x1 << reason);
218        PreferenceManager.getDefaultSharedPreferences(context)
219                .edit()
220                .putLong(PREF_KEY_RECORDING_FAILED_REASONS, reasons)
221                .apply();
222    }
223
224    public static long getLastWatchedChannelId(Context context) {
225        return PreferenceManager.getDefaultSharedPreferences(context)
226                .getLong(PREF_KEY_LAST_WATCHED_CHANNEL_ID, Channel.INVALID_ID);
227    }
228
229    public static long getLastWatchedChannelIdForInput(Context context, String inputId) {
230        return PreferenceManager.getDefaultSharedPreferences(context)
231                .getLong(PREF_KEY_LAST_WATCHED_CHANNEL_ID_FOR_INPUT + inputId, Channel.INVALID_ID);
232    }
233
234    public static String getLastWatchedChannelUri(Context context) {
235        return PreferenceManager.getDefaultSharedPreferences(context)
236                .getString(PREF_KEY_LAST_WATCHED_CHANNEL_URI, null);
237    }
238
239    /** Returns the last watched tuner input id. */
240    public static String getLastWatchedTunerInputId(Context context) {
241        return PreferenceManager.getDefaultSharedPreferences(context)
242                .getString(PREF_KEY_LAST_WATCHED_TUNER_INPUT_ID, null);
243    }
244
245    private static long getRecordingFailedReasons(Context context) {
246        return PreferenceManager.getDefaultSharedPreferences(context)
247                .getLong(PREF_KEY_RECORDING_FAILED_REASONS, RECORDING_FAILED_REASON_NONE);
248    }
249
250    /** Returns the failed scheduled recordings info set. */
251    public static Set<String> getFailedScheduledRecordingInfoSet(Context context) {
252        return PreferenceManager.getDefaultSharedPreferences(context)
253                .getStringSet(PREF_KEY_FAILED_SCHEDULED_RECORDING_INFO_SET, new HashSet<>());
254    }
255
256    /** Checks do recording failed reason exist. */
257    public static boolean hasRecordingFailedReason(Context context, int reason) {
258        long reasons = getRecordingFailedReasons(context);
259        return (reasons & 0x1 << reason) != 0;
260    }
261
262    /**
263     * Returns {@code true}, if {@code uri} specifies an input, which is usually generated from
264     * {@link TvContract#buildChannelsUriForInput}.
265     */
266    public static boolean isChannelUriForInput(Uri uri) {
267        return isTvUri(uri)
268                && PATH_CHANNEL.equals(uri.getPathSegments().get(0))
269                && !TextUtils.isEmpty(uri.getQueryParameter("input"));
270    }
271
272    /**
273     * Returns {@code true}, if {@code uri} is a channel URI for a specific channel. It is copied
274     * from the hidden method TvContract.isChannelUri.
275     */
276    public static boolean isChannelUriForOneChannel(Uri uri) {
277        return isChannelUriForTunerInput(uri) || TvContract.isChannelUriForPassthroughInput(uri);
278    }
279
280    /**
281     * Returns {@code true}, if {@code uri} is a channel URI for a tuner input. It is copied from
282     * the hidden method TvContract.isChannelUriForTunerInput.
283     */
284    public static boolean isChannelUriForTunerInput(Uri uri) {
285        return isTvUri(uri) && isTwoSegmentUriStartingWith(uri, PATH_CHANNEL);
286    }
287
288    private static boolean isTvUri(Uri uri) {
289        return uri != null
290                && ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())
291                && TvContract.AUTHORITY.equals(uri.getAuthority());
292    }
293
294    private static boolean isTwoSegmentUriStartingWith(Uri uri, String pathSegment) {
295        List<String> pathSegments = uri.getPathSegments();
296        return pathSegments.size() == 2 && pathSegment.equals(pathSegments.get(0));
297    }
298
299    /** Returns {@code true}, if {@code uri} is a programs URI. */
300    public static boolean isProgramsUri(Uri uri) {
301        return isTvUri(uri) && PATH_PROGRAM.equals(uri.getPathSegments().get(0));
302    }
303
304    /** Returns {@code true}, if {@code uri} is a programs URI. */
305    public static boolean isRecordedProgramsUri(Uri uri) {
306        return isTvUri(uri) && PATH_RECORDED_PROGRAM.equals(uri.getPathSegments().get(0));
307    }
308
309    /** Gets the info of the program on particular time. */
310    @WorkerThread
311    public static Program getProgramAt(Context context, long channelId, long timeMs) {
312        if (channelId == Channel.INVALID_ID) {
313            Log.e(TAG, "getCurrentProgramAt - channelId is invalid");
314            return null;
315        }
316        if (context.getMainLooper().getThread().equals(Thread.currentThread())) {
317            String message = "getCurrentProgramAt called on main thread";
318            if (DEBUG) {
319                // Generating a stack trace can be expensive, only do it in debug mode.
320                Log.w(TAG, message, new IllegalStateException(message));
321            } else {
322                Log.w(TAG, message);
323            }
324        }
325        Uri uri =
326                TvContract.buildProgramsUriForChannel(
327                        TvContract.buildChannelUri(channelId), timeMs, timeMs);
328        try (Cursor cursor =
329                context.getContentResolver().query(uri, Program.PROJECTION, null, null, null)) {
330            if (cursor != null && cursor.moveToNext()) {
331                return Program.fromCursor(cursor);
332            }
333        }
334        return null;
335    }
336
337    /** Gets the info of the current program. */
338    @WorkerThread
339    public static Program getCurrentProgram(Context context, long channelId) {
340        return getProgramAt(context, channelId, System.currentTimeMillis());
341    }
342
343    /** Returns the round off minutes when convert milliseconds to minutes. */
344    public static int getRoundOffMinsFromMs(long millis) {
345        // Round off the result by adding half minute to the original ms.
346        return (int) TimeUnit.MILLISECONDS.toMinutes(millis + HALF_MINUTE_MS);
347    }
348
349    /**
350     * Returns duration string according to the date & time format. If {@code startUtcMillis} and
351     * {@code endUtcMills} are equal, formatted time will be returned instead.
352     *
353     * @param startUtcMillis start of duration in millis. Should be less than {code endUtcMillis}.
354     * @param endUtcMillis end of duration in millis. Should be larger than {@code startUtcMillis}.
355     * @param useShortFormat {@code true} if abbreviation is needed to save space. In that case,
356     *     date will be omitted if duration starts from today and is less than a day. If it's
357     *     necessary, {@link DateUtils#FORMAT_NUMERIC_DATE} is used otherwise.
358     */
359    public static String getDurationString(
360            Context context, long startUtcMillis, long endUtcMillis, boolean useShortFormat) {
361        return getDurationString(
362                context,
363                System.currentTimeMillis(),
364                startUtcMillis,
365                endUtcMillis,
366                useShortFormat,
367                0);
368    }
369
370    @VisibleForTesting
371    static String getDurationString(
372            Context context,
373            long baseMillis,
374            long startUtcMillis,
375            long endUtcMillis,
376            boolean useShortFormat,
377            int flag) {
378        return getDurationString(
379                context,
380                startUtcMillis,
381                endUtcMillis,
382                useShortFormat,
383                !isInGivenDay(baseMillis, startUtcMillis),
384                true,
385                flag);
386    }
387
388    /**
389     * Returns duration string according to the time format, may not contain date information. Note:
390     * At least one of showDate and showTime should be true.
391     */
392    public static String getDurationString(
393            Context context,
394            long startUtcMillis,
395            long endUtcMillis,
396            boolean useShortFormat,
397            boolean showDate,
398            boolean showTime,
399            int flag) {
400        flag |=
401                DateUtils.FORMAT_ABBREV_MONTH
402                        | ((useShortFormat) ? DateUtils.FORMAT_NUMERIC_DATE : 0);
403        SoftPreconditions.checkArgument(showTime || showDate);
404        if (showTime) {
405            flag |= DateUtils.FORMAT_SHOW_TIME;
406        }
407        if (showDate) {
408            flag |= DateUtils.FORMAT_SHOW_DATE;
409        }
410        if (startUtcMillis != endUtcMillis && useShortFormat) {
411            // Do special handling for 12:00 AM when checking if it's in the given day.
412            // If it's start, it's considered as beginning of the day. (e.g. 12:00 AM - 12:30 AM)
413            // If it's end, it's considered as end of the day (e.g. 11:00 PM - 12:00 AM)
414            if (!isInGivenDay(startUtcMillis, endUtcMillis - 1)
415                    && endUtcMillis - startUtcMillis < TimeUnit.HOURS.toMillis(11)) {
416                // Do not show date for short format.
417                // Subtracting one day is needed because {@link DateUtils@formatDateRange}
418                // automatically shows date if the duration covers multiple days.
419                return DateUtils.formatDateRange(
420                        context, startUtcMillis, endUtcMillis - TimeUnit.DAYS.toMillis(1), flag);
421            }
422        }
423        // Workaround of b/28740989.
424        // Add 1 msec to endUtcMillis to avoid DateUtils' bug with a duration of 12:00AM~12:00AM.
425        String dateRange = DateUtils.formatDateRange(context, startUtcMillis, endUtcMillis, flag);
426        return startUtcMillis == endUtcMillis || dateRange.contains("–")
427                ? dateRange
428                : DateUtils.formatDateRange(context, startUtcMillis, endUtcMillis + 1, flag);
429    }
430
431    /**
432     * Checks if two given time (in milliseconds) are in the same day with regard to the locale
433     * timezone.
434     */
435    public static boolean isInGivenDay(long dayToMatchInMillis, long subjectTimeInMillis) {
436        TimeZone timeZone = Calendar.getInstance().getTimeZone();
437        long offset = timeZone.getRawOffset();
438        if (timeZone.inDaylightTime(new Date(dayToMatchInMillis))) {
439            offset += timeZone.getDSTSavings();
440        }
441        return Utils.floorTime(dayToMatchInMillis + offset, ONE_DAY_MS)
442                == Utils.floorTime(subjectTimeInMillis + offset, ONE_DAY_MS);
443    }
444
445    /** Calculate how many days between two milliseconds. */
446    public static int computeDateDifference(long startTimeMs, long endTimeMs) {
447        Calendar calFrom = Calendar.getInstance();
448        Calendar calTo = Calendar.getInstance();
449        calFrom.setTime(new Date(startTimeMs));
450        calTo.setTime(new Date(endTimeMs));
451        resetCalendar(calFrom);
452        resetCalendar(calTo);
453        return (int) ((calTo.getTimeInMillis() - calFrom.getTimeInMillis()) / ONE_DAY_MS);
454    }
455
456    private static void resetCalendar(Calendar cal) {
457        cal.set(Calendar.HOUR_OF_DAY, 0);
458        cal.set(Calendar.MINUTE, 0);
459        cal.set(Calendar.SECOND, 0);
460        cal.set(Calendar.MILLISECOND, 0);
461    }
462
463    /** Returns the last millisecond of a day which the millis belongs to. */
464    public static long getLastMillisecondOfDay(long millis) {
465        Calendar calender = Calendar.getInstance();
466        calender.setTime(new Date(millis));
467        calender.set(Calendar.HOUR_OF_DAY, 23);
468        calender.set(Calendar.MINUTE, 59);
469        calender.set(Calendar.SECOND, 59);
470        calender.set(Calendar.MILLISECOND, 999);
471        return calender.getTimeInMillis();
472    }
473
474    public static String getAspectRatioString(int width, int height) {
475        if (width == 0 || height == 0) {
476            return "";
477        }
478
479        for (AspectRatio ratio : AspectRatio.values()) {
480            if (Math.abs((float) ratio.height / ratio.width - (float) height / width) < 0.05f) {
481                return ratio.toString();
482            }
483        }
484        return "";
485    }
486
487    public static String getAspectRatioString(float videoDisplayAspectRatio) {
488        if (videoDisplayAspectRatio <= 0) {
489            return "";
490        }
491
492        for (AspectRatio ratio : AspectRatio.values()) {
493            if (Math.abs((float) ratio.width / ratio.height - videoDisplayAspectRatio) < 0.05f) {
494                return ratio.toString();
495            }
496        }
497        return "";
498    }
499
500    public static int getVideoDefinitionLevelFromSize(int width, int height) {
501        if (width >= VIDEO_ULTRA_HD_WIDTH && height >= VIDEO_ULTRA_HD_HEIGHT) {
502            return StreamInfo.VIDEO_DEFINITION_LEVEL_ULTRA_HD;
503        } else if (width >= VIDEO_FULL_HD_WIDTH && height >= VIDEO_FULL_HD_HEIGHT) {
504            return StreamInfo.VIDEO_DEFINITION_LEVEL_FULL_HD;
505        } else if (width >= VIDEO_HD_WIDTH && height >= VIDEO_HD_HEIGHT) {
506            return StreamInfo.VIDEO_DEFINITION_LEVEL_HD;
507        } else if (width >= VIDEO_SD_WIDTH && height >= VIDEO_SD_HEIGHT) {
508            return StreamInfo.VIDEO_DEFINITION_LEVEL_SD;
509        }
510        return StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN;
511    }
512
513    public static String getVideoDefinitionLevelString(Context context, int videoFormat) {
514        switch (videoFormat) {
515            case StreamInfo.VIDEO_DEFINITION_LEVEL_ULTRA_HD:
516                return context.getResources().getString(R.string.video_definition_level_ultra_hd);
517            case StreamInfo.VIDEO_DEFINITION_LEVEL_FULL_HD:
518                return context.getResources().getString(R.string.video_definition_level_full_hd);
519            case StreamInfo.VIDEO_DEFINITION_LEVEL_HD:
520                return context.getResources().getString(R.string.video_definition_level_hd);
521            case StreamInfo.VIDEO_DEFINITION_LEVEL_SD:
522                return context.getResources().getString(R.string.video_definition_level_sd);
523        }
524        return "";
525    }
526
527    public static String getAudioChannelString(Context context, int channelCount) {
528        switch (channelCount) {
529            case 1:
530                return context.getResources().getString(R.string.audio_channel_mono);
531            case 2:
532                return context.getResources().getString(R.string.audio_channel_stereo);
533            case 6:
534                return context.getResources().getString(R.string.audio_channel_5_1);
535            case 8:
536                return context.getResources().getString(R.string.audio_channel_7_1);
537        }
538        return "";
539    }
540
541    public static boolean needToShowSampleRate(Context context, List<TvTrackInfo> tracks) {
542        Set<String> multiAudioStrings = new HashSet<>();
543        for (TvTrackInfo track : tracks) {
544            String multiAudioString = getMultiAudioString(context, track, false);
545            if (multiAudioStrings.contains(multiAudioString)) {
546                return true;
547            }
548            multiAudioStrings.add(multiAudioString);
549        }
550        return false;
551    }
552
553    public static String getMultiAudioString(
554            Context context, TvTrackInfo track, boolean showSampleRate) {
555        if (track.getType() != TvTrackInfo.TYPE_AUDIO) {
556            throw new IllegalArgumentException("Not an audio track: " + track);
557        }
558        String language = context.getString(R.string.multi_audio_unknown_language);
559        if (!TextUtils.isEmpty(track.getLanguage())) {
560            language = new Locale(track.getLanguage()).getDisplayName();
561        } else {
562            Log.d(TAG, "No language information found for the audio track: " + track);
563        }
564
565        StringBuilder metadata = new StringBuilder();
566        switch (track.getAudioChannelCount()) {
567            case AUDIO_CHANNEL_NONE:
568                break;
569            case AUDIO_CHANNEL_MONO:
570                metadata.append(context.getString(R.string.multi_audio_channel_mono));
571                break;
572            case AUDIO_CHANNEL_STEREO:
573                metadata.append(context.getString(R.string.multi_audio_channel_stereo));
574                break;
575            case AUDIO_CHANNEL_SURROUND_6:
576                metadata.append(context.getString(R.string.multi_audio_channel_surround_6));
577                break;
578            case AUDIO_CHANNEL_SURROUND_8:
579                metadata.append(context.getString(R.string.multi_audio_channel_surround_8));
580                break;
581            default:
582                if (track.getAudioChannelCount() > 0) {
583                    metadata.append(
584                            context.getString(
585                                    R.string.multi_audio_channel_suffix,
586                                    track.getAudioChannelCount()));
587                } else {
588                    Log.d(
589                            TAG,
590                            "Invalid audio channel count ("
591                                    + track.getAudioChannelCount()
592                                    + ") found for the audio track: "
593                                    + track);
594                }
595                break;
596        }
597        if (showSampleRate) {
598            int sampleRate = track.getAudioSampleRate();
599            if (sampleRate > 0) {
600                if (metadata.length() > 0) {
601                    metadata.append(", ");
602                }
603                int integerPart = sampleRate / 1000;
604                int tenths = (sampleRate % 1000) / 100;
605                metadata.append(integerPart);
606                if (tenths != 0) {
607                    metadata.append(".");
608                    metadata.append(tenths);
609                }
610                metadata.append("kHz");
611            }
612        }
613
614        if (metadata.length() == 0) {
615            return language;
616        }
617        return context.getString(
618                R.string.multi_audio_display_string_with_channel, language, metadata.toString());
619    }
620
621    public static boolean isEqualLanguage(String lang1, String lang2) {
622        if (lang1 == null) {
623            return lang2 == null;
624        } else if (lang2 == null) {
625            return false;
626        }
627        try {
628            return TextUtils.equals(
629                    new Locale(lang1).getISO3Language(), new Locale(lang2).getISO3Language());
630        } catch (Exception ignored) {
631        }
632        return false;
633    }
634
635    public static boolean isIntentAvailable(Context context, Intent intent) {
636        return context.getPackageManager()
637                        .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
638                        .size()
639                > 0;
640    }
641
642    /** Returns the label for a given input. Returns the custom label, if any. */
643    public static String loadLabel(Context context, TvInputInfo input) {
644        if (input == null) {
645            return null;
646        }
647        TvInputManagerHelper inputManager =
648                TvSingletons.getSingletons(context).getTvInputManagerHelper();
649        CharSequence customLabel = inputManager.loadCustomLabel(input);
650        String label = (customLabel == null) ? null : customLabel.toString();
651        if (TextUtils.isEmpty(label)) {
652            label = inputManager.loadLabel(input).toString();
653        }
654        return label;
655    }
656
657    /** Enable all channels synchronously. */
658    @WorkerThread
659    public static void enableAllChannels(Context context) {
660        ContentValues values = new ContentValues();
661        values.put(Channels.COLUMN_BROWSABLE, 1);
662        context.getContentResolver().update(Channels.CONTENT_URI, values, null, null);
663    }
664
665    /**
666     * Converts time in milliseconds to a String.
667     *
668     * @param fullFormat {@code true} for returning date string with a full format (e.g., Mon Aug 15
669     *     20:08:35 GMT 2016). {@code false} for a short format, {e.g., [8/15/16] 8:08 AM}, in which
670     *     date information would only appears when the target time is not today.
671     */
672    public static String toTimeString(long timeMillis, boolean fullFormat) {
673        if (fullFormat) {
674            return new Date(timeMillis).toString();
675        } else {
676            long currentTime = System.currentTimeMillis();
677            return (String)
678                    DateUtils.formatSameDayTime(
679                            timeMillis,
680                            System.currentTimeMillis(),
681                            SimpleDateFormat.SHORT,
682                            SimpleDateFormat.SHORT);
683        }
684    }
685
686    /** Converts time in milliseconds to a String. */
687    public static String toTimeString(long timeMillis) {
688        return toTimeString(timeMillis, true);
689    }
690
691    /**
692     * Returns a {@link String} object which contains the layout information of the {@code view}.
693     */
694    public static String toRectString(View view) {
695        return "{"
696                + "l="
697                + view.getLeft()
698                + ",r="
699                + view.getRight()
700                + ",t="
701                + view.getTop()
702                + ",b="
703                + view.getBottom()
704                + ",w="
705                + view.getWidth()
706                + ",h="
707                + view.getHeight()
708                + "}";
709    }
710
711    /**
712     * Floors time to the given {@code timeUnit}. For example, if time is 5:32:11 and timeUnit is
713     * one hour (60 * 60 * 1000), then the output will be 5:00:00.
714     */
715    public static long floorTime(long timeMs, long timeUnit) {
716        return timeMs - (timeMs % timeUnit);
717    }
718
719    /**
720     * Ceils time to the given {@code timeUnit}. For example, if time is 5:32:11 and timeUnit is one
721     * hour (60 * 60 * 1000), then the output will be 6:00:00.
722     */
723    public static long ceilTime(long timeMs, long timeUnit) {
724        return timeMs + timeUnit - (timeMs % timeUnit);
725    }
726
727    /** Returns an {@link String#intern() interned} string or null if the input is null. */
728    @Nullable
729    public static String intern(@Nullable String string) {
730        return string == null ? null : string.intern();
731    }
732
733    /**
734     * Check if the index is valid for the collection,
735     *
736     * @param collection the collection
737     * @param index the index position to test
738     * @return index >= 0 && index < collection.size().
739     */
740    public static boolean isIndexValid(@Nullable Collection<?> collection, int index) {
741        return collection != null && (index >= 0 && index < collection.size());
742    }
743
744    /** Returns a localized version of the text resource specified by resourceId. */
745    public static CharSequence getTextForLocale(Context context, Locale locale, int resourceId) {
746        if (locale.equals(context.getResources().getConfiguration().locale)) {
747            return context.getText(resourceId);
748        }
749        Configuration config = new Configuration(context.getResources().getConfiguration());
750        config.setLocale(locale);
751        return context.createConfigurationContext(config).getText(resourceId);
752    }
753
754    /** Checks where there is any internal TV input. */
755    public static boolean hasInternalTvInputs(Context context, boolean tunerInputOnly) {
756        for (TvInputInfo input :
757                TvSingletons.getSingletons(context)
758                        .getTvInputManagerHelper()
759                        .getTvInputInfos(true, tunerInputOnly)) {
760            if (isInternalTvInput(context, input.getId())) {
761                return true;
762            }
763        }
764        return false;
765    }
766
767    /** Returns the internal TV inputs. */
768    public static List<TvInputInfo> getInternalTvInputs(Context context, boolean tunerInputOnly) {
769        List<TvInputInfo> inputs = new ArrayList<>();
770        for (TvInputInfo input :
771                TvSingletons.getSingletons(context)
772                        .getTvInputManagerHelper()
773                        .getTvInputInfos(true, tunerInputOnly)) {
774            if (isInternalTvInput(context, input.getId())) {
775                inputs.add(input);
776            }
777        }
778        return inputs;
779    }
780
781    /** Checks whether the input is internal or not. */
782    public static boolean isInternalTvInput(Context context, String inputId) {
783        return context.getPackageName()
784                .equals(ComponentName.unflattenFromString(inputId).getPackageName());
785    }
786
787    /** Returns the TV input for the given {@code program}. */
788    @Nullable
789    public static TvInputInfo getTvInputInfoForProgram(Context context, Program program) {
790        if (!Program.isValid(program)) {
791            return null;
792        }
793        return getTvInputInfoForChannelId(context, program.getChannelId());
794    }
795
796    /** Returns the TV input for the given channel ID. */
797    @Nullable
798    public static TvInputInfo getTvInputInfoForChannelId(Context context, long channelId) {
799        TvSingletons tvSingletons = TvSingletons.getSingletons(context);
800        Channel channel = tvSingletons.getChannelDataManager().getChannel(channelId);
801        if (channel == null) {
802            return null;
803        }
804        return tvSingletons.getTvInputManagerHelper().getTvInputInfo(channel.getInputId());
805    }
806
807    /** Returns the {@link TvInputInfo} for the given input ID. */
808    @Nullable
809    public static TvInputInfo getTvInputInfoForInputId(Context context, String inputId) {
810        return TvSingletons.getSingletons(context)
811                .getTvInputManagerHelper()
812                .getTvInputInfo(inputId);
813    }
814
815    /** Returns the canonical genre ID's from the {@code genres}. */
816    public static int[] getCanonicalGenreIds(String genres) {
817        if (TextUtils.isEmpty(genres)) {
818            return null;
819        }
820        return getCanonicalGenreIds(Genres.decode(genres));
821    }
822
823    /** Returns the canonical genre ID's from the {@code genres}. */
824    public static int[] getCanonicalGenreIds(String[] canonicalGenres) {
825        if (canonicalGenres != null && canonicalGenres.length > 0) {
826            int[] results = new int[canonicalGenres.length];
827            int i = 0;
828            for (String canonicalGenre : canonicalGenres) {
829                int genreId = GenreItems.getId(canonicalGenre);
830                if (genreId == GenreItems.ID_ALL_CHANNELS) {
831                    // Skip if the genre is unknown.
832                    continue;
833                }
834                results[i++] = genreId;
835            }
836            if (i < canonicalGenres.length) {
837                results = Arrays.copyOf(results, i);
838            }
839            return results;
840        }
841        return null;
842    }
843
844    /** Returns the canonical genres for database. */
845    public static String getCanonicalGenre(int[] canonicalGenreIds) {
846        if (canonicalGenreIds == null || canonicalGenreIds.length == 0) {
847            return null;
848        }
849        String[] genres = new String[canonicalGenreIds.length];
850        for (int i = 0; i < canonicalGenreIds.length; ++i) {
851            genres[i] = GenreItems.getCanonicalGenre(canonicalGenreIds[i]);
852        }
853        return Genres.encode(genres);
854    }
855
856    /**
857     * Runs the method in main thread. If the current thread is not main thread, block it util the
858     * method is finished.
859     */
860    public static void runInMainThreadAndWait(Runnable runnable) {
861        if (Looper.myLooper() == Looper.getMainLooper()) {
862            runnable.run();
863        } else {
864            Future<?> temp = MainThreadExecutor.getInstance().submit(runnable);
865            try {
866                temp.get();
867            } catch (InterruptedException | ExecutionException e) {
868                Log.e(TAG, "failed to finish the execution", e);
869            }
870        }
871    }
872}
873