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