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.Log;
     42 import android.view.View;
     43 import com.android.tv.R;
     44 import com.android.tv.TvSingletons;
     45 import com.android.tv.common.SoftPreconditions;
     46 import com.android.tv.common.util.Clock;
     47 import com.android.tv.data.GenreItems;
     48 import com.android.tv.data.Program;
     49 import com.android.tv.data.StreamInfo;
     50 import com.android.tv.data.api.Channel;
     51 import java.text.SimpleDateFormat;
     52 import java.util.ArrayList;
     53 import java.util.Arrays;
     54 import java.util.Calendar;
     55 import java.util.Collection;
     56 import java.util.Date;
     57 import java.util.HashSet;
     58 import java.util.List;
     59 import java.util.Locale;
     60 import java.util.Set;
     61 import java.util.TimeZone;
     62 import java.util.concurrent.ExecutionException;
     63 import java.util.concurrent.Future;
     64 import java.util.concurrent.TimeUnit;
     65 
     66 /** A class that includes convenience methods for accessing TvProvider database. */
     67 public 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     /**
    371      * Returns duration string according to the date & time format. If {@code startUtcMillis} and
    372      * {@code endUtcMills} are equal, formatted time will be returned instead.
    373      *
    374      * @param clock the clock used to get the current time.
    375      * @param startUtcMillis start of duration in millis. Should be less than {code endUtcMillis}.
    376      * @param endUtcMillis end of duration in millis. Should be larger than {@code startUtcMillis}.
    377      * @param useShortFormat {@code true} if abbreviation is needed to save space. In that case,
    378      *     date will be omitted if duration starts from today and is less than a day. If it's
    379      *     necessary, {@link DateUtils#FORMAT_NUMERIC_DATE} is used otherwise.
    380      */
    381     public static String getDurationString(
    382             Context context,
    383             Clock clock,
    384             long startUtcMillis,
    385             long endUtcMillis,
    386             boolean useShortFormat) {
    387         return getDurationString(
    388                 context,
    389                 clock.currentTimeMillis(),
    390                 startUtcMillis,
    391                 endUtcMillis,
    392                 useShortFormat,
    393                 0);
    394     }
    395 
    396     @VisibleForTesting
    397     static String getDurationString(
    398             Context context,
    399             long baseMillis,
    400             long startUtcMillis,
    401             long endUtcMillis,
    402             boolean useShortFormat,
    403             int flag) {
    404         return getDurationString(
    405                 context,
    406                 startUtcMillis,
    407                 endUtcMillis,
    408                 useShortFormat,
    409                 !isInGivenDay(baseMillis, startUtcMillis),
    410                 true,
    411                 flag);
    412     }
    413 
    414     /**
    415      * Returns duration string according to the time format, may not contain date information. Note:
    416      * At least one of showDate and showTime should be true.
    417      */
    418     public static String getDurationString(
    419             Context context,
    420             long startUtcMillis,
    421             long endUtcMillis,
    422             boolean useShortFormat,
    423             boolean showDate,
    424             boolean showTime,
    425             int flag) {
    426         flag |=
    427                 DateUtils.FORMAT_ABBREV_MONTH
    428                         | ((useShortFormat) ? DateUtils.FORMAT_NUMERIC_DATE : 0);
    429         SoftPreconditions.checkArgument(showTime || showDate);
    430         if (showTime) {
    431             flag |= DateUtils.FORMAT_SHOW_TIME;
    432         }
    433         if (showDate) {
    434             flag |= DateUtils.FORMAT_SHOW_DATE;
    435         }
    436         if (startUtcMillis != endUtcMillis && useShortFormat) {
    437             // Do special handling for 12:00 AM when checking if it's in the given day.
    438             // If it's start, it's considered as beginning of the day. (e.g. 12:00 AM - 12:30 AM)
    439             // If it's end, it's considered as end of the day (e.g. 11:00 PM - 12:00 AM)
    440             if (!isInGivenDay(startUtcMillis, endUtcMillis - 1)
    441                     && endUtcMillis - startUtcMillis < TimeUnit.HOURS.toMillis(11)) {
    442                 // Do not show date for short format.
    443                 // Subtracting one day is needed because {@link DateUtils@formatDateRange}
    444                 // automatically shows date if the duration covers multiple days.
    445                 return DateUtils.formatDateRange(
    446                         context, startUtcMillis, endUtcMillis - TimeUnit.DAYS.toMillis(1), flag);
    447             }
    448         }
    449         // Workaround of b/28740989.
    450         // Add 1 msec to endUtcMillis to avoid DateUtils' bug with a duration of 12:00AM~12:00AM.
    451         String dateRange = DateUtils.formatDateRange(context, startUtcMillis, endUtcMillis, flag);
    452         return startUtcMillis == endUtcMillis || dateRange.contains("")
    453                 ? dateRange
    454                 : DateUtils.formatDateRange(context, startUtcMillis, endUtcMillis + 1, flag);
    455     }
    456 
    457     /**
    458      * Checks if two given time (in milliseconds) are in the same day with regard to the locale
    459      * timezone.
    460      */
    461     public static boolean isInGivenDay(long dayToMatchInMillis, long subjectTimeInMillis) {
    462         TimeZone timeZone = Calendar.getInstance().getTimeZone();
    463         long offset = timeZone.getRawOffset();
    464         if (timeZone.inDaylightTime(new Date(dayToMatchInMillis))) {
    465             offset += timeZone.getDSTSavings();
    466         }
    467         return Utils.floorTime(dayToMatchInMillis + offset, ONE_DAY_MS)
    468                 == Utils.floorTime(subjectTimeInMillis + offset, ONE_DAY_MS);
    469     }
    470 
    471     /** Calculate how many days between two milliseconds. */
    472     public static int computeDateDifference(long startTimeMs, long endTimeMs) {
    473         Calendar calFrom = Calendar.getInstance();
    474         Calendar calTo = Calendar.getInstance();
    475         calFrom.setTime(new Date(startTimeMs));
    476         calTo.setTime(new Date(endTimeMs));
    477         resetCalendar(calFrom);
    478         resetCalendar(calTo);
    479         return (int) ((calTo.getTimeInMillis() - calFrom.getTimeInMillis()) / ONE_DAY_MS);
    480     }
    481 
    482     private static void resetCalendar(Calendar cal) {
    483         cal.set(Calendar.HOUR_OF_DAY, 0);
    484         cal.set(Calendar.MINUTE, 0);
    485         cal.set(Calendar.SECOND, 0);
    486         cal.set(Calendar.MILLISECOND, 0);
    487     }
    488 
    489     /** Returns the last millisecond of a day which the millis belongs to. */
    490     public static long getLastMillisecondOfDay(long millis) {
    491         Calendar calendar = Calendar.getInstance();
    492         calendar.setTime(new Date(millis));
    493         calendar.set(Calendar.HOUR_OF_DAY, 23);
    494         calendar.set(Calendar.MINUTE, 59);
    495         calendar.set(Calendar.SECOND, 59);
    496         calendar.set(Calendar.MILLISECOND, 999);
    497         return calendar.getTimeInMillis();
    498     }
    499 
    500     /** Returns the last millisecond of a day which the millis belongs to. */
    501     public static long getFirstMillisecondOfDay(long millis) {
    502         Calendar calendar = Calendar.getInstance();
    503         calendar.setTime(new Date(millis));
    504         resetCalendar(calendar);
    505         return calendar.getTimeInMillis();
    506     }
    507 
    508     public static String getAspectRatioString(int width, int height) {
    509         if (width == 0 || height == 0) {
    510             return "";
    511         }
    512 
    513         for (AspectRatio ratio : AspectRatio.values()) {
    514             if (Math.abs((float) ratio.height / ratio.width - (float) height / width) < 0.05f) {
    515                 return ratio.toString();
    516             }
    517         }
    518         return "";
    519     }
    520 
    521     public static String getAspectRatioString(float videoDisplayAspectRatio) {
    522         if (videoDisplayAspectRatio <= 0) {
    523             return "";
    524         }
    525 
    526         for (AspectRatio ratio : AspectRatio.values()) {
    527             if (Math.abs((float) ratio.width / ratio.height - videoDisplayAspectRatio) < 0.05f) {
    528                 return ratio.toString();
    529             }
    530         }
    531         return "";
    532     }
    533 
    534     public static int getVideoDefinitionLevelFromSize(int width, int height) {
    535         if (width >= VIDEO_ULTRA_HD_WIDTH && height >= VIDEO_ULTRA_HD_HEIGHT) {
    536             return StreamInfo.VIDEO_DEFINITION_LEVEL_ULTRA_HD;
    537         } else if (width >= VIDEO_FULL_HD_WIDTH && height >= VIDEO_FULL_HD_HEIGHT) {
    538             return StreamInfo.VIDEO_DEFINITION_LEVEL_FULL_HD;
    539         } else if (width >= VIDEO_HD_WIDTH && height >= VIDEO_HD_HEIGHT) {
    540             return StreamInfo.VIDEO_DEFINITION_LEVEL_HD;
    541         } else if (width >= VIDEO_SD_WIDTH && height >= VIDEO_SD_HEIGHT) {
    542             return StreamInfo.VIDEO_DEFINITION_LEVEL_SD;
    543         }
    544         return StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN;
    545     }
    546 
    547     public static String getVideoDefinitionLevelString(Context context, int videoFormat) {
    548         switch (videoFormat) {
    549             case StreamInfo.VIDEO_DEFINITION_LEVEL_ULTRA_HD:
    550                 return context.getResources().getString(R.string.video_definition_level_ultra_hd);
    551             case StreamInfo.VIDEO_DEFINITION_LEVEL_FULL_HD:
    552                 return context.getResources().getString(R.string.video_definition_level_full_hd);
    553             case StreamInfo.VIDEO_DEFINITION_LEVEL_HD:
    554                 return context.getResources().getString(R.string.video_definition_level_hd);
    555             case StreamInfo.VIDEO_DEFINITION_LEVEL_SD:
    556                 return context.getResources().getString(R.string.video_definition_level_sd);
    557         }
    558         return "";
    559     }
    560 
    561     public static String getAudioChannelString(Context context, int channelCount) {
    562         switch (channelCount) {
    563             case 1:
    564                 return context.getResources().getString(R.string.audio_channel_mono);
    565             case 2:
    566                 return context.getResources().getString(R.string.audio_channel_stereo);
    567             case 6:
    568                 return context.getResources().getString(R.string.audio_channel_5_1);
    569             case 8:
    570                 return context.getResources().getString(R.string.audio_channel_7_1);
    571         }
    572         return "";
    573     }
    574 
    575     public static boolean needToShowSampleRate(Context context, List<TvTrackInfo> tracks) {
    576         Set<String> multiAudioStrings = new HashSet<>();
    577         for (TvTrackInfo track : tracks) {
    578             String multiAudioString = getMultiAudioString(context, track, false);
    579             if (multiAudioStrings.contains(multiAudioString)) {
    580                 return true;
    581             }
    582             multiAudioStrings.add(multiAudioString);
    583         }
    584         return false;
    585     }
    586 
    587     public static String getMultiAudioString(
    588             Context context, TvTrackInfo track, boolean showSampleRate) {
    589         if (track.getType() != TvTrackInfo.TYPE_AUDIO) {
    590             throw new IllegalArgumentException("Not an audio track: " + track);
    591         }
    592         String language = context.getString(R.string.multi_audio_unknown_language);
    593         if (!TextUtils.isEmpty(track.getLanguage())) {
    594             language = new Locale(track.getLanguage()).getDisplayName();
    595         } else {
    596             Log.d(TAG, "No language information found for the audio track: " + track);
    597         }
    598 
    599         StringBuilder metadata = new StringBuilder();
    600         switch (track.getAudioChannelCount()) {
    601             case AUDIO_CHANNEL_NONE:
    602                 break;
    603             case AUDIO_CHANNEL_MONO:
    604                 metadata.append(context.getString(R.string.multi_audio_channel_mono));
    605                 break;
    606             case AUDIO_CHANNEL_STEREO:
    607                 metadata.append(context.getString(R.string.multi_audio_channel_stereo));
    608                 break;
    609             case AUDIO_CHANNEL_SURROUND_6:
    610                 metadata.append(context.getString(R.string.multi_audio_channel_surround_6));
    611                 break;
    612             case AUDIO_CHANNEL_SURROUND_8:
    613                 metadata.append(context.getString(R.string.multi_audio_channel_surround_8));
    614                 break;
    615             default:
    616                 if (track.getAudioChannelCount() > 0) {
    617                     metadata.append(
    618                             context.getString(
    619                                     R.string.multi_audio_channel_suffix,
    620                                     track.getAudioChannelCount()));
    621                 } else {
    622                     Log.d(
    623                             TAG,
    624                             "Invalid audio channel count ("
    625                                     + track.getAudioChannelCount()
    626                                     + ") found for the audio track: "
    627                                     + track);
    628                 }
    629                 break;
    630         }
    631         if (showSampleRate) {
    632             int sampleRate = track.getAudioSampleRate();
    633             if (sampleRate > 0) {
    634                 if (metadata.length() > 0) {
    635                     metadata.append(", ");
    636                 }
    637                 int integerPart = sampleRate / 1000;
    638                 int tenths = (sampleRate % 1000) / 100;
    639                 metadata.append(integerPart);
    640                 if (tenths != 0) {
    641                     metadata.append(".");
    642                     metadata.append(tenths);
    643                 }
    644                 metadata.append("kHz");
    645             }
    646         }
    647 
    648         if (metadata.length() == 0) {
    649             return language;
    650         }
    651         return context.getString(
    652                 R.string.multi_audio_display_string_with_channel, language, metadata.toString());
    653     }
    654 
    655     public static boolean isEqualLanguage(String lang1, String lang2) {
    656         if (lang1 == null) {
    657             return lang2 == null;
    658         } else if (lang2 == null) {
    659             return false;
    660         }
    661         try {
    662             return TextUtils.equals(
    663                     new Locale(lang1).getISO3Language(), new Locale(lang2).getISO3Language());
    664         } catch (Exception ignored) {
    665         }
    666         return false;
    667     }
    668 
    669     public static boolean isIntentAvailable(Context context, Intent intent) {
    670         return context.getPackageManager()
    671                         .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
    672                         .size()
    673                 > 0;
    674     }
    675 
    676     /** Returns the label for a given input. Returns the custom label, if any. */
    677     public static String loadLabel(Context context, TvInputInfo input) {
    678         if (input == null) {
    679             return null;
    680         }
    681         TvInputManagerHelper inputManager =
    682                 TvSingletons.getSingletons(context).getTvInputManagerHelper();
    683         CharSequence customLabel = inputManager.loadCustomLabel(input);
    684         String label = (customLabel == null) ? null : customLabel.toString();
    685         if (TextUtils.isEmpty(label)) {
    686             label = inputManager.loadLabel(input).toString();
    687         }
    688         return label;
    689     }
    690 
    691     /** Enable all channels synchronously. */
    692     @WorkerThread
    693     public static void enableAllChannels(Context context) {
    694         ContentValues values = new ContentValues();
    695         values.put(Channels.COLUMN_BROWSABLE, 1);
    696         context.getContentResolver().update(Channels.CONTENT_URI, values, null, null);
    697     }
    698 
    699     /**
    700      * Converts time in milliseconds to a String.
    701      *
    702      * @param fullFormat {@code true} for returning date string with a full format (e.g., Mon Aug 15
    703      *     20:08:35 GMT 2016). {@code false} for a short format, {e.g., 8/15/16 or 8:08 AM}, in
    704      *     which only the time is shown if the time is on the same day as now, and only the date is
    705      *     shown if it's a different day.
    706      */
    707     public static String toTimeString(long timeMillis, boolean fullFormat) {
    708         if (fullFormat) {
    709             return new Date(timeMillis).toString();
    710         } else {
    711             long currentTime = System.currentTimeMillis();
    712             return (String)
    713                     DateUtils.formatSameDayTime(
    714                             timeMillis,
    715                             System.currentTimeMillis(),
    716                             SimpleDateFormat.SHORT,
    717                             SimpleDateFormat.SHORT);
    718         }
    719     }
    720 
    721     /** Converts time in milliseconds to a String. */
    722     public static String toTimeString(long timeMillis) {
    723         return toTimeString(timeMillis, true);
    724     }
    725 
    726     /**
    727      * Returns a {@link String} object which contains the layout information of the {@code view}.
    728      */
    729     public static String toRectString(View view) {
    730         return "{"
    731                 + "l="
    732                 + view.getLeft()
    733                 + ",r="
    734                 + view.getRight()
    735                 + ",t="
    736                 + view.getTop()
    737                 + ",b="
    738                 + view.getBottom()
    739                 + ",w="
    740                 + view.getWidth()
    741                 + ",h="
    742                 + view.getHeight()
    743                 + "}";
    744     }
    745 
    746     /**
    747      * Floors time to the given {@code timeUnit}. For example, if time is 5:32:11 and timeUnit is
    748      * one hour (60 * 60 * 1000), then the output will be 5:00:00.
    749      */
    750     public static long floorTime(long timeMs, long timeUnit) {
    751         return timeMs - (timeMs % timeUnit);
    752     }
    753 
    754     /**
    755      * Ceils time to the given {@code timeUnit}. For example, if time is 5:32:11 and timeUnit is one
    756      * hour (60 * 60 * 1000), then the output will be 6:00:00.
    757      */
    758     public static long ceilTime(long timeMs, long timeUnit) {
    759         return timeMs + timeUnit - (timeMs % timeUnit);
    760     }
    761 
    762     /** Returns an {@link String#intern() interned} string or null if the input is null. */
    763     @Nullable
    764     public static String intern(@Nullable String string) {
    765         return string == null ? null : string.intern();
    766     }
    767 
    768     /**
    769      * Check if the index is valid for the collection,
    770      *
    771      * @param collection the collection
    772      * @param index the index position to test
    773      * @return index >= 0 && index < collection.size().
    774      */
    775     public static boolean isIndexValid(@Nullable Collection<?> collection, int index) {
    776         return collection != null && (index >= 0 && index < collection.size());
    777     }
    778 
    779     /** Returns a localized version of the text resource specified by resourceId. */
    780     public static CharSequence getTextForLocale(Context context, Locale locale, int resourceId) {
    781         if (locale.equals(context.getResources().getConfiguration().locale)) {
    782             return context.getText(resourceId);
    783         }
    784         Configuration config = new Configuration(context.getResources().getConfiguration());
    785         config.setLocale(locale);
    786         return context.createConfigurationContext(config).getText(resourceId);
    787     }
    788 
    789     /** Checks where there is any internal TV input. */
    790     public static boolean hasInternalTvInputs(Context context, boolean tunerInputOnly) {
    791         for (TvInputInfo input :
    792                 TvSingletons.getSingletons(context)
    793                         .getTvInputManagerHelper()
    794                         .getTvInputInfos(true, tunerInputOnly)) {
    795             if (isInternalTvInput(context, input.getId())) {
    796                 return true;
    797             }
    798         }
    799         return false;
    800     }
    801 
    802     /** Returns the internal TV inputs. */
    803     public static List<TvInputInfo> getInternalTvInputs(Context context, boolean tunerInputOnly) {
    804         List<TvInputInfo> inputs = new ArrayList<>();
    805         for (TvInputInfo input :
    806                 TvSingletons.getSingletons(context)
    807                         .getTvInputManagerHelper()
    808                         .getTvInputInfos(true, tunerInputOnly)) {
    809             if (isInternalTvInput(context, input.getId())) {
    810                 inputs.add(input);
    811             }
    812         }
    813         return inputs;
    814     }
    815 
    816     /** Checks whether the input is internal or not. */
    817     public static boolean isInternalTvInput(Context context, String inputId) {
    818         return context.getPackageName()
    819                 .equals(ComponentName.unflattenFromString(inputId).getPackageName());
    820     }
    821 
    822     /** Returns the TV input for the given {@code program}. */
    823     @Nullable
    824     public static TvInputInfo getTvInputInfoForProgram(Context context, Program program) {
    825         if (!Program.isProgramValid(program)) {
    826             return null;
    827         }
    828         return getTvInputInfoForChannelId(context, program.getChannelId());
    829     }
    830 
    831     /** Returns the TV input for the given channel ID. */
    832     @Nullable
    833     public static TvInputInfo getTvInputInfoForChannelId(Context context, long channelId) {
    834         TvSingletons tvSingletons = TvSingletons.getSingletons(context);
    835         Channel channel = tvSingletons.getChannelDataManager().getChannel(channelId);
    836         if (channel == null) {
    837             return null;
    838         }
    839         return tvSingletons.getTvInputManagerHelper().getTvInputInfo(channel.getInputId());
    840     }
    841 
    842     /** Returns the {@link TvInputInfo} for the given input ID. */
    843     @Nullable
    844     public static TvInputInfo getTvInputInfoForInputId(Context context, String inputId) {
    845         return TvSingletons.getSingletons(context)
    846                 .getTvInputManagerHelper()
    847                 .getTvInputInfo(inputId);
    848     }
    849 
    850     /** Returns the canonical genre ID's from the {@code genres}. */
    851     public static int[] getCanonicalGenreIds(String genres) {
    852         if (TextUtils.isEmpty(genres)) {
    853             return null;
    854         }
    855         return getCanonicalGenreIds(Genres.decode(genres));
    856     }
    857 
    858     /** Returns the canonical genre ID's from the {@code genres}. */
    859     public static int[] getCanonicalGenreIds(String[] canonicalGenres) {
    860         if (canonicalGenres != null && canonicalGenres.length > 0) {
    861             int[] results = new int[canonicalGenres.length];
    862             int i = 0;
    863             for (String canonicalGenre : canonicalGenres) {
    864                 int genreId = GenreItems.getId(canonicalGenre);
    865                 if (genreId == GenreItems.ID_ALL_CHANNELS) {
    866                     // Skip if the genre is unknown.
    867                     continue;
    868                 }
    869                 results[i++] = genreId;
    870             }
    871             if (i < canonicalGenres.length) {
    872                 results = Arrays.copyOf(results, i);
    873             }
    874             return results;
    875         }
    876         return null;
    877     }
    878 
    879     /** Returns the canonical genres for database. */
    880     public static String getCanonicalGenre(int[] canonicalGenreIds) {
    881         if (canonicalGenreIds == null || canonicalGenreIds.length == 0) {
    882             return null;
    883         }
    884         String[] genres = new String[canonicalGenreIds.length];
    885         for (int i = 0; i < canonicalGenreIds.length; ++i) {
    886             genres[i] = GenreItems.getCanonicalGenre(canonicalGenreIds[i]);
    887         }
    888         return Genres.encode(genres);
    889     }
    890 
    891     /**
    892      * Runs the method in main thread. If the current thread is not main thread, block it util the
    893      * method is finished.
    894      */
    895     public static void runInMainThreadAndWait(Runnable runnable) {
    896         if (Looper.myLooper() == Looper.getMainLooper()) {
    897             runnable.run();
    898         } else {
    899             Future<?> temp = MainThreadExecutor.getInstance().submit(runnable);
    900             try {
    901                 temp.get();
    902             } catch (InterruptedException | ExecutionException e) {
    903                 Log.e(TAG, "failed to finish the execution", e);
    904             }
    905         }
    906     }
    907 }
    908