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