Home | History | Annotate | Download | only in util
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.tv.util;
     18 
     19 import android.annotation.SuppressLint;
     20 import android.content.ComponentName;
     21 import android.content.ContentResolver;
     22 import android.content.ContentValues;
     23 import android.content.Context;
     24 import android.content.Intent;
     25 import android.content.pm.PackageManager;
     26 import android.content.res.Configuration;
     27 import android.database.Cursor;
     28 import android.media.tv.TvContract;
     29 import android.media.tv.TvContract.Channels;
     30 import android.media.tv.TvContract.Programs.Genres;
     31 import android.media.tv.TvInputInfo;
     32 import android.media.tv.TvTrackInfo;
     33 import android.net.Uri;
     34 import android.os.Looper;
     35 import android.preference.PreferenceManager;
     36 import android.support.annotation.Nullable;
     37 import android.support.annotation.VisibleForTesting;
     38 import android.support.annotation.WorkerThread;
     39 import android.text.TextUtils;
     40 import android.text.format.DateUtils;
     41 import android.util.ArraySet;
     42 import android.util.Log;
     43 import android.view.View;
     44 
     45 import com.android.tv.ApplicationSingletons;
     46 import com.android.tv.R;
     47 import com.android.tv.TvApplication;
     48 import com.android.tv.common.BuildConfig;
     49 import com.android.tv.common.SoftPreconditions;
     50 import com.android.tv.data.Channel;
     51 import com.android.tv.data.GenreItems;
     52 import com.android.tv.data.Program;
     53 import com.android.tv.data.StreamInfo;
     54 import com.android.tv.experiments.Experiments;
     55 
     56 import java.io.File;
     57 import java.text.SimpleDateFormat;
     58 import java.util.ArrayList;
     59 import java.util.Arrays;
     60 import java.util.Calendar;
     61 import java.util.Collection;
     62 import java.util.Date;
     63 import java.util.HashSet;
     64 import java.util.List;
     65 import java.util.Locale;
     66 import java.util.Set;
     67 import java.util.TimeZone;
     68 import java.util.concurrent.ExecutionException;
     69 import java.util.concurrent.Future;
     70 import java.util.concurrent.TimeUnit;
     71 
     72 /**
     73  * A class that includes convenience methods for accessing TvProvider database.
     74  */
     75 public 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