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