Utils.java revision 944779887775bd950cf1abf348d2df461593f6ab
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 17package com.android.tv.util; 18 19import android.annotation.SuppressLint; 20import android.content.ComponentName; 21import android.content.ContentResolver; 22import android.content.ContentValues; 23import android.content.Context; 24import android.content.Intent; 25import android.content.pm.PackageManager; 26import android.content.res.Configuration; 27import android.database.Cursor; 28import android.media.tv.TvContract; 29import android.media.tv.TvContract.Channels; 30import android.media.tv.TvContract.Programs.Genres; 31import android.media.tv.TvInputInfo; 32import android.media.tv.TvTrackInfo; 33import android.net.Uri; 34import android.os.Looper; 35import android.preference.PreferenceManager; 36import android.support.annotation.Nullable; 37import android.support.annotation.VisibleForTesting; 38import android.support.annotation.WorkerThread; 39import android.text.TextUtils; 40import android.text.format.DateUtils; 41import android.util.Log; 42import android.view.View; 43import com.android.tv.R; 44import com.android.tv.TvSingletons; 45import com.android.tv.common.SoftPreconditions; 46import com.android.tv.data.Channel; 47import com.android.tv.data.GenreItems; 48import com.android.tv.data.Program; 49import com.android.tv.data.StreamInfo; 50import java.text.SimpleDateFormat; 51import java.util.ArrayList; 52import java.util.Arrays; 53import java.util.Calendar; 54import java.util.Collection; 55import java.util.Date; 56import java.util.HashSet; 57import java.util.List; 58import java.util.Locale; 59import java.util.Set; 60import java.util.TimeZone; 61import java.util.concurrent.ExecutionException; 62import java.util.concurrent.Future; 63import java.util.concurrent.TimeUnit; 64 65/** A class that includes convenience methods for accessing TvProvider database. */ 66@SuppressWarnings("TryWithResources") // TODO(b/62143348): remove when error prone check fixed 67public 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 @VisibleForTesting 371 static String getDurationString( 372 Context context, 373 long baseMillis, 374 long startUtcMillis, 375 long endUtcMillis, 376 boolean useShortFormat, 377 int flag) { 378 return getDurationString( 379 context, 380 startUtcMillis, 381 endUtcMillis, 382 useShortFormat, 383 !isInGivenDay(baseMillis, startUtcMillis), 384 true, 385 flag); 386 } 387 388 /** 389 * Returns duration string according to the time format, may not contain date information. Note: 390 * At least one of showDate and showTime should be true. 391 */ 392 public static String getDurationString( 393 Context context, 394 long startUtcMillis, 395 long endUtcMillis, 396 boolean useShortFormat, 397 boolean showDate, 398 boolean showTime, 399 int flag) { 400 flag |= 401 DateUtils.FORMAT_ABBREV_MONTH 402 | ((useShortFormat) ? DateUtils.FORMAT_NUMERIC_DATE : 0); 403 SoftPreconditions.checkArgument(showTime || showDate); 404 if (showTime) { 405 flag |= DateUtils.FORMAT_SHOW_TIME; 406 } 407 if (showDate) { 408 flag |= DateUtils.FORMAT_SHOW_DATE; 409 } 410 if (startUtcMillis != endUtcMillis && useShortFormat) { 411 // Do special handling for 12:00 AM when checking if it's in the given day. 412 // If it's start, it's considered as beginning of the day. (e.g. 12:00 AM - 12:30 AM) 413 // If it's end, it's considered as end of the day (e.g. 11:00 PM - 12:00 AM) 414 if (!isInGivenDay(startUtcMillis, endUtcMillis - 1) 415 && endUtcMillis - startUtcMillis < TimeUnit.HOURS.toMillis(11)) { 416 // Do not show date for short format. 417 // Subtracting one day is needed because {@link DateUtils@formatDateRange} 418 // automatically shows date if the duration covers multiple days. 419 return DateUtils.formatDateRange( 420 context, startUtcMillis, endUtcMillis - TimeUnit.DAYS.toMillis(1), flag); 421 } 422 } 423 // Workaround of b/28740989. 424 // Add 1 msec to endUtcMillis to avoid DateUtils' bug with a duration of 12:00AM~12:00AM. 425 String dateRange = DateUtils.formatDateRange(context, startUtcMillis, endUtcMillis, flag); 426 return startUtcMillis == endUtcMillis || dateRange.contains("–") 427 ? dateRange 428 : DateUtils.formatDateRange(context, startUtcMillis, endUtcMillis + 1, flag); 429 } 430 431 /** 432 * Checks if two given time (in milliseconds) are in the same day with regard to the locale 433 * timezone. 434 */ 435 public static boolean isInGivenDay(long dayToMatchInMillis, long subjectTimeInMillis) { 436 TimeZone timeZone = Calendar.getInstance().getTimeZone(); 437 long offset = timeZone.getRawOffset(); 438 if (timeZone.inDaylightTime(new Date(dayToMatchInMillis))) { 439 offset += timeZone.getDSTSavings(); 440 } 441 return Utils.floorTime(dayToMatchInMillis + offset, ONE_DAY_MS) 442 == Utils.floorTime(subjectTimeInMillis + offset, ONE_DAY_MS); 443 } 444 445 /** Calculate how many days between two milliseconds. */ 446 public static int computeDateDifference(long startTimeMs, long endTimeMs) { 447 Calendar calFrom = Calendar.getInstance(); 448 Calendar calTo = Calendar.getInstance(); 449 calFrom.setTime(new Date(startTimeMs)); 450 calTo.setTime(new Date(endTimeMs)); 451 resetCalendar(calFrom); 452 resetCalendar(calTo); 453 return (int) ((calTo.getTimeInMillis() - calFrom.getTimeInMillis()) / ONE_DAY_MS); 454 } 455 456 private static void resetCalendar(Calendar cal) { 457 cal.set(Calendar.HOUR_OF_DAY, 0); 458 cal.set(Calendar.MINUTE, 0); 459 cal.set(Calendar.SECOND, 0); 460 cal.set(Calendar.MILLISECOND, 0); 461 } 462 463 /** Returns the last millisecond of a day which the millis belongs to. */ 464 public static long getLastMillisecondOfDay(long millis) { 465 Calendar calender = Calendar.getInstance(); 466 calender.setTime(new Date(millis)); 467 calender.set(Calendar.HOUR_OF_DAY, 23); 468 calender.set(Calendar.MINUTE, 59); 469 calender.set(Calendar.SECOND, 59); 470 calender.set(Calendar.MILLISECOND, 999); 471 return calender.getTimeInMillis(); 472 } 473 474 public static String getAspectRatioString(int width, int height) { 475 if (width == 0 || height == 0) { 476 return ""; 477 } 478 479 for (AspectRatio ratio : AspectRatio.values()) { 480 if (Math.abs((float) ratio.height / ratio.width - (float) height / width) < 0.05f) { 481 return ratio.toString(); 482 } 483 } 484 return ""; 485 } 486 487 public static String getAspectRatioString(float videoDisplayAspectRatio) { 488 if (videoDisplayAspectRatio <= 0) { 489 return ""; 490 } 491 492 for (AspectRatio ratio : AspectRatio.values()) { 493 if (Math.abs((float) ratio.width / ratio.height - videoDisplayAspectRatio) < 0.05f) { 494 return ratio.toString(); 495 } 496 } 497 return ""; 498 } 499 500 public static int getVideoDefinitionLevelFromSize(int width, int height) { 501 if (width >= VIDEO_ULTRA_HD_WIDTH && height >= VIDEO_ULTRA_HD_HEIGHT) { 502 return StreamInfo.VIDEO_DEFINITION_LEVEL_ULTRA_HD; 503 } else if (width >= VIDEO_FULL_HD_WIDTH && height >= VIDEO_FULL_HD_HEIGHT) { 504 return StreamInfo.VIDEO_DEFINITION_LEVEL_FULL_HD; 505 } else if (width >= VIDEO_HD_WIDTH && height >= VIDEO_HD_HEIGHT) { 506 return StreamInfo.VIDEO_DEFINITION_LEVEL_HD; 507 } else if (width >= VIDEO_SD_WIDTH && height >= VIDEO_SD_HEIGHT) { 508 return StreamInfo.VIDEO_DEFINITION_LEVEL_SD; 509 } 510 return StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN; 511 } 512 513 public static String getVideoDefinitionLevelString(Context context, int videoFormat) { 514 switch (videoFormat) { 515 case StreamInfo.VIDEO_DEFINITION_LEVEL_ULTRA_HD: 516 return context.getResources().getString(R.string.video_definition_level_ultra_hd); 517 case StreamInfo.VIDEO_DEFINITION_LEVEL_FULL_HD: 518 return context.getResources().getString(R.string.video_definition_level_full_hd); 519 case StreamInfo.VIDEO_DEFINITION_LEVEL_HD: 520 return context.getResources().getString(R.string.video_definition_level_hd); 521 case StreamInfo.VIDEO_DEFINITION_LEVEL_SD: 522 return context.getResources().getString(R.string.video_definition_level_sd); 523 } 524 return ""; 525 } 526 527 public static String getAudioChannelString(Context context, int channelCount) { 528 switch (channelCount) { 529 case 1: 530 return context.getResources().getString(R.string.audio_channel_mono); 531 case 2: 532 return context.getResources().getString(R.string.audio_channel_stereo); 533 case 6: 534 return context.getResources().getString(R.string.audio_channel_5_1); 535 case 8: 536 return context.getResources().getString(R.string.audio_channel_7_1); 537 } 538 return ""; 539 } 540 541 public static boolean needToShowSampleRate(Context context, List<TvTrackInfo> tracks) { 542 Set<String> multiAudioStrings = new HashSet<>(); 543 for (TvTrackInfo track : tracks) { 544 String multiAudioString = getMultiAudioString(context, track, false); 545 if (multiAudioStrings.contains(multiAudioString)) { 546 return true; 547 } 548 multiAudioStrings.add(multiAudioString); 549 } 550 return false; 551 } 552 553 public static String getMultiAudioString( 554 Context context, TvTrackInfo track, boolean showSampleRate) { 555 if (track.getType() != TvTrackInfo.TYPE_AUDIO) { 556 throw new IllegalArgumentException("Not an audio track: " + track); 557 } 558 String language = context.getString(R.string.multi_audio_unknown_language); 559 if (!TextUtils.isEmpty(track.getLanguage())) { 560 language = new Locale(track.getLanguage()).getDisplayName(); 561 } else { 562 Log.d(TAG, "No language information found for the audio track: " + track); 563 } 564 565 StringBuilder metadata = new StringBuilder(); 566 switch (track.getAudioChannelCount()) { 567 case AUDIO_CHANNEL_NONE: 568 break; 569 case AUDIO_CHANNEL_MONO: 570 metadata.append(context.getString(R.string.multi_audio_channel_mono)); 571 break; 572 case AUDIO_CHANNEL_STEREO: 573 metadata.append(context.getString(R.string.multi_audio_channel_stereo)); 574 break; 575 case AUDIO_CHANNEL_SURROUND_6: 576 metadata.append(context.getString(R.string.multi_audio_channel_surround_6)); 577 break; 578 case AUDIO_CHANNEL_SURROUND_8: 579 metadata.append(context.getString(R.string.multi_audio_channel_surround_8)); 580 break; 581 default: 582 if (track.getAudioChannelCount() > 0) { 583 metadata.append( 584 context.getString( 585 R.string.multi_audio_channel_suffix, 586 track.getAudioChannelCount())); 587 } else { 588 Log.d( 589 TAG, 590 "Invalid audio channel count (" 591 + track.getAudioChannelCount() 592 + ") found for the audio track: " 593 + track); 594 } 595 break; 596 } 597 if (showSampleRate) { 598 int sampleRate = track.getAudioSampleRate(); 599 if (sampleRate > 0) { 600 if (metadata.length() > 0) { 601 metadata.append(", "); 602 } 603 int integerPart = sampleRate / 1000; 604 int tenths = (sampleRate % 1000) / 100; 605 metadata.append(integerPart); 606 if (tenths != 0) { 607 metadata.append("."); 608 metadata.append(tenths); 609 } 610 metadata.append("kHz"); 611 } 612 } 613 614 if (metadata.length() == 0) { 615 return language; 616 } 617 return context.getString( 618 R.string.multi_audio_display_string_with_channel, language, metadata.toString()); 619 } 620 621 public static boolean isEqualLanguage(String lang1, String lang2) { 622 if (lang1 == null) { 623 return lang2 == null; 624 } else if (lang2 == null) { 625 return false; 626 } 627 try { 628 return TextUtils.equals( 629 new Locale(lang1).getISO3Language(), new Locale(lang2).getISO3Language()); 630 } catch (Exception ignored) { 631 } 632 return false; 633 } 634 635 public static boolean isIntentAvailable(Context context, Intent intent) { 636 return context.getPackageManager() 637 .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY) 638 .size() 639 > 0; 640 } 641 642 /** Returns the label for a given input. Returns the custom label, if any. */ 643 public static String loadLabel(Context context, TvInputInfo input) { 644 if (input == null) { 645 return null; 646 } 647 TvInputManagerHelper inputManager = 648 TvSingletons.getSingletons(context).getTvInputManagerHelper(); 649 CharSequence customLabel = inputManager.loadCustomLabel(input); 650 String label = (customLabel == null) ? null : customLabel.toString(); 651 if (TextUtils.isEmpty(label)) { 652 label = inputManager.loadLabel(input).toString(); 653 } 654 return label; 655 } 656 657 /** Enable all channels synchronously. */ 658 @WorkerThread 659 public static void enableAllChannels(Context context) { 660 ContentValues values = new ContentValues(); 661 values.put(Channels.COLUMN_BROWSABLE, 1); 662 context.getContentResolver().update(Channels.CONTENT_URI, values, null, null); 663 } 664 665 /** 666 * Converts time in milliseconds to a String. 667 * 668 * @param fullFormat {@code true} for returning date string with a full format (e.g., Mon Aug 15 669 * 20:08:35 GMT 2016). {@code false} for a short format, {e.g., [8/15/16] 8:08 AM}, in which 670 * date information would only appears when the target time is not today. 671 */ 672 public static String toTimeString(long timeMillis, boolean fullFormat) { 673 if (fullFormat) { 674 return new Date(timeMillis).toString(); 675 } else { 676 long currentTime = System.currentTimeMillis(); 677 return (String) 678 DateUtils.formatSameDayTime( 679 timeMillis, 680 System.currentTimeMillis(), 681 SimpleDateFormat.SHORT, 682 SimpleDateFormat.SHORT); 683 } 684 } 685 686 /** Converts time in milliseconds to a String. */ 687 public static String toTimeString(long timeMillis) { 688 return toTimeString(timeMillis, true); 689 } 690 691 /** 692 * Returns a {@link String} object which contains the layout information of the {@code view}. 693 */ 694 public static String toRectString(View view) { 695 return "{" 696 + "l=" 697 + view.getLeft() 698 + ",r=" 699 + view.getRight() 700 + ",t=" 701 + view.getTop() 702 + ",b=" 703 + view.getBottom() 704 + ",w=" 705 + view.getWidth() 706 + ",h=" 707 + view.getHeight() 708 + "}"; 709 } 710 711 /** 712 * Floors time to the given {@code timeUnit}. For example, if time is 5:32:11 and timeUnit is 713 * one hour (60 * 60 * 1000), then the output will be 5:00:00. 714 */ 715 public static long floorTime(long timeMs, long timeUnit) { 716 return timeMs - (timeMs % timeUnit); 717 } 718 719 /** 720 * Ceils time to the given {@code timeUnit}. For example, if time is 5:32:11 and timeUnit is one 721 * hour (60 * 60 * 1000), then the output will be 6:00:00. 722 */ 723 public static long ceilTime(long timeMs, long timeUnit) { 724 return timeMs + timeUnit - (timeMs % timeUnit); 725 } 726 727 /** Returns an {@link String#intern() interned} string or null if the input is null. */ 728 @Nullable 729 public static String intern(@Nullable String string) { 730 return string == null ? null : string.intern(); 731 } 732 733 /** 734 * Check if the index is valid for the collection, 735 * 736 * @param collection the collection 737 * @param index the index position to test 738 * @return index >= 0 && index < collection.size(). 739 */ 740 public static boolean isIndexValid(@Nullable Collection<?> collection, int index) { 741 return collection != null && (index >= 0 && index < collection.size()); 742 } 743 744 /** Returns a localized version of the text resource specified by resourceId. */ 745 public static CharSequence getTextForLocale(Context context, Locale locale, int resourceId) { 746 if (locale.equals(context.getResources().getConfiguration().locale)) { 747 return context.getText(resourceId); 748 } 749 Configuration config = new Configuration(context.getResources().getConfiguration()); 750 config.setLocale(locale); 751 return context.createConfigurationContext(config).getText(resourceId); 752 } 753 754 /** Checks where there is any internal TV input. */ 755 public static boolean hasInternalTvInputs(Context context, boolean tunerInputOnly) { 756 for (TvInputInfo input : 757 TvSingletons.getSingletons(context) 758 .getTvInputManagerHelper() 759 .getTvInputInfos(true, tunerInputOnly)) { 760 if (isInternalTvInput(context, input.getId())) { 761 return true; 762 } 763 } 764 return false; 765 } 766 767 /** Returns the internal TV inputs. */ 768 public static List<TvInputInfo> getInternalTvInputs(Context context, boolean tunerInputOnly) { 769 List<TvInputInfo> inputs = new ArrayList<>(); 770 for (TvInputInfo input : 771 TvSingletons.getSingletons(context) 772 .getTvInputManagerHelper() 773 .getTvInputInfos(true, tunerInputOnly)) { 774 if (isInternalTvInput(context, input.getId())) { 775 inputs.add(input); 776 } 777 } 778 return inputs; 779 } 780 781 /** Checks whether the input is internal or not. */ 782 public static boolean isInternalTvInput(Context context, String inputId) { 783 return context.getPackageName() 784 .equals(ComponentName.unflattenFromString(inputId).getPackageName()); 785 } 786 787 /** Returns the TV input for the given {@code program}. */ 788 @Nullable 789 public static TvInputInfo getTvInputInfoForProgram(Context context, Program program) { 790 if (!Program.isValid(program)) { 791 return null; 792 } 793 return getTvInputInfoForChannelId(context, program.getChannelId()); 794 } 795 796 /** Returns the TV input for the given channel ID. */ 797 @Nullable 798 public static TvInputInfo getTvInputInfoForChannelId(Context context, long channelId) { 799 TvSingletons tvSingletons = TvSingletons.getSingletons(context); 800 Channel channel = tvSingletons.getChannelDataManager().getChannel(channelId); 801 if (channel == null) { 802 return null; 803 } 804 return tvSingletons.getTvInputManagerHelper().getTvInputInfo(channel.getInputId()); 805 } 806 807 /** Returns the {@link TvInputInfo} for the given input ID. */ 808 @Nullable 809 public static TvInputInfo getTvInputInfoForInputId(Context context, String inputId) { 810 return TvSingletons.getSingletons(context) 811 .getTvInputManagerHelper() 812 .getTvInputInfo(inputId); 813 } 814 815 /** Returns the canonical genre ID's from the {@code genres}. */ 816 public static int[] getCanonicalGenreIds(String genres) { 817 if (TextUtils.isEmpty(genres)) { 818 return null; 819 } 820 return getCanonicalGenreIds(Genres.decode(genres)); 821 } 822 823 /** Returns the canonical genre ID's from the {@code genres}. */ 824 public static int[] getCanonicalGenreIds(String[] canonicalGenres) { 825 if (canonicalGenres != null && canonicalGenres.length > 0) { 826 int[] results = new int[canonicalGenres.length]; 827 int i = 0; 828 for (String canonicalGenre : canonicalGenres) { 829 int genreId = GenreItems.getId(canonicalGenre); 830 if (genreId == GenreItems.ID_ALL_CHANNELS) { 831 // Skip if the genre is unknown. 832 continue; 833 } 834 results[i++] = genreId; 835 } 836 if (i < canonicalGenres.length) { 837 results = Arrays.copyOf(results, i); 838 } 839 return results; 840 } 841 return null; 842 } 843 844 /** Returns the canonical genres for database. */ 845 public static String getCanonicalGenre(int[] canonicalGenreIds) { 846 if (canonicalGenreIds == null || canonicalGenreIds.length == 0) { 847 return null; 848 } 849 String[] genres = new String[canonicalGenreIds.length]; 850 for (int i = 0; i < canonicalGenreIds.length; ++i) { 851 genres[i] = GenreItems.getCanonicalGenre(canonicalGenreIds[i]); 852 } 853 return Genres.encode(genres); 854 } 855 856 /** 857 * Runs the method in main thread. If the current thread is not main thread, block it util the 858 * method is finished. 859 */ 860 public static void runInMainThreadAndWait(Runnable runnable) { 861 if (Looper.myLooper() == Looper.getMainLooper()) { 862 runnable.run(); 863 } else { 864 Future<?> temp = MainThreadExecutor.getInstance().submit(runnable); 865 try { 866 temp.get(); 867 } catch (InterruptedException | ExecutionException e) { 868 Log.e(TAG, "failed to finish the execution", e); 869 } 870 } 871 } 872} 873