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
17package com.android.tv.util;
18
19import android.annotation.SuppressLint;
20import android.content.ComponentName;
21import android.content.ContentResolver;
22import android.content.ContentValues;
23import android.content.Context;
24import android.content.Intent;
25import android.content.pm.PackageManager;
26import android.content.res.Configuration;
27import android.database.Cursor;
28import android.media.tv.TvContract;
29import android.media.tv.TvContract.Channels;
30import android.media.tv.TvContract.Programs.Genres;
31import android.media.tv.TvInputInfo;
32import android.media.tv.TvTrackInfo;
33import android.net.Uri;
34import android.preference.PreferenceManager;
35import android.support.annotation.Nullable;
36import android.support.annotation.VisibleForTesting;
37import android.support.annotation.WorkerThread;
38import android.text.TextUtils;
39import android.text.format.DateUtils;
40import android.util.ArraySet;
41import android.util.Log;
42import android.view.View;
43
44import com.android.tv.ApplicationSingletons;
45import com.android.tv.R;
46import com.android.tv.TvApplication;
47import com.android.tv.common.SoftPreconditions;
48import com.android.tv.data.Channel;
49import com.android.tv.data.GenreItems;
50import com.android.tv.data.Program;
51import com.android.tv.data.StreamInfo;
52
53import java.io.File;
54import java.text.SimpleDateFormat;
55import java.util.ArrayList;
56import java.util.Arrays;
57import java.util.Calendar;
58import java.util.Collection;
59import java.util.Date;
60import java.util.HashSet;
61import java.util.List;
62import java.util.Locale;
63import java.util.Set;
64import java.util.TimeZone;
65import java.util.concurrent.TimeUnit;
66
67/**
68 * A class that includes convenience methods for accessing TvProvider database.
69 */
70public 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
136        AspectRatio(int width, int height) {
137            this.width = width;
138            this.height = height;
139        }
140
141        @Override
142        @SuppressLint("DefaultLocale")
143        public String toString() {
144            return String.format("%d:%d", width, height);
145        }
146    }
147
148    private Utils() {
149    }
150
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
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
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     */
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     */
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
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
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
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     */
237    public static String getLastWatchedTunerInputId(Context context) {
238        return PreferenceManager.getDefaultSharedPreferences(context)
239                .getString(PREF_KEY_LAST_WATCHED_TUNER_INPUT_ID, null);
240    }
241
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     */
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     */
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     */
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     */
277    public static boolean isChannelUriForTunerInput(Uri uri) {
278        return isTvUri(uri) && isTwoSegmentUriStartingWith(uri, PATH_CHANNEL);
279    }
280
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
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     */
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
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
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     */
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
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     */
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
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     */
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
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     */
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
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
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
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
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
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
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
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
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
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     */
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
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     */
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     */
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     */
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     */
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     */
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     */
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
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     */
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     */
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     */
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     */
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     */
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
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
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
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     */
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     */
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     */
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     */
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     */
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     */
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