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