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