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