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 120 AspectRatio(int width, int height) { 121 this.width = width; 122 this.height = height; 123 } 124 125 @Override 126 @SuppressLint("DefaultLocale") 127 public String toString() { 128 return String.format("%d:%d", width, height); 129 } 130 } 131 132 private Utils() {} 133 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 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 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. */ 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. */ 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. */ 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. */ 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 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 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 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. */ 240 public static String getLastWatchedTunerInputId(Context context) { 241 return PreferenceManager.getDefaultSharedPreferences(context) 242 .getString(PREF_KEY_LAST_WATCHED_TUNER_INPUT_ID, null); 243 } 244 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. */ 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. */ 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 */ 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 */ 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 */ 284 public static boolean isChannelUriForTunerInput(Uri uri) { 285 return isTvUri(uri) && isTwoSegmentUriStartingWith(uri, PATH_CHANNEL); 286 } 287 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 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. */ 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. */ 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 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 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. */ 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 */ 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 */ 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 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 */ 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 */ 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. */ 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 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. */ 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. */ 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 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 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 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 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 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 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 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 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 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. */ 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 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 */ 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. */ 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 */ 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 */ 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 */ 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 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 */ 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. */ 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. */ 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. */ 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. */ 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 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 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 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}. */ 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}. */ 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. */ 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 */ 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