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 141 AspectRatio(int width, int height) { 142 this.width = width; 143 this.height = height; 144 } 145 146 @Override 147 @SuppressLint("DefaultLocale") 148 public String toString() { 149 return String.format("%d:%d", width, height); 150 } 151 } 152 153 private Utils() { 154 } 155 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 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 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 */ 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 */ 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 */ 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 */ 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 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 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 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 */ 264 public static String getLastWatchedTunerInputId(Context context) { 265 return PreferenceManager.getDefaultSharedPreferences(context) 266 .getString(PREF_KEY_LAST_WATCHED_TUNER_INPUT_ID, null); 267 } 268 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 */ 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 */ 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 */ 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 */ 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 */ 312 public static boolean isChannelUriForTunerInput(Uri uri) { 313 return isTvUri(uri) && isTwoSegmentUriStartingWith(uri, PATH_CHANNEL); 314 } 315 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 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 */ 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 */ 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 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 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 */ 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 */ 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 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 */ 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 */ 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 */ 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 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 */ 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 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 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 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 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 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 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 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 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 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 */ 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 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 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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