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