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