From 633eb826b8c97731dfc5ef12c7bf78a63734275d Mon Sep 17 00:00:00 2001 From: Nick Chalko Date: Tue, 3 Oct 2017 10:16:37 -0700 Subject: Sync to match Live Channels 1.15(ncis) aka ub-tv-dev at a73a150bb7d0d1ce867ef980c6ac8411899d40ad Bug: 64021596 Change-Id: I7c544fd15e2c58784f8babc31877ad0dfeebb4c0 --- src/com/android/tv/util/AsyncDbTask.java | 4 +- src/com/android/tv/util/BitmapUtils.java | 10 + src/com/android/tv/util/Debug.java | 60 +++ src/com/android/tv/util/DurationTimer.java | 91 +++++ src/com/android/tv/util/ImageLoader.java | 3 +- src/com/android/tv/util/LocationUtils.java | 25 +- src/com/android/tv/util/NetworkTrafficTags.java | 64 ++++ src/com/android/tv/util/OnboardingUtils.java | 41 +- src/com/android/tv/util/Partner.java | 181 +++++++++ src/com/android/tv/util/PermissionUtils.java | 5 + src/com/android/tv/util/PipInputManager.java | 432 ---------------------- src/com/android/tv/util/RecurringRunner.java | 9 +- src/com/android/tv/util/SearchManagerHelper.java | 61 --- src/com/android/tv/util/SetupUtils.java | 25 +- src/com/android/tv/util/StringUtils.java | 38 ++ src/com/android/tv/util/TimeShiftUtils.java | 4 +- src/com/android/tv/util/TvInputManagerHelper.java | 358 +++++++++++++++++- src/com/android/tv/util/TvProviderUriMatcher.java | 72 ---- src/com/android/tv/util/TvSettings.java | 160 +++----- src/com/android/tv/util/TvTrackInfoUtils.java | 37 +- src/com/android/tv/util/TvUriMatcher.java | 80 ++++ src/com/android/tv/util/Utils.java | 100 ++++- src/com/android/tv/util/ViewCache.java | 100 +++++ 23 files changed, 1183 insertions(+), 777 deletions(-) create mode 100644 src/com/android/tv/util/Debug.java create mode 100644 src/com/android/tv/util/DurationTimer.java create mode 100644 src/com/android/tv/util/NetworkTrafficTags.java create mode 100644 src/com/android/tv/util/Partner.java delete mode 100644 src/com/android/tv/util/PipInputManager.java delete mode 100644 src/com/android/tv/util/SearchManagerHelper.java create mode 100644 src/com/android/tv/util/StringUtils.java delete mode 100644 src/com/android/tv/util/TvProviderUriMatcher.java create mode 100644 src/com/android/tv/util/TvUriMatcher.java create mode 100644 src/com/android/tv/util/ViewCache.java (limited to 'src/com/android/tv/util') diff --git a/src/com/android/tv/util/AsyncDbTask.java b/src/com/android/tv/util/AsyncDbTask.java index 78243642..477412e4 100644 --- a/src/com/android/tv/util/AsyncDbTask.java +++ b/src/com/android/tv/util/AsyncDbTask.java @@ -31,7 +31,7 @@ import android.util.Range; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; import com.android.tv.data.Program; -import com.android.tv.dvr.RecordedProgram; +import com.android.tv.dvr.data.RecordedProgram; import java.util.ArrayList; import java.util.List; @@ -76,7 +76,7 @@ public abstract class AsyncDbTask * accepted for execution * @throws NullPointerException if command is null */ - public static void execute(Runnable command) { + public static void executeOnDbThread(Runnable command) { DB_EXECUTOR.execute(command); } diff --git a/src/com/android/tv/util/BitmapUtils.java b/src/com/android/tv/util/BitmapUtils.java index d45a8dce..fbaab023 100644 --- a/src/com/android/tv/util/BitmapUtils.java +++ b/src/com/android/tv/util/BitmapUtils.java @@ -24,6 +24,7 @@ import android.graphics.BitmapFactory; import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.net.TrafficStats; import android.net.Uri; import android.support.annotation.NonNull; import android.text.TextUtils; @@ -56,6 +57,12 @@ public final class BitmapUtils { return Bitmap.createScaledBitmap(bm, rect.right, rect.bottom, false); } + public static Bitmap getScaledMutableBitmap(Bitmap bm, int maxWidth, int maxHeight) { + Bitmap scaledBitmap = scaleBitmap(bm, maxWidth, maxHeight); + return scaledBitmap.isMutable() ? scaledBitmap + : scaledBitmap.copy(Bitmap.Config.ARGB_8888, true); + } + private static Rect calculateNewSize(Bitmap bm, int maxWidth, int maxHeight) { final double ratio = maxHeight / (double) maxWidth; final double bmRatio = bm.getHeight() / (double) bm.getWidth(); @@ -89,6 +96,8 @@ public final class BitmapUtils { boolean isResourceUri = isContentResolverUri(uri); URLConnection urlConnection = null; InputStream inputStream = null; + final int oldTag = TrafficStats.getThreadStatsTag(); + TrafficStats.setThreadStatsTag(NetworkTrafficTags.LOGO_FETCHER); try { if (isResourceUri) { inputStream = context.getContentResolver().openInputStream(uri); @@ -142,6 +151,7 @@ public final class BitmapUtils { return null; } finally { close(inputStream, urlConnection); + TrafficStats.setThreadStatsTag(oldTag); } } diff --git a/src/com/android/tv/util/Debug.java b/src/com/android/tv/util/Debug.java new file mode 100644 index 00000000..67a2683d --- /dev/null +++ b/src/com/android/tv/util/Debug.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.util; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * A class only for help developers. + */ +public class Debug { + /** + * A threshold of start up time, when the start up time of Live TV is more than it, + * a warning will show to the developer. + */ + public static final long TIME_START_UP_DURATION_THRESHOLD = TimeUnit.SECONDS.toMillis(6); + /** + * Tag for measuring start up time of Live TV. + */ + public static final String TAG_START_UP_TIMER = "start_up_timer"; + + /** + * A global map for duration timers. + */ + private final static Map sTimerMap = new HashMap<>(); + + /** + * Returns the global duration timer by tag. + */ + public static DurationTimer getTimer(String tag) { + if (sTimerMap.get(tag) != null) { + return sTimerMap.get(tag); + } + DurationTimer timer = new DurationTimer(tag, true); + sTimerMap.put(tag, timer); + return timer; + } + + /** + * Removes the global duration timer by tag. + */ + public static DurationTimer removeTimer(String tag) { + return sTimerMap.remove(tag); + } +} diff --git a/src/com/android/tv/util/DurationTimer.java b/src/com/android/tv/util/DurationTimer.java new file mode 100644 index 00000000..1f057bf6 --- /dev/null +++ b/src/com/android/tv/util/DurationTimer.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.util; + +import android.os.SystemClock; +import android.util.Log; + +import com.android.tv.common.BuildConfig; + +/** + * Times a duration. + */ +public final class DurationTimer { + private static final String TAG = "DurationTimer"; + public static final long TIME_NOT_SET = -1; + + private long mStartTimeMs = TIME_NOT_SET; + private String mTag = TAG; + private boolean mLogEngOnly; + + public DurationTimer() { } + + public DurationTimer(String tag, boolean logEngOnly) { + mTag = tag; + mLogEngOnly = logEngOnly; + } + + /** + * Returns true if the timer is running. + */ + public boolean isRunning() { + return mStartTimeMs != TIME_NOT_SET; + } + + /** + * Start the timer. + */ + public void start() { + mStartTimeMs = SystemClock.elapsedRealtime(); + } + + /** + * Returns true if timer is started. + */ + public boolean isStarted() { + return mStartTimeMs != TIME_NOT_SET; + } + + /** + * Returns the current duration in milliseconds or {@link #TIME_NOT_SET} if the timer is not + * running. + */ + public long getDuration() { + return isRunning() ? SystemClock.elapsedRealtime() - mStartTimeMs : TIME_NOT_SET; + } + + /** + * Stops the timer and resets its value to {@link #TIME_NOT_SET}. + * + * @return the current duration in milliseconds or {@link #TIME_NOT_SET} if the timer is not + * running. + */ + public long reset() { + long duration = getDuration(); + mStartTimeMs = TIME_NOT_SET; + return duration; + } + + /** + * Adds information and duration time to the log. + */ + public void log(String message) { + if (isRunning() && (!mLogEngOnly || BuildConfig.ENG)) { + Log.i(mTag, message + " : " + getDuration() + "ms"); + } + } +} diff --git a/src/com/android/tv/util/ImageLoader.java b/src/com/android/tv/util/ImageLoader.java index 04bb478a..86bb94c1 100644 --- a/src/com/android/tv/util/ImageLoader.java +++ b/src/com/android/tv/util/ImageLoader.java @@ -292,7 +292,8 @@ public final class ImageLoader { * Checks if a reload would be needed if the results of other was available. */ private boolean isReloadNeeded(LoadBitmapTask other) { - return mMaxHeight >= other.mMaxHeight * 2 || mMaxWidth >= other.mMaxWidth * 2; + return (other.mMaxHeight != Integer.MAX_VALUE && mMaxHeight >= other.mMaxHeight * 2) + || (other.mMaxWidth != Integer.MAX_VALUE && mMaxWidth >= other.mMaxWidth * 2); } @Nullable diff --git a/src/com/android/tv/util/LocationUtils.java b/src/com/android/tv/util/LocationUtils.java index 8e3b59e9..d5d7bee3 100644 --- a/src/com/android/tv/util/LocationUtils.java +++ b/src/com/android/tv/util/LocationUtils.java @@ -16,15 +16,20 @@ package com.android.tv.util; +import android.Manifest; import android.content.Context; +import android.content.pm.PackageManager; import android.location.Address; import android.location.Geocoder; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.text.TextUtils; import android.util.Log; +import com.android.tv.tuner.util.PostalCodeUtils; import java.io.IOException; import java.util.List; @@ -39,6 +44,7 @@ public class LocationUtils { private static Context sApplicationContext; private static Address sAddress; + private static String sCountry; private static IOException sError; /** @@ -59,6 +65,18 @@ public class LocationUtils { return null; } + /** Returns the current country. */ + @NonNull + public static synchronized String getCurrentCountry(Context context) { + if (sCountry != null) { + return sCountry; + } + if (TextUtils.isEmpty(sCountry)) { + sCountry = context.getResources().getConfiguration().locale.getCountry(); + } + return sCountry; + } + private static void updateAddress(Location location) { if (DEBUG) Log.d(TAG, "Updating address with " + location); if (location == null) { @@ -68,9 +86,14 @@ public class LocationUtils { try { List
addresses = geocoder.getFromLocation( location.getLatitude(), location.getLongitude(), 1); - if (addresses != null) { + if (addresses != null && !addresses.isEmpty()) { sAddress = addresses.get(0); if (DEBUG) Log.d(TAG, "Got " + sAddress); + try { + PostalCodeUtils.updatePostalCode(sApplicationContext); + } catch (Exception e) { + // Do nothing + } } else { if (DEBUG) Log.d(TAG, "No address returned"); } diff --git a/src/com/android/tv/util/NetworkTrafficTags.java b/src/com/android/tv/util/NetworkTrafficTags.java new file mode 100644 index 00000000..2dca613c --- /dev/null +++ b/src/com/android/tv/util/NetworkTrafficTags.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.util; + +import android.net.TrafficStats; +import android.support.annotation.NonNull; + +import java.util.concurrent.Executor; + +/** Constants for tagging network traffic in the Live channels app. */ +public final class NetworkTrafficTags { + + public static final int DEFAULT_LIVE_CHANNELS = 1; + public static final int LOGO_FETCHER = 2; + public static final int HDHOMERUN = 3; + public static final int EPG_FETCH = 4; + + /** + * An executor which simply wraps a provided delegate executor, but calls {@link + * TrafficStats#setThreadStatsTag(int)} before executing any task. + */ + public static class TrafficStatsTaggingExecutor implements Executor { + private final Executor delegateExecutor; + private final int tag; + + public TrafficStatsTaggingExecutor(Executor delegateExecutor, int tag) { + this.delegateExecutor = delegateExecutor; + this.tag = tag; + } + + @Override + public void execute(final @NonNull Runnable command) { + // TODO(b/62038127): robolectric does not support lamdas in unbundled apps + delegateExecutor.execute( + new Runnable() { + @Override + public void run() { + TrafficStats.setThreadStatsTag(tag); + try { + command.run(); + } finally { + TrafficStats.clearThreadStatsTag(); + } + } + }); + } + } + + private NetworkTrafficTags() {} +} diff --git a/src/com/android/tv/util/OnboardingUtils.java b/src/com/android/tv/util/OnboardingUtils.java index 3040020e..49b02b82 100644 --- a/src/com/android/tv/util/OnboardingUtils.java +++ b/src/com/android/tv/util/OnboardingUtils.java @@ -16,17 +16,10 @@ package com.android.tv.util; -import android.content.ContentResolver; import android.content.Context; import android.content.Intent; -import android.database.Cursor; -import android.media.tv.TvContract.Channels; import android.net.Uri; import android.preference.PreferenceManager; -import android.support.annotation.UiThread; - -import com.android.tv.TvApplication; -import com.android.tv.data.ChannelDataManager; /** * A utility class related to onboarding experience. @@ -81,42 +74,12 @@ public final class OnboardingUtils { .putInt(PREF_KEY_ONBOARDING_VERSION_CODE, ONBOARDING_VERSION).apply(); } - /** - * Checks whether the onboarding screen should be shown or not. - */ - public static boolean needToShowOnboarding(Context context) { - return isFirstRunWithCurrentVersion(context) || !areChannelsAvailable(context); - } - - /** - * Checks if there are any available tuner channels. - */ - @UiThread - public static boolean areChannelsAvailable(Context context) { - ChannelDataManager manager = TvApplication.getSingletons(context).getChannelDataManager(); - if (manager.isDbLoadFinished()) { - return manager.getChannelCount() != 0; - } - // This method should block the UI thread. - ContentResolver resolver = context.getContentResolver(); - try (Cursor c = resolver.query(Channels.CONTENT_URI, new String[] {Channels._ID}, null, - null, null)) { - return c != null && c.getCount() != 0; - } - } - - /** - * Checks if there are any available TV inputs. - */ - public static boolean areInputsAvailable(Context context) { - return TvApplication.getSingletons(context).getTvInputManagerHelper() - .getTvInputInfos(true, false).size() > 0; - } - /** * Returns merchant collection URL. */ private static String getMerchantCollectionUrl() { return "TODO: add a merchant collection url"; } + + private OnboardingUtils() {} } diff --git a/src/com/android/tv/util/Partner.java b/src/com/android/tv/util/Partner.java new file mode 100644 index 00000000..e3688392 --- /dev/null +++ b/src/com/android/tv/util/Partner.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.util; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.media.tv.TvInputInfo; +import android.text.TextUtils; +import android.util.Log; + +import java.util.HashMap; +import java.util.Map; + +/** + * This file refers to Partner.java in LeanbackLauncher. Interact with partner customizations. There + * can only be one set of customizations on a device, and it must be bundled with the system. + */ +public class Partner { + private static final String TAG = "Partner"; + /** Marker action used to discover partner */ + private static final String ACTION_PARTNER_CUSTOMIZATION = + "com.google.android.leanbacklauncher.action.PARTNER_CUSTOMIZATION"; + + /** ID tags for device input types */ + public static final String INPUT_TYPE_BUNDLED_TUNER = "input_type_combined_tuners"; + public static final String INPUT_TYPE_TUNER = "input_type_tuner"; + public static final String INPUT_TYPE_CEC_LOGICAL = "input_type_cec_logical"; + public static final String INPUT_TYPE_CEC_RECORDER = "input_type_cec_recorder"; + public static final String INPUT_TYPE_CEC_PLAYBACK = "input_type_cec_playback"; + public static final String INPUT_TYPE_MHL_MOBILE = "input_type_mhl_mobile"; + public static final String INPUT_TYPE_HDMI = "input_type_hdmi"; + public static final String INPUT_TYPE_DVI = "input_type_dvi"; + public static final String INPUT_TYPE_COMPONENT = "input_type_component"; + public static final String INPUT_TYPE_SVIDEO = "input_type_svideo"; + public static final String INPUT_TYPE_COMPOSITE = "input_type_composite"; + public static final String INPUT_TYPE_DISPLAY_PORT = "input_type_displayport"; + public static final String INPUT_TYPE_VGA = "input_type_vga"; + public static final String INPUT_TYPE_SCART = "input_type_scart"; + public static final String INPUT_TYPE_OTHER = "input_type_other"; + + private static final String INPUTS_ORDER = "home_screen_inputs_ordering"; + private static final String TYPE_ARRAY = "array"; + + private static Partner sPartner; + private static final Object sLock = new Object(); + + private final String mPackageName; + private final String mReceiverName; + private final Resources mResources; + + private static final Map INPUT_TYPE_MAP = new HashMap<>(); + static { + INPUT_TYPE_MAP.put(INPUT_TYPE_BUNDLED_TUNER, TvInputManagerHelper.TYPE_BUNDLED_TUNER); + INPUT_TYPE_MAP.put(INPUT_TYPE_TUNER, TvInputInfo.TYPE_TUNER); + INPUT_TYPE_MAP.put(INPUT_TYPE_CEC_LOGICAL, TvInputManagerHelper.TYPE_CEC_DEVICE); + INPUT_TYPE_MAP.put(INPUT_TYPE_CEC_RECORDER, TvInputManagerHelper.TYPE_CEC_DEVICE_RECORDER); + INPUT_TYPE_MAP.put(INPUT_TYPE_CEC_PLAYBACK, TvInputManagerHelper.TYPE_CEC_DEVICE_PLAYBACK); + INPUT_TYPE_MAP.put(INPUT_TYPE_MHL_MOBILE, TvInputManagerHelper.TYPE_MHL_MOBILE); + INPUT_TYPE_MAP.put(INPUT_TYPE_HDMI, TvInputInfo.TYPE_HDMI); + INPUT_TYPE_MAP.put(INPUT_TYPE_DVI, TvInputInfo.TYPE_DVI); + INPUT_TYPE_MAP.put(INPUT_TYPE_COMPONENT, TvInputInfo.TYPE_COMPONENT); + INPUT_TYPE_MAP.put(INPUT_TYPE_SVIDEO, TvInputInfo.TYPE_SVIDEO); + INPUT_TYPE_MAP.put(INPUT_TYPE_COMPOSITE, TvInputInfo.TYPE_COMPOSITE); + INPUT_TYPE_MAP.put(INPUT_TYPE_DISPLAY_PORT, TvInputInfo.TYPE_DISPLAY_PORT); + INPUT_TYPE_MAP.put(INPUT_TYPE_VGA, TvInputInfo.TYPE_VGA); + INPUT_TYPE_MAP.put(INPUT_TYPE_SCART, TvInputInfo.TYPE_SCART); + INPUT_TYPE_MAP.put(INPUT_TYPE_OTHER, TvInputInfo.TYPE_OTHER); + } + + private Partner(String packageName, String receiverName, Resources res) { + mPackageName = packageName; + mReceiverName = receiverName; + mResources = res; + } + + /** Returns partner instance. */ + public static Partner getInstance(Context context) { + PackageManager pm = context.getPackageManager(); + synchronized (sLock) { + ResolveInfo info = getPartnerResolveInfo(pm); + if (info != null) { + final String packageName = info.activityInfo.packageName; + final String receiverName = info.activityInfo.name; + try { + final Resources res = pm.getResourcesForApplication(packageName); + sPartner = new Partner(packageName, receiverName, res); + sPartner.sendInitBroadcast(context); + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Failed to find resources for " + packageName); + } + } + if (sPartner == null) { + sPartner = new Partner(null, null, null); + } + } + return sPartner; + } + + /** Resets the Partner instance to handle the partner package has changed. */ + public static void reset(Context context, String packageName) { + synchronized (sLock) { + if (sPartner != null && !TextUtils.isEmpty(packageName)) { + if (packageName.equals(sPartner.mPackageName)) { + // Force a refresh, so we send an Init to the updated package + sPartner = null; + getInstance(context); + } + } + } + } + + /** This method is used to send init broadcast to the new/changed partner package. */ + private void sendInitBroadcast(Context context) { + if (!TextUtils.isEmpty(mPackageName) && !TextUtils.isEmpty(mReceiverName)) { + Intent intent = new Intent(ACTION_PARTNER_CUSTOMIZATION); + final ComponentName componentName = new ComponentName(mPackageName, mReceiverName); + intent.setComponent(componentName); + intent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND); + context.sendBroadcast(intent); + } + } + + /** Returns the order of inputs. */ + public Map getInputsOrderMap() { + HashMap map = new HashMap<>(); + if (mResources != null && !TextUtils.isEmpty(mPackageName)) { + String[] inputsArray = null; + final int resId = mResources.getIdentifier(INPUTS_ORDER, TYPE_ARRAY, mPackageName); + if (resId != 0) { + inputsArray = mResources.getStringArray(resId); + } + if (inputsArray != null) { + int priority = 0; + for (String input : inputsArray) { + Integer type = INPUT_TYPE_MAP.get(input); + if (type != null) { + map.put(type, priority++); + } + } + } + } + return map; + } + + private static ResolveInfo getPartnerResolveInfo(PackageManager pm) { + final Intent intent = new Intent(ACTION_PARTNER_CUSTOMIZATION); + ResolveInfo partnerInfo = null; + for (ResolveInfo info : pm.queryBroadcastReceivers(intent, 0)) { + if (isSystemApp(info)) { + partnerInfo = info; + break; + } + } + return partnerInfo; + } + + protected static boolean isSystemApp(ResolveInfo info) { + return (info.activityInfo != null + && info.activityInfo.applicationInfo != null + && (info.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0); + } +} diff --git a/src/com/android/tv/util/PermissionUtils.java b/src/com/android/tv/util/PermissionUtils.java index 453885a4..a355be99 100644 --- a/src/com/android/tv/util/PermissionUtils.java +++ b/src/com/android/tv/util/PermissionUtils.java @@ -47,4 +47,9 @@ public class PermissionUtils { return context.checkSelfPermission(PERMISSION_READ_TV_LISTINGS) == PackageManager.PERMISSION_GRANTED; } + + public static boolean hasInternet(Context context) { + return context.checkSelfPermission("android.permission.INTERNET") + == PackageManager.PERMISSION_GRANTED; + } } diff --git a/src/com/android/tv/util/PipInputManager.java b/src/com/android/tv/util/PipInputManager.java deleted file mode 100644 index 2c51d5a0..00000000 --- a/src/com/android/tv/util/PipInputManager.java +++ /dev/null @@ -1,432 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.util; - -import android.content.Context; -import android.media.tv.TvInputInfo; -import android.media.tv.TvInputManager; -import android.media.tv.TvInputManager.TvInputCallback; -import android.util.ArraySet; -import android.util.Log; - -import com.android.tv.ChannelTuner; -import com.android.tv.R; -import com.android.tv.data.Channel; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * A class that manages inputs for PIP. All tuner inputs are represented to one tuner input for PIP. - * Hidden inputs should not be visible to the users. - */ -public class PipInputManager { - private static final String TAG = "PipInputManager"; - - // Tuner inputs aren't distinguished each other in PipInput. They are handled as one input. - // Therefore, we define a fake input id for the unified input. - private static final String TUNER_INPUT_ID = "tuner_input_id"; - - private final Context mContext; - private final TvInputManagerHelper mInputManager; - private final ChannelTuner mChannelTuner; - private boolean mStarted; - private final Map mPipInputMap = new HashMap<>(); // inputId -> PipInput - private final Set mListeners = new ArraySet<>(); - - private final TvInputCallback mTvInputCallback = new TvInputCallback() { - @Override - public void onInputAdded(String inputId) { - TvInputInfo input = mInputManager.getTvInputInfo(inputId); - if (input.isPassthroughInput()) { - boolean available = mInputManager.getInputState(input) - == TvInputManager.INPUT_STATE_CONNECTED; - mPipInputMap.put(inputId, new PipInput(inputId, available)); - } else if (!mPipInputMap.containsKey(TUNER_INPUT_ID)) { - boolean available = mChannelTuner.getBrowsableChannelCount() != 0; - mPipInputMap.put(TUNER_INPUT_ID, new PipInput(TUNER_INPUT_ID, available)); - } else { - return; - } - for (Listener l : mListeners) { - l.onPipInputListUpdated(); - } - } - - @Override - public void onInputRemoved(String inputId) { - PipInput pipInput = mPipInputMap.remove(inputId); - if (pipInput == null) { - if (!mPipInputMap.containsKey(TUNER_INPUT_ID)) { - Log.w(TAG, "A TV input (" + inputId + ") isn't tracked in PipInputManager"); - return; - } - if (mInputManager.getTunerTvInputSize() > 0) { - return; - } - mPipInputMap.remove(TUNER_INPUT_ID); - } - for (Listener l : mListeners) { - l.onPipInputListUpdated(); - } - } - - @Override - public void onInputStateChanged(String inputId, int state) { - PipInput pipInput = mPipInputMap.get(inputId); - if (pipInput == null) { - // For tuner input, state change is handled in mChannelTunerListener. - return; - } - pipInput.updateAvailability(); - } - }; - - private final ChannelTuner.Listener mChannelTunerListener = new ChannelTuner.Listener() { - @Override - public void onLoadFinished() { } - - @Override - public void onCurrentChannelUnavailable(Channel channel) { } - - @Override - public void onBrowsableChannelListChanged() { - PipInput tunerInput = mPipInputMap.get(TUNER_INPUT_ID); - if (tunerInput == null) { - return; - } - tunerInput.updateAvailability(); - } - - @Override - public void onChannelChanged(Channel previousChannel, Channel currentChannel) { - if (previousChannel != null && currentChannel != null - && !previousChannel.isPassthrough() && !currentChannel.isPassthrough()) { - // Channel change between channels for tuner inputs. - return; - } - PipInput previousMainInput = getPipInput(previousChannel); - if (previousMainInput != null) { - previousMainInput.updateAvailability(); - } - PipInput currentMainInput = getPipInput(currentChannel); - if (currentMainInput != null) { - currentMainInput.updateAvailability(); - } - } - }; - - public PipInputManager(Context context, TvInputManagerHelper inputManager, - ChannelTuner channelTuner) { - mContext = context; - mInputManager = inputManager; - mChannelTuner = channelTuner; - } - - /** - * Starts {@link PipInputManager}. - */ - public void start() { - if (mStarted) { - return; - } - mStarted = true; - mInputManager.addCallback(mTvInputCallback); - mChannelTuner.addListener(mChannelTunerListener); - initializePipInputList(); - } - - /** - * Stops {@link PipInputManager}. - */ - public void stop() { - if (!mStarted) { - return; - } - mStarted = false; - mInputManager.removeCallback(mTvInputCallback); - mChannelTuner.removeListener(mChannelTunerListener); - mPipInputMap.clear(); - } - - /** - * Adds a {@link PipInputManager.Listener}. - */ - public void addListener(Listener listener) { - mListeners.add(listener); - } - - /** - * Removes a {@link PipInputManager.Listener}. - */ - public void removeListener(Listener listener) { - mListeners.remove(listener); - } - - /** - * Gets the size of inputs for PIP. - * - *

The hidden inputs are not counted. - * - * @param availableOnly If {@code true}, it counts only available PIP inputs. Please see {@link - * PipInput#isAvailable()} for the details of availability. - */ - public int getPipInputSize(boolean availableOnly) { - int count = 0; - for (PipInput pipInput : mPipInputMap.values()) { - if (!pipInput.isHidden() && (!availableOnly || pipInput.mAvailable)) { - ++count; - } - if (pipInput.isPassthrough()) { - TvInputInfo info = pipInput.getInputInfo(); - // Do not count HDMI ports if a CEC device is directly connected to the port. - if (info.getParentId() != null && !info.isConnectedToHdmiSwitch()) { - --count; - } - } - } - return count; - } - - /** - * Gets the list of inputs for PIP.. - * - *

The hidden inputs are excluded. - * - * @param availableOnly If true, it returns only available PIP inputs. Please see {@link - * PipInput#isAvailable()} for the details of availability. - */ - public List getPipInputList(boolean availableOnly) { - List pipInputs = new ArrayList<>(); - List removeInputs = new ArrayList<>(); - for (PipInput pipInput : mPipInputMap.values()) { - if (!pipInput.isHidden() && (!availableOnly || pipInput.mAvailable)) { - pipInputs.add(pipInput); - } - if (pipInput.isPassthrough()) { - TvInputInfo info = pipInput.getInputInfo(); - // Do not show HDMI ports if a CEC device is directly connected to the port. - if (info.getParentId() != null && !info.isConnectedToHdmiSwitch()) { - removeInputs.add(mPipInputMap.get(info.getParentId())); - } - } - } - if (!removeInputs.isEmpty()) { - pipInputs.removeAll(removeInputs); - } - Collections.sort(pipInputs, new Comparator() { - @Override - public int compare(PipInput lhs, PipInput rhs) { - if (!lhs.mIsPassthrough) { - return -1; - } - if (!rhs.mIsPassthrough) { - return 1; - } - String a = lhs.getLabel(); - String b = rhs.getLabel(); - return a.compareTo(b); - } - }); - return pipInputs; - } - - /** - * Returns an PIP input corresponding to {@code channel}. - */ - public PipInput getPipInput(Channel channel) { - if (channel == null) { - return null; - } - if (channel.isPassthrough()) { - return mPipInputMap.get(channel.getInputId()); - } else { - return mPipInputMap.get(TUNER_INPUT_ID); - } - } - - /** - * Returns true, if {@code channel1} and {@code channel2} belong to the same input. For example, - * two channels from different tuner inputs are also in the same input "Tuner" from PIP - * point of view. - */ - public boolean areInSamePipInput(Channel channel1, Channel channel2) { - PipInput input1 = getPipInput(channel1); - PipInput input2 = getPipInput(channel2); - return input1 != null && input2 != null - && getPipInput(channel1).equals(getPipInput(channel2)); - } - - private void initializePipInputList() { - boolean hasTunerInput = false; - for (TvInputInfo input : mInputManager.getTvInputInfos(false, false)) { - if (input.isPassthroughInput()) { - boolean available = mInputManager.getInputState(input) - == TvInputManager.INPUT_STATE_CONNECTED; - mPipInputMap.put(input.getId(), new PipInput(input.getId(), available)); - } else if (!hasTunerInput) { - hasTunerInput = true; - boolean available = mChannelTuner.getBrowsableChannelCount() != 0; - mPipInputMap.put(TUNER_INPUT_ID, new PipInput(TUNER_INPUT_ID, available)); - } - } - PipInput input = getPipInput(mChannelTuner.getCurrentChannel()); - if (input != null) { - input.updateAvailability(); - } - for (Listener l : mListeners) { - l.onPipInputListUpdated(); - } - } - - /** - * Listeners to notify PIP input state changes. - */ - public interface Listener { - /** - * Called when the state (availability) of PIP inputs is changed. - */ - void onPipInputStateUpdated(); - - /** - * Called when the list of PIP inputs is changed. - */ - void onPipInputListUpdated(); - } - - /** - * Input class for PIP. It has useful methods for PIP handling. - */ - public class PipInput { - private final String mInputId; - private final boolean mIsPassthrough; - private final TvInputInfo mInputInfo; - private boolean mAvailable; - - private PipInput(String inputId, boolean available) { - mInputId = inputId; - mIsPassthrough = !mInputId.equals(TUNER_INPUT_ID); - if (mIsPassthrough) { - mInputInfo = mInputManager.getTvInputInfo(mInputId); - } else { - mInputInfo = null; - } - mAvailable = available; - } - - /** - * Returns the {@link TvInputInfo} object that matches to this PIP input. - */ - public TvInputInfo getInputInfo() { - return mInputInfo; - } - - /** - * Returns {@code true}, if the input is available for PIP. If a channel of an input is - * already played or an input is not connected state or there is no browsable channel, the - * input is unavailable. - */ - public boolean isAvailable() { - return mAvailable; - } - - /** - * Returns true, if the input is a passthrough TV input. - */ - public boolean isPassthrough() { - return mIsPassthrough; - } - - /** - * Gets a channel to play in a PIP view. - */ - public Channel getChannel() { - if (mIsPassthrough) { - return Channel.createPassthroughChannel(mInputId); - } else { - return mChannelTuner.findNearestBrowsableChannel( - Utils.getLastWatchedChannelId(mContext)); - } - } - - /** - * Gets a label of the input. - */ - public String getLabel() { - if (mIsPassthrough) { - return mInputInfo.loadLabel(mContext).toString(); - } else { - return mContext.getString(R.string.input_selector_tuner_label); - } - } - - /** - * Gets a long label including a customized label. - */ - public String getLongLabel() { - if (mIsPassthrough) { - String customizedLabel = Utils.loadLabel(mContext, mInputInfo); - String label = getLabel(); - if (label.equals(customizedLabel)) { - return customizedLabel; - } - return customizedLabel + " (" + label + ")"; - } else { - return mContext.getString(R.string.input_long_label_for_tuner); - } - } - - /** - * Updates availability. It returns true, if availability is changed. - */ - private void updateAvailability() { - boolean available; - // current playing input cannot be available for PIP. - Channel currentChannel = mChannelTuner.getCurrentChannel(); - if (mIsPassthrough) { - if (currentChannel != null && currentChannel.getInputId().equals(mInputId)) { - available = false; - } else { - available = mInputManager.getInputState(mInputId) - == TvInputManager.INPUT_STATE_CONNECTED; - } - } else { - if (currentChannel != null && !currentChannel.isPassthrough()) { - available = false; - } else { - available = mChannelTuner.getBrowsableChannelCount() > 0; - } - } - if (mAvailable != available) { - mAvailable = available; - for (Listener l : mListeners) { - l.onPipInputStateUpdated(); - } - } - } - - private boolean isHidden() { - // mInputInfo is null for the tuner input and it's always visible. - return mInputInfo != null && mInputInfo.isHidden(mContext); - } - } -} diff --git a/src/com/android/tv/util/RecurringRunner.java b/src/com/android/tv/util/RecurringRunner.java index 4135bd4e..8b45131b 100644 --- a/src/com/android/tv/util/RecurringRunner.java +++ b/src/com/android/tv/util/RecurringRunner.java @@ -57,12 +57,15 @@ public final class RecurringRunner { mHandler = new Handler(mContext.getMainLooper()); } - public void start() { + public void start(boolean resetNextRunTime) { SoftPreconditions.checkState(!mRunning, TAG, mName + " start is called twice."); if (mRunning) { return; } mRunning = true; + if (resetNextRunTime) { + resetNextRunTime(); + } new AsyncTask() { @Override protected Long doInBackground(Void... params) { @@ -76,6 +79,10 @@ public final class RecurringRunner { }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } + public void start() { + start(false); + } + public void stop() { mRunning = false; mHandler.removeCallbacksAndMessages(null); diff --git a/src/com/android/tv/util/SearchManagerHelper.java b/src/com/android/tv/util/SearchManagerHelper.java deleted file mode 100644 index b6e34d7a..00000000 --- a/src/com/android/tv/util/SearchManagerHelper.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.util; - -import android.app.SearchManager; -import android.content.Context; -import android.os.Bundle; -import android.os.UserHandle; -import android.util.Log; - -import java.lang.reflect.InvocationTargetException; - -/** - * A convenience class for calling methods in android.app.SearchManager. - */ -public final class SearchManagerHelper { - private static final String TAG = "SearchManagerHelper"; - - private static final Object sLock = new Object(); - private static SearchManagerHelper sInstance; - - private final SearchManager mSearchManager; - - private SearchManagerHelper(Context context) { - mSearchManager = ((android.app.SearchManager) context.getSystemService( - Context.SEARCH_SERVICE)); - } - - public static SearchManagerHelper getInstance(Context context) { - synchronized (sLock) { - if (sInstance == null) { - sInstance = new SearchManagerHelper(context.getApplicationContext()); - } - return sInstance; - } - } - - public void launchAssistAction() { - try { - SearchManager.class.getDeclaredMethod("launchLegacyAssist", String.class, Integer.TYPE, - Bundle.class).invoke(mSearchManager, null, UserHandle.myUserId(), null); - } catch (NoSuchMethodException | IllegalArgumentException | IllegalAccessException - | InvocationTargetException e) { - Log.e(TAG, "Fail to call SearchManager.launchAssistAction", e); - } - } -} diff --git a/src/com/android/tv/util/SetupUtils.java b/src/com/android/tv/util/SetupUtils.java index 8223a81c..32e3a81f 100644 --- a/src/com/android/tv/util/SetupUtils.java +++ b/src/com/android/tv/util/SetupUtils.java @@ -37,8 +37,6 @@ import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; -import com.android.tv.data.epg.EpgFetcher; -import com.android.tv.experiments.Experiments; import com.android.tv.tuner.tvinput.TunerTvInputService; import java.util.Collections; @@ -114,7 +112,7 @@ public class SetupUtils { @Override public void onLoadFinished() { manager.removeListener(this); - updateChannelBrowsable(mTvApplication, inputId, postRunnable); + updateChannelsAfterSetup(mTvApplication, inputId, postRunnable); } @Override @@ -124,17 +122,18 @@ public class SetupUtils { public void onChannelBrowsableChanged() { } }); } else { - updateChannelBrowsable(mTvApplication, inputId, postRunnable); + updateChannelsAfterSetup(mTvApplication, inputId, postRunnable); } } - private static void updateChannelBrowsable(Context context, final String inputId, + private static void updateChannelsAfterSetup(Context context, final String inputId, final Runnable postRunnable) { ApplicationSingletons appSingletons = TvApplication.getSingletons(context); final ChannelDataManager manager = appSingletons.getChannelDataManager(); manager.updateChannels(new Runnable() { @Override public void run() { + Channel firstChannelForInput = null; boolean browsableChanged = false; for (Channel channel : manager.getChannelList()) { if (channel.getInputId().equals(inputId)) { @@ -142,8 +141,14 @@ public class SetupUtils { manager.updateBrowsable(channel.getId(), true, true); browsableChanged = true; } + if (firstChannelForInput == null) { + firstChannelForInput = channel; + } } } + if (firstChannelForInput != null) { + Utils.setLastWatchedChannel(context, firstChannelForInput); + } if (browsableChanged) { manager.notifyChannelBrowsableChanged(); manager.applyUpdatedValuesToDb(); @@ -382,13 +387,5 @@ public class SetupUtils { mSetUpInputs.add(inputId); mSharedPreferences.edit().putStringSet(PREF_KEY_SET_UP_INPUTS, mSetUpInputs).apply(); } - // Start fetching program guide data for internal tuners. - Context context = mTvApplication.getApplicationContext(); - if (Utils.isInternalTvInput(context, inputId)) { - if (context.checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION) - == PackageManager.PERMISSION_GRANTED && Experiments.CLOUD_EPG.get()) { - EpgFetcher.getInstance(context).startImmediately(); - } - } } -} +} \ No newline at end of file diff --git a/src/com/android/tv/util/StringUtils.java b/src/com/android/tv/util/StringUtils.java new file mode 100644 index 00000000..659807e2 --- /dev/null +++ b/src/com/android/tv/util/StringUtils.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.util; + +/** + * Utility class for handling {@link String}. + */ +public final class StringUtils { + + private StringUtils() { } + + /** + * Returns compares two strings lexicographically and handles null values quietly. + */ + public static int compare(String a, String b) { + if (a == null) { + return b == null ? 0 : -1; + } + if (b == null) { + return 1; + } + return a.compareTo(b); + } +} diff --git a/src/com/android/tv/util/TimeShiftUtils.java b/src/com/android/tv/util/TimeShiftUtils.java index 238d0e74..8038a78f 100644 --- a/src/com/android/tv/util/TimeShiftUtils.java +++ b/src/com/android/tv/util/TimeShiftUtils.java @@ -18,7 +18,6 @@ package com.android.tv.util; import java.util.concurrent.TimeUnit; -// TODO: move related functions in TimeShiftManger here. /** * A class that includes convenience methods for time shift plays. */ @@ -40,7 +39,7 @@ public class TimeShiftUtils { * Returns real speeds used in time shift play. This method is only for fast-forwarding and * rewinding. The normal play speed is not addressed here. * - * @param speedLevel the valid value is ranged from 0 to {@link MAX_SPPED_LEVEL}. + * @param speedLevel the valid value is ranged from 0 to {@link #MAX_SPEED_LEVEL}. * @param programDurationMillis the length of program under playing. * @throws IndexOutOfBoundsException if speed level is out of its range. */ @@ -60,4 +59,3 @@ public class TimeShiftUtils { : SHORT_PROGRAM_SPEED_FACTORS[MAX_SPEED_LEVEL]; } } - diff --git a/src/com/android/tv/util/TvInputManagerHelper.java b/src/com/android/tv/util/TvInputManagerHelper.java index 121f56ed..730a985b 100644 --- a/src/com/android/tv/util/TvInputManagerHelper.java +++ b/src/com/android/tv/util/TvInputManagerHelper.java @@ -18,20 +18,26 @@ package com.android.tv.util; import android.content.Context; import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.hardware.hdmi.HdmiDeviceInfo; import android.media.tv.TvInputInfo; import android.media.tv.TvInputManager; import android.media.tv.TvInputManager.TvInputCallback; import android.os.Handler; import android.support.annotation.VisibleForTesting; import android.text.TextUtils; +import android.util.ArrayMap; import android.util.Log; import com.android.tv.Features; import com.android.tv.common.SoftPreconditions; +import com.android.tv.common.TvCommonUtils; import com.android.tv.parental.ContentRatingsManager; import com.android.tv.parental.ParentalControlSettings; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; @@ -42,14 +48,64 @@ import java.util.Map; public class TvInputManagerHelper { private static final String TAG = "TvInputManagerHelper"; private static final boolean DEBUG = false; + + /** + * Types of HDMI device and bundled tuner. + */ + public static final int TYPE_CEC_DEVICE = -2; + public static final int TYPE_BUNDLED_TUNER = -3; + public static final int TYPE_CEC_DEVICE_RECORDER = -4; + public static final int TYPE_CEC_DEVICE_PLAYBACK = -5; + public static final int TYPE_MHL_MOBILE = -6; + + private static final String PERMISSION_ACCESS_ALL_EPG_DATA = + "com.android.providers.tv.permission.ACCESS_ALL_EPG_DATA"; + private static final String [] mPhysicalTunerBlackList = { + }; + private static final String META_LABEL_SORT_KEY = "input_sort_key"; + + /** + * The default tv input priority to show. + */ + private static final ArrayList DEFAULT_TV_INPUT_PRIORITY = new ArrayList<>(); + static { + DEFAULT_TV_INPUT_PRIORITY.add(TYPE_BUNDLED_TUNER); + DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_TUNER); + DEFAULT_TV_INPUT_PRIORITY.add(TYPE_CEC_DEVICE); + DEFAULT_TV_INPUT_PRIORITY.add(TYPE_CEC_DEVICE_RECORDER); + DEFAULT_TV_INPUT_PRIORITY.add(TYPE_CEC_DEVICE_PLAYBACK); + DEFAULT_TV_INPUT_PRIORITY.add(TYPE_MHL_MOBILE); + DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_HDMI); + DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_DVI); + DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_COMPONENT); + DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_SVIDEO); + DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_COMPOSITE); + DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_DISPLAY_PORT); + DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_VGA); + DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_SCART); + DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_OTHER); + } + private static final String[] PARTNER_TUNER_INPUT_PREFIX_BLACKLIST = { }; + private static final String[] TESTABLE_INPUTS = { + "com.android.tv.testinput/.TestTvInputService" + }; + private final Context mContext; + private final PackageManager mPackageManager; private final TvInputManager mTvInputManager; private final Map mInputStateMap = new HashMap<>(); private final Map mInputMap = new HashMap<>(); + private final Map mTvInputLabels = new ArrayMap<>(); + private final Map mTvInputCustomLabels = new ArrayMap<>(); private final Map mInputIdToPartnerInputMap = new HashMap<>(); + + private final Map mTvInputApplicationLabels = new ArrayMap<>(); + private final Map mTvInputApplicationIcons = new ArrayMap<>(); + private final Map mTvInputAppliactionBanners = new ArrayMap<>(); + private final TvInputCallback mInternalCallback = new TvInputCallback() { @Override public void onInputStateChanged(String inputId, int state) { @@ -72,6 +128,11 @@ public class TvInputManagerHelper { TvInputInfo info = mTvInputManager.getTvInputInfo(inputId); if (info != null) { mInputMap.put(inputId, info); + mTvInputLabels.put(inputId, info.loadLabel(mContext).toString()); + CharSequence inputCustomLabel = info.loadCustomLabel(mContext); + if (inputCustomLabel != null) { + mTvInputCustomLabels.put(inputId, inputCustomLabel.toString()); + } mInputStateMap.put(inputId, mTvInputManager.getInputState(inputId)); mInputIdToPartnerInputMap.put(inputId, isPartnerInput(info)); } @@ -85,6 +146,11 @@ public class TvInputManagerHelper { public void onInputRemoved(String inputId) { if (DEBUG) Log.d(TAG, "onInputRemoved " + inputId); mInputMap.remove(inputId); + mTvInputLabels.remove(inputId); + mTvInputCustomLabels.remove(inputId); + mTvInputApplicationLabels.remove(inputId); + mTvInputApplicationIcons.remove(inputId); + mTvInputAppliactionBanners.remove(inputId); mInputStateMap.remove(inputId); mInputIdToPartnerInputMap.remove(inputId); mContentRatingsManager.update(); @@ -103,6 +169,14 @@ public class TvInputManagerHelper { } TvInputInfo info = mTvInputManager.getTvInputInfo(inputId); mInputMap.put(inputId, info); + mTvInputLabels.put(inputId, info.loadLabel(mContext).toString()); + CharSequence inputCustomLabel = info.loadCustomLabel(mContext); + if (inputCustomLabel != null) { + mTvInputCustomLabels.put(inputId, inputCustomLabel.toString()); + } + mTvInputApplicationLabels.remove(inputId); + mTvInputApplicationIcons.remove(inputId); + mTvInputAppliactionBanners.remove(inputId); for (TvInputCallback callback : mCallbacks) { callback.onInputUpdated(inputId); } @@ -114,6 +188,11 @@ public class TvInputManagerHelper { public void onTvInputInfoUpdated(TvInputInfo inputInfo) { if (DEBUG) Log.d(TAG, "onTvInputInfoUpdated " + inputInfo); mInputMap.put(inputInfo.getId(), inputInfo); + mTvInputLabels.put(inputInfo.getId(), inputInfo.loadLabel(mContext).toString()); + CharSequence inputCustomLabel = inputInfo.loadCustomLabel(mContext); + if (inputCustomLabel != null) { + mTvInputCustomLabels.put(inputInfo.getId(), inputCustomLabel.toString()); + } for (TvInputCallback callback : mCallbacks) { callback.onTvInputInfoUpdated(inputInfo); } @@ -131,13 +210,18 @@ public class TvInputManagerHelper { public TvInputManagerHelper(Context context) { mContext = context.getApplicationContext(); + mPackageManager = context.getPackageManager(); mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE); mContentRatingsManager = new ContentRatingsManager(context); mParentalControlSettings = new ParentalControlSettings(context); - mTvInputInfoComparator = new TvInputInfoComparator(this); + mTvInputInfoComparator = new InputComparatorInternal(this); } public void start() { + if (!hasTvInputManager()) { + // Not a TV device + return; + } if (mStarted) { return; } @@ -145,6 +229,11 @@ public class TvInputManagerHelper { mStarted = true; mTvInputManager.registerCallback(mInternalCallback, mHandler); mInputMap.clear(); + mTvInputLabels.clear(); + mTvInputCustomLabels.clear(); + mTvInputApplicationLabels.clear(); + mTvInputApplicationIcons.clear(); + mTvInputAppliactionBanners.clear(); mInputStateMap.clear(); mInputIdToPartnerInputMap.clear(); for (TvInputInfo input : mTvInputManager.getTvInputList()) { @@ -171,9 +260,23 @@ public class TvInputManagerHelper { mStarted = false; mInputStateMap.clear(); mInputMap.clear(); + mTvInputLabels.clear(); + mTvInputCustomLabels.clear(); + mTvInputApplicationLabels.clear(); + mTvInputApplicationIcons.clear(); + mTvInputAppliactionBanners.clear();; mInputIdToPartnerInputMap.clear(); } + /** + * Clears the TvInput labels map. + */ + public void clearTvInputLabels() { + mTvInputLabels.clear(); + mTvInputCustomLabels.clear(); + mTvInputApplicationLabels.clear(); + } + public List getTvInputInfos(boolean availableOnly, boolean tunerOnly) { ArrayList list = new ArrayList<>(); for (Map.Entry pair : mInputStateMap.entrySet()) { @@ -192,7 +295,7 @@ public class TvInputManagerHelper { /** * Returns the default comparator for {@link TvInputInfo}. - * See {@link TvInputInfoComparator} for detail. + * See {@link InputComparatorInternal} for detail. */ public Comparator getDefaultTvInputInfoComparator() { return mTvInputInfoComparator; @@ -237,15 +340,81 @@ public class TvInputManagerHelper { } /** - * Loads label of {@code info}. + * Is (Context.TV_INPUT_SERVICE) available. * - * It's visible for comparator test to mock TvInputInfo. - * Package private is enough for this method, but public is necessary to workaround mockito - * bug. + *

This is only available on TV devices. + */ + public boolean hasTvInputManager() { + return mTvInputManager != null; + } + + /** + * Loads label of {@code info}. */ - @VisibleForTesting public String loadLabel(TvInputInfo info) { - return info.loadLabel(mContext).toString(); + String label = mTvInputLabels.get(info.getId()); + if (label == null) { + label = info.loadLabel(mContext).toString(); + mTvInputLabels.put(info.getId(), label); + } + return label; + } + + /** + * Loads custom label of {@code info} + */ + public String loadCustomLabel(TvInputInfo info) { + String customLabel = mTvInputCustomLabels.get(info.getId()); + if (customLabel == null) { + CharSequence customLabelCharSequence = info.loadCustomLabel(mContext); + if (customLabelCharSequence != null) { + customLabel = customLabelCharSequence.toString(); + mTvInputCustomLabels.put(info.getId(), customLabel); + } + } + return customLabel; + } + + /** + * Gets the tv input application's label. + */ + public CharSequence getTvInputApplicationLabel(CharSequence inputId) { + return mTvInputApplicationLabels.get(inputId); + } + + /** + * Stores the tv input application's label. + */ + public void setTvInputApplicationLabel(String inputId, CharSequence label) { + mTvInputApplicationLabels.put(inputId, label); + } + + /** + * Gets the tv input application's icon. + */ + public Drawable getTvInputApplicationIcon(String inputId) { + return mTvInputApplicationIcons.get(inputId); + } + + /** + * Stores the tv input application's icon. + */ + public void setTvInputApplicationIcon(String inputId, Drawable icon) { + mTvInputApplicationIcons.put(inputId, icon); + } + + /** + * Gets the tv input application's banner. + */ + public Drawable getTvInputApplicationBanner(String inputId) { + return mTvInputAppliactionBanners.get(inputId); + } + + /** + * Stores the tv input application's banner. + */ + public void setTvInputApplicationBanner(String inputId, Drawable banner) { + mTvInputAppliactionBanners.put(inputId, banner); } /** @@ -321,14 +490,54 @@ public class TvInputManagerHelper { return mContentRatingsManager; } - private boolean isInBlackList(String inputId) { - if (!Features.USE_PARTNER_INPUT_BLACKLIST.isEnabled(mContext)) { + private int getInputSortKey(TvInputInfo input) { + return input.getServiceInfo().metaData.getInt(META_LABEL_SORT_KEY, + Integer.MAX_VALUE); + } + + private boolean isInputPhysicalTuner(TvInputInfo input) { + String packageName = input.getServiceInfo().packageName; + if (Arrays.asList(mPhysicalTunerBlackList).contains(packageName)) { return false; } - for (String disabledTunerInputPrefix : PARTNER_TUNER_INPUT_PREFIX_BLACKLIST) { - if (inputId.contains(disabledTunerInputPrefix)) { - return true; + + if (input.createSetupIntent() == null) { + return false; + } else { + boolean mayBeTunerInput = mPackageManager.checkPermission( + PERMISSION_ACCESS_ALL_EPG_DATA, input.getServiceInfo().packageName) + == PackageManager.PERMISSION_GRANTED; + if (!mayBeTunerInput) { + try { + ApplicationInfo ai = mPackageManager.getApplicationInfo( + input.getServiceInfo().packageName, 0); + if ((ai.flags & (ApplicationInfo.FLAG_SYSTEM + | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)) == 0) { + return false; + } + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + } + return true; + } + + private boolean isInBlackList(String inputId) { + if (Features.USE_PARTNER_INPUT_BLACKLIST.isEnabled(mContext)) { + for (String disabledTunerInputPrefix : PARTNER_TUNER_INPUT_PREFIX_BLACKLIST) { + if (inputId.contains(disabledTunerInputPrefix)) { + return true; + } + } + } + if (TvCommonUtils.isRunningInTest()) { + for (String testableInput : TESTABLE_INPUTS) { + if (testableInput.equals(inputId)) { + return false; + } } + return true; } return false; } @@ -342,10 +551,10 @@ public class TvInputManagerHelper { * (i.e. Mockito's spy doesn't work) */ @VisibleForTesting - static class TvInputInfoComparator implements Comparator { + static class InputComparatorInternal implements Comparator { private final TvInputManagerHelper mInputManager; - public TvInputInfoComparator(TvInputManagerHelper inputManager) { + public InputComparatorInternal(TvInputManagerHelper inputManager) { mInputManager = inputManager; } @@ -357,4 +566,123 @@ public class TvInputManagerHelper { return mInputManager.loadLabel(lhs).compareTo(mInputManager.loadLabel(rhs)); } } + + /** + * A comparator used for {@link com.android.tv.ui.SelectInputView} to show the list of + * TV inputs. + */ + public static class HardwareInputComparator implements Comparator { + private Map mTypePriorities = new HashMap<>(); + private final TvInputManagerHelper mTvInputManagerHelper; + private final Context mContext; + + public HardwareInputComparator(Context context, TvInputManagerHelper tvInputManagerHelper) { + mContext = context; + mTvInputManagerHelper = tvInputManagerHelper; + setupDeviceTypePriorities(); + } + + @Override + public int compare(TvInputInfo lhs, TvInputInfo rhs) { + if (lhs == null) { + return (rhs == null) ? 0 : 1; + } + if (rhs == null) { + return -1; + } + + boolean enabledL = (mTvInputManagerHelper.getInputState(lhs) + != TvInputManager.INPUT_STATE_DISCONNECTED); + boolean enabledR = (mTvInputManagerHelper.getInputState(rhs) + != TvInputManager.INPUT_STATE_DISCONNECTED); + if (enabledL != enabledR) { + return enabledL ? -1 : 1; + } + + int priorityL = getPriority(lhs); + int priorityR = getPriority(rhs); + if (priorityL != priorityR) { + return priorityL - priorityR; + } + + if (lhs.getType() == TvInputInfo.TYPE_TUNER + && rhs.getType() == TvInputInfo.TYPE_TUNER) { + boolean isPhysicalL = mTvInputManagerHelper.isInputPhysicalTuner(lhs); + boolean isPhysicalR = mTvInputManagerHelper.isInputPhysicalTuner(rhs); + if (isPhysicalL != isPhysicalR) { + return isPhysicalL ? -1 : 1; + } + } + + int sortKeyL = mTvInputManagerHelper.getInputSortKey(lhs); + int sortKeyR = mTvInputManagerHelper.getInputSortKey(rhs); + if (sortKeyL != sortKeyR) { + return sortKeyR - sortKeyL; + } + + String parentLabelL = lhs.getParentId() != null + ? getLabel(mTvInputManagerHelper.getTvInputInfo(lhs.getParentId())) + : getLabel(mTvInputManagerHelper.getTvInputInfo(lhs.getId())); + String parentLabelR = rhs.getParentId() != null + ? getLabel(mTvInputManagerHelper.getTvInputInfo(rhs.getParentId())) + : getLabel(mTvInputManagerHelper.getTvInputInfo(rhs.getId())); + + if (!TextUtils.equals(parentLabelL, parentLabelR)) { + return parentLabelL.compareToIgnoreCase(parentLabelR); + } + return getLabel(lhs).compareToIgnoreCase(getLabel(rhs)); + } + + private String getLabel(TvInputInfo input) { + if (input == null) { + return ""; + } + String label = mTvInputManagerHelper.loadCustomLabel(input); + if (TextUtils.isEmpty(label)) { + label = mTvInputManagerHelper.loadLabel(input); + } + return label; + } + + private int getPriority(TvInputInfo info) { + Integer priority = null; + if (mTypePriorities != null) { + priority = mTypePriorities.get(getTvInputTypeForPriority(info)); + } + if (priority != null) { + return priority; + } + return Integer.MAX_VALUE; + } + + private void setupDeviceTypePriorities() { + mTypePriorities = Partner.getInstance(mContext).getInputsOrderMap(); + + // Fill in any missing priorities in the map we got from the OEM + int priority = mTypePriorities.size(); + for (int type : DEFAULT_TV_INPUT_PRIORITY) { + if (!mTypePriorities.containsKey(type)) { + mTypePriorities.put(type, priority++); + } + } + } + + private int getTvInputTypeForPriority(TvInputInfo info) { + if (info.getHdmiDeviceInfo() != null) { + if (info.getHdmiDeviceInfo().isCecDevice()) { + switch (info.getHdmiDeviceInfo().getDeviceType()) { + case HdmiDeviceInfo.DEVICE_RECORDER: + return TYPE_CEC_DEVICE_RECORDER; + case HdmiDeviceInfo.DEVICE_PLAYBACK: + return TYPE_CEC_DEVICE_PLAYBACK; + default: + return TYPE_CEC_DEVICE; + } + } else if (info.getHdmiDeviceInfo().isMhlDevice()) { + return TYPE_MHL_MOBILE; + } + } + return info.getType(); + } + } } diff --git a/src/com/android/tv/util/TvProviderUriMatcher.java b/src/com/android/tv/util/TvProviderUriMatcher.java deleted file mode 100644 index 749e4aa3..00000000 --- a/src/com/android/tv/util/TvProviderUriMatcher.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.util; - -import android.content.UriMatcher; -import android.media.tv.TvContract; -import android.net.Uri; -import android.support.annotation.IntDef; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -/** - * Utility class to aid in matching URIs in TvProvider. - */ -public class TvProviderUriMatcher { - private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); - - @Retention(RetentionPolicy.SOURCE) - @IntDef({MATCH_CHANNEL, MATCH_CHANNEL_ID, MATCH_PROGRAM, MATCH_PROGRAM_ID, - MATCH_RECORDED_PROGRAM, MATCH_RECORDED_PROGRAM_ID, MATCH_WATCHED_PROGRAM_ID}) - private @interface TvProviderUriMatchCode {} - /** The code for the channels URI. */ - public static final int MATCH_CHANNEL = 1; - /** The code for the channel URI. */ - public static final int MATCH_CHANNEL_ID = 2; - /** The code for the programs URI. */ - public static final int MATCH_PROGRAM = 3; - /** The code for the program URI. */ - public static final int MATCH_PROGRAM_ID = 4; - /** The code for the recorded programs URI. */ - public static final int MATCH_RECORDED_PROGRAM = 5; - /** The code for the recorded program URI. */ - public static final int MATCH_RECORDED_PROGRAM_ID = 6; - /** The code for the watched program URI. */ - public static final int MATCH_WATCHED_PROGRAM_ID = 7; - static { - URI_MATCHER.addURI(TvContract.AUTHORITY, "channel", MATCH_CHANNEL); - URI_MATCHER.addURI(TvContract.AUTHORITY, "channel/#", MATCH_CHANNEL_ID); - URI_MATCHER.addURI(TvContract.AUTHORITY, "program", MATCH_PROGRAM); - URI_MATCHER.addURI(TvContract.AUTHORITY, "program/#", MATCH_PROGRAM_ID); - URI_MATCHER.addURI(TvContract.AUTHORITY, "recorded_program", MATCH_RECORDED_PROGRAM); - URI_MATCHER.addURI(TvContract.AUTHORITY, "recorded_program/#", MATCH_RECORDED_PROGRAM_ID); - URI_MATCHER.addURI(TvContract.AUTHORITY, "watched_program/#", MATCH_WATCHED_PROGRAM_ID); - } - - private TvProviderUriMatcher() { } - - /** - * Try to match against the path in a url. - * - * @see UriMatcher#match - */ - @SuppressWarnings("WrongConstant") - @TvProviderUriMatchCode public static int match(Uri uri) { - return URI_MATCHER.match(uri); - } -} diff --git a/src/com/android/tv/util/TvSettings.java b/src/com/android/tv/util/TvSettings.java index 97ff59d6..c5fde317 100644 --- a/src/com/android/tv/util/TvSettings.java +++ b/src/com/android/tv/util/TvSettings.java @@ -17,6 +17,8 @@ package com.android.tv.util; import android.content.Context; +import android.content.SharedPreferences; +import android.media.tv.TvTrackInfo; import android.preference.PreferenceManager; import android.support.annotation.IntDef; @@ -26,53 +28,27 @@ import java.util.Collections; import java.util.HashSet; import java.util.Set; - /** * A class about the constants for TV settings. * Objects that are returned from the various {@code get} methods must be treated as immutable. */ public final class TvSettings { - private TvSettings() {} - public static final String PREF_DISPLAY_MODE = "display_mode"; // int value - public static final String PREF_PIP_LAYOUT = "pip_layout"; // int value - public static final String PREF_PIP_SIZE = "pip_size"; // int value public static final String PREF_PIN = "pin"; // 4-digit string value. Otherwise, it's not set. - // PIP sounds - @Retention(RetentionPolicy.SOURCE) - @IntDef({ - PIP_SOUND_MAIN, PIP_SOUND_PIP_WINDOW }) - public @interface PipSound {} - public static final int PIP_SOUND_MAIN = 0; - public static final int PIP_SOUND_PIP_WINDOW = PIP_SOUND_MAIN + 1; - - // PIP layouts - @Retention(RetentionPolicy.SOURCE) - @IntDef({ - PIP_LAYOUT_BOTTOM_RIGHT, PIP_LAYOUT_TOP_RIGHT, PIP_LAYOUT_TOP_LEFT, - PIP_LAYOUT_BOTTOM_LEFT, PIP_LAYOUT_SIDE_BY_SIDE }) - public @interface PipLayout {} - public static final int PIP_LAYOUT_BOTTOM_RIGHT = 0; - public static final int PIP_LAYOUT_TOP_RIGHT = PIP_LAYOUT_BOTTOM_RIGHT + 1; - public static final int PIP_LAYOUT_TOP_LEFT = PIP_LAYOUT_TOP_RIGHT + 1; - public static final int PIP_LAYOUT_BOTTOM_LEFT = PIP_LAYOUT_TOP_LEFT + 1; - public static final int PIP_LAYOUT_SIDE_BY_SIDE = PIP_LAYOUT_BOTTOM_LEFT + 1; - public static final int PIP_LAYOUT_LAST = PIP_LAYOUT_SIDE_BY_SIDE; - - // PIP sizes - @Retention(RetentionPolicy.SOURCE) - @IntDef({ PIP_SIZE_SMALL, PIP_SIZE_BIG }) - public @interface PipSize {} - public static final int PIP_SIZE_SMALL = 0; - public static final int PIP_SIZE_BIG = PIP_SIZE_SMALL + 1; - public static final int PIP_SIZE_LAST = PIP_SIZE_BIG; - // Multi-track audio settings private static final String PREF_MULTI_AUDIO_ID = "pref.multi_audio_id"; private static final String PREF_MULTI_AUDIO_LANGUAGE = "pref.multi_audio_language"; private static final String PREF_MULTI_AUDIO_CHANNEL_COUNT = "pref.multi_audio_channel_count"; + // DVR Multi-audio and subtitle settings + private static final String PREF_DVR_MULTI_AUDIO_ID = "pref.dvr_multi_audio_id"; + private static final String PREF_DVR_MULTI_AUDIO_LANGUAGE = "pref.dvr_multi_audio_language"; + private static final String PREF_DVR_MULTI_AUDIO_CHANNEL_COUNT = + "pref.dvr_multi_audio_channel_count"; + private static final String PREF_DVR_SUBTITLE_ID = "pref.dvr_subtitle_id"; + private static final String PREF_DVR_SUBTITLE_LANGUAGE = "pref.dvr_subtitle_language"; + // Parental Control settings private static final String PREF_CONTENT_RATING_SYSTEMS = "pref.content_rating_systems"; private static final String PREF_CONTENT_RATING_LEVEL = "pref.content_rating_level"; @@ -89,58 +65,7 @@ public final class TvSettings { public static final int CONTENT_RATING_LEVEL_LOW = 3; public static final int CONTENT_RATING_LEVEL_CUSTOM = 4; - // PIP settings - /** - * Returns the layout of the PIP window stored in the shared preferences. - * - * @return the saved layout of the PIP window. This value is one of - * {@link #PIP_LAYOUT_TOP_LEFT}, {@link #PIP_LAYOUT_TOP_RIGHT}, - * {@link #PIP_LAYOUT_BOTTOM_LEFT}, {@link #PIP_LAYOUT_BOTTOM_RIGHT} and - * {@link #PIP_LAYOUT_SIDE_BY_SIDE}. If the preference value does not exist, - * {@link #PIP_LAYOUT_BOTTOM_RIGHT} is returned. - */ - @SuppressWarnings("ResourceType") - @PipLayout - public static int getPipLayout(Context context) { - return PreferenceManager.getDefaultSharedPreferences(context).getInt( - PREF_PIP_LAYOUT, PIP_LAYOUT_BOTTOM_RIGHT); - } - - /** - * Stores the layout of PIP window to the shared preferences. - * - * @param pipLayout This value should be one of {@link #PIP_LAYOUT_TOP_LEFT}, - * {@link #PIP_LAYOUT_TOP_RIGHT}, {@link #PIP_LAYOUT_BOTTOM_LEFT}, - * {@link #PIP_LAYOUT_BOTTOM_RIGHT} and {@link #PIP_LAYOUT_SIDE_BY_SIDE}. - */ - public static void setPipLayout(Context context, @PipLayout int pipLayout) { - PreferenceManager.getDefaultSharedPreferences(context).edit().putInt( - PREF_PIP_LAYOUT, pipLayout).apply(); - } - - /** - * Returns the size of the PIP view stored in the shared preferences. - * - * @return the saved size of the PIP view. This value is one of - * {@link #PIP_SIZE_SMALL} and {@link #PIP_SIZE_BIG}. If the preference value does not - * exist, {@link #PIP_SIZE_SMALL} is returned. - */ - @SuppressWarnings("ResourceType") - @PipSize - public static int getPipSize(Context context) { - return PreferenceManager.getDefaultSharedPreferences(context).getInt( - PREF_PIP_SIZE, PIP_SIZE_SMALL); - } - - /** - * Stores the size of PIP view to the shared preferences. - * - * @param pipSize This value should be one of {@link #PIP_SIZE_SMALL} and {@link #PIP_SIZE_BIG}. - */ - public static void setPipSize(Context context, @PipSize int pipSize) { - PreferenceManager.getDefaultSharedPreferences(context).edit().putInt( - PREF_PIP_SIZE, pipSize).apply(); - } + private TvSettings() {} // Multi-track audio settings public static String getMultiAudioId(Context context) { @@ -173,26 +98,61 @@ public final class TvSettings { PREF_MULTI_AUDIO_CHANNEL_COUNT, channelCount).apply(); } - // Parental Control settings - public static void addContentRatingSystems(Context context, Set ids) { - Set contentRatingSystemSet = getContentRatingSystemSet(context); - if (contentRatingSystemSet.addAll(ids)) { - PreferenceManager.getDefaultSharedPreferences(context).edit() - .putStringSet(PREF_CONTENT_RATING_SYSTEMS, contentRatingSystemSet).apply(); + public static void setDvrPlaybackTrackSettings(Context context, int trackType, + TvTrackInfo info) { + if (trackType == TvTrackInfo.TYPE_AUDIO) { + if (info == null) { + PreferenceManager.getDefaultSharedPreferences(context).edit() + .remove(PREF_DVR_MULTI_AUDIO_ID).apply(); + } else { + PreferenceManager.getDefaultSharedPreferences(context).edit() + .putString(PREF_DVR_MULTI_AUDIO_LANGUAGE, info.getLanguage()) + .putInt(PREF_DVR_MULTI_AUDIO_CHANNEL_COUNT, info.getAudioChannelCount()) + .putString(PREF_DVR_MULTI_AUDIO_ID, info.getId()).apply(); + } + } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { + if (info == null) { + PreferenceManager.getDefaultSharedPreferences(context).edit() + .remove(PREF_DVR_SUBTITLE_ID).apply(); + } else { + PreferenceManager.getDefaultSharedPreferences(context).edit() + .putString(PREF_DVR_SUBTITLE_LANGUAGE, info.getLanguage()) + .putString(PREF_DVR_SUBTITLE_ID, info.getId()).apply(); + } } } - public static void addContentRatingSystem(Context context, String id) { - Set contentRatingSystemSet = getContentRatingSystemSet(context); - if (contentRatingSystemSet.add(id)) { - PreferenceManager.getDefaultSharedPreferences(context).edit() - .putStringSet(PREF_CONTENT_RATING_SYSTEMS, contentRatingSystemSet).apply(); + public static TvTrackInfo getDvrPlaybackTrackSettings(Context context, + int trackType) { + String language; + String trackId; + int channelCount; + SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context); + if (trackType == TvTrackInfo.TYPE_AUDIO) { + trackId = pref.getString(PREF_DVR_MULTI_AUDIO_ID, null); + if (trackId == null) { + return null; + } + language = pref.getString(PREF_DVR_MULTI_AUDIO_LANGUAGE, null); + channelCount = pref.getInt(PREF_DVR_MULTI_AUDIO_CHANNEL_COUNT, 0); + return new TvTrackInfo.Builder(trackType, trackId) + .setLanguage(language).setAudioChannelCount(channelCount).build(); + } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { + trackId = pref.getString(PREF_DVR_SUBTITLE_ID, null); + if (trackId == null) { + return null; + } + language = pref.getString(PREF_DVR_SUBTITLE_LANGUAGE, null); + return new TvTrackInfo.Builder(trackType, trackId).setLanguage(language).build(); + } else { + return null; } } - public static void removeContentRatingSystems(Context context, Set ids) { + // Parental Control settings + public static void addContentRatingSystem(Context context, String id) { Set contentRatingSystemSet = getContentRatingSystemSet(context); - if (contentRatingSystemSet.removeAll(ids)) { + if (contentRatingSystemSet.add(id)) { PreferenceManager.getDefaultSharedPreferences(context).edit() .putStringSet(PREF_CONTENT_RATING_SYSTEMS, contentRatingSystemSet).apply(); } @@ -254,4 +214,4 @@ public final class TvSettings { PreferenceManager.getDefaultSharedPreferences(context).edit().putLong( PREF_DISABLE_PIN_UNTIL, timeMillis).apply(); } -} +} \ No newline at end of file diff --git a/src/com/android/tv/util/TvTrackInfoUtils.java b/src/com/android/tv/util/TvTrackInfoUtils.java index c004f001..667cc9bf 100644 --- a/src/com/android/tv/util/TvTrackInfoUtils.java +++ b/src/com/android/tv/util/TvTrackInfoUtils.java @@ -52,35 +52,22 @@ public class TvTrackInfoUtils { } // Assumes {@code null} language matches to any language since it means user hasn't // selected any track before or selected a track without language information. - boolean rhsLangMatch = language == null || Utils.isEqualLanguage(rhs.getLanguage(), - language); boolean lhsLangMatch = language == null || Utils.isEqualLanguage(lhs.getLanguage(), language); - if (rhsLangMatch) { - if (lhsLangMatch) { - boolean rhsCountMatch = rhs.getAudioChannelCount() == channelCount; - boolean lhsCountMatch = lhs.getAudioChannelCount() == channelCount; - if (rhsCountMatch) { - if (lhsCountMatch) { - boolean rhsIdMatch = rhs.getId().equals(id); - boolean lhsIdMatch = lhs.getId().equals(id); - if (rhsIdMatch) { - return lhsIdMatch ? 0 : -1; - } else { - return lhsIdMatch ? 1 : 0; - } - - } else { - return -1; - } - } else { - return lhsCountMatch ? 1 : 0; - } + boolean rhsLangMatch = language == null || Utils.isEqualLanguage(rhs.getLanguage(), + language); + if (lhsLangMatch && rhsLangMatch) { + boolean lhsCountMatch = lhs.getType() != TvTrackInfo.TYPE_AUDIO + || lhs.getAudioChannelCount() == channelCount; + boolean rhsCountMatch = rhs.getType() != TvTrackInfo.TYPE_AUDIO + || rhs.getAudioChannelCount() == channelCount; + if (lhsCountMatch && rhsCountMatch) { + return Boolean.compare(lhs.getId().equals(id), rhs.getId().equals(id)); } else { - return -1; + return Boolean.compare(lhsCountMatch, rhsCountMatch); } } else { - return lhsLangMatch ? 1 : 0; + return Boolean.compare(lhsLangMatch, rhsLangMatch); } } }; @@ -112,4 +99,4 @@ public class TvTrackInfoUtils { private TvTrackInfoUtils() { } -} +} \ No newline at end of file diff --git a/src/com/android/tv/util/TvUriMatcher.java b/src/com/android/tv/util/TvUriMatcher.java new file mode 100644 index 00000000..3d91cdad --- /dev/null +++ b/src/com/android/tv/util/TvUriMatcher.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.util; + +import android.app.SearchManager; +import android.content.UriMatcher; +import android.media.tv.TvContract; +import android.net.Uri; +import android.support.annotation.IntDef; + +import com.android.tv.search.LocalSearchProvider; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Utility class to aid in matching URIs in TvProvider. + */ +public class TvUriMatcher { + private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); + + @Retention(RetentionPolicy.SOURCE) + @IntDef({MATCH_CHANNEL, MATCH_CHANNEL_ID, MATCH_PROGRAM, MATCH_PROGRAM_ID, + MATCH_RECORDED_PROGRAM, MATCH_RECORDED_PROGRAM_ID, MATCH_WATCHED_PROGRAM_ID, + MATCH_ON_DEVICE_SEARCH}) + private @interface TvProviderUriMatchCode {} + /** The code for the channels URI. */ + public static final int MATCH_CHANNEL = 1; + /** The code for the channel URI. */ + public static final int MATCH_CHANNEL_ID = 2; + /** The code for the programs URI. */ + public static final int MATCH_PROGRAM = 3; + /** The code for the program URI. */ + public static final int MATCH_PROGRAM_ID = 4; + /** The code for the recorded programs URI. */ + public static final int MATCH_RECORDED_PROGRAM = 5; + /** The code for the recorded program URI. */ + public static final int MATCH_RECORDED_PROGRAM_ID = 6; + /** The code for the watched program URI. */ + public static final int MATCH_WATCHED_PROGRAM_ID = 7; + /** The code for the on-device search URI. */ + public static final int MATCH_ON_DEVICE_SEARCH = 8; + static { + URI_MATCHER.addURI(TvContract.AUTHORITY, "channel", MATCH_CHANNEL); + URI_MATCHER.addURI(TvContract.AUTHORITY, "channel/#", MATCH_CHANNEL_ID); + URI_MATCHER.addURI(TvContract.AUTHORITY, "program", MATCH_PROGRAM); + URI_MATCHER.addURI(TvContract.AUTHORITY, "program/#", MATCH_PROGRAM_ID); + URI_MATCHER.addURI(TvContract.AUTHORITY, "recorded_program", MATCH_RECORDED_PROGRAM); + URI_MATCHER.addURI(TvContract.AUTHORITY, "recorded_program/#", MATCH_RECORDED_PROGRAM_ID); + URI_MATCHER.addURI(TvContract.AUTHORITY, "watched_program/#", MATCH_WATCHED_PROGRAM_ID); + URI_MATCHER.addURI(LocalSearchProvider.AUTHORITY, + SearchManager.SUGGEST_URI_PATH_QUERY + "/*", MATCH_ON_DEVICE_SEARCH); + } + + private TvUriMatcher() { } + + /** + * Try to match against the path in a url. + * + * @see UriMatcher#match + */ + @SuppressWarnings("WrongConstant") + @TvProviderUriMatchCode public static int match(Uri uri) { + return URI_MATCHER.match(uri); + } +} diff --git a/src/com/android/tv/util/Utils.java b/src/com/android/tv/util/Utils.java index 99d34431..d11bab3c 100644 --- a/src/com/android/tv/util/Utils.java +++ b/src/com/android/tv/util/Utils.java @@ -31,6 +31,7 @@ import android.media.tv.TvContract.Programs.Genres; import android.media.tv.TvInputInfo; import android.media.tv.TvTrackInfo; import android.net.Uri; +import android.os.Looper; import android.preference.PreferenceManager; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; @@ -44,11 +45,13 @@ import android.view.View; import com.android.tv.ApplicationSingletons; import com.android.tv.R; import com.android.tv.TvApplication; +import com.android.tv.common.BuildConfig; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; import com.android.tv.data.GenreItems; import com.android.tv.data.Program; import com.android.tv.data.StreamInfo; +import com.android.tv.experiments.Experiments; import java.io.File; import java.text.SimpleDateFormat; @@ -62,6 +65,8 @@ import java.util.List; import java.util.Locale; import java.util.Set; import java.util.TimeZone; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; /** @@ -74,7 +79,6 @@ public class Utils { private static final SimpleDateFormat ISO_8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US); - public static final String EXTRA_KEY_KEYCODE = "keycode"; public static final String EXTRA_KEY_ACTION = "action"; public static final String EXTRA_ACTION_SHOW_TV_INPUT ="show_tv_input"; public static final String EXTRA_KEY_FROM_LAUNCHER = "from_launcher"; @@ -83,11 +87,9 @@ public class Utils { public static final String EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED = "recorded_program_pin_checked"; - // Query parameter in the intent of starting MainActivity. - public static final String PARAM_SOURCE = "source"; - private static final String PATH_CHANNEL = "channel"; private static final String PATH_PROGRAM = "program"; + private static final String PATH_RECORDED_PROGRAM = "recorded_program"; private static final String PREF_KEY_LAST_WATCHED_CHANNEL_ID = "last_watched_channel_id"; private static final String PREF_KEY_LAST_WATCHED_CHANNEL_ID_FOR_INPUT = @@ -97,6 +99,8 @@ public class Utils { "last_watched_tuner_input_id"; private static final String PREF_KEY_RECORDING_FAILED_REASONS = "recording_failed_reasons"; + private static final String PREF_KEY_FAILED_SCHEDULED_RECORDING_INFO_SET = + "failed_scheduled_recording_info_set"; private static final int VIDEO_SD_WIDTH = 704; private static final int VIDEO_SD_HEIGHT = 480; @@ -114,6 +118,7 @@ public class Utils { private static final int AUDIO_CHANNEL_SURROUND_8 = 8; private static final long RECORDING_FAILED_REASON_NONE = 0; + private static final long HALF_MINUTE_MS = TimeUnit.SECONDS.toMillis(30); private static final long ONE_DAY_MS = TimeUnit.DAYS.toMillis(1); // Hardcoded list for known bundled inputs not written by OEM/SOCs. @@ -206,6 +211,28 @@ public class Utils { .apply(); } + /** + * Adds the info of failed scheduled recording. + */ + public static void addFailedScheduledRecordingInfo(Context context, + String scheduledRecordingInfo) { + Set failedScheduledRecordingInfoSet = getFailedScheduledRecordingInfoSet(context); + failedScheduledRecordingInfoSet.add(scheduledRecordingInfo); + PreferenceManager.getDefaultSharedPreferences(context).edit() + .putStringSet(PREF_KEY_FAILED_SCHEDULED_RECORDING_INFO_SET, + failedScheduledRecordingInfoSet) + .apply(); + } + + /** + * Clears the failed scheduled recording info set. + */ + public static void clearFailedScheduledRecordingInfoSet(Context context) { + PreferenceManager.getDefaultSharedPreferences(context).edit() + .remove(PREF_KEY_FAILED_SCHEDULED_RECORDING_INFO_SET) + .apply(); + } + /** * Clears recording failed reason. */ @@ -245,6 +272,14 @@ public class Utils { RECORDING_FAILED_REASON_NONE); } + /** + * Returns the failed scheduled recordings info set. + */ + public static Set getFailedScheduledRecordingInfoSet(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context) + .getStringSet(PREF_KEY_FAILED_SCHEDULED_RECORDING_INFO_SET, new HashSet<>()); + } + /** * Checks do recording failed reason exist. */ @@ -295,6 +330,13 @@ public class Utils { return isTvUri(uri) && PATH_PROGRAM.equals(uri.getPathSegments().get(0)); } + /** + * Returns {@code true}, if {@code uri} is a programs URI. + */ + public static boolean isRecordedProgramsUri(Uri uri) { + return isTvUri(uri) && PATH_RECORDED_PROGRAM.equals(uri.getPathSegments().get(0)); + } + /** * Gets the info of the program on particular time. */ @@ -332,6 +374,14 @@ public class Utils { return getProgramAt(context, channelId, System.currentTimeMillis()); } + /** + * Returns the round off minutes when convert milliseconds to minutes. + */ + public static int getRoundOffMinsFromMs(long millis) { + // Round off the result by adding half minute to the original ms. + return (int) TimeUnit.MILLISECONDS.toMinutes(millis + HALF_MINUTE_MS); + } + /** * Returns duration string according to the date & time format. * If {@code startUtcMillis} and {@code endUtcMills} are equal, @@ -392,16 +442,18 @@ public class Utils { : DateUtils.formatDateRange(context, startUtcMillis, endUtcMillis + 1, flag); } - @VisibleForTesting + /** + * Checks if two given time (in milliseconds) are in the same day with regard to the + * locale timezone. + */ public static boolean isInGivenDay(long dayToMatchInMillis, long subjectTimeInMillis) { - final long DAY_IN_MS = TimeUnit.DAYS.toMillis(1); TimeZone timeZone = Calendar.getInstance().getTimeZone(); long offset = timeZone.getRawOffset(); if (timeZone.inDaylightTime(new Date(dayToMatchInMillis))) { offset += timeZone.getDSTSavings(); } - return Utils.floorTime(dayToMatchInMillis + offset, DAY_IN_MS) - == Utils.floorTime(subjectTimeInMillis + offset, DAY_IN_MS); + return Utils.floorTime(dayToMatchInMillis + offset, ONE_DAY_MS) + == Utils.floorTime(subjectTimeInMillis + offset, ONE_DAY_MS); } /** @@ -523,7 +575,7 @@ public class Utils { if (track.getType() != TvTrackInfo.TYPE_AUDIO) { throw new IllegalArgumentException("Not an audio track: " + track); } - String language = context.getString(R.string.default_language); + String language = context.getString(R.string.multi_audio_unknown_language); if (!TextUtils.isEmpty(track.getLanguage())) { language = new Locale(track.getLanguage()).getDisplayName(); } else { @@ -606,10 +658,12 @@ public class Utils { if (input == null) { return null; } - CharSequence customLabel = input.loadCustomLabel(context); + TvInputManagerHelper inputManager = + TvApplication.getSingletons(context).getTvInputManagerHelper(); + CharSequence customLabel = inputManager.loadCustomLabel(input); String label = (customLabel == null) ? null : customLabel.toString(); if (TextUtils.isEmpty(label)) { - label = input.loadLabel(context).toString(); + label = inputManager.loadLabel(input).toString(); } return label; } @@ -860,4 +914,28 @@ public class Utils { } return Genres.encode(genres); } + + /** + * Returns true if the current user is a developer. + */ + public static boolean isDeveloper() { + return BuildConfig.ENG || Experiments.ENABLE_DEVELOPER_FEATURES.get(); + } + + /** + * Runs the method in main thread. If the current thread is not main thread, block it util + * the method is finished. + */ + public static void runInMainThreadAndWait(Runnable runnable) { + if (Looper.myLooper() == Looper.getMainLooper()) { + runnable.run(); + } else { + Future temp = MainThreadExecutor.getInstance().submit(runnable); + try { + temp.get(); + } catch (InterruptedException | ExecutionException e) { + Log.e(TAG, "failed to finish the execution", e); + } + } + } } diff --git a/src/com/android/tv/util/ViewCache.java b/src/com/android/tv/util/ViewCache.java new file mode 100644 index 00000000..ed9a8ff6 --- /dev/null +++ b/src/com/android/tv/util/ViewCache.java @@ -0,0 +1,100 @@ +package com.android.tv.util; + +import android.content.Context; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import java.util.ArrayList; + +/** + * A cache for the views. + */ +public class ViewCache { + private final static SparseArray> mViews = new SparseArray(); + + private static ViewCache sViewCache; + + private ViewCache() { } + + /** + * Returns an instance of the view cache. + */ + public static ViewCache getInstance() { + if (sViewCache == null) { + sViewCache = new ViewCache(); + } + return sViewCache; + } + + /** + * Returns if the view cache is empty. + */ + public boolean isEmpty() { + return mViews.size() == 0; + } + + /** + * Stores a view into this view cache. + */ + public void putView(int resId, View view) { + ArrayList views = mViews.get(resId); + if (views == null) { + views = new ArrayList(); + mViews.put(resId, views); + } + views.add(view); + } + + /** + * Stores multi specific views into the view cache. + */ + public void putView(Context context, int resId, ViewGroup fakeParent, int num) { + LayoutInflater inflater = + (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + ArrayList views = mViews.get(resId); + if (views == null) { + views = new ArrayList<>(); + mViews.put(resId, views); + } + for (int i = 0; i < num; i++) { + View view = inflater.inflate(resId, fakeParent, false); + views.add(view); + } + } + + /** + * Returns the view for specific resource id. + */ + public View getView(int resId) { + ArrayList views = mViews.get(resId); + if (views != null && !views.isEmpty()) { + View view = views.remove(views.size() - 1); + if (views.isEmpty()) { + mViews.remove(resId); + } + return view; + } else { + return null; + } + } + + /** + * Returns the view if exists, or create a new view for the specific resource id. + */ + public View getOrCreateView(LayoutInflater inflater, int resId, ViewGroup container) { + View view = getView(resId); + if (view == null) { + view = inflater.inflate(resId, container, false); + } + return view; + } + + /** + * Clears the view cache. + */ + public void clear() { + mViews.clear(); + } +} -- cgit v1.2.3