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