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