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