| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.tv.util; |
| |
| import android.annotation.SuppressLint; |
| import android.content.ComponentName; |
| import android.content.ContentResolver; |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.PackageManager; |
| import android.content.res.Configuration; |
| import android.database.Cursor; |
| import android.media.tv.TvContract; |
| import android.media.tv.TvContract.Channels; |
| import android.media.tv.TvContract.Programs.Genres; |
| import android.media.tv.TvInputInfo; |
| import android.net.Uri; |
| import android.os.Looper; |
| import android.preference.PreferenceManager; |
| import android.support.annotation.Nullable; |
| import android.support.annotation.VisibleForTesting; |
| import android.support.annotation.WorkerThread; |
| import android.text.TextUtils; |
| import android.text.format.DateUtils; |
| import android.util.Log; |
| import android.view.View; |
| import com.android.tv.R; |
| import com.android.tv.TvSingletons; |
| import com.android.tv.common.BaseSingletons; |
| import com.android.tv.common.SoftPreconditions; |
| import com.android.tv.common.util.Clock; |
| import com.android.tv.data.GenreItems; |
| import com.android.tv.data.Program; |
| import com.android.tv.data.StreamInfo; |
| import com.android.tv.data.api.Channel; |
| import java.text.SimpleDateFormat; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Calendar; |
| import java.util.Collection; |
| import java.util.Date; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Set; |
| import java.util.TimeZone; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.Future; |
| import java.util.concurrent.TimeUnit; |
| |
| /** A class that includes convenience methods for accessing TvProvider database. */ |
| public class Utils { |
| private static final String TAG = "Utils"; |
| private static final boolean DEBUG = false; |
| |
| public static final String EXTRA_KEY_ACTION = "action"; |
| public static final String EXTRA_ACTION_SHOW_TV_INPUT = "show_tv_input"; |
| public static final String EXTRA_KEY_FROM_LAUNCHER = "from_launcher"; |
| public static final String EXTRA_KEY_RECORDED_PROGRAM_ID = "recorded_program_id"; |
| public static final String EXTRA_KEY_RECORDED_PROGRAM_SEEK_TIME = "recorded_program_seek_time"; |
| public static final String EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED = |
| "recorded_program_pin_checked"; |
| |
| private static final String PATH_CHANNEL = "channel"; |
| private static final String PATH_PROGRAM = "program"; |
| private static final String PATH_RECORDED_PROGRAM = "recorded_program"; |
| |
| private static final String PREF_KEY_LAST_WATCHED_CHANNEL_ID = "last_watched_channel_id"; |
| private static final String PREF_KEY_LAST_WATCHED_CHANNEL_ID_FOR_INPUT = |
| "last_watched_channel_id_for_input_"; |
| private static final String PREF_KEY_LAST_WATCHED_CHANNEL_URI = "last_watched_channel_uri"; |
| private static final String PREF_KEY_LAST_WATCHED_TUNER_INPUT_ID = |
| "last_watched_tuner_input_id"; |
| private static final String PREF_KEY_RECORDING_FAILED_REASONS = "recording_failed_reasons"; |
| private static final String PREF_KEY_FAILED_SCHEDULED_RECORDING_INFO_SET = |
| "failed_scheduled_recording_info_set"; |
| |
| private static final int VIDEO_SD_WIDTH = 704; |
| private static final int VIDEO_SD_HEIGHT = 480; |
| private static final int VIDEO_HD_WIDTH = 1280; |
| private static final int VIDEO_HD_HEIGHT = 720; |
| private static final int VIDEO_FULL_HD_WIDTH = 1920; |
| private static final int VIDEO_FULL_HD_HEIGHT = 1080; |
| private static final int VIDEO_ULTRA_HD_WIDTH = 2048; |
| private static final int VIDEO_ULTRA_HD_HEIGHT = 1536; |
| |
| private static final long RECORDING_FAILED_REASON_NONE = 0; |
| private static final long HALF_MINUTE_MS = TimeUnit.SECONDS.toMillis(30); |
| private static final long ONE_DAY_MS = TimeUnit.DAYS.toMillis(1); |
| |
| private enum AspectRatio { |
| ASPECT_RATIO_4_3(4, 3), |
| ASPECT_RATIO_16_9(16, 9), |
| ASPECT_RATIO_21_9(21, 9); |
| |
| final int width; |
| final int height; |
| |
| AspectRatio(int width, int height) { |
| this.width = width; |
| this.height = height; |
| } |
| |
| @Override |
| @SuppressLint("DefaultLocale") |
| public String toString() { |
| return String.format("%d:%d", width, height); |
| } |
| } |
| |
| private Utils() {} |
| |
| public static String buildSelectionForIds(String idName, List<Long> ids) { |
| StringBuilder sb = new StringBuilder(); |
| sb.append(idName).append(" in (").append(ids.get(0)); |
| for (int i = 1; i < ids.size(); ++i) { |
| sb.append(",").append(ids.get(i)); |
| } |
| sb.append(")"); |
| return sb.toString(); |
| } |
| |
| @Nullable |
| @WorkerThread |
| public static String getInputIdForChannel(Context context, long channelId) { |
| if (channelId == Channel.INVALID_ID) { |
| return null; |
| } |
| Uri channelUri = TvContract.buildChannelUri(channelId); |
| String[] projection = {TvContract.Channels.COLUMN_INPUT_ID}; |
| try (Cursor cursor = |
| context.getContentResolver().query(channelUri, projection, null, null, null)) { |
| if (cursor != null && cursor.moveToNext()) { |
| return Utils.intern(cursor.getString(0)); |
| } |
| } catch (Exception e) { |
| Log.e(TAG, "Error get input id for channel", e); |
| } |
| return null; |
| } |
| |
| public static void setLastWatchedChannel(Context context, Channel channel) { |
| if (channel == null) { |
| Log.e(TAG, "setLastWatchedChannel: channel cannot be null"); |
| return; |
| } |
| PreferenceManager.getDefaultSharedPreferences(context) |
| .edit() |
| .putString(PREF_KEY_LAST_WATCHED_CHANNEL_URI, channel.getUri().toString()) |
| .apply(); |
| if (!channel.isPassthrough()) { |
| long channelId = channel.getId(); |
| if (channel.getId() < 0) { |
| throw new IllegalArgumentException("channelId should be equal to or larger than 0"); |
| } |
| PreferenceManager.getDefaultSharedPreferences(context) |
| .edit() |
| .putLong(PREF_KEY_LAST_WATCHED_CHANNEL_ID, channelId) |
| .putLong( |
| PREF_KEY_LAST_WATCHED_CHANNEL_ID_FOR_INPUT + channel.getInputId(), |
| channelId) |
| .putString(PREF_KEY_LAST_WATCHED_TUNER_INPUT_ID, channel.getInputId()) |
| .apply(); |
| } |
| } |
| |
| /** Sets recording failed reason. */ |
| public static void setRecordingFailedReason(Context context, int reason) { |
| long reasons = getRecordingFailedReasons(context) | 0x1 << reason; |
| PreferenceManager.getDefaultSharedPreferences(context) |
| .edit() |
| .putLong(PREF_KEY_RECORDING_FAILED_REASONS, reasons) |
| .apply(); |
| } |
| |
| /** Adds the info of failed scheduled recording. */ |
| public static void addFailedScheduledRecordingInfo( |
| Context context, String scheduledRecordingInfo) { |
| Set<String> failedScheduledRecordingInfoSet = getFailedScheduledRecordingInfoSet(context); |
| failedScheduledRecordingInfoSet.add(scheduledRecordingInfo); |
| PreferenceManager.getDefaultSharedPreferences(context) |
| .edit() |
| .putStringSet( |
| PREF_KEY_FAILED_SCHEDULED_RECORDING_INFO_SET, |
| failedScheduledRecordingInfoSet) |
| .apply(); |
| } |
| |
| /** Clears the failed scheduled recording info set. */ |
| public static void clearFailedScheduledRecordingInfoSet(Context context) { |
| PreferenceManager.getDefaultSharedPreferences(context) |
| .edit() |
| .remove(PREF_KEY_FAILED_SCHEDULED_RECORDING_INFO_SET) |
| .apply(); |
| } |
| |
| /** Clears recording failed reason. */ |
| public static void clearRecordingFailedReason(Context context, int reason) { |
| long reasons = getRecordingFailedReasons(context) & ~(0x1 << reason); |
| PreferenceManager.getDefaultSharedPreferences(context) |
| .edit() |
| .putLong(PREF_KEY_RECORDING_FAILED_REASONS, reasons) |
| .apply(); |
| } |
| |
| public static long getLastWatchedChannelId(Context context) { |
| return PreferenceManager.getDefaultSharedPreferences(context) |
| .getLong(PREF_KEY_LAST_WATCHED_CHANNEL_ID, Channel.INVALID_ID); |
| } |
| |
| public static long getLastWatchedChannelIdForInput(Context context, String inputId) { |
| return PreferenceManager.getDefaultSharedPreferences(context) |
| .getLong(PREF_KEY_LAST_WATCHED_CHANNEL_ID_FOR_INPUT + inputId, Channel.INVALID_ID); |
| } |
| |
| public static String getLastWatchedChannelUri(Context context) { |
| return PreferenceManager.getDefaultSharedPreferences(context) |
| .getString(PREF_KEY_LAST_WATCHED_CHANNEL_URI, null); |
| } |
| |
| /** Returns the last watched tuner input id. */ |
| public static String getLastWatchedTunerInputId(Context context) { |
| return PreferenceManager.getDefaultSharedPreferences(context) |
| .getString(PREF_KEY_LAST_WATCHED_TUNER_INPUT_ID, null); |
| } |
| |
| private static long getRecordingFailedReasons(Context context) { |
| return PreferenceManager.getDefaultSharedPreferences(context) |
| .getLong(PREF_KEY_RECORDING_FAILED_REASONS, RECORDING_FAILED_REASON_NONE); |
| } |
| |
| /** Returns the failed scheduled recordings info set. */ |
| public static Set<String> getFailedScheduledRecordingInfoSet(Context context) { |
| return PreferenceManager.getDefaultSharedPreferences(context) |
| .getStringSet(PREF_KEY_FAILED_SCHEDULED_RECORDING_INFO_SET, new HashSet<>()); |
| } |
| |
| /** Checks do recording failed reason exist. */ |
| public static boolean hasRecordingFailedReason(Context context, int reason) { |
| long reasons = getRecordingFailedReasons(context); |
| return (reasons & 0x1 << reason) != 0; |
| } |
| |
| /** |
| * Returns {@code true}, if {@code uri} specifies an input, which is usually generated from |
| * {@link TvContract#buildChannelsUriForInput}. |
| */ |
| public static boolean isChannelUriForInput(Uri uri) { |
| return isTvUri(uri) |
| && PATH_CHANNEL.equals(uri.getPathSegments().get(0)) |
| && !TextUtils.isEmpty(uri.getQueryParameter("input")); |
| } |
| |
| /** |
| * Returns {@code true}, if {@code uri} is a channel URI for a specific channel. It is copied |
| * from the hidden method TvContract.isChannelUri. |
| */ |
| public static boolean isChannelUriForOneChannel(Uri uri) { |
| return isChannelUriForTunerInput(uri) || TvContract.isChannelUriForPassthroughInput(uri); |
| } |
| |
| /** |
| * Returns {@code true}, if {@code uri} is a channel URI for a tuner input. It is copied from |
| * the hidden method TvContract.isChannelUriForTunerInput. |
| */ |
| public static boolean isChannelUriForTunerInput(Uri uri) { |
| return isTvUri(uri) && isTwoSegmentUriStartingWith(uri, PATH_CHANNEL); |
| } |
| |
| private static boolean isTvUri(Uri uri) { |
| return uri != null |
| && ContentResolver.SCHEME_CONTENT.equals(uri.getScheme()) |
| && TvContract.AUTHORITY.equals(uri.getAuthority()); |
| } |
| |
| private static boolean isTwoSegmentUriStartingWith(Uri uri, String pathSegment) { |
| List<String> pathSegments = uri.getPathSegments(); |
| return pathSegments.size() == 2 && pathSegment.equals(pathSegments.get(0)); |
| } |
| |
| /** Returns {@code true}, if {@code uri} is a programs URI. */ |
| public static boolean isProgramsUri(Uri uri) { |
| return isTvUri(uri) && PATH_PROGRAM.equals(uri.getPathSegments().get(0)); |
| } |
| |
| /** Returns {@code true}, if {@code uri} is a programs URI. */ |
| public static boolean isRecordedProgramsUri(Uri uri) { |
| return isTvUri(uri) && PATH_RECORDED_PROGRAM.equals(uri.getPathSegments().get(0)); |
| } |
| |
| /** Gets the info of the program on particular time. */ |
| @WorkerThread |
| public static Program getProgramAt(Context context, long channelId, long timeMs) { |
| if (channelId == Channel.INVALID_ID) { |
| Log.e(TAG, "getCurrentProgramAt - channelId is invalid"); |
| return null; |
| } |
| if (context.getMainLooper().getThread().equals(Thread.currentThread())) { |
| String message = "getCurrentProgramAt called on main thread"; |
| if (DEBUG) { |
| // Generating a stack trace can be expensive, only do it in debug mode. |
| Log.w(TAG, message, new IllegalStateException(message)); |
| } else { |
| Log.w(TAG, message); |
| } |
| } |
| Uri uri = |
| TvContract.buildProgramsUriForChannel( |
| TvContract.buildChannelUri(channelId), timeMs, timeMs); |
| ContentResolver resolver = context.getContentResolver(); |
| |
| String[] projection = Program.PROJECTION; |
| if (TvProviderUtils.checkSeriesIdColumn(context, TvContract.Programs.CONTENT_URI)) { |
| if (Utils.isProgramsUri(uri)) { |
| projection = |
| TvProviderUtils.addExtraColumnsToProjection( |
| projection, TvProviderUtils.EXTRA_PROGRAM_COLUMN_SERIES_ID); |
| } |
| } |
| try (Cursor cursor = resolver.query(uri, projection, null, null, null)) { |
| if (cursor != null && cursor.moveToNext()) { |
| return Program.fromCursor(cursor); |
| } |
| } |
| return null; |
| } |
| |
| /** Gets the info of the current program. */ |
| @WorkerThread |
| public static Program getCurrentProgram(Context context, long channelId) { |
| return getProgramAt(context, channelId, System.currentTimeMillis()); |
| } |
| |
| /** Returns the round off minutes when convert milliseconds to minutes. */ |
| public static int getRoundOffMinsFromMs(long millis) { |
| // Round off the result by adding half minute to the original ms. |
| return (int) TimeUnit.MILLISECONDS.toMinutes(millis + HALF_MINUTE_MS); |
| } |
| |
| /** |
| * Returns duration string according to the date & time format. If {@code startUtcMillis} and |
| * {@code endUtcMills} are equal, formatted time will be returned instead. |
| * |
| * @param startUtcMillis start of duration in millis. Should be less than {code endUtcMillis}. |
| * @param endUtcMillis end of duration in millis. Should be larger than {@code startUtcMillis}. |
| * @param useShortFormat {@code true} if abbreviation is needed to save space. In that case, |
| * date will be omitted if duration starts from today and is less than a day. If it's |
| * necessary, {@link DateUtils#FORMAT_NUMERIC_DATE} is used otherwise. |
| */ |
| public static String getDurationString( |
| Context context, long startUtcMillis, long endUtcMillis, boolean useShortFormat) { |
| return getDurationString( |
| context, |
| ((BaseSingletons) context.getApplicationContext()).getClock(), |
| startUtcMillis, |
| endUtcMillis, |
| useShortFormat); |
| } |
| |
| /** |
| * Returns duration string according to the date & time format. If {@code startUtcMillis} and |
| * {@code endUtcMills} are equal, formatted time will be returned instead. |
| * |
| * @param clock the clock used to get the current time. |
| * @param startUtcMillis start of duration in millis. Should be less than {code endUtcMillis}. |
| * @param endUtcMillis end of duration in millis. Should be larger than {@code startUtcMillis}. |
| * @param useShortFormat {@code true} if abbreviation is needed to save space. In that case, |
| * date will be omitted if duration starts from today and is less than a day. If it's |
| * necessary, {@link DateUtils#FORMAT_NUMERIC_DATE} is used otherwise. |
| */ |
| public static String getDurationString( |
| Context context, |
| Clock clock, |
| long startUtcMillis, |
| long endUtcMillis, |
| boolean useShortFormat) { |
| return getDurationString( |
| context, |
| clock.currentTimeMillis(), |
| startUtcMillis, |
| endUtcMillis, |
| useShortFormat, |
| 0); |
| } |
| |
| @VisibleForTesting |
| static String getDurationString( |
| Context context, |
| long baseMillis, |
| long startUtcMillis, |
| long endUtcMillis, |
| boolean useShortFormat, |
| int flags) { |
| return getDurationString( |
| context, |
| startUtcMillis, |
| endUtcMillis, |
| useShortFormat, |
| !isInGivenDay(baseMillis, startUtcMillis), |
| true, |
| flags); |
| } |
| |
| /** |
| * Returns duration string according to the time format, may not contain date information. Note: |
| * At least one of showDate and showTime should be true. |
| */ |
| public static String getDurationString( |
| Context context, |
| long startUtcMillis, |
| long endUtcMillis, |
| boolean useShortFormat, |
| boolean showDate, |
| boolean showTime, |
| int flags) { |
| flags |= |
| DateUtils.FORMAT_ABBREV_MONTH |
| | ((useShortFormat) ? DateUtils.FORMAT_NUMERIC_DATE : 0); |
| SoftPreconditions.checkArgument(showTime || showDate); |
| if (showTime) { |
| flags |= DateUtils.FORMAT_SHOW_TIME; |
| } |
| if (showDate) { |
| flags |= DateUtils.FORMAT_SHOW_DATE; |
| } |
| if (!showDate || (flags & DateUtils.FORMAT_SHOW_YEAR) == 0) { |
| // year is not shown unless DateUtils.FORMAT_SHOW_YEAR is set explicitly |
| flags |= DateUtils.FORMAT_NO_YEAR; |
| } |
| if (startUtcMillis != endUtcMillis && useShortFormat) { |
| // Do special handling for 12:00 AM when checking if it's in the given day. |
| // If it's start, it's considered as beginning of the day. (e.g. 12:00 AM - 12:30 AM) |
| // If it's end, it's considered as end of the day (e.g. 11:00 PM - 12:00 AM) |
| if (!isInGivenDay(startUtcMillis, endUtcMillis - 1) |
| && endUtcMillis - startUtcMillis < TimeUnit.HOURS.toMillis(11)) { |
| // Do not show date for short format. |
| // Subtracting one day is needed because {@link DateUtils@formatDateRange} |
| // automatically shows date if the duration covers multiple days. |
| return DateUtils.formatDateRange( |
| context, startUtcMillis, endUtcMillis - TimeUnit.DAYS.toMillis(1), flags); |
| } |
| } |
| // Workaround of b/28740989. |
| // Add 1 msec to endUtcMillis to avoid DateUtils' bug with a duration of 12:00AM~12:00AM. |
| String dateRange = DateUtils.formatDateRange(context, startUtcMillis, endUtcMillis, flags); |
| return startUtcMillis == endUtcMillis || dateRange.contains("–") |
| ? dateRange |
| : DateUtils.formatDateRange(context, startUtcMillis, endUtcMillis + 1, flags); |
| } |
| |
| /** |
| * Checks if two given time (in milliseconds) are in the same day with regard to the locale |
| * timezone. |
| */ |
| public static boolean isInGivenDay(long dayToMatchInMillis, long subjectTimeInMillis) { |
| TimeZone timeZone = Calendar.getInstance().getTimeZone(); |
| long offset = timeZone.getRawOffset(); |
| if (timeZone.inDaylightTime(new Date(dayToMatchInMillis))) { |
| offset += timeZone.getDSTSavings(); |
| } |
| return Utils.floorTime(dayToMatchInMillis + offset, ONE_DAY_MS) |
| == Utils.floorTime(subjectTimeInMillis + offset, ONE_DAY_MS); |
| } |
| |
| /** Calculate how many days between two milliseconds. */ |
| public static int computeDateDifference(long startTimeMs, long endTimeMs) { |
| Calendar calFrom = Calendar.getInstance(); |
| Calendar calTo = Calendar.getInstance(); |
| calFrom.setTime(new Date(startTimeMs)); |
| calTo.setTime(new Date(endTimeMs)); |
| resetCalendar(calFrom); |
| resetCalendar(calTo); |
| return (int) ((calTo.getTimeInMillis() - calFrom.getTimeInMillis()) / ONE_DAY_MS); |
| } |
| |
| private static void resetCalendar(Calendar cal) { |
| cal.set(Calendar.HOUR_OF_DAY, 0); |
| cal.set(Calendar.MINUTE, 0); |
| cal.set(Calendar.SECOND, 0); |
| cal.set(Calendar.MILLISECOND, 0); |
| } |
| |
| /** Returns the last millisecond of a day which the millis belongs to. */ |
| public static long getLastMillisecondOfDay(long millis) { |
| Calendar calendar = Calendar.getInstance(); |
| calendar.setTime(new Date(millis)); |
| calendar.set(Calendar.HOUR_OF_DAY, 23); |
| calendar.set(Calendar.MINUTE, 59); |
| calendar.set(Calendar.SECOND, 59); |
| calendar.set(Calendar.MILLISECOND, 999); |
| return calendar.getTimeInMillis(); |
| } |
| |
| /** Returns the last millisecond of a day which the millis belongs to. */ |
| public static long getFirstMillisecondOfDay(long millis) { |
| Calendar calendar = Calendar.getInstance(); |
| calendar.setTime(new Date(millis)); |
| resetCalendar(calendar); |
| return calendar.getTimeInMillis(); |
| } |
| |
| public static String getAspectRatioString(int width, int height) { |
| if (width == 0 || height == 0) { |
| return ""; |
| } |
| |
| for (AspectRatio ratio : AspectRatio.values()) { |
| if (Math.abs((float) ratio.height / ratio.width - (float) height / width) < 0.05f) { |
| return ratio.toString(); |
| } |
| } |
| return ""; |
| } |
| |
| public static String getAspectRatioString(float videoDisplayAspectRatio) { |
| if (videoDisplayAspectRatio <= 0) { |
| return ""; |
| } |
| |
| for (AspectRatio ratio : AspectRatio.values()) { |
| if (Math.abs((float) ratio.width / ratio.height - videoDisplayAspectRatio) < 0.05f) { |
| return ratio.toString(); |
| } |
| } |
| return ""; |
| } |
| |
| public static int getVideoDefinitionLevelFromSize(int width, int height) { |
| if (width >= VIDEO_ULTRA_HD_WIDTH && height >= VIDEO_ULTRA_HD_HEIGHT) { |
| return StreamInfo.VIDEO_DEFINITION_LEVEL_ULTRA_HD; |
| } else if (width >= VIDEO_FULL_HD_WIDTH && height >= VIDEO_FULL_HD_HEIGHT) { |
| return StreamInfo.VIDEO_DEFINITION_LEVEL_FULL_HD; |
| } else if (width >= VIDEO_HD_WIDTH && height >= VIDEO_HD_HEIGHT) { |
| return StreamInfo.VIDEO_DEFINITION_LEVEL_HD; |
| } else if (width >= VIDEO_SD_WIDTH && height >= VIDEO_SD_HEIGHT) { |
| return StreamInfo.VIDEO_DEFINITION_LEVEL_SD; |
| } |
| return StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN; |
| } |
| |
| public static String getVideoDefinitionLevelString(Context context, int videoFormat) { |
| switch (videoFormat) { |
| case StreamInfo.VIDEO_DEFINITION_LEVEL_ULTRA_HD: |
| return context.getResources().getString(R.string.video_definition_level_ultra_hd); |
| case StreamInfo.VIDEO_DEFINITION_LEVEL_FULL_HD: |
| return context.getResources().getString(R.string.video_definition_level_full_hd); |
| case StreamInfo.VIDEO_DEFINITION_LEVEL_HD: |
| return context.getResources().getString(R.string.video_definition_level_hd); |
| case StreamInfo.VIDEO_DEFINITION_LEVEL_SD: |
| return context.getResources().getString(R.string.video_definition_level_sd); |
| } |
| return ""; |
| } |
| |
| public static String getAudioChannelString(Context context, int channelCount) { |
| switch (channelCount) { |
| case 1: |
| return context.getResources().getString(R.string.audio_channel_mono); |
| case 2: |
| return context.getResources().getString(R.string.audio_channel_stereo); |
| case 6: |
| return context.getResources().getString(R.string.audio_channel_5_1); |
| case 8: |
| return context.getResources().getString(R.string.audio_channel_7_1); |
| } |
| return ""; |
| } |
| |
| public static boolean isEqualLanguage(String lang1, String lang2) { |
| if (lang1 == null) { |
| return lang2 == null; |
| } else if (lang2 == null) { |
| return false; |
| } |
| try { |
| return TextUtils.equals( |
| new Locale(lang1).getISO3Language(), new Locale(lang2).getISO3Language()); |
| } catch (Exception ignored) { |
| } |
| return false; |
| } |
| |
| public static boolean isIntentAvailable(Context context, Intent intent) { |
| return context.getPackageManager() |
| .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY) |
| .size() |
| > 0; |
| } |
| |
| /** Returns the label for a given input. Returns the custom label, if any. */ |
| public static String loadLabel(Context context, TvInputInfo input) { |
| if (input == null) { |
| return null; |
| } |
| TvInputManagerHelper inputManager = |
| TvSingletons.getSingletons(context).getTvInputManagerHelper(); |
| CharSequence customLabel = inputManager.loadCustomLabel(input); |
| String label = (customLabel == null) ? null : customLabel.toString(); |
| if (TextUtils.isEmpty(label)) { |
| label = inputManager.loadLabel(input).toString(); |
| } |
| return label; |
| } |
| |
| /** Enable all channels synchronously. */ |
| @WorkerThread |
| public static void enableAllChannels(Context context) { |
| ContentValues values = new ContentValues(); |
| values.put(Channels.COLUMN_BROWSABLE, 1); |
| context.getContentResolver().update(Channels.CONTENT_URI, values, null, null); |
| } |
| |
| /** |
| * Converts time in milliseconds to a String. |
| * |
| * @param fullFormat {@code true} for returning date string with a full format (e.g., Mon Aug 15 |
| * 20:08:35 GMT 2016). {@code false} for a short format, {e.g., 8/15/16 or 8:08 AM}, in |
| * which only the time is shown if the time is on the same day as now, and only the date is |
| * shown if it's a different day. |
| */ |
| public static String toTimeString(long timeMillis, boolean fullFormat) { |
| if (fullFormat) { |
| return new Date(timeMillis).toString(); |
| } else { |
| return (String) |
| DateUtils.formatSameDayTime( |
| timeMillis, |
| System.currentTimeMillis(), |
| SimpleDateFormat.SHORT, |
| SimpleDateFormat.SHORT); |
| } |
| } |
| |
| /** Converts time in milliseconds to a String. */ |
| public static String toTimeString(long timeMillis) { |
| return toTimeString(timeMillis, true); |
| } |
| |
| /** |
| * Returns a {@link String} object which contains the layout information of the {@code view}. |
| */ |
| public static String toRectString(View view) { |
| return "{" |
| + "l=" |
| + view.getLeft() |
| + ",r=" |
| + view.getRight() |
| + ",t=" |
| + view.getTop() |
| + ",b=" |
| + view.getBottom() |
| + ",w=" |
| + view.getWidth() |
| + ",h=" |
| + view.getHeight() |
| + "}"; |
| } |
| |
| /** |
| * Floors time to the given {@code timeUnit}. For example, if time is 5:32:11 and timeUnit is |
| * one hour (60 * 60 * 1000), then the output will be 5:00:00. |
| */ |
| public static long floorTime(long timeMs, long timeUnit) { |
| return timeMs - (timeMs % timeUnit); |
| } |
| |
| /** |
| * Ceils time to the given {@code timeUnit}. For example, if time is 5:32:11 and timeUnit is one |
| * hour (60 * 60 * 1000), then the output will be 6:00:00. |
| */ |
| public static long ceilTime(long timeMs, long timeUnit) { |
| return timeMs + timeUnit - (timeMs % timeUnit); |
| } |
| |
| /** Returns an {@link String#intern() interned} string or null if the input is null. */ |
| @Nullable |
| public static String intern(@Nullable String string) { |
| return string == null ? null : string.intern(); |
| } |
| |
| /** |
| * Check if the index is valid for the collection, |
| * |
| * @param collection the collection |
| * @param index the index position to test |
| * @return index >= 0 && index < collection.size(). |
| */ |
| public static boolean isIndexValid(@Nullable Collection<?> collection, int index) { |
| return collection != null && (index >= 0 && index < collection.size()); |
| } |
| |
| /** Returns a localized version of the text resource specified by resourceId. */ |
| public static CharSequence getTextForLocale(Context context, Locale locale, int resourceId) { |
| if (locale.equals(context.getResources().getConfiguration().locale)) { |
| return context.getText(resourceId); |
| } |
| Configuration config = new Configuration(context.getResources().getConfiguration()); |
| config.setLocale(locale); |
| return context.createConfigurationContext(config).getText(resourceId); |
| } |
| |
| /** Checks where there is any internal TV input. */ |
| public static boolean hasInternalTvInputs(Context context, boolean tunerInputOnly) { |
| for (TvInputInfo input : |
| TvSingletons.getSingletons(context) |
| .getTvInputManagerHelper() |
| .getTvInputInfos(true, tunerInputOnly)) { |
| if (isInternalTvInput(context, input.getId())) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** Returns the internal TV inputs. */ |
| public static List<TvInputInfo> getInternalTvInputs(Context context, boolean tunerInputOnly) { |
| List<TvInputInfo> inputs = new ArrayList<>(); |
| for (TvInputInfo input : |
| TvSingletons.getSingletons(context) |
| .getTvInputManagerHelper() |
| .getTvInputInfos(true, tunerInputOnly)) { |
| if (isInternalTvInput(context, input.getId())) { |
| inputs.add(input); |
| } |
| } |
| return inputs; |
| } |
| |
| /** Checks whether the input is internal or not. */ |
| public static boolean isInternalTvInput(Context context, String inputId) { |
| ComponentName unflattenInputId = ComponentName.unflattenFromString(inputId); |
| if (unflattenInputId == null) { |
| return false; |
| } |
| return context.getPackageName().equals(unflattenInputId.getPackageName()); |
| } |
| |
| /** Returns the TV input for the given {@code program}. */ |
| @Nullable |
| public static TvInputInfo getTvInputInfoForProgram(Context context, Program program) { |
| if (!Program.isProgramValid(program)) { |
| return null; |
| } |
| return getTvInputInfoForChannelId(context, program.getChannelId()); |
| } |
| |
| /** Returns the TV input for the given channel ID. */ |
| @Nullable |
| public static TvInputInfo getTvInputInfoForChannelId(Context context, long channelId) { |
| TvSingletons tvSingletons = TvSingletons.getSingletons(context); |
| Channel channel = tvSingletons.getChannelDataManager().getChannel(channelId); |
| if (channel == null) { |
| return null; |
| } |
| return tvSingletons.getTvInputManagerHelper().getTvInputInfo(channel.getInputId()); |
| } |
| |
| /** Returns the {@link TvInputInfo} for the given input ID. */ |
| @Nullable |
| public static TvInputInfo getTvInputInfoForInputId(Context context, String inputId) { |
| return TvSingletons.getSingletons(context) |
| .getTvInputManagerHelper() |
| .getTvInputInfo(inputId); |
| } |
| |
| /** Returns the canonical genre ID's from the {@code genres}. */ |
| public static int[] getCanonicalGenreIds(String genres) { |
| if (TextUtils.isEmpty(genres)) { |
| return null; |
| } |
| return getCanonicalGenreIds(Genres.decode(genres)); |
| } |
| |
| /** Returns the canonical genre ID's from the {@code genres}. */ |
| public static int[] getCanonicalGenreIds(String[] canonicalGenres) { |
| if (canonicalGenres != null && canonicalGenres.length > 0) { |
| int[] results = new int[canonicalGenres.length]; |
| int i = 0; |
| for (String canonicalGenre : canonicalGenres) { |
| int genreId = GenreItems.getId(canonicalGenre); |
| if (genreId == GenreItems.ID_ALL_CHANNELS) { |
| // Skip if the genre is unknown. |
| continue; |
| } |
| results[i++] = genreId; |
| } |
| if (i < canonicalGenres.length) { |
| results = Arrays.copyOf(results, i); |
| } |
| return results; |
| } |
| return null; |
| } |
| |
| /** Returns the canonical genres for database. */ |
| public static String getCanonicalGenre(int[] canonicalGenreIds) { |
| if (canonicalGenreIds == null || canonicalGenreIds.length == 0) { |
| return null; |
| } |
| String[] genres = new String[canonicalGenreIds.length]; |
| for (int i = 0; i < canonicalGenreIds.length; ++i) { |
| genres[i] = GenreItems.getCanonicalGenre(canonicalGenreIds[i]); |
| } |
| return Genres.encode(genres); |
| } |
| |
| /** |
| * Runs the method in main thread. If the current thread is not main thread, block it util the |
| * method is finished. |
| */ |
| public static void runInMainThreadAndWait(Runnable runnable) { |
| if (Looper.myLooper() == Looper.getMainLooper()) { |
| runnable.run(); |
| } else { |
| Future<?> temp = MainThreadExecutor.getInstance().submit(runnable); |
| try { |
| temp.get(); |
| } catch (InterruptedException | ExecutionException e) { |
| Log.e(TAG, "failed to finish the execution", e); |
| } |
| } |
| } |
| } |