• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
AspectRatio(int width, int height)141         AspectRatio(int width, int height) {
142             this.width = width;
143             this.height = height;
144         }
145 
146         @Override
147         @SuppressLint("DefaultLocale")
toString()148         public String toString() {
149             return String.format("%d:%d", width, height);
150         }
151     }
152 
Utils()153     private Utils() {
154     }
155 
buildSelectionForIds(String idName, List<Long> ids)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
getInputIdForChannel(Context context, long channelId)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 
setLastWatchedChannel(Context context, Channel channel)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      */
setRecordingFailedReason(Context context, int reason)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      */
addFailedScheduledRecordingInfo(Context context, String scheduledRecordingInfo)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      */
clearFailedScheduledRecordingInfoSet(Context context)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      */
clearRecordingFailedReason(Context context, int reason)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 
getLastWatchedChannelId(Context context)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 
getLastWatchedChannelIdForInput(Context context, String inputId)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 
getLastWatchedChannelUri(Context context)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      */
getLastWatchedTunerInputId(Context context)264     public static String getLastWatchedTunerInputId(Context context) {
265         return PreferenceManager.getDefaultSharedPreferences(context)
266                 .getString(PREF_KEY_LAST_WATCHED_TUNER_INPUT_ID, null);
267     }
268 
getRecordingFailedReasons(Context context)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      */
getFailedScheduledRecordingInfoSet(Context context)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      */
hasRecordingFailedReason(Context context, int reason)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      */
isChannelUriForInput(Uri uri)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      */
isChannelUriForOneChannel(Uri uri)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      */
isChannelUriForTunerInput(Uri uri)312     public static boolean isChannelUriForTunerInput(Uri uri) {
313         return isTvUri(uri) && isTwoSegmentUriStartingWith(uri, PATH_CHANNEL);
314     }
315 
isTvUri(Uri uri)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 
isTwoSegmentUriStartingWith(Uri uri, String pathSegment)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      */
isProgramsUri(Uri uri)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      */
isRecordedProgramsUri(Uri uri)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
getProgramAt(Context context, long channelId, long timeMs)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
getCurrentProgram(Context context, long channelId)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      */
getRoundOffMinsFromMs(long millis)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      */
getDurationString( Context context, long startUtcMillis, long endUtcMillis, boolean useShortFormat)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
getDurationString(Context context, long baseMillis, long startUtcMillis, long endUtcMillis, boolean useShortFormat, int flag)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      */
getDurationString(Context context, long startUtcMillis, long endUtcMillis, boolean useShortFormat, boolean showDate, boolean showTime, int flag)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      */
isInGivenDay(long dayToMatchInMillis, long subjectTimeInMillis)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      */
computeDateDifference(long startTimeMs, long endTimeMs)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 
resetCalendar(Calendar cal)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      */
getLastMillisecondOfDay(long millis)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 
getAspectRatioString(int width, int height)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 
getAspectRatioString(float videoDisplayAspectRatio)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 
getVideoDefinitionLevelFromSize(int width, int height)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 
getVideoDefinitionLevelString(Context context, int videoFormat)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 
getAudioChannelString(Context context, int channelCount)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 
needToShowSampleRate(Context context, List<TvTrackInfo> tracks)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 
getMultiAudioString(Context context, TvTrackInfo track, boolean showSampleRate)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 
isEqualLanguage(String lang1, String lang2)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 
isIntentAvailable(Context context, Intent intent)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      */
loadLabel(Context context, TvInputInfo input)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
enableAllChannels(Context context)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      */
toTimeString(long timeMillis, boolean fullFormat)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      */
toTimeString(long timeMillis)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      */
toIsoDateTimeString(long timeMillis)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      */
toRectString(View view)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      */
floorTime(long timeMs, long timeUnit)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      */
ceilTime(long timeMs, long timeUnit)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
intern(@ullable String string)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      */
isIndexValid(@ullable Collection<?> collection, int index)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      */
getTextForLocale(Context context, Locale locale, int resourceId)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      */
hasInternalTvInputs(Context context, boolean tunerInputOnly)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      */
getInternalTvInputs(Context context, boolean tunerInputOnly)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      */
isInternalTvInput(Context context, String inputId)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
getTvInputInfoForProgram(Context context, Program program)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
getTvInputInfoForChannelId(Context context, long channelId)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
getTvInputInfoForInputId(Context context, String inputId)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      */
deleteDirOrFile(File fileOrDirectory)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      */
isInBundledPackageSet(String packageName)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      */
isBundledInput(String inputId)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      */
getCanonicalGenreIds(String genres)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      */
getCanonicalGenreIds(String[] canonicalGenres)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      */
getCanonicalGenre(int[] canonicalGenreIds)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      */
isDeveloper()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      */
runInMainThreadAndWait(Runnable runnable)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