Utils.java revision d41f0075a7d2ea826204e81fcec57d0aa57171a9
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.preference.PreferenceManager; 35import android.support.annotation.Nullable; 36import android.support.annotation.VisibleForTesting; 37import android.support.annotation.WorkerThread; 38import android.text.TextUtils; 39import android.text.format.DateUtils; 40import android.util.ArraySet; 41import android.util.Log; 42import android.view.View; 43 44import com.android.tv.ApplicationSingletons; 45import com.android.tv.R; 46import com.android.tv.TvApplication; 47import com.android.tv.common.SoftPreconditions; 48import com.android.tv.data.Channel; 49import com.android.tv.data.GenreItems; 50import com.android.tv.data.Program; 51import com.android.tv.data.StreamInfo; 52 53import java.io.File; 54import java.text.SimpleDateFormat; 55import java.util.ArrayList; 56import java.util.Arrays; 57import java.util.Calendar; 58import java.util.Collection; 59import java.util.Date; 60import java.util.HashSet; 61import java.util.List; 62import java.util.Locale; 63import java.util.Set; 64import java.util.TimeZone; 65import java.util.concurrent.TimeUnit; 66 67/** 68 * A class that includes convenience methods for accessing TvProvider database. 69 */ 70public 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 136 AspectRatio(int width, int height) { 137 this.width = width; 138 this.height = height; 139 } 140 141 @Override 142 @SuppressLint("DefaultLocale") 143 public String toString() { 144 return String.format("%d:%d", width, height); 145 } 146 } 147 148 private Utils() { 149 } 150 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 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 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 */ 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 */ 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 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 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 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 */ 237 public static String getLastWatchedTunerInputId(Context context) { 238 return PreferenceManager.getDefaultSharedPreferences(context) 239 .getString(PREF_KEY_LAST_WATCHED_TUNER_INPUT_ID, null); 240 } 241 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 */ 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 */ 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 */ 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 */ 277 public static boolean isChannelUriForTunerInput(Uri uri) { 278 return isTvUri(uri) && isTwoSegmentUriStartingWith(uri, PATH_CHANNEL); 279 } 280 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 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 */ 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 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 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 */ 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 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 */ 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 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 */ 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 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 */ 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 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 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 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 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 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 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 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 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 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 */ 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 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 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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