aboutsummaryrefslogtreecommitdiff
path: root/src/com
diff options
context:
space:
mode:
authorNick Chalko <nchalko@google.com>2015-11-04 15:15:29 -0800
committerNick Chalko <nchalko@google.com>2015-11-10 13:59:03 -0800
commit7d67089aa1e9aa2123c3cd2f386d7019a1544db1 (patch)
tree6f90c2065a853628dd7704788dd41b787acb6fae /src/com
parent07b043dc3db83d6d20f0e8513b946830ab00e37b (diff)
downloadTV-7d67089aa1e9aa2123c3cd2f386d7019a1544db1.tar.gz
Sync to ub-tv-glee at 1.07.007
hash dce17da9f45fc4304787b1898d9915511b1df954 Change-Id: I08ac6fc0123a6653644281155e35c11b71bc5fa0
Diffstat (limited to 'src/com')
-rw-r--r--src/com/android/tv/ChannelTuner.java50
-rw-r--r--src/com/android/tv/Features.java66
-rw-r--r--src/com/android/tv/MainActivity.java286
-rw-r--r--src/com/android/tv/SelectInputActivity.java99
-rw-r--r--src/com/android/tv/SetupPassthroughActivity.java93
-rw-r--r--src/com/android/tv/TvApplication.java154
-rw-r--r--src/com/android/tv/analytics/Analytics.java18
-rw-r--r--src/com/android/tv/analytics/ConfigurationInfo.java30
-rw-r--r--src/com/android/tv/analytics/OptOutPreferenceHelper.java112
-rw-r--r--src/com/android/tv/analytics/SendConfigInfoRunnable.java53
-rw-r--r--src/com/android/tv/analytics/StubAnalytics.java19
-rw-r--r--src/com/android/tv/analytics/StubTracker.java12
-rw-r--r--src/com/android/tv/analytics/Tracker.java26
-rw-r--r--src/com/android/tv/data/Channel.java2
-rw-r--r--src/com/android/tv/data/ChannelDataManager.java75
-rw-r--r--src/com/android/tv/data/ChannelLogoFetcher.java28
-rw-r--r--src/com/android/tv/data/ChannelNumber.java35
-rw-r--r--src/com/android/tv/data/ProgramDataManager.java135
-rw-r--r--src/com/android/tv/data/WatchedHistoryManager.java323
-rw-r--r--src/com/android/tv/guide/ProgramGrid.java7
-rw-r--r--src/com/android/tv/guide/ProgramGuide.java6
-rw-r--r--src/com/android/tv/guide/ProgramItemView.java8
-rw-r--r--src/com/android/tv/guide/ProgramManager.java64
-rw-r--r--src/com/android/tv/guide/ProgramTableAdapter.java12
-rw-r--r--src/com/android/tv/menu/AppLinkCardView.java32
-rw-r--r--src/com/android/tv/menu/ChannelCardView.java10
-rw-r--r--src/com/android/tv/menu/ChannelsPosterPrefetcher.java4
-rw-r--r--src/com/android/tv/menu/ChannelsRowAdapter.java4
-rw-r--r--src/com/android/tv/menu/Menu.java16
-rw-r--r--src/com/android/tv/menu/MenuLayoutManager.java28
-rw-r--r--src/com/android/tv/menu/MenuRowView.java4
-rw-r--r--src/com/android/tv/menu/TvOptionsRowAdapter.java9
-rw-r--r--src/com/android/tv/onboarding/AppOverviewFragment.java106
-rw-r--r--src/com/android/tv/onboarding/OnboardingActivity.java234
-rw-r--r--src/com/android/tv/onboarding/PagingIndicator.java235
-rw-r--r--src/com/android/tv/onboarding/SetupSourcesFragment.java64
-rw-r--r--src/com/android/tv/onboarding/WelcomeFragment.java101
-rw-r--r--src/com/android/tv/onboarding/WelcomePageFragment.java49
-rw-r--r--src/com/android/tv/receiver/AudioCapabilitiesReceiver.java62
-rw-r--r--src/com/android/tv/receiver/BootCompletedReceiver.java62
-rw-r--r--src/com/android/tv/receiver/PackageIntentsReceiver.java35
-rw-r--r--src/com/android/tv/recommendation/NotificationService.java25
-rw-r--r--src/com/android/tv/recommendation/RecentChannelEvaluator.java2
-rw-r--r--src/com/android/tv/recommendation/RecommendationDataManager.java58
-rw-r--r--src/com/android/tv/search/DataManagerSearch.java254
-rw-r--r--src/com/android/tv/search/LocalSearchProvider.java16
-rw-r--r--src/com/android/tv/search/ProgramGuideSearchFragment.java11
-rw-r--r--src/com/android/tv/search/SearchInterface.java41
-rw-r--r--src/com/android/tv/search/TvProviderSearch.java18
-rw-r--r--src/com/android/tv/ui/ChannelBannerView.java5
-rw-r--r--src/com/android/tv/ui/KeypadChannelSwitchView.java11
-rw-r--r--src/com/android/tv/ui/SelectInputView.java170
-rw-r--r--src/com/android/tv/ui/SetupView.java2
-rw-r--r--src/com/android/tv/ui/TunableTvView.java87
-rw-r--r--src/com/android/tv/ui/TvTransitionManager.java1
-rw-r--r--src/com/android/tv/ui/TvViewUiManager.java6
-rw-r--r--src/com/android/tv/ui/sidepanel/AboutFragment.java84
-rw-r--r--src/com/android/tv/ui/sidepanel/ActionItem.java2
-rw-r--r--src/com/android/tv/ui/sidepanel/ChannelSourcesFragment.java2
-rw-r--r--src/com/android/tv/ui/sidepanel/CheckBoxItem.java2
-rw-r--r--src/com/android/tv/ui/sidepanel/Item.java2
-rw-r--r--src/com/android/tv/ui/sidepanel/RadioButtonItem.java2
-rw-r--r--src/com/android/tv/ui/sidepanel/SwitchItem.java10
-rw-r--r--src/com/android/tv/ui/sidepanel/parentalcontrols/ProgramRestrictionsFragment.java2
-rw-r--r--src/com/android/tv/util/AsyncDbTask.java3
-rw-r--r--src/com/android/tv/util/BooleanSystemProperty.java95
-rw-r--r--src/com/android/tv/util/Clock.java2
-rw-r--r--src/com/android/tv/util/CollectionUtils.java41
-rw-r--r--src/com/android/tv/util/EngOnlyFeature.java (renamed from src/com/android/tv/receiver/NotificationReceiver.java)20
-rw-r--r--src/com/android/tv/util/ImageCache.java3
-rw-r--r--src/com/android/tv/util/MainThreadExecutor.java86
-rw-r--r--src/com/android/tv/util/MemoryManageable.java29
-rw-r--r--src/com/android/tv/util/MultiLongSparseArray.java117
-rw-r--r--src/com/android/tv/util/OnboardingUtils.java99
-rw-r--r--src/com/android/tv/util/PermissionUtils.java70
-rw-r--r--src/com/android/tv/util/PipInputManager.java3
-rw-r--r--src/com/android/tv/util/RecurringRunner.java2
-rw-r--r--src/com/android/tv/util/SetupUtils.java98
-rw-r--r--src/com/android/tv/util/SoftPreconditions.java146
-rw-r--r--src/com/android/tv/util/SystemProperties.java8
-rw-r--r--src/com/android/tv/util/TvInputManagerHelper.java26
-rw-r--r--src/com/android/tv/util/Utils.java122
82 files changed, 3835 insertions, 716 deletions
diff --git a/src/com/android/tv/ChannelTuner.java b/src/com/android/tv/ChannelTuner.java
index f5114193..d0d7c595 100644
--- a/src/com/android/tv/ChannelTuner.java
+++ b/src/com/android/tv/ChannelTuner.java
@@ -16,21 +16,19 @@
package com.android.tv;
-import android.content.Context;
-import android.database.Cursor;
import android.media.tv.TvContract;
import android.net.Uri;
import android.os.Handler;
-import android.support.annotation.WorkerThread;
import android.util.Log;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
+import com.android.tv.util.CollectionUtils;
+import com.android.tv.util.SoftPreconditions;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -42,9 +40,6 @@ import java.util.Set;
public class ChannelTuner {
private static final String TAG = "ChannelTuner";
- private static final int INVALID_INDEX = -1;
-
- private final Context mContext;
private boolean mStarted;
private boolean mChannelDataManagerLoaded;
private final List<Channel> mChannels = new ArrayList<>();
@@ -56,7 +51,7 @@ public class ChannelTuner {
private final Handler mHandler = new Handler();
private final ChannelDataManager mChannelDataManager;
- private final Set<Listener> mListeners = new HashSet<>();
+ private final Set<Listener> mListeners = CollectionUtils.createSmallSet();
private Channel mCurrentChannel;
private final ChannelDataManager.Listener mChannelDataManagerListener =
@@ -84,8 +79,7 @@ public class ChannelTuner {
}
};
- public ChannelTuner(Context context, ChannelDataManager channelDataManager) {
- mContext = context;
+ public ChannelTuner(ChannelDataManager channelDataManager) {
mChannelDataManager = channelDataManager;
}
@@ -286,12 +280,11 @@ public class ChannelTuner {
setCurrentChannelAndNotify(channel);
return true;
}
+ SoftPreconditions.checkState(mChannelDataManagerLoaded, TAG, "Channel data is not loaded");
Channel newChannel = mChannelMap.get(channel.getId());
if (newChannel != null) {
setCurrentChannelAndNotify(newChannel);
return true;
- } else if (!mChannelDataManagerLoaded) {
- return loadChannel(channel.getId()) != null;
}
return false;
}
@@ -386,37 +379,4 @@ public class ChannelTuner {
}
}
}
-
- /**
- * Loads and returns a channel which has the given channel ID.
- *
- * @param channelId The ID of the channel to be loaded.
- * @return a channel if it has been loaded. {@code null} if the channel is not found.
- */
- @WorkerThread
- public Channel loadChannel(long channelId) {
- if (channelId < 0) {
- return null;
- }
- if (mChannelDataManagerLoaded) {
- return mChannelMap.get(channelId);
- }
- Channel channel = mChannelMap.get(channelId);
- if (channel != null) {
- return channel;
- }
-
- Uri uri = TvContract.buildChannelUri(channelId);
- try (Cursor c = mContext.getContentResolver().query(uri, Channel.PROJECTION,
- null, null, null)) {
- if (c != null && c.moveToNext()) {
- channel = Channel.fromCursor(c);
- List<Channel> channels = new ArrayList<>(mChannels);
- channels.add(channel);
- updateChannelData(channels);
- return channel;
- }
- }
- return null;
- }
}
diff --git a/src/com/android/tv/Features.java b/src/com/android/tv/Features.java
new file mode 100644
index 00000000..f62987ab
--- /dev/null
+++ b/src/com/android/tv/Features.java
@@ -0,0 +1,66 @@
+/*
+ * 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;
+
+import static com.android.tv.common.feature.FeatureUtils.AND;
+import static com.android.tv.common.feature.FeatureUtils.ON;
+import static com.android.tv.common.feature.FeatureUtils.OR;
+
+import android.support.annotation.VisibleForTesting;
+
+import com.android.tv.common.feature.Feature;
+import com.android.tv.common.feature.GServiceFeature;
+import com.android.tv.common.feature.PropertyFeature;
+import com.android.tv.util.EngOnlyFeature;
+
+/**
+ * List of {@link Feature} for the Live TV App.
+ *
+ * <p>Remove the {@code Feature} once it is launched.
+ */
+public final class Features {
+ /**
+ * UI for opting out of analytics.
+ *
+ * <p>See <a href="http://b/20228119">b/20228119</a>
+ */
+ public static Feature ANALYTICS_OPT_OUT = new EngOnlyFeature();
+
+ /**
+ * Analytics that include sensitive information such as channel or program identifiers.
+ *
+ * <p>See <a href="http://b/22062676">b/22062676</a>
+ */
+ public static Feature ANALYTICS_V2 = AND(ON, ANALYTICS_OPT_OUT);
+
+ public static Feature EPG_SEARCH = new PropertyFeature("feature_tv_use_epg_search", false);
+
+
+ /**
+ * A flag which indicates that the on-boarding experience is used or not.
+ *
+ * <p>See <a href="http://b/24070322">b/24070322</a>
+ */
+ public static Feature ONBOARDING_EXPERIENCE = new PropertyFeature(
+ "feature_tv_use_onboarding_exp", false);
+
+ @VisibleForTesting
+ public static Feature TEST_FEATURE = new PropertyFeature("test_feature", false);
+
+ private Features() {
+ }
+}
diff --git a/src/com/android/tv/MainActivity.java b/src/com/android/tv/MainActivity.java
index db4cbffd..92cbd462 100644
--- a/src/com/android/tv/MainActivity.java
+++ b/src/com/android/tv/MainActivity.java
@@ -24,6 +24,7 @@ import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.PixelFormat;
import android.graphics.Point;
@@ -39,6 +40,7 @@ import android.media.tv.TvInputManager;
import android.media.tv.TvTrackInfo;
import android.media.tv.TvView.OnUnhandledInputEventListener;
import android.net.Uri;
+import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
@@ -61,6 +63,7 @@ import android.widget.FrameLayout;
import android.widget.Toast;
import com.android.tv.analytics.DurationTimer;
+import com.android.tv.analytics.SendConfigInfoRunnable;
import com.android.tv.analytics.Tracker;
import com.android.tv.common.WeakHandler;
import com.android.tv.data.Channel;
@@ -69,9 +72,11 @@ import com.android.tv.data.OnCurrentProgramUpdatedListener;
import com.android.tv.data.Program;
import com.android.tv.data.ProgramDataManager;
import com.android.tv.data.StreamInfo;
+import com.android.tv.data.WatchedHistoryManager;
import com.android.tv.dialog.PinDialogFragment;
import com.android.tv.dialog.SafeDismissDialogFragment;
import com.android.tv.menu.Menu;
+import com.android.tv.onboarding.OnboardingActivity;
import com.android.tv.parental.ContentRatingsManager;
import com.android.tv.parental.ParentalControlSettings;
import com.android.tv.receiver.AudioCapabilitiesReceiver;
@@ -83,6 +88,7 @@ import com.android.tv.ui.InputBannerView;
import com.android.tv.ui.KeypadChannelSwitchView;
import com.android.tv.ui.OverlayRootView;
import com.android.tv.ui.SelectInputView;
+import com.android.tv.ui.SelectInputView.OnInputSelectedCallback;
import com.android.tv.ui.TunableTvView;
import com.android.tv.ui.TunableTvView.OnTuneListener;
import com.android.tv.ui.TvOverlayManager;
@@ -97,10 +103,15 @@ import com.android.tv.ui.sidepanel.SideFragment;
import com.android.tv.ui.sidepanel.parentalcontrols.ParentalControlsFragment;
import com.android.tv.util.CaptionSettings;
import com.android.tv.util.ImageCache;
+import com.android.tv.util.MemoryManageable;
+import com.android.tv.util.OnboardingUtils;
+import com.android.tv.util.PermissionUtils;
import com.android.tv.util.PipInputManager;
import com.android.tv.util.PipInputManager.PipInput;
+import com.android.tv.util.RecurringRunner;
import com.android.tv.util.SearchManagerHelper;
import com.android.tv.util.SetupUtils;
+import com.android.tv.util.SoftPreconditions;
import com.android.tv.util.SystemProperties;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.TvSettings;
@@ -143,6 +154,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
private static final float FRAME_RATE_EPSILON = 0.1f;
+ private static final int PERMISSIONS_REQUEST_READ_TV_LISTINGS = 1;
+ private static final String PERMISSION_READ_TV_LISTINGS = "android.permission.READ_TV_LISTINGS";
+
// Tracker screen names.
public static final String SCREEN_NAME = "Main";
private static final String SCREEN_BEHIND_NAME = "Behind";
@@ -201,6 +215,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
private AccessibilityManager mAccessibilityManager;
private ChannelDataManager mChannelDataManager;
private ProgramDataManager mProgramDataManager;
+ private WatchedHistoryManager mWatchedHistoryManager;
private TvInputManagerHelper mTvInputManagerHelper;
private ChannelTuner mChannelTuner;
private PipInputManager mPipInputManager;
@@ -271,6 +286,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
private final ArrayDeque<Long> mRecentChannels = new ArrayDeque<>(MAX_RECENT_CHANNELS);
private AudioCapabilitiesReceiver mAudioCapabilitiesReceiver;
+ private RecurringRunner mSendConfigInfoRecurringRunner;
// A caller which started this activity. (e.g. TvSearch)
private String mSource;
@@ -375,20 +391,28 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
@Override
protected void onCreate(Bundle savedInstanceState) {
if (DEBUG) Log.d(TAG,"onCreate()");
- if(BuildConfig.ENG && SystemProperties.ALLOW_STRICT_MODE.getValue()) {
- Toast.makeText(this, "Using Strict Mode for eng builds", Toast.LENGTH_SHORT).show();
- }
super.onCreate(savedInstanceState);
-
TvApplication tvApplication = (TvApplication) getApplication();
tvApplication.setMainActivity(this);
+
+ if (Features.ONBOARDING_EXPERIENCE.isEnabled(this)
+ && OnboardingUtils.needToShowOnboarding(this)) {
+ startActivity(OnboardingActivity.buildIntent(this, getIntent()));
+ finish();
+ return;
+ }
+
+ if (BuildConfig.ENG && SystemProperties.ALLOW_STRICT_MODE.getValue()) {
+ Toast.makeText(this, "Using Strict Mode for eng builds", Toast.LENGTH_SHORT).show();
+ }
mTracker = tvApplication.getTracker();
mTvInputManagerHelper = tvApplication.getTvInputManagerHelper();
- mChannelDataManager = new ChannelDataManager(this, mTvInputManagerHelper, mTracker);
- mProgramDataManager = new ProgramDataManager(this);
+ mChannelDataManager = tvApplication.getChannelDataManager();
+ mProgramDataManager = tvApplication.getProgramDataManager();
mProgramDataManager.addOnCurrentProgramUpdatedListener(Channel.INVALID_ID,
mOnCurrentProgramUpdatedListener);
- mChannelTuner = new ChannelTuner(this, mChannelDataManager);
+ mProgramDataManager.setPrefetchEnabled(true);
+ mChannelTuner = new ChannelTuner(mChannelDataManager);
mChannelTuner.addListener(mChannelTunerListener);
mChannelTuner.start();
mPipInputManager = new PipInputManager(this, mTvInputManagerHelper, mChannelTuner);
@@ -458,6 +482,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mPipView.initialize((AppLayerTvView) findViewById(R.id.pip_tv_view), true, screenHeight,
shrunkenTvViewHeight);
+ if (!PermissionUtils.hasAccessWatchedHistory(this)) {
+ mWatchedHistoryManager = new WatchedHistoryManager(getApplicationContext());
+ mWatchedHistoryManager.start();
+ mTvView.setWatchedHistoryManager(mWatchedHistoryManager);
+ }
mTvViewUiManager = new TvViewUiManager(this, mTvView, mPipView,
(FrameLayout) findViewById(android.R.id.content), mTvOptionsManager);
@@ -473,6 +502,36 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
.inflate(R.layout.input_banner, sceneContainer, false);
SelectInputView selectInputView = (SelectInputView) getLayoutInflater()
.inflate(R.layout.select_input, sceneContainer, false);
+ selectInputView.setOnInputSelectedCallback(new OnInputSelectedCallback() {
+ @Override
+ public void onTunerInputSelected() {
+ Channel currentChannel = mChannelTuner.getCurrentChannel();
+ if (currentChannel != null && !currentChannel.isPassthrough()) {
+ hideOverlays();
+ } else {
+ tuneToLastWatchedChannelForTunerInput();
+ }
+ }
+
+ @Override
+ public void onPassthroughInputSelected(TvInputInfo input) {
+ Channel currentChannel = mChannelTuner.getCurrentChannel();
+ String currentInputId = currentChannel == null ? null : currentChannel.getInputId();
+ if (TextUtils.equals(input.getId(), currentInputId)) {
+ hideOverlays();
+ } else {
+ tuneToChannel(Channel.createPassthroughChannel(input.getId()));
+ }
+ }
+
+ private void hideOverlays() {
+ getOverlayManager().hideOverlays(
+ TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG
+ | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS
+ | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE
+ | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_MENU);
+ }
+ });
mSearchFragment = new ProgramGuideSearchFragment();
mOverlayManager = new TvOverlayManager(this, mChannelTuner,
mKeypadChannelSwitchView, mChannelBannerView, inputBannerView,
@@ -486,27 +545,36 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
finish();
return;
}
- if (!TvContract.isChannelUriForPassthroughInput(mInitChannelUri)) {
- // Preloads the initial channel to reduce the initial tuning time.
- Channel channel = loadInitialChannel(mInitChannelUri);
- mInitChannelUri = channel == null ? null : channel.getUri();
- }
- // mChannelDataManager.start() and mProgramDataManager.start() are called
- // after loadInitialChannel. Unless, loadInitialChannel can be blocked
- // by channel data and program data loading.
- mChannelDataManager.start();
- mProgramDataManager.start();
- mAudioCapabilitiesReceiver = new AudioCapabilitiesReceiver(this, mTracker);
+ mAudioCapabilitiesReceiver = new AudioCapabilitiesReceiver(this, null);
mAudioCapabilitiesReceiver.register();
mAccessibilityManager =
(AccessibilityManager) getSystemService(Context.ACCESSIBILITY_SERVICE);
-
+ mSendConfigInfoRecurringRunner = new RecurringRunner(this, TimeUnit.DAYS.toMillis(1),
+ new SendConfigInfoRunnable(mTracker, mTvInputManagerHelper));
+ mSendConfigInfoRecurringRunner.start();
initForTest();
}
@Override
+ public void onRequestPermissionsResult(int requestCode, String[] permissions,
+ int[] grantResults) {
+ if (requestCode == PERMISSIONS_REQUEST_READ_TV_LISTINGS) {
+ if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ // Restart live channels.
+ Intent intent = getIntent();
+ finish();
+ startActivity(intent);
+ } else {
+ Toast.makeText(this, R.string.msg_read_tv_listing_permission_denied,
+ Toast.LENGTH_LONG).show();
+ finish();
+ }
+ }
+ }
+
+ @Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
WindowManager.LayoutParams windowParams = new WindowManager.LayoutParams(
@@ -582,6 +650,16 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
protected void onResume() {
if (DEBUG) Log.d(TAG, "onResume()");
super.onResume();
+ if (!PermissionUtils.hasAccessAllEpg(this)) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ Toast.makeText(this, R.string.msg_not_supported_device, Toast.LENGTH_LONG).show();
+ finish();
+ } else if (checkSelfPermission(PERMISSION_READ_TV_LISTINGS)
+ != PackageManager.PERMISSION_GRANTED) {
+ requestPermissions(new String[]{PERMISSION_READ_TV_LISTINGS},
+ PERMISSIONS_REQUEST_READ_TV_LISTINGS);
+ }
+ }
mTracker.sendScreenView(SCREEN_NAME);
SystemProperties.updateSystemProperties();
@@ -600,9 +678,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
if (mChannelTuner.areAllChannelsLoaded()) {
markNewChannelsBrowsable();
+ resumeTvIfNeeded();
+ resumePipIfNeeded();
}
- resumeTvIfNeeded();
- resumePipIfNeeded();
mOverlayManager.showMenuWithTimeShiftPauseIfNeeded();
// Note: The following codes are related to pop up an overlay UI after resume.
@@ -692,6 +770,13 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
return mActivityResumed;
}
+ /**
+ * Returns true if {@link #onStart} is called and {@link #onStop} is not called yet.
+ */
+ public boolean isActivityStarted() {
+ return mActivityStarted;
+ }
+
@Override
public boolean requestVisibleBehind(boolean enable) {
boolean state = super.requestVisibleBehind(enable);
@@ -732,10 +817,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
Set<String> newInputsWithChannels = new HashSet<>();
for (TvInputInfo input : mTvInputManagerHelper.getTvInputInfos(true, true)) {
String inputId = input.getId();
- if (!setupUtils.hasSetupLaunched(inputId)
+ if (!setupUtils.isSetupDone(inputId)
&& mChannelDataManager.getChannelCountForInput(inputId) > 0) {
- setupUtils.onSetupLaunched(inputId);
- setupUtils.markAsKnownInput(inputId);
+ setupUtils.onSetupDone(inputId);
newInputsWithChannels.add(inputId);
if (DEBUG) {
Log.d(TAG, "New input " + inputId + " has "
@@ -754,32 +838,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
}
- private Channel loadInitialChannel(Uri channelUri) {
- if (TvContract.isChannelUriForPassthroughInput(channelUri)) {
- throw new IllegalArgumentException("channelUri should be null or tuner input channel");
- }
- if (channelUri == null) {
- // If any initial channel id is not given, remember the last channel the user watched.
- long channelId = Utils.getLastWatchedChannelId(this);
- if (channelId != Channel.INVALID_ID) {
- channelUri = TvContract.buildChannelUri(channelId);
- }
- }
- if (channelUri == null) {
- return null;
- }
- long channelId = ContentUris.parseId(channelUri);
- Channel channel = mChannelTuner.loadChannel(channelId);
- if (channel == null) {
- // If the requested channel doesn't exist, it's better to tune to the
- // last watched channel.
- channel = mChannelTuner.loadChannel(Utils.getLastWatchedChannelId(this));
- Log.w(TAG, "The requested channel (id=" + channelId + ") doesn't exist. "
- + "The last watched channel will be tuned to.");
- }
- return channel;
- }
-
private void startTv(Uri channelUri) {
if (DEBUG) Log.d(TAG, "startTv Uri=" + channelUri);
if ((channelUri == null || !TvContract.isChannelUriForPassthroughInput(channelUri))
@@ -789,6 +847,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
// is playing, we stop the passthrough TV input.
stopTv();
}
+ SoftPreconditions.checkState(TvContract.isChannelUriForPassthroughInput(channelUri)
+ || mChannelTuner.areAllChannelsLoaded(),
+ TAG, "startTV assumes that ChannelDataManager is already loaded.");
if (mTvView.isPlaying()) {
// TV has already started.
if (channelUri == null) {
@@ -822,11 +883,10 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
Channel channel = Channel.createPassthroughChannel(channelUri);
mChannelTuner.moveToChannel(channel);
} else {
- Channel channel = loadInitialChannel(channelUri);
+ long channelId = ContentUris.parseId(channelUri);
+ Channel channel = mChannelDataManager.getChannel(channelId);
if (channel == null || !mChannelTuner.moveToChannel(channel)) {
mChannelTuner.moveToChannel(mChannelTuner.findNearestBrowsableChannel(0));
- long channelId = (channel == null) ? ContentUris.parseId(channelUri)
- : channel.getId();
Log.w(TAG, "The requested channel (id=" + channelId + ") doesn't exist. "
+ "The first channel will be tuned to.");
}
@@ -880,8 +940,12 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mInputIdUnderSetup = input.getId();
mIsSetupActivityCalledByDialog = calledByDialog;
+ // Call requestVisibleBehind(false) before starting other activity.
+ // In Activity.requestVisibleBehind(false), this activity is scheduled to be stopped
+ // immediately if other activity is about to start. And this activity is scheduled to
+ // to be stopped again after onPause().
+ stopTv("startSetupActivity()", false);
startActivityForResultSafe(intent, REQUEST_CODE_START_SETUP_ACTIVITY);
- SetupUtils.getInstance(this).onSetupLaunched(input.getId());
} catch (ActivityNotFoundException e) {
mInputIdUnderSetup = null;
Toast.makeText(this, getString(R.string.msg_unable_to_start_setup_activity,
@@ -895,7 +959,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION
| TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY);
}
- stopTv("startSetupActivity()", false);
}
public boolean hasCaptioningSettingsActivity() {
@@ -1120,31 +1183,12 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
public void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case REQUEST_CODE_START_SETUP_ACTIVITY:
- if (resultCode == Activity.RESULT_OK) {
+ if (resultCode == RESULT_OK) {
final String inputId = mInputIdUnderSetup;
- // When TIS adds several channels, ChannelDataManager.Listener.onChannelList
- // Updated() can be called several times. In this case, it is hard to detect
- // which one is the last callback. To reduce error prune, we update channel
- // list again and make all channels of {@code inputId} browsable.
- mChannelDataManager.updateChannels(new Runnable() {
+ SetupUtils.getInstance(this).onTvInputSetupFinished(inputId, new Runnable() {
@Override
public void run() {
- int count = 0;
- boolean browsableChanged = false;
- for (Channel channel : mChannelDataManager.getChannelList()) {
- if (channel.getInputId().equals(inputId)) {
- if (!channel.isBrowsable()) {
- mChannelDataManager.updateBrowsable(channel.getId(), true,
- true);
- browsableChanged = true;
- }
- ++count;
- }
- }
- if (browsableChanged) {
- mChannelDataManager.notifyChannelBrowsableChanged();
- mChannelDataManager.applyUpdatedValuesToDb();
- }
+ int count = mChannelDataManager.getChannelCountForInput(inputId);
String text;
if (count > 0) {
text = getResources().getQuantityString(R.plurals.msg_channel_added,
@@ -1502,6 +1546,20 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
}
+ /**
+ * Says {@code text} when accessibility is turned on.
+ */
+ public void sendAccessiblityText(String text) {
+ if (mAccessibilityManager.isEnabled()) {
+ AccessibilityEvent event = AccessibilityEvent.obtain();
+ event.setClassName(getClass().getName());
+ event.setPackageName(getPackageName());
+ event.setEventType(AccessibilityEvent.TYPE_ANNOUNCEMENT);
+ event.getText().add(text);
+ mAccessibilityManager.sendAccessibilityEvent(event);
+ }
+ }
+
private void tune() {
if (DEBUG) Log.d(TAG, "tune()");
mTuneDurationTimer.start();
@@ -1596,6 +1654,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
if (TvContract.isChannelUriForPassthroughInput(channel.getUri())) {
TvInputInfo input = mTvInputManagerHelper.getTvInputInfo(channel.getInputId());
event.getText().add(Utils.loadLabel(this, input));
+ } else if (TextUtils.isEmpty(channel.getDisplayName())) {
+ event.getText().add(channel.getDisplayNumber());
} else {
event.getText().add(channel.getDisplayNumber() + " " + channel.getDisplayName());
}
@@ -1822,8 +1882,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
boolean needToShowBanner = (reason == UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW
|| reason == UPDATE_CHANNEL_BANNER_REASON_TUNE
|| reason == UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST);
- boolean noOverlayUiWhenResume
- = mInputToSetUp == null && !mShowProgramGuide && !mShowSelectInputView;
+ boolean noOverlayUiWhenResume =
+ mInputToSetUp == null && !mShowProgramGuide && !mShowSelectInputView;
if (needToShowBanner && noOverlayUiWhenResume
&& mOverlayManager.getCurrentDialog() == null) {
if (mChannelTuner.getCurrentChannel() == null) {
@@ -1994,19 +2054,39 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
@Override
protected void onDestroy() {
if (DEBUG) Log.d(TAG, "onDestroy()");
- mChannelTuner.removeListener(mChannelTunerListener);
- mProgramDataManager.removeOnCurrentProgramUpdatedListener(
- Channel.INVALID_ID, mOnCurrentProgramUpdatedListener);
- mProgramDataManager.stop();
- mChannelDataManager.stop();
- mPipInputManager.stop();
- mOverlayManager.release();
- mChannelTuner.stop();
- mKeypadChannelSwitchView.setChannels(null);
+ if (mChannelTuner != null) {
+ mChannelTuner.removeListener(mChannelTunerListener);
+ mChannelTuner.stop();
+ }
+ TvApplication application = ((TvApplication) getApplication());
+ if (mProgramDataManager != null) {
+ mProgramDataManager.removeOnCurrentProgramUpdatedListener(
+ Channel.INVALID_ID, mOnCurrentProgramUpdatedListener);
+ if (application.isCurrentMainActivity(this)) {
+ mProgramDataManager.setPrefetchEnabled(false);
+ }
+ }
+ if (mPipInputManager != null) {
+ mPipInputManager.stop();
+ }
+ if (mOverlayManager != null) {
+ mOverlayManager.release();
+ }
+ if (mKeypadChannelSwitchView != null) {
+ mKeypadChannelSwitchView.setChannels(null);
+ }
mMemoryManageables.clear();
- ((TvApplication) getApplication()).setMainActivity(null);
- mAudioCapabilitiesReceiver.unregister();
+ if (mAudioCapabilitiesReceiver != null) {
+ mAudioCapabilitiesReceiver.unregister();
+ }
mHandler.removeCallbacksAndMessages(null);
+ if (application.isCurrentMainActivity(this)) {
+ application.setMainActivity(null);
+ }
+ if (mSendConfigInfoRecurringRunner != null) {
+ mSendConfigInfoRecurringRunner.stop();
+ mSendConfigInfoRecurringRunner = null;
+ }
super.onDestroy();
}
@@ -2130,6 +2210,10 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_RIGHT:
+ if (!PermissionUtils.hasModifyParentalControls(this)) {
+ // TODO: support this feature for non-system LC app. b/23939816
+ return true;
+ }
PinDialogFragment dialog = null;
if (mTvView.isScreenBlocked()) {
dialog = new PinDialogFragment(
@@ -2284,7 +2368,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
@Override
public void onUserInteraction() {
super.onUserInteraction();
- mOverlayManager.onUserInteraction();
+ if (mOverlayManager != null) {
+ mOverlayManager.onUserInteraction();
+ }
}
public void togglePipView() {
@@ -2636,14 +2722,13 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
int stringId;
switch (info.getVideoUnavailableReason()) {
case TunableTvView.VIDEO_UNAVAILABLE_REASON_NOT_TUNED:
- return;
case TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING:
+ case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING:
+ case TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY:
return;
case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL:
stringId = R.string.msg_channel_unavailable_weak_signal;
break;
- case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING:
- return;
case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN:
default:
stringId = R.string.msg_channel_unavailable_unknown;
@@ -2712,19 +2797,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
}
- /**
- * Interface for the fine-grained memory management.
- * The class which wants to release memory based on the system constraints should inherit
- * this interface and implement {@link #performTrimMemory}.
- */
- public interface MemoryManageable {
- /**
- * This method is called when the {@link MainActivity#onTrimMemory} is called.
- * For more information, see {@link android.content.ComponentCallbacks2#onTrimMemory}.
- */
- void performTrimMemory(int level);
- }
-
private static class MainActivityHandler extends WeakHandler<MainActivity> {
MainActivityHandler(MainActivity mainActivity) {
super(mainActivity);
diff --git a/src/com/android/tv/SelectInputActivity.java b/src/com/android/tv/SelectInputActivity.java
new file mode 100644
index 00000000..c68a1ad0
--- /dev/null
+++ b/src/com/android/tv/SelectInputActivity.java
@@ -0,0 +1,99 @@
+/*
+ * 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;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.media.tv.TvContract;
+import android.media.tv.TvInputInfo;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.KeyEvent;
+
+import com.android.tv.data.Channel;
+import com.android.tv.ui.SelectInputView;
+import com.android.tv.ui.SelectInputView.OnInputSelectedCallback;
+import com.android.tv.util.Utils;
+
+/**
+ * An activity to select input.
+ */
+public class SelectInputActivity extends Activity {
+ private SelectInputView mSelectInputView;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ ((TvApplication) getApplicationContext()).setSelectInputActivity(this);
+ setContentView(R.layout.activity_select_input);
+ mSelectInputView = (SelectInputView) findViewById(R.id.scene_transition_common);
+ mSelectInputView.setOnInputSelectedCallback(new OnInputSelectedCallback() {
+ @Override
+ public void onTunerInputSelected() {
+ startTvWithChannel(TvContract.Channels.CONTENT_URI);
+ }
+
+ @Override
+ public void onPassthroughInputSelected(TvInputInfo input) {
+ startTvWithChannel(TvContract.buildChannelUriForPassthroughInput(input.getId()));
+ }
+
+ private void startTvWithChannel(Uri channelUri) {
+ Intent intent = new Intent(Intent.ACTION_VIEW, channelUri,
+ SelectInputActivity.this, MainActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ finish();
+ }
+ });
+ String channelUriString = Utils.getLastWatchedChannelUri(this);
+ if (channelUriString != null) {
+ Uri channelUri = Uri.parse(channelUriString);
+ if (TvContract.isChannelUriForPassthroughInput(channelUri)) {
+ mSelectInputView.setCurrentChannel(Channel.createPassthroughChannel(channelUri));
+ }
+ // No need to set the tuner channel because it's the default selection.
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mSelectInputView.onEnterAction(true);
+ }
+
+ @Override
+ protected void onPause() {
+ mSelectInputView.onExitAction();
+ super.onPause();
+ }
+
+ @Override
+ protected void onDestroy() {
+ ((TvApplication) getApplicationContext()).setSelectInputActivity(null);
+ super.onDestroy();
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_I || keyCode == KeyEvent.KEYCODE_TV_INPUT) {
+ mSelectInputView.onKeyUp(keyCode, event);
+ return true;
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+}
diff --git a/src/com/android/tv/SetupPassthroughActivity.java b/src/com/android/tv/SetupPassthroughActivity.java
new file mode 100644
index 00000000..45b99776
--- /dev/null
+++ b/src/com/android/tv/SetupPassthroughActivity.java
@@ -0,0 +1,93 @@
+/*
+ * 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;
+
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+import android.media.tv.TvInputInfo;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.android.tv.common.TvCommonConstants;
+import com.android.tv.util.SetupUtils;
+import com.android.tv.util.TvInputManagerHelper;
+
+/**
+ * An activity to launch a TV input setup activity.
+ *
+ * <p> After setup activity is finished, all channels will be browsable.
+ */
+public class SetupPassthroughActivity extends Activity {
+ private static final String TAG = "SetupPassthroughActivity";
+ private static final boolean DEBUG = false;
+
+ private static final int REQUEST_START_SETUP_ACTIVITY = 200;
+
+ private TvInputInfo mTvInputInfo;
+ private Intent mActivityAfterCompletion;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ TvApplication tvApplication = (TvApplication) getApplication();
+ TvInputManagerHelper inputManager = tvApplication.getTvInputManagerHelper();
+ // It is not only for setting the variable but also for early initialization of
+ // ChannelDataManager.
+ String inputId = getIntent().getStringExtra(TvCommonConstants.EXTRA_INPUT_ID);
+ mTvInputInfo = inputManager.getTvInputInfo(inputId);
+ if (DEBUG) Log.d(TAG, "TvInputId " + inputId + " / TvInputInfo " + mTvInputInfo);
+ if (mTvInputInfo == null) {
+ Log.w(TAG, "There is no input with the ID " + inputId + ".");
+ finish();
+ return;
+ }
+ Intent setupIntent = mTvInputInfo.createSetupIntent();
+ if (setupIntent == null) {
+ Log.w(TAG, "The input (" + mTvInputInfo.getId() + ") doesn't have setup.");
+ finish();
+ return;
+ }
+ SetupUtils.grantEpgPermission(this, mTvInputInfo.getServiceInfo().packageName);
+ mActivityAfterCompletion = getIntent().getParcelableExtra(
+ TvCommonConstants.EXTRA_ACTIVITY_AFTER_COMPLETION);
+ if (DEBUG) Log.d(TAG, "Activity after completion " + mActivityAfterCompletion);
+ setupIntent.putExtras(getIntent().getExtras());
+ startActivityForResult(setupIntent, REQUEST_START_SETUP_ACTIVITY);
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode != REQUEST_START_SETUP_ACTIVITY || resultCode != Activity.RESULT_OK) {
+ finish();
+ return;
+ }
+ SetupUtils.getInstance(this).onTvInputSetupFinished(mTvInputInfo.getId(), new Runnable() {
+ @Override
+ public void run() {
+ if (mActivityAfterCompletion != null) {
+ try {
+ startActivity(mActivityAfterCompletion);
+ } catch (ActivityNotFoundException e) {
+ Log.w(TAG, "Activity launch failed", e);
+ }
+ }
+ finish();
+ }
+ });
+ }
+}
diff --git a/src/com/android/tv/TvApplication.java b/src/com/android/tv/TvApplication.java
index d3a8facb..9c699389 100644
--- a/src/com/android/tv/TvApplication.java
+++ b/src/com/android/tv/TvApplication.java
@@ -16,6 +16,7 @@
package com.android.tv;
+import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
@@ -23,6 +24,7 @@ import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.media.tv.TvInputInfo;
import android.media.tv.TvInputManager;
+import android.os.AsyncTask;
import android.os.Bundle;
import android.os.StrictMode;
import android.util.Log;
@@ -30,25 +32,30 @@ import android.view.KeyEvent;
import com.android.tv.analytics.Analytics;
import com.android.tv.analytics.StubAnalytics;
+import com.android.tv.analytics.OptOutPreferenceHelper;
import com.android.tv.analytics.StubAnalytics;
import com.android.tv.analytics.Tracker;
-import com.android.tv.util.RecurringRunner;
+import com.android.tv.data.ChannelDataManager;
+import com.android.tv.data.ProgramDataManager;
import com.android.tv.util.SystemProperties;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
import java.util.List;
-import java.util.concurrent.TimeUnit;
public class TvApplication extends Application {
private static final String TAG = "TvApplication";
private static final boolean DEBUG = false;
private static String versionName = "";
- private MainActivity mActivity;
+ private MainActivity mMainActivity;
+ private SelectInputActivity mSelectInputActivity;
+ private Analytics mAnalytics;
private Tracker mTracker;
private TvInputManagerHelper mTvInputManagerHelper;
- private RecurringRunner mSendConfigInfoRecurringRunner;
+ private ChannelDataManager mChannelDataManager;
+ private ProgramDataManager mProgramDataManager;
+ private OptOutPreferenceHelper mOptPreferenceHelper;
@Override
public void onCreate() {
@@ -66,13 +73,33 @@ public class TvApplication extends Application {
}
StrictMode.setVmPolicy(vmPolicyBuilder.build());
}
- Analytics analytics;
+
if (BuildConfig.ENG && !SystemProperties.ALLOW_ANALYTICS_IN_ENG.getValue()) {
- analytics = StubAnalytics.getInstance(this);
+ mAnalytics = StubAnalytics.getInstance(this);
} else {
- analytics = StubAnalytics.getInstance(this);
+ mAnalytics = StubAnalytics.getInstance(this);
+ }
+ mTracker = mAnalytics.getDefaultTracker();
+ if(Features.ANALYTICS_OPT_OUT.isEnabled(this)) {
+ mOptPreferenceHelper = new OptOutPreferenceHelper(this);
+ mOptPreferenceHelper.registerChangeListener(mAnalytics,
+ OptOutPreferenceHelper.ANALYTICS_OPT_OUT_DEFAULT_VALUE);
+ // always start with analytics off
+ mAnalytics.setAppOptOut(true);
+ // then update with the saved preference in an AsyncTask.
+ new AsyncTask<Void, Void, Boolean>() {
+ @Override
+ protected Boolean doInBackground(Void... voids) {
+ return mOptPreferenceHelper.getOptOutPreference(
+ OptOutPreferenceHelper.ANALYTICS_OPT_OUT_DEFAULT_VALUE);
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ mAnalytics.setAppOptOut(result);
+ }
+ }.execute();
}
- mTracker = analytics.getDefaultTracker();
try {
PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
versionName = pInfo.versionName;
@@ -82,16 +109,52 @@ public class TvApplication extends Application {
}
mTvInputManagerHelper = new TvInputManagerHelper(this);
mTvInputManagerHelper.start();
- mSendConfigInfoRecurringRunner = new RecurringRunner(this, TimeUnit.DAYS.toMillis(1),
- new SendConfigInfoRunnable());
- mSendConfigInfoRecurringRunner.start();
if (DEBUG) Log.i(TAG, "Started Live TV " + versionName);
}
+ /**
+ * Returns the {@link Analytics}.
+ */
+ public Analytics getAnalytics() {
+ return mAnalytics;
+ }
+
+ /**
+ * Returns the default tracker.
+ */
public Tracker getTracker() {
return mTracker;
}
+ public OptOutPreferenceHelper getOptPreferenceHelper(){
+ return mOptPreferenceHelper;
+ }
+
+ /**
+ * Returns {@link ChannelDataManager}.
+ */
+ public ChannelDataManager getChannelDataManager() {
+ if (mChannelDataManager == null) {
+ mChannelDataManager = new ChannelDataManager(this, mTvInputManagerHelper, mTracker);
+ mChannelDataManager.start();
+ }
+ return mChannelDataManager;
+ }
+
+ /**
+ * Returns {@link ProgramDataManager}.
+ */
+ public ProgramDataManager getProgramDataManager() {
+ if (mProgramDataManager == null) {
+ mProgramDataManager = new ProgramDataManager(this);
+ mProgramDataManager.start();
+ }
+ return mProgramDataManager;
+ }
+
+ /**
+ * Returns {@link TvInputManagerHelper}.
+ */
public TvInputManagerHelper getTvInputManagerHelper() {
return mTvInputManagerHelper;
}
@@ -101,21 +164,39 @@ public class TvApplication extends Application {
* {@link MainActivity#onDestroy}.
*/
public void setMainActivity(MainActivity activity) {
- mActivity = activity;
+ mMainActivity = activity;
+ }
+
+ /**
+ * SelectInputActivity is set in {@link SelectInputActivity#onCreate} and cleared in
+ * {@link SelectInputActivity#onDestroy}.
+ */
+ public void setSelectInputActivity(SelectInputActivity activity) {
+ mSelectInputActivity = activity;
}
/**
* Checks if MainActivity is set or not.
*/
public boolean hasMainActivity() {
- return (mActivity != null);
+ return (mMainActivity != null);
+ }
+
+ /**
+ * Returns true, if {@code activity} is the current activity.
+ *
+ * Note: MainActivity can start while another MainActivity destroys. In this case, the current
+ * activity is the newly created activity.
+ */
+ public boolean isCurrentMainActivity(MainActivity activity) {
+ return mMainActivity == activity;
}
/**
* Handles the global key KEYCODE_TV.
*/
public void handleTvKey() {
- if (mActivity == null || !mActivity.isActivityResumed()) {
+ if (mMainActivity == null || !mMainActivity.isActivityResumed()) {
startMainActivity(null);
}
}
@@ -139,16 +220,22 @@ public class TvApplication extends Application {
if (inputCount < 2) {
return;
}
- if (mActivity != null && mActivity.isActivityResumed()) {
+ Activity activityToHandle = mMainActivity != null && mMainActivity.isActivityResumed()
+ ? mMainActivity : mSelectInputActivity;
+ if (activityToHandle != null) {
// If startActivity is called, MainActivity.onPause is unnecessarily called. To
// prevent it, MainActivity.dispatchKeyEvent is directly called.
- mActivity.dispatchKeyEvent(
+ activityToHandle.dispatchKeyEvent(
new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_TV_INPUT));
- mActivity.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_TV_INPUT));
- } else {
+ activityToHandle.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP,
+ KeyEvent.KEYCODE_TV_INPUT));
+ } else if (mMainActivity != null && mMainActivity.isActivityStarted()) {
Bundle extras = new Bundle();
extras.putString(Utils.EXTRA_KEY_ACTION, Utils.EXTRA_ACTION_SHOW_TV_INPUT);
startMainActivity(extras);
+ } else {
+ startActivity(new Intent(this, SelectInputActivity.class).setFlags(
+ Intent.FLAG_ACTIVITY_NEW_TASK));
}
}
@@ -168,35 +255,4 @@ public class TvApplication extends Application {
return versionName;
}
- /**
- * Data useful for tracking that doesn't change often.
- */
- public static class ConfigurationInfo {
- public final int systemInputCount;
- public final int nonSystemInputCount;
-
- public ConfigurationInfo(int systemInputCount, int nonSystemInputCount) {
- this.systemInputCount = systemInputCount;
- this.nonSystemInputCount = nonSystemInputCount;
- }
- }
-
- private class SendConfigInfoRunnable implements Runnable {
- @Override
- public void run() {
- List<TvInputInfo> infoList = mTvInputManagerHelper.getTvInputInfos(false, false);
- int systemInputCount = 0;
- int nonSystemInputCount = 0;
- for (TvInputInfo info : infoList) {
- if (mTvInputManagerHelper.isSystemInput(info)) {
- systemInputCount++;
- } else {
- nonSystemInputCount++;
- }
- }
- ConfigurationInfo configurationInfo = new ConfigurationInfo(systemInputCount,
- nonSystemInputCount);
- mTracker.sendConfigurationInfo(configurationInfo);
- }
- }
}
diff --git a/src/com/android/tv/analytics/Analytics.java b/src/com/android/tv/analytics/Analytics.java
index d5b99247..27085de7 100644
--- a/src/com/android/tv/analytics/Analytics.java
+++ b/src/com/android/tv/analytics/Analytics.java
@@ -17,8 +17,22 @@
package com.android.tv.analytics;
/**
- * Provides a Tracker used for user activity analysis.
+ * Provides Trackers used for user activity analysis.
*/
public interface Analytics {
- Tracker getDefaultTracker();
+ Tracker getDefaultTracker();
+
+ /**
+ * Returns whether the state of the application-level opt is on.
+ */
+ boolean isAppOptOut();
+
+ /**
+ * Sets or resets the application-level opt out flag. If set, no hits will be sent.
+ * The value of this flag will <i>not</i> persist across application starts. The
+ * correct value should thus be set in application initialization code.
+ *
+ * @param optOut {@code true} if application-level opt out should be enforced.
+ */
+ void setAppOptOut(boolean optOut);
}
diff --git a/src/com/android/tv/analytics/ConfigurationInfo.java b/src/com/android/tv/analytics/ConfigurationInfo.java
new file mode 100644
index 00000000..41e8baeb
--- /dev/null
+++ b/src/com/android/tv/analytics/ConfigurationInfo.java
@@ -0,0 +1,30 @@
+/*
+ * 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.analytics;
+
+/**
+ * Data useful for tracking that doesn't change often.
+ */
+public class ConfigurationInfo {
+ public final int systemInputCount;
+ public final int nonSystemInputCount;
+
+ public ConfigurationInfo(int systemInputCount, int nonSystemInputCount) {
+ this.systemInputCount = systemInputCount;
+ this.nonSystemInputCount = nonSystemInputCount;
+ }
+}
diff --git a/src/com/android/tv/analytics/OptOutPreferenceHelper.java b/src/com/android/tv/analytics/OptOutPreferenceHelper.java
new file mode 100644
index 00000000..7fefaa46
--- /dev/null
+++ b/src/com/android/tv/analytics/OptOutPreferenceHelper.java
@@ -0,0 +1,112 @@
+/*
+ * 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.analytics;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.preference.PreferenceManager;
+
+/**
+ * Handles the opt out preference for analytics, including updating {@link Analytics} with the
+ * preference changes.
+ */
+public final class OptOutPreferenceHelper {
+ /**
+ * The {@link SharedPreferences SharedPreferences} key
+ * "{@value #ANALYTICS_OPT_OUT_KEY}", true means the user has chosen NOT to send
+ * analytics.
+ */
+ public static final String ANALYTICS_OPT_OUT_KEY = "analytics_opt_out";
+
+ /**
+ * The default value for the {@link SharedPreferences SharedPreferences} key
+ * "{@value #ANALYTICS_OPT_OUT_KEY}" is
+ * {@value #ANALYTICS_OPT_OUT_DEFAULT_VALUE}
+ */
+ public static final boolean ANALYTICS_OPT_OUT_DEFAULT_VALUE = false;
+
+ private final SharedPreferences userPrefs;
+
+ public OptOutPreferenceHelper(Context context) {
+ userPrefs = PreferenceManager.getDefaultSharedPreferences(context);
+ }
+
+ /**
+ * Creates and registers a change listener that will update analytics.
+ *
+ * @param analytics the analytics to update when opt out settings change.
+ * @param defaultValue the default opt out values
+ * @return the newly created OptOutChangeListener, keep this pass to
+ * {@link #unRegisterChangeListener(OptOutChangeListener)}
+ */
+ public OptOutChangeListener registerChangeListener(Analytics analytics, boolean defaultValue) {
+ OptOutChangeListener changeListener = new OptOutChangeListener(analytics, defaultValue);
+ userPrefs.registerOnSharedPreferenceChangeListener(changeListener);
+ return changeListener;
+ }
+
+ /**
+ * Unregister a {@link OptOutChangeListener} created by
+ * {@link #registerChangeListener(Analytics, boolean)}
+ */
+ public void unRegisterChangeListener(OptOutChangeListener changeListener) {
+ userPrefs.registerOnSharedPreferenceChangeListener(changeListener);
+ }
+
+ /**
+ * Returns the saved opt out preference or {@code defaultValue} if it has been set.
+ */
+ public boolean getOptOutPreference(boolean defaultValue) {
+ return userPrefs.getBoolean(ANALYTICS_OPT_OUT_KEY, defaultValue);
+ }
+
+ /**
+ * Sets the opt out preference.
+ */
+ public void setOptOutPreference(boolean optOut) {
+ userPrefs.edit().putBoolean(ANALYTICS_OPT_OUT_KEY, optOut).apply();
+ }
+
+ /**
+ * Updates Analytics when opt out preference is changed.
+ *
+ * <p>{@link OnSharedPreferenceChangeListener} is used so the {@code analytics} object is
+ * updated even if the preference are modified directly and not by
+ * {@link OptOutPreferenceHelper}.
+ */
+ public static final class OptOutChangeListener implements OnSharedPreferenceChangeListener {
+ private final Analytics mAnalytics;
+ private final boolean mDefaultValue;
+
+ private OptOutChangeListener(Analytics analytics, boolean defaultValue) {
+ mAnalytics = analytics;
+ mDefaultValue = defaultValue;
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+ switch (key) {
+ case ANALYTICS_OPT_OUT_KEY:
+ mAnalytics.setAppOptOut(
+ sharedPreferences.getBoolean(ANALYTICS_OPT_OUT_KEY, mDefaultValue));
+ break;
+ default:
+ }
+ }
+ }
+}
diff --git a/src/com/android/tv/analytics/SendConfigInfoRunnable.java b/src/com/android/tv/analytics/SendConfigInfoRunnable.java
new file mode 100644
index 00000000..c2d5c5fb
--- /dev/null
+++ b/src/com/android/tv/analytics/SendConfigInfoRunnable.java
@@ -0,0 +1,53 @@
+/*
+ * 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.analytics;
+
+import android.media.tv.TvInputInfo;
+
+import com.android.tv.util.TvInputManagerHelper;
+
+import java.util.List;
+
+/**
+ * Sends ConfigurationInfo once a day.
+ */
+public class SendConfigInfoRunnable implements Runnable {
+ private Tracker mTracker;
+ private TvInputManagerHelper mTvInputManagerHelper;
+
+ public SendConfigInfoRunnable(Tracker tracker, TvInputManagerHelper tvInputManagerHelper) {
+ this.mTracker = tracker;
+ this.mTvInputManagerHelper = tvInputManagerHelper;
+ }
+
+ @Override
+ public void run() {
+ List<TvInputInfo> infoList = mTvInputManagerHelper.getTvInputInfos(false, false);
+ int systemInputCount = 0;
+ int nonSystemInputCount = 0;
+ for (TvInputInfo info : infoList) {
+ if (mTvInputManagerHelper.isSystemInput(info)) {
+ systemInputCount++;
+ } else {
+ nonSystemInputCount++;
+ }
+ }
+ ConfigurationInfo configurationInfo = new ConfigurationInfo(systemInputCount,
+ nonSystemInputCount);
+ mTracker.sendConfigurationInfo(configurationInfo);
+ }
+}
diff --git a/src/com/android/tv/analytics/StubAnalytics.java b/src/com/android/tv/analytics/StubAnalytics.java
index 9b54e7d2..ae4cdafa 100644
--- a/src/com/android/tv/analytics/StubAnalytics.java
+++ b/src/com/android/tv/analytics/StubAnalytics.java
@@ -17,23 +17,34 @@
package com.android.tv.analytics;
import android.app.Application;
+import android.content.Context;
/**
* An implementation of {@link Analytics} that returns a {@link StubTracker}.
*/
public final class StubAnalytics implements Analytics {
- private static final StubAnalytics INSTANCE = new StubAnalytics();
-
public static StubAnalytics getInstance(Application application) {
- return INSTANCE;
+ return new StubAnalytics(application);
}
private final Tracker mTracker = new StubTracker();
+ private boolean mOptOut = OptOutPreferenceHelper.ANALYTICS_OPT_OUT_DEFAULT_VALUE;
- private StubAnalytics() { }
+ private StubAnalytics(Context context) {
+ }
@Override
public Tracker getDefaultTracker() {
return mTracker;
}
+
+ @Override
+ public boolean isAppOptOut() {
+ return mOptOut;
+ }
+
+ @Override
+ public void setAppOptOut(boolean optOut) {
+ mOptOut = optOut;
+ }
}
diff --git a/src/com/android/tv/analytics/StubTracker.java b/src/com/android/tv/analytics/StubTracker.java
index f7efcb92..6e64ebca 100644
--- a/src/com/android/tv/analytics/StubTracker.java
+++ b/src/com/android/tv/analytics/StubTracker.java
@@ -19,7 +19,6 @@ package com.android.tv.analytics;
import android.support.annotation.VisibleForTesting;
import com.android.tv.TimeShiftManager;
-import com.android.tv.TvApplication;
import com.android.tv.data.Channel;
/**
@@ -31,7 +30,7 @@ public class StubTracker implements Tracker {
public void sendChannelCount(int browsableChannelCount, int totalChannelCount) { }
@Override
- public void sendConfigurationInfo(TvApplication.ConfigurationInfo info) { }
+ public void sendConfigurationInfo(ConfigurationInfo info) { }
@Override
public void sendMainStart() { }
@@ -97,9 +96,18 @@ public class StubTracker implements Tracker {
public void sendChannelNumberItemChosenByTimeout() { }
@Override
+ public void sendChannelVideoUnavailable(Channel channel, int reason) { }
+
+ @Override
public void sendAc3PassthroughCapabilities(boolean isSupported) { }
@Override
+ public void sendInputConnectionFailure(String inputId) { }
+
+ @Override
+ public void sendInputDisconnected(String inputId) { }
+
+ @Override
public void sendShowInputSelection() { }
@Override
diff --git a/src/com/android/tv/analytics/Tracker.java b/src/com/android/tv/analytics/Tracker.java
index 05638871..291fc9ce 100644
--- a/src/com/android/tv/analytics/Tracker.java
+++ b/src/com/android/tv/analytics/Tracker.java
@@ -17,7 +17,6 @@
package com.android.tv.analytics;
import com.android.tv.TimeShiftManager;
-import com.android.tv.TvApplication;
import com.android.tv.data.Channel;
/**
@@ -44,7 +43,7 @@ public interface Tracker {
*
* @param info the configuration info.
*/
- void sendConfigurationInfo(TvApplication.ConfigurationInfo info);
+ void sendConfigurationInfo(ConfigurationInfo info);
/**
* Sends tracking information for starting the MainActivity.
@@ -179,6 +178,11 @@ public interface Tracker {
void sendChannelNumberItemChosenByTimeout();
/**
+ * Sends tracking for the reason video is unavailable on a channel.
+ */
+ void sendChannelVideoUnavailable(Channel channel, int reason);
+
+ /**
* Sends HDMI AC3 passthrough capabilities.
*
* @param isSupported {@code true} if the feature is supported; otherwise {@code false}.
@@ -186,6 +190,22 @@ public interface Tracker {
void sendAc3PassthroughCapabilities(boolean isSupported);
/**
+ * Sends tracking for input a connection failure.
+ * <p><strong>WARNING</strong> callers must ensure no PII is included in the inputId.
+ *
+ * @param inputId the input the failure happened on
+ */
+ void sendInputConnectionFailure(String inputId);
+
+ /**
+ * Sends tracking for input disconnected.
+ * <p><strong>WARNING</strong> callers must ensure no PII is included in the inputId.
+ *
+ * @param inputId the input the failure happened on
+ */
+ void sendInputDisconnected(String inputId);
+
+ /**
* Sends tracking information for showing the input selection view.
*/
void sendShowInputSelection();
@@ -224,7 +244,7 @@ public interface Tracker {
/**
* Sends time shift action (pause, ff, etc).
*
- * @param actionId The label of the side panel
+ * @param actionId The {@link com.android.tv.TimeShiftManager.TimeShiftActionId}
*/
void sendTimeShiftAction(@TimeShiftManager.TimeShiftActionId int actionId);
}
diff --git a/src/com/android/tv/data/Channel.java b/src/com/android/tv/data/Channel.java
index 49244c14..7901a01f 100644
--- a/src/com/android/tv/data/Channel.java
+++ b/src/com/android/tv/data/Channel.java
@@ -25,6 +25,7 @@ import android.media.tv.TvContract;
import android.media.tv.TvInputInfo;
import android.net.Uri;
import android.os.Build;
+import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
@@ -198,6 +199,7 @@ public final class Channel {
return mDisplayNumber;
}
+ @Nullable
public String getDisplayName() {
return mDisplayName;
}
diff --git a/src/com/android/tv/data/ChannelDataManager.java b/src/com/android/tv/data/ChannelDataManager.java
index 2325952f..067f2583 100644
--- a/src/com/android/tv/data/ChannelDataManager.java
+++ b/src/com/android/tv/data/ChannelDataManager.java
@@ -19,6 +19,8 @@ package com.android.tv.data;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
import android.database.ContentObserver;
import android.media.tv.TvContract;
import android.media.tv.TvContract.Channels;
@@ -34,6 +36,8 @@ import android.util.MutableInt;
import com.android.tv.analytics.Tracker;
import com.android.tv.common.WeakHandler;
import com.android.tv.util.AsyncDbTask;
+import com.android.tv.util.CollectionUtils;
+import com.android.tv.util.PermissionUtils;
import com.android.tv.util.RecurringRunner;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
@@ -60,6 +64,7 @@ public class ChannelDataManager {
private static final int MSG_UPDATE_CHANNELS = 1000;
private static final long SEND_CHANNEL_STATUS_INTERVAL_MS = TimeUnit.DAYS.toMillis(1);
+ private static final String SHARED_PREF_BROWSABLE = "browsable_shared_preference";
private final Context mContext;
private final TvInputManagerHelper mInputManager;
@@ -71,7 +76,7 @@ public class ChannelDataManager {
private RecurringRunner mRecurringRunner;
private final Tracker mTracker;
- private final Set<Listener> mListeners = new HashSet<>();
+ private final Set<Listener> mListeners = CollectionUtils.createSmallSet();
private final Map<Long, ChannelWrapper> mChannelWrapperMap = new HashMap<>();
private final Map<String, MutableInt> mChannelCountMap = new HashMap<>();
private final Channel.DefaultComparator mChannelComparator;
@@ -83,6 +88,8 @@ public class ChannelDataManager {
private final ContentResolver mContentResolver;
private final ContentObserver mChannelObserver;
+ private final boolean mStoreBrowsableInSharedPreferences;
+ private final SharedPreferences mBrowsableSharedPreferences;
private final TvInputCallback mTvInputCallback = new TvInputCallback() {
@Override
@@ -134,19 +141,19 @@ public class ChannelDataManager {
public ChannelDataManager(Context context, TvInputManagerHelper inputManager,
Tracker tracker) {
- this(context, inputManager, tracker, context.getContentResolver(), Looper.myLooper());
+ this(context, inputManager, tracker, context.getContentResolver());
}
@VisibleForTesting
ChannelDataManager(Context context, TvInputManagerHelper inputManager, Tracker tracker,
- ContentResolver contentResolver, Looper looper) {
+ ContentResolver contentResolver) {
mContext = context;
mInputManager = inputManager;
mContentResolver = contentResolver;
mChannelComparator = new Channel.DefaultComparator(context, inputManager);
// Detect duplicate channels while sorting.
mChannelComparator.setDetectDuplicatesEnabled(true);
- mHandler = new ChannelDataManagerHandler(looper, this);
+ mHandler = new ChannelDataManagerHandler(this);
mChannelObserver = new ContentObserver(mHandler) {
@Override
public void onChange(boolean selfChange) {
@@ -158,6 +165,9 @@ public class ChannelDataManager {
mTracker = tracker;
mRecurringRunner = new RecurringRunner(mContext, SEND_CHANNEL_STATUS_INTERVAL_MS,
new SendChannelStatusRunnable());
+ mStoreBrowsableInSharedPreferences = !PermissionUtils.hasAccessAllEpg(mContext);
+ mBrowsableSharedPreferences = context.getSharedPreferences(SHARED_PREF_BROWSABLE,
+ Context.MODE_PRIVATE);
}
@VisibleForTesting
@@ -185,6 +195,7 @@ public class ChannelDataManager {
* Stops the manager. It clears manager states and runs pending DB operations. Added listeners
* aren't automatically removed by this method.
*/
+ @VisibleForTesting
public void stop() {
if (!mStarted) {
return;
@@ -413,11 +424,22 @@ public class ChannelDataManager {
channelWrapper.mBrowsableInDb = channelWrapper.mChannel.isBrowsable();
}
String column = TvContract.Channels.COLUMN_BROWSABLE;
- if (browsableIds.size() != 0) {
- updateOneColumnValue(column, 1, browsableIds);
- }
- if (unbrowsableIds.size() != 0) {
- updateOneColumnValue(column, 0, unbrowsableIds);
+ if (mStoreBrowsableInSharedPreferences) {
+ Editor editor = mBrowsableSharedPreferences.edit();
+ for (Long id : browsableIds) {
+ editor.putBoolean(getBrowsableKey(getChannel(id)), true);
+ }
+ for (Long id : unbrowsableIds) {
+ editor.putBoolean(getBrowsableKey(getChannel(id)), false);
+ }
+ editor.apply();
+ } else {
+ if (browsableIds.size() != 0) {
+ updateOneColumnValue(column, 1, browsableIds);
+ }
+ if (unbrowsableIds.size() != 0) {
+ updateOneColumnValue(column, 0, unbrowsableIds);
+ }
}
mBrowsableUpdateChannelIds.clear();
@@ -507,7 +529,7 @@ public class ChannelDataManager {
}
private class ChannelWrapper {
- final Set<ChannelListener> mChannelListeners = new HashSet<>();
+ final Set<ChannelListener> mChannelListeners = CollectionUtils.createSmallSet();
final Channel mChannel;
boolean mBrowsableInDb;
boolean mLockedInDb;
@@ -561,7 +583,17 @@ public class ChannelDataManager {
boolean channelAdded = false;
boolean channelUpdated = false;
boolean channelRemoved = false;
+ Map<String, ?> deletedBrowsableMap = null;
+ if (mStoreBrowsableInSharedPreferences) {
+ deletedBrowsableMap = new HashMap<>(mBrowsableSharedPreferences.getAll());
+ }
for (Channel channel : channels) {
+ if (mStoreBrowsableInSharedPreferences) {
+ String browsableKey = getBrowsableKey(channel);
+ channel.setBrowsable(mBrowsableSharedPreferences.getBoolean(browsableKey,
+ false));
+ deletedBrowsableMap.remove(browsableKey);
+ }
long channelId = channel.getId();
boolean newlyAdded = !removedChannelIds.remove(channelId);
ChannelWrapper channelWrapper;
@@ -591,6 +623,17 @@ public class ChannelDataManager {
}
}
}
+ if (mStoreBrowsableInSharedPreferences && !deletedBrowsableMap.isEmpty()
+ && PermissionUtils.hasReadTvListings(mContext)) {
+ // If hasReadTvListings(mContext) is false, the given channel list would
+ // empty. In this case, we skip the browsable data clean up process.
+ Editor editor = mBrowsableSharedPreferences.edit();
+ for (String key : deletedBrowsableMap.keySet()) {
+ if (DEBUG) Log.d(TAG, "remove key: " + key);
+ editor.remove(key);
+ }
+ editor.apply();
+ }
for (long id : removedChannelIds) {
ChannelWrapper channelWrapper = mChannelWrapperMap.remove(id);
@@ -639,6 +682,10 @@ public class ChannelDataManager {
*/
private void updateOneColumnValue(
final String columnName, final int columnValue, final List<Long> ids) {
+ if (!PermissionUtils.hasAccessAllEpg(mContext)) {
+ // TODO: support this feature for non-system LC app. b/23939816
+ return;
+ }
AsyncDbTask.execute(new Runnable() {
@Override
public void run() {
@@ -650,9 +697,13 @@ public class ChannelDataManager {
});
}
+ private String getBrowsableKey(Channel channel) {
+ return channel.getInputId() + "|" + channel.getId();
+ }
+
private static class ChannelDataManagerHandler extends WeakHandler<ChannelDataManager> {
- public ChannelDataManagerHandler(Looper looper, ChannelDataManager channelDataManager) {
- super(looper, channelDataManager);
+ public ChannelDataManagerHandler(ChannelDataManager channelDataManager) {
+ super(Looper.getMainLooper(), channelDataManager);
}
@Override
diff --git a/src/com/android/tv/data/ChannelLogoFetcher.java b/src/com/android/tv/data/ChannelLogoFetcher.java
index 166b1d87..5a549f83 100644
--- a/src/com/android/tv/data/ChannelLogoFetcher.java
+++ b/src/com/android/tv/data/ChannelLogoFetcher.java
@@ -30,6 +30,7 @@ import android.util.Log;
import com.android.tv.util.AsyncDbTask;
import com.android.tv.util.BitmapUtils;
import com.android.tv.util.BitmapUtils.ScaledBitmapInfo;
+import com.android.tv.util.PermissionUtils;
import java.io.BufferedReader;
import java.io.IOException;
@@ -78,6 +79,10 @@ public class ChannelLogoFetcher {
* The previous task is canceled and a new task starts.
*/
public static void startFetchingChannelLogos(Context context) {
+ if (!PermissionUtils.hasAccessAllEpg(context)) {
+ // TODO: support this feature for non-system LC app. b/23939816
+ return;
+ }
synchronized (sLock) {
stopFetchingChannelLogos();
if (DEBUG) Log.d(TAG, "Request to start fetching logos.");
@@ -201,6 +206,14 @@ public class ChannelLogoFetcher {
return null;
}
// Download the channel logo.
+ if (TextUtils.isEmpty(channel.getDisplayName())) {
+ if (DEBUG) {
+ Log.d(TAG, "The channel with ID (" + channel.getId()
+ + ") doesn't have the display name.");
+ }
+ sChannelIdBlackListSet.add(channel.getId());
+ continue;
+ }
String channelName = channel.getDisplayName().trim();
String logoUri = channelNameLogoUriMap.get(channelName);
if (TextUtils.isEmpty(logoUri)) {
@@ -216,16 +229,21 @@ public class ChannelLogoFetcher {
sb.append(splitName);
}
logoUri = channelNameLogoUriMap.get(sb.toString());
- if (DEBUG && TextUtils.isEmpty(logoUri)) {
- Log.d(TAG, "Can't find a logo URI for channel '" + sb.toString() + "'");
+ if (DEBUG) {
+ if (TextUtils.isEmpty(logoUri)) {
+ Log.d(TAG, "Can't find a logo URI for channel '" + sb.toString()
+ + "'");
+ }
}
}
if (TextUtils.isEmpty(logoUri)
&& splitNames[0].length() != channelName.length()) {
logoUri = channelNameLogoUriMap.get(splitNames[0]);
- if (DEBUG && TextUtils.isEmpty(logoUri)) {
- Log.d(TAG, "Can't find a logo URI for channel '" + splitNames[0]
- + "'");
+ if (DEBUG) {
+ if (TextUtils.isEmpty(logoUri)) {
+ Log.d(TAG, "Can't find a logo URI for channel '" + splitNames[0]
+ + "'");
+ }
}
}
}
diff --git a/src/com/android/tv/data/ChannelNumber.java b/src/com/android/tv/data/ChannelNumber.java
index 8e9c3cb2..59021609 100644
--- a/src/com/android/tv/data/ChannelNumber.java
+++ b/src/com/android/tv/data/ChannelNumber.java
@@ -101,22 +101,41 @@ public final class ChannelNumber implements Comparable<ChannelNumber> {
}
if (indexOfDelimiter < 0) {
ret.majorNumber = number;
- return ret;
+ if (!isInteger(ret.majorNumber)) {
+ return null;
+ }
+ } else {
+ ret.hasDelimiter = true;
+ ret.majorNumber = number.substring(0, indexOfDelimiter);
+ ret.minorNumber = number.substring(indexOfDelimiter + 1);
+ if (!isInteger(ret.majorNumber) || !isInteger(ret.minorNumber)) {
+ return null;
+ }
}
- ret.hasDelimiter = true;
- ret.majorNumber = number.substring(0, indexOfDelimiter);
- ret.minorNumber = number.substring(indexOfDelimiter + 1);
return ret;
}
public static int compare(String lhs, String rhs) {
- if (lhs == null && rhs == null) {
+ ChannelNumber lhsNumber = parseChannelNumber(lhs);
+ ChannelNumber rhsNumber = parseChannelNumber(rhs);
+ if (lhsNumber == null && rhsNumber == null) {
return 0;
- } else if (lhs == null /* && rhs != null */) {
+ } else if (lhsNumber == null /* && rhsNumber != null */) {
return -1;
- } else if (lhs != null && rhs == null) {
+ } else if (lhsNumber != null && rhsNumber == null) {
return 1;
}
- return parseChannelNumber(lhs).compareTo(parseChannelNumber(rhs));
+ return lhsNumber.compareTo(rhsNumber);
+ }
+
+ public static boolean isInteger(String string) {
+ try {
+ Integer.parseInt(string);
+ } catch(NumberFormatException e) {
+ return false;
+ } catch(NullPointerException e) {
+ return false;
+ }
+ return true;
}
}
diff --git a/src/com/android/tv/data/ProgramDataManager.java b/src/com/android/tv/data/ProgramDataManager.java
index c12e7094..3f527433 100644
--- a/src/com/android/tv/data/ProgramDataManager.java
+++ b/src/com/android/tv/data/ProgramDataManager.java
@@ -32,20 +32,21 @@ import android.util.LongSparseArray;
import android.util.LruCache;
import com.android.tv.BuildConfig;
-import com.android.tv.MainActivity.MemoryManageable;
import com.android.tv.util.AsyncDbTask;
import com.android.tv.util.Clock;
+import com.android.tv.util.CollectionUtils;
+import com.android.tv.util.MemoryManageable;
+import com.android.tv.util.MultiLongSparseArray;
+import com.android.tv.util.SoftPreconditions;
import com.android.tv.util.Utils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
-import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
-import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@@ -81,20 +82,21 @@ public class ProgramDataManager implements MemoryManageable {
private final Clock mClock;
private final ContentResolver mContentResolver;
private boolean mStarted;
- private long mProgramPrefetchUpdateWaitMs;
private ProgramsUpdateTask mProgramsUpdateTask;
private final LongSparseArray<UpdateCurrentProgramForChannelTask> mProgramUpdateTaskMap =
new LongSparseArray<>();
- private long mLastPrefetchTaskRunMs;
- private ProgramsPrefetchTask mProgramsPrefetchTask;
private final Map<Long, Program> mChannelIdCurrentProgramMap = new HashMap<>();
- private final Map<Long, List<OnCurrentProgramUpdatedListener>>
- mOnCurrentProgramUpdatedListenersMap = new HashMap<>();
+ private final MultiLongSparseArray<OnCurrentProgramUpdatedListener>
+ mChannelId2ProgramUpdatedListeners = new MultiLongSparseArray<>();
private final Handler mHandler;
- private final List<Listener> mListeners = new ArrayList<>();
+ private final Set<Listener> mListeners = CollectionUtils.createSmallSet();
private final ContentObserver mProgramObserver;
+ private boolean mPrefetchEnabled;
+ private long mProgramPrefetchUpdateWaitMs;
+ private long mLastPrefetchTaskRunMs;
+ private ProgramsPrefetchTask mProgramsPrefetchTask;
private Map<Long, ArrayList<Program>> mChannelIdProgramCache = new HashMap<>();
// Any program that ends prior to this time will be removed from the cache
@@ -123,11 +125,13 @@ public class ProgramDataManager implements MemoryManageable {
if (isProgramUpdatePaused()) {
return;
}
- // The delay time of an existing MSG_UPDATE_PREFETCH_PROGRAM could be quite long
- // up to PROGRAM_GUIDE_SNAP_TIME_MS. So we need to remove the existing message and
- // send MSG_UPDATE_PREFETCH_PROGRAM again.
- mHandler.removeMessages(MSG_UPDATE_PREFETCH_PROGRAM);
- mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM);
+ if (mPrefetchEnabled) {
+ // The delay time of an existing MSG_UPDATE_PREFETCH_PROGRAM could be quite long
+ // up to PROGRAM_GUIDE_SNAP_TIME_MS. So we need to remove the existing message and
+ // send MSG_UPDATE_PREFETCH_PROGRAM again.
+ mHandler.removeMessages(MSG_UPDATE_PREFETCH_PROGRAM);
+ mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM);
+ }
}
};
mProgramPrefetchUpdateWaitMs = PROGRAM_PREFETCH_UPDATE_WAIT_MS;
@@ -159,7 +163,9 @@ public class ProgramDataManager implements MemoryManageable {
// Should be called directly instead of posting MSG_UPDATE_CURRENT_PROGRAMS message
// to the handler. If not, another DB task can be executed before loading current programs.
handleUpdateCurrentPrograms();
- mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM);
+ if (mPrefetchEnabled) {
+ mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM);
+ }
mContentResolver.registerContentObserver(Programs.CONTENT_URI,
true, mProgramObserver);
}
@@ -168,6 +174,7 @@ public class ProgramDataManager implements MemoryManageable {
* Stops the manager. It clears manager states and runs pending DB operations. Added listeners
* aren't automatically removed by this method.
*/
+ @VisibleForTesting
public void stop() {
if (!mStarted) {
return;
@@ -219,12 +226,36 @@ public class ProgramDataManager implements MemoryManageable {
}
/**
+ * Enables or Disables program prefetch.
+ */
+ public void setPrefetchEnabled(boolean enable) {
+ if (mPrefetchEnabled == enable) {
+ return;
+ }
+ if (enable) {
+ mPrefetchEnabled = true;
+ mLastPrefetchTaskRunMs = 0;
+ if (mStarted) {
+ mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM);
+ }
+ } else {
+ mPrefetchEnabled = false;
+ cancelPrefetchTask();
+ mChannelIdProgramCache.clear();
+ mHandler.removeMessages(MSG_UPDATE_PREFETCH_PROGRAM);
+ }
+ }
+
+ /**
* Returns the programs for the given channel which ends after the given start time.
*
+ * <p> Prefetch should be enabled to call it.
+ *
* @return {@link List} with Programs. It may includes dummy program if the entry needs DB
* operations to get.
*/
public List<Program> getPrograms(long channelId, long startTime) {
+ SoftPreconditions.checkState(mPrefetchEnabled, TAG, "Prefetch is disabled.");
ArrayList<Program> cachedPrograms = mChannelIdProgramCache.get(channelId);
if (cachedPrograms == null) {
return Collections.emptyList();
@@ -267,13 +298,8 @@ public class ProgramDataManager implements MemoryManageable {
*/
public void addOnCurrentProgramUpdatedListener(
long channelId, OnCurrentProgramUpdatedListener listener) {
- List<OnCurrentProgramUpdatedListener> listeners =
- mOnCurrentProgramUpdatedListenersMap.get(channelId);
- if (listeners == null) {
- listeners = new ArrayList<>();
- mOnCurrentProgramUpdatedListenersMap.put(channelId, listeners);
- }
- listeners.add(listener);
+ mChannelId2ProgramUpdatedListeners
+ .put(channelId, listener);
}
/**
@@ -282,36 +308,28 @@ public class ProgramDataManager implements MemoryManageable {
*/
public void removeOnCurrentProgramUpdatedListener(
long channelId, OnCurrentProgramUpdatedListener listener) {
- List<OnCurrentProgramUpdatedListener> listeners =
- mOnCurrentProgramUpdatedListenersMap.get(channelId);
- if (listeners != null) {
- listeners.remove(listener);
- // Do not remove list from map although it's empty to reduce GC.
- // The unused list is removed in {@link #performTrimMemory} which is called when
- // the memory usage is high.
- }
+ mChannelId2ProgramUpdatedListeners
+ .remove(channelId, listener);
}
private void notifyCurrentProgramUpdate(long channelId, Program program) {
- List<OnCurrentProgramUpdatedListener> listeners =
- mOnCurrentProgramUpdatedListenersMap.get(channelId);
- if (listeners != null) {
- for (OnCurrentProgramUpdatedListener listener : listeners) {
- listener.onCurrentProgramUpdated(channelId, program);
+
+ for (OnCurrentProgramUpdatedListener listener : mChannelId2ProgramUpdatedListeners
+ .get(channelId)) {
+ listener.onCurrentProgramUpdated(channelId, program);
}
- }
- listeners = mOnCurrentProgramUpdatedListenersMap.get(Channel.INVALID_ID);
- if (listeners != null) {
- for (OnCurrentProgramUpdatedListener listener : listeners) {
- listener.onCurrentProgramUpdated(channelId, program);
+ for (OnCurrentProgramUpdatedListener listener : mChannelId2ProgramUpdatedListeners
+ .get(Channel.INVALID_ID)) {
+ listener.onCurrentProgramUpdated(channelId, program);
}
- }
}
private void updateCurrentProgram(long channelId, Program program) {
Program previousProgram = mChannelIdCurrentProgramMap.put(channelId, program);
if (!Objects.equals(program, previousProgram)) {
- removePreviousProgramsAndUpdateCurrentProgramInCache(channelId, program);
+ if (mPrefetchEnabled) {
+ removePreviousProgramsAndUpdateCurrentProgramInCache(channelId, program);
+ }
notifyCurrentProgramUpdate(channelId, program);
}
@@ -329,6 +347,7 @@ public class ProgramDataManager implements MemoryManageable {
private void removePreviousProgramsAndUpdateCurrentProgramInCache(
long channelId, Program currentProgram) {
+ SoftPreconditions.checkState(mPrefetchEnabled, TAG, "Prefetch is disabled.");
if (!Program.isValid(currentProgram)) {
return;
}
@@ -369,6 +388,11 @@ public class ProgramDataManager implements MemoryManageable {
}
break;
}
+ if (cachedPrograms.isEmpty()) {
+ // If all the cached programs finish before mPrefetchTimeRangeStartMs, the
+ // currentProgram would not have a chance to be inserted to the cache.
+ cachedPrograms.add(currentProgram);
+ }
mChannelIdProgramCache.put(channelId, cachedPrograms);
}
@@ -452,6 +476,8 @@ public class ProgramDataManager implements MemoryManageable {
if (DEBUG) {
Log.d(TAG, "Database is changed while querying. Will retry.");
}
+ } catch (SecurityException e) {
+ Log.d(TAG, "Security exception during program data query", e);
}
}
if (DEBUG) {
@@ -540,7 +566,9 @@ public class ProgramDataManager implements MemoryManageable {
removedChannelIds.remove(channelId);
}
for (Long channelId : removedChannelIds) {
- mChannelIdProgramCache.remove(channelId);
+ if (mPrefetchEnabled) {
+ mChannelIdProgramCache.remove(channelId);
+ }
mChannelIdCurrentProgramMap.remove(channelId);
notifyCurrentProgramUpdate(channelId, null);
}
@@ -623,8 +651,11 @@ public class ProgramDataManager implements MemoryManageable {
* Pause program update.
* Updating program data will result in UI refresh,
* but UI is fragile to handle it so we'd better disable it for a while.
+ *
+ * <p> Prefetch should be enabled to call it.
*/
public void setPauseProgramUpdate(boolean pauseProgramUpdate) {
+ SoftPreconditions.checkState(mPrefetchEnabled, TAG, "Prefetch is disabled.");
if (mPauseProgramUpdate && !pauseProgramUpdate) {
if (!mHandler.hasMessages(MSG_UPDATE_PREFETCH_PROGRAM)) {
// MSG_UPDATE_PRFETCH_PROGRAM can be empty
@@ -645,11 +676,16 @@ public class ProgramDataManager implements MemoryManageable {
* Sets program data prefetch time range.
* Any program data that ends before the start time will be removed from the cache later.
* Note that there's no limit for end time.
+ *
+ * <p> Prefetch should be enabled to call it.
*/
public void setPrefetchTimeRange(long startTimeMs) {
+ SoftPreconditions.checkState(mPrefetchEnabled, TAG, "Prefetch is disabled.");
if (mPrefetchTimeRangeStartMs > startTimeMs) {
- // TODO: Do not throw exception and simply refresh the cache.
- throw new IllegalArgumentException("");
+ // Fetch the programs immediately to re-create the cache.
+ if (!mHandler.hasMessages(MSG_UPDATE_PREFETCH_PROGRAM)) {
+ mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM);
+ }
}
mPrefetchTimeRangeStartMs = startTimeMs;
}
@@ -692,13 +728,6 @@ public class ProgramDataManager implements MemoryManageable {
@Override
public void performTrimMemory(int level) {
- // Removes unused listeners.
- Iterator<Entry<Long, List<OnCurrentProgramUpdatedListener>>> it =
- mOnCurrentProgramUpdatedListenersMap.entrySet().iterator();
- while (it.hasNext()) {
- if (it.next().getValue().isEmpty()) {
- it.remove();
- }
- }
+ mChannelId2ProgramUpdatedListeners.clearEmptyCache();
}
}
diff --git a/src/com/android/tv/data/WatchedHistoryManager.java b/src/com/android/tv/data/WatchedHistoryManager.java
new file mode 100644
index 00000000..13707ae6
--- /dev/null
+++ b/src/com/android/tv/data/WatchedHistoryManager.java
@@ -0,0 +1,323 @@
+package com.android.tv.data;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.MainThread;
+import android.support.annotation.VisibleForTesting;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Scanner;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A class to manage watched history.
+ *
+ * <p>When there is no access to watched table of TvProvider,
+ * this class is used to build up watched history and to compute recent channels.
+ */
+public class WatchedHistoryManager {
+ private final static String TAG = "WatchedHistoryManager";
+ private final boolean DEBUG = false;
+
+ private static final int MAX_HISTORY_SIZE = 10000;
+ private static final String SHARED_PREF_WATCHED_HISTORY = "watched_history_shared_preference";
+ private static final String PREF_KEY_LAST_INDEX = "last_index";
+ private static final long MIN_DURATION_MS = TimeUnit.SECONDS.toMillis(10);
+ private static final long RECENT_CHANNEL_THRESHOLD_MS = TimeUnit.MINUTES.toMillis(5);
+
+ private final List<WatchedRecord> mWatchedHistory = new ArrayList<>();
+ private final List<WatchedRecord> mPendingRecords = new ArrayList<>();
+ private long mLastIndex;
+ private boolean mStarted;
+ private boolean mLoaded;
+ private SharedPreferences mSharedPreferences;
+ private SharedPreferences.OnSharedPreferenceChangeListener mOnSharedPreferenceChangeListener =
+ new SharedPreferences.OnSharedPreferenceChangeListener() {
+ @Override
+ @MainThread
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
+ String key) {
+ if (key.equals(PREF_KEY_LAST_INDEX)) {
+ final long lastIndex = mSharedPreferences.getLong(PREF_KEY_LAST_INDEX, -1);
+ if (lastIndex <= mLastIndex) {
+ return;
+ }
+ // onSharedPreferenceChanged is always called in a main thread.
+ // onNewRecordAdded will be called in the same thread as the thread
+ // which created this instance.
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ for (long i = mLastIndex + 1; i <= lastIndex; ++i) {
+ WatchedRecord record = decode(
+ mSharedPreferences.getString(getSharedPreferencesKey(i),
+ null));
+ if (record != null) {
+ mWatchedHistory.add(record);
+ if (mListener != null) {
+ mListener.onNewRecordAdded(record);
+ }
+ }
+ }
+ mLastIndex = lastIndex;
+ }
+ });
+ }
+ }
+ };
+
+ private final Context mContext;
+ private Listener mListener;
+ private final int mMaxHistorySize;
+ private Handler mHandler;
+
+ public WatchedHistoryManager(Context context) {
+ this(context, MAX_HISTORY_SIZE);
+ }
+
+ @VisibleForTesting
+ WatchedHistoryManager(Context context, int maxHistorySize) {
+ mContext = context.getApplicationContext();
+ mMaxHistorySize = maxHistorySize;
+ if (Looper.myLooper() == null) {
+ mHandler = new Handler(Looper.getMainLooper());
+ } else {
+ mHandler = new Handler();
+ }
+ }
+
+ /**
+ * Starts the manager. It loads history data from {@link SharedPreferences}.
+ */
+ public void start() {
+ if (mStarted) {
+ return;
+ }
+ mStarted = true;
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ mSharedPreferences = mContext.getSharedPreferences(
+ SHARED_PREF_WATCHED_HISTORY, Context.MODE_PRIVATE);
+ mLastIndex = mSharedPreferences.getLong(PREF_KEY_LAST_INDEX, -1);
+ if (mLastIndex >= 0 && mLastIndex < mMaxHistorySize) {
+ for (int i = 0; i <= mLastIndex; ++i) {
+ WatchedRecord record =
+ decode(mSharedPreferences.getString(getSharedPreferencesKey(i),
+ null));
+ if (record != null) {
+ mWatchedHistory.add(record);
+ }
+ }
+ } else if (mLastIndex >= mMaxHistorySize) {
+ for (long i = mLastIndex - mMaxHistorySize + 1; i <= mLastIndex; ++i) {
+ WatchedRecord record = decode(mSharedPreferences.getString(
+ getSharedPreferencesKey(i), null));
+ if (record != null) {
+ mWatchedHistory.add(record);
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void params) {
+ mLoaded = true;
+ if (DEBUG) {
+ Log.d(TAG, "Loaded: size=" + mWatchedHistory.size() + " index=" + mLastIndex);
+ }
+ if (!mPendingRecords.isEmpty()) {
+ Editor editor = mSharedPreferences.edit();
+ for (WatchedRecord record : mPendingRecords) {
+ mWatchedHistory.add(record);
+ ++mLastIndex;
+ editor.putString(getSharedPreferencesKey(mLastIndex), encode(record));
+ }
+ editor.putLong(PREF_KEY_LAST_INDEX, mLastIndex).apply();
+ mPendingRecords.clear();
+ }
+ if (mListener != null) {
+ mListener.onLoadFinished();
+ }
+ mSharedPreferences.registerOnSharedPreferenceChangeListener(
+ mOnSharedPreferenceChangeListener);
+ }
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ @VisibleForTesting
+ public boolean isLoaded() {
+ return mLoaded;
+ }
+
+ /**
+ * Logs the record of the watched channel.
+ */
+ public void logChannelViewStop(Channel channel, long endTime, long duration) {
+ if (duration < MIN_DURATION_MS) {
+ return;
+ }
+ WatchedRecord record = new WatchedRecord(channel.getId(), endTime - duration, duration);
+ if (mLoaded) {
+ if (DEBUG) Log.d(TAG, "Log a watched record. " + record);
+ mWatchedHistory.add(record);
+ ++mLastIndex;
+ mSharedPreferences.edit()
+ .putString(getSharedPreferencesKey(mLastIndex), encode(record))
+ .putLong(PREF_KEY_LAST_INDEX, mLastIndex)
+ .apply();
+ if (mListener != null) {
+ mListener.onNewRecordAdded(record);
+ }
+ } else {
+ mPendingRecords.add(record);
+ }
+ }
+
+ /**
+ * Sets {@link Listener}.
+ */
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ /**
+ * Returns watched history in the ascending order of time. In other words, the first element
+ * is the oldest and the last element is the latest record.
+ */
+ public List<WatchedRecord> getWatchedHistory() {
+ return Collections.unmodifiableList(mWatchedHistory);
+ }
+
+ /**
+ * Returns the list of recently watched channels.
+ */
+ public List<Channel> buildRecentChannel(ChannelDataManager channelDataManager, int maxCount) {
+ List<Channel> list = new ArrayList<>();
+ Map<Long, Long> durationMap = new HashMap<>();
+ for (int i = mWatchedHistory.size() - 1; i >= 0; --i) {
+ WatchedRecord record = mWatchedHistory.get(i);
+ long channelId = record.channelId;
+ Channel channel = channelDataManager.getChannel(channelId);
+ if (channel == null || !channel.isBrowsable()) {
+ continue;
+ }
+ Long duration = durationMap.get(channelId);
+ if (duration == null) {
+ duration = 0l;
+ }
+ if (duration >= RECENT_CHANNEL_THRESHOLD_MS) {
+ continue;
+ }
+ if (list.isEmpty()) {
+ // We put the first recent channel regardless of RECENT_CHANNEL_THREASHOLD.
+ // It has the similar functionality as the previous channel in a usual remote
+ // controller.
+ list.add(channel);
+ durationMap.put(channelId, RECENT_CHANNEL_THRESHOLD_MS);
+ } else {
+ duration += record.duration;
+ durationMap.put(channelId, duration);
+ if (duration >= RECENT_CHANNEL_THRESHOLD_MS) {
+ list.add(channel);
+ }
+ }
+ if (list.size() >= maxCount) {
+ break;
+ }
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Build recent channel");
+ for (Channel channel : list) {
+ Log.d(TAG, "recent channel: " + channel);
+ }
+ }
+ return list;
+ }
+
+ @VisibleForTesting
+ WatchedRecord getRecord(int reverseIndex) {
+ return mWatchedHistory.get(mWatchedHistory.size() - 1 - reverseIndex);
+ }
+
+ @VisibleForTesting
+ WatchedRecord getRecordFromSharedPreferences(int reverseIndex) {
+ long lastIndex = mSharedPreferences.getLong(PREF_KEY_LAST_INDEX, -1);
+ long index = lastIndex - reverseIndex;
+ return decode(mSharedPreferences.getString(getSharedPreferencesKey(index), null));
+ }
+
+ private String getSharedPreferencesKey(long index) {
+ return Long.toString(index % mMaxHistorySize);
+ }
+
+ public static class WatchedRecord {
+ public final long channelId;
+ public final long watchedStartTime;
+ public final long duration;
+
+ WatchedRecord(long channelId, long watchedStartTime, long duration) {
+ this.channelId = channelId;
+ this.watchedStartTime = watchedStartTime;
+ this.duration = duration;
+ }
+
+ @Override
+ public String toString() {
+ return "WatchedRecord: id=" + channelId + ",watchedStartTime=" + watchedStartTime
+ + ",duration=" + duration;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof WatchedRecord) {
+ WatchedRecord that = (WatchedRecord) o;
+ return Objects.equals(channelId, that.channelId)
+ && Objects.equals(watchedStartTime, that.watchedStartTime)
+ && Objects.equals(duration, that.duration);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(channelId, watchedStartTime, duration);
+ }
+ }
+
+ @VisibleForTesting
+ String encode(WatchedRecord record) {
+ return record.channelId + " " + record.watchedStartTime + " " + record.duration;
+ }
+
+ @VisibleForTesting
+ WatchedRecord decode(String encodedString) {
+ try (Scanner scanner = new Scanner(encodedString)) {
+ long channelId = scanner.nextLong();
+ long watchedStartTime = scanner.nextLong();
+ long duration = scanner.nextLong();
+ return new WatchedRecord(channelId, watchedStartTime, duration);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ public interface Listener {
+ /**
+ * Called when history is loaded.
+ */
+ void onLoadFinished();
+ void onNewRecordAdded(WatchedRecord watchedRecord);
+ }
+}
diff --git a/src/com/android/tv/guide/ProgramGrid.java b/src/com/android/tv/guide/ProgramGrid.java
index 99da84b0..1339ddf8 100644
--- a/src/com/android/tv/guide/ProgramGrid.java
+++ b/src/com/android/tv/guide/ProgramGrid.java
@@ -16,20 +16,19 @@
package com.android.tv.guide;
-import com.android.tv.R;
-import com.android.tv.ui.OnRepeatedKeyInterceptListener;
-
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Rect;
import android.support.v17.leanback.widget.VerticalGridView;
import android.util.AttributeSet;
import android.util.Log;
-import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
+import com.android.tv.R;
+import com.android.tv.ui.OnRepeatedKeyInterceptListener;
+
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
diff --git a/src/com/android/tv/guide/ProgramGuide.java b/src/com/android/tv/guide/ProgramGuide.java
index 468f10e0..d4e4a99d 100644
--- a/src/com/android/tv/guide/ProgramGuide.java
+++ b/src/com/android/tv/guide/ProgramGuide.java
@@ -38,12 +38,12 @@ import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.View;
import android.view.View.MeasureSpec;
-import android.view.View.OnScrollChangeListener;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewTreeObserver;
import com.android.tv.ChannelTuner;
+import com.android.tv.Features;
import com.android.tv.MainActivity;
import com.android.tv.R;
import com.android.tv.analytics.DurationTimer;
@@ -53,8 +53,6 @@ import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.GenreItems;
import com.android.tv.data.ProgramDataManager;
import com.android.tv.ui.HardwareLayerAnimatorListenerAdapter;
-import com.android.tv.ui.OnRepeatedKeyInterceptListener;
-import com.android.tv.util.SystemProperties;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
@@ -214,7 +212,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
mSidePanelGridView.setWindowAlignmentOffsetPercent(
VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
// TODO: Remove this check when we ship TV with epg search enabled.
- if (SystemProperties.USE_EPG_SEARCH.getValue()) {
+ if (Features.EPG_SEARCH.isEnabled(mActivity)) {
mSearchOrb = (SearchOrbView) mContainer.findViewById(
R.id.program_guide_side_panel_search_orb);
mSearchOrb.setVisibility(View.VISIBLE);
diff --git a/src/com/android/tv/guide/ProgramItemView.java b/src/com/android/tv/guide/ProgramItemView.java
index 1babc255..57678c05 100644
--- a/src/com/android/tv/guide/ProgramItemView.java
+++ b/src/com/android/tv/guide/ProgramItemView.java
@@ -151,13 +151,13 @@ public class ProgramItemView extends TextView {
sItemPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_item_padding);
- ColorStateList programTitleColor = ColorStateList.valueOf(res.getColor(
+ ColorStateList programTitleColor = ColorStateList.valueOf(Utils.getColor(res,
R.color.program_guide_table_item_program_title_text_color));
- ColorStateList grayedOutProgramTitleColor = res.getColorStateList(
+ ColorStateList grayedOutProgramTitleColor = Utils.getColorStateList(res,
R.color.program_guide_table_item_grayed_out_program_text_color);
- ColorStateList episodeTitleColor = ColorStateList.valueOf(res.getColor(
+ ColorStateList episodeTitleColor = ColorStateList.valueOf(Utils.getColor(res,
R.color.program_guide_table_item_program_episode_title_text_color));
- ColorStateList grayedOutEpisodeTitleColor = ColorStateList.valueOf(res.getColor(
+ ColorStateList grayedOutEpisodeTitleColor = ColorStateList.valueOf(Utils.getColor(res,
R.color.program_guide_table_item_grayed_out_program_episode_title_text_color));
int programTitleSize = res.getDimensionPixelSize(
R.dimen.program_guide_table_item_program_title_font_size);
diff --git a/src/com/android/tv/guide/ProgramManager.java b/src/com/android/tv/guide/ProgramManager.java
index fde903d1..216fcf3c 100644
--- a/src/com/android/tv/guide/ProgramManager.java
+++ b/src/com/android/tv/guide/ProgramManager.java
@@ -23,6 +23,7 @@ import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.GenreItems;
import com.android.tv.data.Program;
import com.android.tv.data.ProgramDataManager;
+import com.android.tv.util.CollectionUtils;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
@@ -30,6 +31,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
@@ -163,43 +165,53 @@ public class ProgramManager {
// Should be matched with mSelectedGenreId always.
private List<Channel> mFilteredChannels = mChannels;
- private final List<Listener> mListeners = new ArrayList<>();
- private final List<TableEntriesUpdatedListener>
- mTableEntriesUpdatedListeners = new ArrayList<>();
+ private final Set<Listener> mListeners = CollectionUtils.createSmallSet();
+ private final Set<TableEntriesUpdatedListener> mTableEntriesUpdatedListeners = CollectionUtils
+ .createSmallSet();
+
+ private final ChannelDataManager.Listener mChannelDataManagerListener =
+ new ChannelDataManager.Listener() {
+ @Override
+ public void onLoadFinished() {
+ updateChannels(true, false);
+ }
+
+ @Override
+ public void onChannelListUpdated() {
+ updateChannels(true, false);
+ }
+
+ @Override
+ public void onChannelBrowsableChanged() {
+ updateChannels(true, false);
+ }
+ };
+
+ private final ProgramDataManager.Listener mProgramDataManagerListener =
+ new ProgramDataManager.Listener() {
+ @Override
+ public void onProgramUpdated() {
+ updateTableEntries(true, true);
+ }
+ };
public ProgramManager(TvInputManagerHelper tvInputManagerHelper,
ChannelDataManager channelDataManager,
ProgramDataManager programDataManager) {
mTvInputManagerHelper = tvInputManagerHelper;
mChannelDataManager = channelDataManager;
- mChannelDataManager.addListener(new ChannelDataManager.Listener() {
- @Override
- public void onLoadFinished() {
- updateChannels(true, false);
- }
-
- @Override
- public void onChannelListUpdated() {
- updateChannels(true, false);
- }
-
- @Override
- public void onChannelBrowsableChanged() {
- updateChannels(true, false);
- }
- });
-
mProgramDataManager = programDataManager;
- mProgramDataManager.addListener(new ProgramDataManager.Listener() {
- @Override
- public void onProgramUpdated() {
- updateTableEntries(true, true);
- }
- });
}
public void programGuideVisibilityChanged(boolean visible) {
mProgramDataManager.setPauseProgramUpdate(visible);
+ if (visible) {
+ mChannelDataManager.addListener(mChannelDataManagerListener);
+ mProgramDataManager.addListener(mProgramDataManagerListener);
+ } else {
+ mChannelDataManager.removeListener(mChannelDataManagerListener);
+ mProgramDataManager.removeListener(mProgramDataManagerListener);
+ }
}
/**
diff --git a/src/com/android/tv/guide/ProgramTableAdapter.java b/src/com/android/tv/guide/ProgramTableAdapter.java
index df764668..cd0e9611 100644
--- a/src/com/android/tv/guide/ProgramTableAdapter.java
+++ b/src/com/android/tv/guide/ProgramTableAdapter.java
@@ -101,13 +101,13 @@ public class ProgramTableAdapter extends
R.string.program_title_for_no_information);
mProgramTitleForBlockedChannel = res.getString(
R.string.program_title_for_blocked_channel);
- mChannelTextColor = res.getColor(
+ mChannelTextColor = Utils.getColor(res,
R.color.program_guide_table_header_column_channel_number_text_color);
- mChannelBlockedTextColor = res.getColor(R.color
- .program_guide_table_header_column_channel_number_blocked_text_color);
- mDetailTextColor = res.getColor(
+ mChannelBlockedTextColor = Utils.getColor(res,
+ R.color.program_guide_table_header_column_channel_number_blocked_text_color);
+ mDetailTextColor = Utils.getColor(res,
R.color.program_guide_table_detail_title_text_color);
- mDetailGrayedTextColor = res.getColor(
+ mDetailGrayedTextColor = Utils.getColor(res,
R.color.program_guide_table_detail_title_grayed_text_color);
mAnimationDuration =
res.getInteger(R.integer.program_guide_table_detail_fade_anim_duration);
@@ -117,7 +117,7 @@ public class ProgramTableAdapter extends
int episodeTitleSize = res.getDimensionPixelSize(
R.dimen.program_guide_table_detail_episode_title_text_size);
ColorStateList episodeTitleColor = ColorStateList.valueOf(
- res.getColor(R.color.program_guide_table_detail_episode_title_text_color));
+ Utils.getColor(res, R.color.program_guide_table_detail_episode_title_text_color));
mEpisodeTitleStyle = new TextAppearanceSpan(null, 0, episodeTitleSize,
episodeTitleColor, null);
diff --git a/src/com/android/tv/menu/AppLinkCardView.java b/src/com/android/tv/menu/AppLinkCardView.java
index 19f22cc1..54564403 100644
--- a/src/com/android/tv/menu/AppLinkCardView.java
+++ b/src/com/android/tv/menu/AppLinkCardView.java
@@ -38,6 +38,7 @@ import com.android.tv.R;
import com.android.tv.data.Channel;
import com.android.tv.util.BitmapUtils;
import com.android.tv.util.TvInputManagerHelper;
+import com.android.tv.util.Utils;
/**
* A view to render an app link card.
@@ -94,7 +95,7 @@ public class AppLinkCardView extends BaseCardView<Channel> implements Channel.Lo
R.dimen.card_meta_layout_height);
mExtendedTextViewCardHeight = getResources().getDimensionPixelOffset(
R.dimen.card_meta_layout_height_extended);
- mIconColorFilter = getResources().getColor(R.color.app_link_card_icon_color_filter, null);
+ mIconColorFilter = Utils.getColor(getResources(), R.color.app_link_card_icon_color_filter);
}
/**
@@ -107,7 +108,7 @@ public class AppLinkCardView extends BaseCardView<Channel> implements Channel.Lo
@Override
public void onBind(Channel channel, boolean selected) {
if (DEBUG) {
- Log.d(TAG, "onBind(channel=" + channel.getDisplayName() + ", selected=" + selected
+ Log.d(TAG, "onBind(channelName=" + channel.getDisplayName() + ", selected=" + selected
+ ")");
}
mChannel = channel;
@@ -241,27 +242,26 @@ public class AppLinkCardView extends BaseCardView<Channel> implements Channel.Lo
}
// Try to set the card image with following order:
- // 1) Provided poster art image, 2) Activity banner, 3) Application banner,
- // 4) Activity logo, 5) Application logo, and 6) default image.
+ // 1) Provided poster art image, 2) Activity banner, 3) Activity icon, 4) Application banner,
+ // 5) Application icon, and 6) default image.
private void setCardImageWithBanner(ApplicationInfo appInfo) {
Drawable banner = null;
try {
banner = mPackageManager.getActivityBanner(mIntent);
+ if (banner == null) {
+ banner = mPackageManager.getActivityIcon(mIntent);
+ }
} catch (PackageManager.NameNotFoundException e) {
// do nothing.
}
- if (banner == null && appInfo != null && appInfo.banner != 0) {
- banner = mPackageManager.getApplicationBanner(appInfo);
- }
- if (banner == null) {
- try {
- banner = mPackageManager.getActivityLogo(mIntent);
- } catch (PackageManager.NameNotFoundException e) {
- // do nothing.
+
+ if (banner == null && appInfo != null) {
+ if (appInfo.banner != 0) {
+ banner = mPackageManager.getApplicationBanner(appInfo);
+ }
+ if (banner == null && appInfo.icon != 0) {
+ banner = mPackageManager.getApplicationIcon(appInfo);
}
- }
- if (banner == null && appInfo != null && appInfo.logo != 0) {
- banner = mPackageManager.getApplicationLogo(appInfo);
}
if (banner == null) {
@@ -285,7 +285,7 @@ public class AppLinkCardView extends BaseCardView<Channel> implements Channel.Lo
@Override
public void onGenerated(Palette palette) {
mMetaViewHolder.setBackgroundColor(palette.getDarkVibrantColor(
- getResources().getColor(R.color.channel_card_meta_background, null)));
+ Utils.getColor(getResources(), R.color.channel_card_meta_background)));
}
});
}
diff --git a/src/com/android/tv/menu/ChannelCardView.java b/src/com/android/tv/menu/ChannelCardView.java
index d6086910..ea4f31e9 100644
--- a/src/com/android/tv/menu/ChannelCardView.java
+++ b/src/com/android/tv/menu/ChannelCardView.java
@@ -96,13 +96,17 @@ public class ChannelCardView extends BaseCardView<Channel> implements
@Override
public void onBind(Channel channel, boolean selected) {
if (DEBUG) {
- Log.d(TAG, "onBind(channel=" + channel.getDisplayName() + ", selected=" + selected
+ Log.d(TAG, "onBind(channelName=" + channel.getDisplayName() + ", selected=" + selected
+ ")");
}
mChannel = channel;
mProgram = null;
- mChannelNumberNameView.setText(mChannel.getDisplayNumber() + " "
- + mChannel.getDisplayName());
+ if (TextUtils.isEmpty(mChannel.getDisplayName())) {
+ mChannelNumberNameView.setText(mChannel.getDisplayNumber());
+ } else {
+ mChannelNumberNameView.setText(mChannel.getDisplayNumber() + " "
+ + mChannel.getDisplayName());
+ }
mChannelNumberNameView.setVisibility(VISIBLE);
mImageView.setImageResource(R.drawable.ic_recent_thumbnail_default);
mImageView.setBackgroundResource(R.color.channel_card);
diff --git a/src/com/android/tv/menu/ChannelsPosterPrefetcher.java b/src/com/android/tv/menu/ChannelsPosterPrefetcher.java
index b008fa65..d576342c 100644
--- a/src/com/android/tv/menu/ChannelsPosterPrefetcher.java
+++ b/src/com/android/tv/menu/ChannelsPosterPrefetcher.java
@@ -27,7 +27,7 @@ import com.android.tv.common.WeakHandler;
import com.android.tv.data.Channel;
import com.android.tv.data.Program;
import com.android.tv.data.ProgramDataManager;
-import com.android.tv.util.Utils;
+import com.android.tv.util.SoftPreconditions;
import java.util.List;
@@ -68,8 +68,8 @@ public class ChannelsPosterPrefetcher {
* Start prefetching of program poster art of recommendation.
*/
public void prefetch() {
+ SoftPreconditions.checkState(!isCanceled, TAG, "Prefetch called after cancel was called.");
if (isCanceled) {
- Utils.engThrowElseWarn(TAG, "Prefetch called after cancel was called.");
return;
}
if (DEBUG) {
diff --git a/src/com/android/tv/menu/ChannelsRowAdapter.java b/src/com/android/tv/menu/ChannelsRowAdapter.java
index 8190c976..7a1eacf3 100644
--- a/src/com/android/tv/menu/ChannelsRowAdapter.java
+++ b/src/com/android/tv/menu/ChannelsRowAdapter.java
@@ -30,7 +30,6 @@ import com.android.tv.recommendation.Recommender;
import com.android.tv.util.SetupUtils;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
/**
@@ -187,7 +186,6 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channel>
}
}
}
-
- return Collections.unmodifiableList(channelList);
+ return channelList;
}
}
diff --git a/src/com/android/tv/menu/Menu.java b/src/com/android/tv/menu/Menu.java
index 323ce9c5..1f33bd67 100644
--- a/src/com/android/tv/menu/Menu.java
+++ b/src/com/android/tv/menu/Menu.java
@@ -132,7 +132,7 @@ public class Menu {
mHideAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
- mMenuView.onHide();
+ hideInternal();
}
});
mHideAnimator.setTarget(mMenuView);
@@ -222,11 +222,15 @@ public class Menu {
// mMenuView.onHide() is called in AnimatorListener.
mHideAnimator.end();
} else {
- mMenuView.onHide();
- mTracker.sendHideMenu(mVisibleTimer.reset());
- if (mOnMenuVisibilityChangeListener != null) {
- mOnMenuVisibilityChangeListener.onMenuVisibilityChange(false);
- }
+ hideInternal();
+ }
+ }
+
+ private void hideInternal() {
+ mMenuView.onHide();
+ mTracker.sendHideMenu(mVisibleTimer.reset());
+ if (mOnMenuVisibilityChangeListener != null) {
+ mOnMenuVisibilityChangeListener.onMenuVisibilityChange(false);
}
}
diff --git a/src/com/android/tv/menu/MenuLayoutManager.java b/src/com/android/tv/menu/MenuLayoutManager.java
index 187d0e14..265ad840 100644
--- a/src/com/android/tv/menu/MenuLayoutManager.java
+++ b/src/com/android/tv/menu/MenuLayoutManager.java
@@ -35,6 +35,7 @@ import android.view.ViewGroup.MarginLayoutParams;
import android.widget.TextView;
import com.android.tv.R;
+import com.android.tv.util.SoftPreconditions;
import com.android.tv.util.Utils;
import java.util.ArrayList;
@@ -312,16 +313,16 @@ public class MenuLayoutManager {
if (mSelectedPosition == position) {
return;
}
- if (position < 0 || position >= mMenuRowViews.size()) {
- String msg = "Invalid position: " + position;
- Utils.engThrowElseWarn(TAG, msg, new IllegalArgumentException(msg));
+ boolean indexValid = Utils.isIndexValid(mMenuRowViews, position);
+ SoftPreconditions.checkArgument(indexValid, TAG, "position " + position);
+ if (!indexValid) {
return;
}
- if (mSelectedPosition >= 0 && mSelectedPosition < mMenuRowViews.size()) {
+ if (Utils.isIndexValid(mMenuRowViews, mSelectedPosition)) {
mMenuRowViews.get(mSelectedPosition).onDeselected();
}
mSelectedPosition = position;
- if (mSelectedPosition >= 0 && mSelectedPosition < mMenuRowViews.size()) {
+ if (Utils.isIndexValid(mMenuRowViews, mSelectedPosition)) {
mMenuRowViews.get(mSelectedPosition).onSelected(false);
}
if (mMenuView.getVisibility() == View.VISIBLE) {
@@ -348,14 +349,15 @@ public class MenuLayoutManager {
if (mSelectedPosition == position) {
return;
}
- if (mSelectedPosition < 0 || mSelectedPosition >= mMenuRowViews.size()) {
- String msg = "No previous selection: " + mSelectedPosition;
- Utils.engThrowElseWarn(TAG, msg, new IllegalStateException(msg));
+ boolean oldIndexValid = Utils.isIndexValid(mMenuRowViews, mSelectedPosition);
+ SoftPreconditions
+ .checkState(oldIndexValid, TAG, "No previous selection: " + mSelectedPosition);
+ if (!oldIndexValid) {
return;
}
- if (position < 0 || position >= mMenuRowViews.size()) {
- String msg = "Invalid position: " + position;
- Utils.engThrowElseWarn(TAG, msg, new IllegalArgumentException(msg));
+ boolean newIndexValid = Utils.isIndexValid(mMenuRowViews, position);
+ SoftPreconditions.checkArgument(newIndexValid, TAG, "position " + position);
+ if (!newIndexValid) {
return;
}
if (mAnimatorSet != null) {
@@ -629,8 +631,8 @@ public class MenuLayoutManager {
if (mMenuView.getVisibility() != View.VISIBLE) {
int count = mMenuRowViews.size();
for (int i = 0; i < count; ++i) {
- mMenuRowViews.get(i).setVisibility(mMenuRows.get(i).isVisible() ? View.VISIBLE
- : View.GONE);
+ mMenuRowViews.get(i)
+ .setVisibility(mMenuRows.get(i).isVisible() ? View.VISIBLE : View.GONE);
}
return;
}
diff --git a/src/com/android/tv/menu/MenuRowView.java b/src/com/android/tv/menu/MenuRowView.java
index a6d8c990..6b3b6b5f 100644
--- a/src/com/android/tv/menu/MenuRowView.java
+++ b/src/com/android/tv/menu/MenuRowView.java
@@ -236,7 +236,11 @@ public abstract class MenuRowView extends LinearLayout {
mTitleView.setScaleX(mTitleViewScaleSelected);
mTitleView.setScaleY(mTitleViewScaleSelected);
}
+ // Making the content view visible will cause it to set a focus item
+ // So we store mLastFocusView and reset it
+ View lastFocusView = mLastFocusView;
mContentsView.setVisibility(VISIBLE);
+ mLastFocusView = lastFocusView;
}
/**
diff --git a/src/com/android/tv/menu/TvOptionsRowAdapter.java b/src/com/android/tv/menu/TvOptionsRowAdapter.java
index b7814fa5..1977dde1 100644
--- a/src/com/android/tv/menu/TvOptionsRowAdapter.java
+++ b/src/com/android/tv/menu/TvOptionsRowAdapter.java
@@ -29,6 +29,7 @@ import com.android.tv.ui.sidepanel.AboutFragment;
import com.android.tv.ui.sidepanel.ClosedCaptionFragment;
import com.android.tv.ui.sidepanel.DisplayModeFragment;
import com.android.tv.ui.sidepanel.MultiAudioFragment;
+import com.android.tv.util.PermissionUtils;
import com.android.tv.util.PipInputManager;
import java.util.ArrayList;
@@ -54,7 +55,13 @@ public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter {
mPositionPipAction = actionList.size() - 1;
actionList.add(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION);
actionList.add(MenuAction.CHANNEL_SOURCES_ACTION);
- actionList.add(MenuAction.PARENTAL_CONTROLS_ACTION);
+ if (PermissionUtils.hasModifyParentalControls(getMainActivity())) {
+ actionList.add(MenuAction.PARENTAL_CONTROLS_ACTION);
+ } else {
+ // Note: parental control is turned off, when MODIFY_PARENTAL_CONTROLS is not granted.
+ // But, we may be able to turn on channel lock feature regardless of the permission.
+ // It's TBD.
+ }
actionList.add(MenuAction.ABOUT_ACTION);
for (MenuAction action : actionList) {
diff --git a/src/com/android/tv/onboarding/AppOverviewFragment.java b/src/com/android/tv/onboarding/AppOverviewFragment.java
new file mode 100644
index 00000000..3427b122
--- /dev/null
+++ b/src/com/android/tv/onboarding/AppOverviewFragment.java
@@ -0,0 +1,106 @@
+/*
+ * 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.onboarding;
+
+import android.app.Fragment;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
+import android.support.v17.leanback.widget.GuidedAction;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.common.ui.setup.SetupGuidedStepFragment;
+import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
+
+import java.util.List;
+
+/**
+ * A fragment for channel source info/setup.
+ */
+public class AppOverviewFragment extends SetupMultiPaneFragment {
+ public static final int ACTION_SETUP_SOURCE = 1;
+ public static final int ACTION_GET_MORE_CHANNELS = 2;
+ public static final int ACTION_SETUP_USB_TUNER = 3;
+
+ public static final String KEY_AC3_SUPPORT = "key_ac3_support";
+
+ private boolean mAc3Supported;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = super.onCreateView(inflater, container, savedInstanceState);
+ Bundle bundle = getArguments();
+ mAc3Supported = bundle.getBoolean(KEY_AC3_SUPPORT);
+ return view;
+ }
+
+ @Override
+ protected Fragment getContentFragment() {
+ return new ContentFragment();
+ }
+
+ @Override
+ protected boolean needsDoneButton() {
+ return false;
+ }
+
+ // AppOverviewFragment should inherit OnboardingPageFragment for animation and command execution
+ // purpose. So child fragment which inherits GuidedStepFragment is needed.
+ private class ContentFragment extends SetupGuidedStepFragment {
+ @Override
+ public Guidance onCreateGuidance(Bundle savedInstanceState) {
+ String title = getString(R.string.app_overview_text);
+ String description = mAc3Supported
+ ? getString(R.string.app_overview_description_has_ac3)
+ : getString(R.string.app_overview_description_no_ac3);
+ return new Guidance(title, description, null, null);
+ }
+
+ @Override
+ public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
+ boolean hasTvInput = ((TvApplication) getActivity().getApplicationContext())
+ .getTvInputManagerHelper().getTunerTvInputSize() > 0;
+ Resources res = getResources();
+ if (hasTvInput) {
+ actions.add(new GuidedAction.Builder()
+ .id(ACTION_SETUP_SOURCE)
+ .title(res.getString(R.string.app_overview_action_text_setup_source))
+ .description(res.getString(
+ R.string.app_overview_action_description_setup_source))
+ .build());
+ }
+ actions.add(new GuidedAction.Builder()
+ .id(ACTION_GET_MORE_CHANNELS)
+ .title(res.getString(R.string.app_overview_action_text_play_store))
+ .description(res.getString(R.string.app_overview_action_description_play_store))
+ .build());
+ if (mAc3Supported) {
+ actions.add(new GuidedAction.Builder()
+ .id(ACTION_SETUP_USB_TUNER)
+ .title(res.getString(R.string.app_overview_action_text_usb_tuner))
+ .description(res.getString(
+ R.string.app_overview_action_description_usb_tuner))
+ .build());
+ }
+ }
+ }
+}
diff --git a/src/com/android/tv/onboarding/OnboardingActivity.java b/src/com/android/tv/onboarding/OnboardingActivity.java
new file mode 100644
index 00000000..4176a70f
--- /dev/null
+++ b/src/com/android/tv/onboarding/OnboardingActivity.java
@@ -0,0 +1,234 @@
+/*
+ * 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.onboarding;
+
+import android.app.Fragment;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.media.tv.TvInputInfo;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.android.tv.R;
+import com.android.tv.common.WeakHandler;
+import com.android.tv.common.ui.setup.SetupStep;
+import com.android.tv.common.ui.setup.SteppedSetupActivity;
+import com.android.tv.receiver.AudioCapabilitiesReceiver;
+import com.android.tv.util.OnboardingUtils;
+import com.android.tv.util.SetupUtils;
+import com.android.tv.util.SoftPreconditions;
+import com.android.tv.util.Utils;
+
+import java.util.concurrent.TimeUnit;
+
+public class OnboardingActivity extends SteppedSetupActivity {
+ private static final String TAG = "OnboardingActivity";
+
+ private static final String KEY_INTENT_AFTER_COMPLETION = "key_intent_after_completion";
+
+ private static final int MSG_CHECK_RECEIVED_AC3_CAPABILITY_NOTIFICATION = 1;
+ private static final long AC3_CHECK_WAIT_TIMEOUT = TimeUnit.SECONDS.toMillis(1);
+
+ private static final int REQUEST_CODE_SETUP_USB_TUNER = 1;
+
+ private Handler mHandler = new OnboardingActivityHandler(this);
+ private AudioCapabilitiesReceiver mAudioCapabilitiesReceiver;
+ private Boolean mAc3Supported;
+
+ /**
+ * Returns an intent to start {@link OnboardingActivity}.
+ *
+ * @param context context to create an intent. Should not be {@code null}.
+ * @param intentAfterCompletion intent which will be used to start a new activity when this
+ * activity finishes. Should not be {@code null}.
+ */
+ public static Intent buildIntent(@NonNull Context context,
+ @NonNull Intent intentAfterCompletion) {
+ return new Intent(context, OnboardingActivity.class)
+ .putExtra(OnboardingActivity.KEY_INTENT_AFTER_COMPLETION, intentAfterCompletion);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Register a receiver for HDMI audio plug and wait for the response.
+ mAudioCapabilitiesReceiver = new AudioCapabilitiesReceiver(this,
+ new AudioCapabilitiesReceiver.OnAc3PassthroughCapabilityChangeListener() {
+ @Override
+ public void onAc3PassthroughCapabilityChange(boolean capability) {
+ mAudioCapabilitiesReceiver.unregister();
+ mAudioCapabilitiesReceiver = null;
+ mHandler.removeMessages(MSG_CHECK_RECEIVED_AC3_CAPABILITY_NOTIFICATION);
+ mAc3Supported = capability;
+ startFirstStep();
+ }
+ });
+ mAudioCapabilitiesReceiver.register();
+ mHandler.sendEmptyMessageDelayed(MSG_CHECK_RECEIVED_AC3_CAPABILITY_NOTIFICATION,
+ AC3_CHECK_WAIT_TIMEOUT);
+ }
+
+ @Override
+ protected SetupStep onCreateInitialStep() {
+ if (mAc3Supported == null) {
+ return null;
+ }
+ if (OnboardingUtils.isFirstRun(this)) {
+ return new WelcomeStep(null);
+ }
+ return new AppOverviewStep(null);
+ }
+
+ @Override
+ protected void onDestroy() {
+ mHandler.removeCallbacksAndMessages(null);
+ if (mAudioCapabilitiesReceiver != null) {
+ mAudioCapabilitiesReceiver.unregister();
+ mAudioCapabilitiesReceiver = null;
+ }
+ super.onDestroy();
+ }
+
+ void startFirstStep() {
+ SoftPreconditions.checkNotNull(mAc3Supported, TAG,
+ "AC3 passthrough support check hasn't been completed yet.");
+ startInitialStep();
+ }
+
+
+ private static class OnboardingActivityHandler extends WeakHandler<OnboardingActivity> {
+ OnboardingActivityHandler(OnboardingActivity activity) {
+ // Should run on main thread because onAc3SupportChanged will be called on main thread.
+ super(Looper.getMainLooper(), activity);
+ }
+
+ @Override
+ protected void handleMessage(Message msg, OnboardingActivity activity) {
+ if (msg.what == MSG_CHECK_RECEIVED_AC3_CAPABILITY_NOTIFICATION) {
+ activity.mAudioCapabilitiesReceiver.unregister();
+ activity.mAudioCapabilitiesReceiver = null;
+ activity.startFirstStep();
+ }
+ }
+ }
+
+ void finishActivity() {
+ Intent intentForNextActivity = (Intent) getIntent().getParcelableExtra(
+ KEY_INTENT_AFTER_COMPLETION);
+ if (intentForNextActivity != null) {
+ startActivity(intentForNextActivity);
+ }
+ finish();
+ }
+
+ private class WelcomeStep extends SetupStep {
+ public WelcomeStep(@Nullable SetupStep previousStep) {
+ super(getFragmentManager(), previousStep);
+ }
+
+ @Override
+ public Fragment onCreateFragment() {
+ return new WelcomeFragment();
+ }
+
+ @Override
+ protected boolean needsToBeAddedToBackStack() {
+ return false;
+ }
+
+ @Override
+ protected boolean needsFragmentTransitionAnimation() {
+ return false;
+ }
+
+ @Override
+ public void executeAction(int actionId) {
+ switch (actionId) {
+ case WelcomeFragment.ACTION_NEXT:
+ OnboardingUtils.setFirstRunCompleted(OnboardingActivity.this);
+ if (!OnboardingUtils.areChannelsAvailable(OnboardingActivity.this)) {
+ startStep(new AppOverviewStep(this));
+ } else {
+ // TODO: Go to the correct step.
+ finishActivity();
+ }
+ break;
+ }
+ }
+ }
+
+ private class AppOverviewStep extends SetupStep {
+ public AppOverviewStep(@Nullable SetupStep previousStep) {
+ super(getFragmentManager(), previousStep);
+ }
+
+ @Override
+ public Fragment onCreateFragment() {
+ Fragment fragment = new AppOverviewFragment();
+ Bundle bundle = new Bundle();
+ bundle.putBoolean(AppOverviewFragment.KEY_AC3_SUPPORT, mAc3Supported);
+ fragment.setArguments(bundle);
+ return fragment;
+ }
+
+ @Override
+ protected boolean needsToBeAddedToBackStack() {
+ return false;
+ }
+
+ @Override
+ public void executeAction(int actionId) {
+ switch (actionId) {
+ case AppOverviewFragment.ACTION_SETUP_SOURCE:
+ startStep(new SetupSourcesStep(this));
+ break;
+ case AppOverviewFragment.ACTION_GET_MORE_CHANNELS:
+ // TODO: Implement this.
+ Toast.makeText(OnboardingActivity.this, "Not implemented yet.",
+ Toast.LENGTH_SHORT).show();
+ break;
+ }
+ }
+ }
+
+ private class SetupSourcesStep extends SetupStep {
+ public SetupSourcesStep(@Nullable SetupStep previousStep) {
+ super(getFragmentManager(), previousStep);
+ }
+
+ @Override
+ public Fragment onCreateFragment() {
+ return new SetupSourcesFragment();
+ }
+
+ @Override
+ public void executeAction(int actionId) {
+ switch (actionId) {
+ case SetupSourcesFragment.ACTION_DONE:
+ finishActivity();
+ break;
+ }
+ }
+ }
+}
diff --git a/src/com/android/tv/onboarding/PagingIndicator.java b/src/com/android/tv/onboarding/PagingIndicator.java
new file mode 100644
index 00000000..128fa996
--- /dev/null
+++ b/src/com/android/tv/onboarding/PagingIndicator.java
@@ -0,0 +1,235 @@
+/*
+ * 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.onboarding;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.animation.DecelerateInterpolator;
+
+import com.android.tv.R;
+import com.android.tv.util.Utils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A page indicator with dots.
+ */
+public class PagingIndicator extends View {
+ // attribute
+ private final int mDotDiameter;
+ private final int mDotRadius;
+ private final int mDotGap;
+ private int[] mDotCenterX;
+ private int mDotCenterY;
+
+ // state
+ private int mPageCount;
+ private int mCurrentPage;
+ private int mPreviousPage;
+
+ // drawing
+ private final Paint mUnselectedPaint;
+ private final Paint mSelectedPaint;
+ private final Paint mUnselectingPaint;
+ private final Paint mSelectingPaint;
+ private final AnimatorSet mAnimator = new AnimatorSet();
+
+ public PagingIndicator(Context context) {
+ this(context, null, 0);
+ }
+
+ public PagingIndicator(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public PagingIndicator(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ Resources res = getResources();
+ mDotRadius = res.getDimensionPixelSize(R.dimen.onboarding_dot_radius);
+ mDotDiameter = mDotRadius * 2;
+ mDotGap = res.getDimensionPixelSize(R.dimen.onboarding_dot_gap);
+ mUnselectedPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ // Deprecated method is used because this code should run on L platform.
+ int unselectedColor = Utils.getColor(res, R.color.onboarding_dot_unselected);
+ int selectedColor = Utils.getColor(res, R.color.onboarding_dot_selected);
+ mUnselectedPaint.setColor(unselectedColor);
+ mSelectedPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mSelectedPaint.setColor(selectedColor);
+ mUnselectingPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mSelectingPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ // Initialize animations.
+ int duration = res.getInteger(R.integer.setup_slide_anim_duration);
+ List<Animator> animators = new ArrayList<>();
+ animators.add(createColorAnimator(selectedColor, unselectedColor, duration,
+ mUnselectingPaint));
+ animators.add(createColorAnimator(unselectedColor, selectedColor, duration,
+ mSelectingPaint));
+ mAnimator.playTogether(animators);
+ }
+
+ private Animator createColorAnimator(int fromColor, int toColor, int duration,
+ final Paint paint) {
+ ValueAnimator animator = ValueAnimator.ofArgb(fromColor, toColor);
+ animator.setDuration(duration);
+ animator.setInterpolator(new DecelerateInterpolator());
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ invalidate();
+ }
+ });
+ animator.addUpdateListener(new AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animator) {
+ paint.setColor((int) animator.getAnimatedValue());
+ invalidate();
+ }
+ });
+ return animator;
+ }
+
+ /**
+ * Sets the page count.
+ */
+ public void setPageCount(int pages) {
+ mPageCount = pages;
+ calculateDotPositions();
+ setSelectedPage(0);
+ }
+
+ /**
+ * Called when the page has been selected.
+ */
+ public void onPageSelected(int pageIndex, boolean withAnimation) {
+ if (mAnimator.isStarted()) {
+ mAnimator.end();
+ }
+ if (withAnimation) {
+ mPreviousPage = mCurrentPage;
+ mAnimator.start();
+ }
+ setSelectedPage(pageIndex);
+ }
+
+ private void calculateDotPositions() {
+ int left = getPaddingLeft();
+ int top = getPaddingTop();
+ int right = getWidth() - getPaddingRight();
+ int requiredWidth = getRequiredWidth();
+ int startLeft = left + ((right - left - requiredWidth) / 2) + mDotRadius;
+ mDotCenterX = new int[mPageCount];
+ for (int i = 0; i < mPageCount; i++) {
+ mDotCenterX[i] = startLeft + i * (mDotDiameter + mDotGap);
+ }
+ mDotCenterY = top + mDotRadius;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int desiredHeight = getDesiredHeight();
+ int height;
+ switch (MeasureSpec.getMode(heightMeasureSpec)) {
+ case MeasureSpec.EXACTLY:
+ height = MeasureSpec.getSize(heightMeasureSpec);
+ break;
+ case MeasureSpec.AT_MOST:
+ height = Math.min(desiredHeight, MeasureSpec.getSize(heightMeasureSpec));
+ break;
+ case MeasureSpec.UNSPECIFIED:
+ default:
+ height = desiredHeight;
+ break;
+ }
+ int desiredWidth = getDesiredWidth();
+ int width;
+ switch (MeasureSpec.getMode(widthMeasureSpec)) {
+ case MeasureSpec.EXACTLY:
+ width = MeasureSpec.getSize(widthMeasureSpec);
+ break;
+ case MeasureSpec.AT_MOST:
+ width = Math.min(desiredWidth, MeasureSpec.getSize(widthMeasureSpec));
+ break;
+ case MeasureSpec.UNSPECIFIED:
+ default:
+ width = desiredWidth;
+ break;
+ }
+ setMeasuredDimension(width, height);
+ calculateDotPositions();
+ }
+
+ @Override
+ protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
+ setMeasuredDimension(width, height);
+ calculateDotPositions();
+ }
+
+ private int getDesiredHeight() {
+ return getPaddingTop() + mDotDiameter + getPaddingBottom();
+ }
+
+ private int getRequiredWidth() {
+ return mPageCount * mDotDiameter + (mPageCount - 1) * mDotGap;
+ }
+
+ private int getDesiredWidth() {
+ return getPaddingLeft() + getRequiredWidth() + getPaddingRight();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ drawUnselected(canvas);
+ if (mAnimator.isStarted()) {
+ drawAnimator(canvas);
+ } else {
+ drawSelected(canvas);
+ }
+ }
+
+ private void drawUnselected(Canvas canvas) {
+ for (int page = 0; page < mPageCount; page++) {
+ canvas.drawCircle(mDotCenterX[page], mDotCenterY, mDotRadius, mUnselectedPaint);
+ }
+ }
+
+ private void drawSelected(Canvas canvas) {
+ canvas.drawCircle(mDotCenterX[mCurrentPage], mDotCenterY, mDotRadius, mSelectedPaint);
+ }
+
+ private void drawAnimator(Canvas canvas) {
+ canvas.drawCircle(mDotCenterX[mPreviousPage], mDotCenterY, mDotRadius, mUnselectingPaint);
+ canvas.drawCircle(mDotCenterX[mCurrentPage], mDotCenterY, mDotRadius, mSelectingPaint);
+ }
+
+ private void setSelectedPage(int now) {
+ if (now == mCurrentPage) {
+ return;
+ }
+ mCurrentPage = now;
+ invalidate();
+ }
+}
diff --git a/src/com/android/tv/onboarding/SetupSourcesFragment.java b/src/com/android/tv/onboarding/SetupSourcesFragment.java
new file mode 100644
index 00000000..3572a209
--- /dev/null
+++ b/src/com/android/tv/onboarding/SetupSourcesFragment.java
@@ -0,0 +1,64 @@
+/*
+ * 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.onboarding;
+
+import android.app.Fragment;
+import android.os.Bundle;
+import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
+import android.support.v17.leanback.widget.GuidedAction;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.tv.R;
+import com.android.tv.common.ui.setup.SetupGuidedStepFragment;
+import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
+
+import java.util.List;
+
+/**
+ * A fragment for channel source info/setup.
+ */
+public class SetupSourcesFragment extends SetupMultiPaneFragment {
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = super.onCreateView(inflater, container, savedInstanceState);
+ setOnClickAction(view.findViewById(R.id.button_done), ACTION_DONE);
+ return view;
+ }
+
+ @Override
+ protected Fragment getContentFragment() {
+ return new ContentFragment();
+ }
+
+ private class ContentFragment extends SetupGuidedStepFragment {
+ @Override
+ public Guidance onCreateGuidance(Bundle savedInstanceState) {
+ String title = getString(R.string.setup_sources_text);
+ String description = getString(R.string.setup_sources_description);
+ return new Guidance(title, description, null, null);
+ }
+
+ @Override
+ public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
+ actions.add(new GuidedAction.Builder().id(ACTION_DONE).title("Done")
+ .description("Return to previous page").build());
+ }
+ }
+}
diff --git a/src/com/android/tv/onboarding/WelcomeFragment.java b/src/com/android/tv/onboarding/WelcomeFragment.java
new file mode 100644
index 00000000..baeb1b29
--- /dev/null
+++ b/src/com/android/tv/onboarding/WelcomeFragment.java
@@ -0,0 +1,101 @@
+/*
+ * 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.onboarding;
+
+import android.app.FragmentTransaction;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.Button;
+
+import com.android.tv.R;
+import com.android.tv.common.ui.setup.SetupFragment;
+
+/**
+ * A fragment for the onboarding screen.
+ */
+public class WelcomeFragment extends SetupFragment {
+ public static final int ACTION_NEXT = 1;
+
+ private int mNumPages;
+ private String[] mPageTitles;
+ private String[] mPageDescriptions;
+ private int mCurrentPageIndex;
+
+ private PagingIndicator mPageIndicator;
+ private Button mButton;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = super.onCreateView(inflater, container, savedInstanceState);
+ mPageTitles = getResources().getStringArray(R.array.welcome_page_titles);
+ mPageDescriptions = getResources().getStringArray(R.array.welcome_page_descriptions);
+ mNumPages = mPageTitles.length;
+ mCurrentPageIndex = 0;
+ mPageIndicator = (PagingIndicator) view.findViewById(R.id.page_indicator);
+ mPageIndicator.setPageCount(mNumPages);
+ mButton = (Button) view.findViewById(R.id.button);
+ mButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (mCurrentPageIndex == mNumPages - 1) {
+ onActionClick(ACTION_NEXT);
+ } else {
+ showPage(++mCurrentPageIndex);
+ }
+ }
+ });
+ showPage(mCurrentPageIndex);
+ return view;
+ }
+
+ @Override
+ protected int getLayoutResourceId() {
+ return R.layout.fragment_welcome;
+ }
+
+ /*
+ * Should return {@link SetupFragment} for the custom animations.
+ */
+ private SetupFragment getPage(int index) {
+ Bundle args = new Bundle();
+ args.putString(WelcomePageFragment.KEY_TITLE, mPageTitles[index]);
+ args.putString(WelcomePageFragment.KEY_DESCRIPTION, mPageDescriptions[index]);
+ SetupFragment fragment = new WelcomePageFragment();
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ private void showPage(int pageIndex) {
+ SetupFragment fragment = getPage(pageIndex);
+ FragmentTransaction ft = getFragmentManager().beginTransaction();
+ if (pageIndex != 0) {
+ ft.setCustomAnimations(SetupFragment.ANIM_ENTER,
+ SetupFragment.ANIM_EXIT);
+ }
+ ft.replace(R.id.page_container, fragment).commit();
+ if (pageIndex == mNumPages - 1) {
+ mButton.setText(R.string.welcome_start_button_text);
+ } else {
+ mButton.setText(R.string.welcome_next_button_text);
+ }
+ mPageIndicator.onPageSelected(pageIndex, pageIndex != 0);
+ }
+}
diff --git a/src/com/android/tv/onboarding/WelcomePageFragment.java b/src/com/android/tv/onboarding/WelcomePageFragment.java
new file mode 100644
index 00000000..3c6cd679
--- /dev/null
+++ b/src/com/android/tv/onboarding/WelcomePageFragment.java
@@ -0,0 +1,49 @@
+/*
+ * 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.onboarding;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.android.tv.R;
+import com.android.tv.common.ui.setup.SetupFragment;
+
+/**
+ * A fragment for the onboarding screen.
+ */
+public class WelcomePageFragment extends SetupFragment {
+ public static final String KEY_TITLE = "key_title";
+ public static final String KEY_DESCRIPTION = "key_description";
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = super.onCreateView(inflater, container, savedInstanceState);
+ Bundle args = getArguments();
+ ((TextView) view.findViewById(R.id.title)).setText(args.getString(KEY_TITLE));
+ ((TextView) view.findViewById(R.id.description)).setText(args.getString(KEY_DESCRIPTION));
+ return view;
+ }
+
+ @Override
+ protected int getLayoutResourceId() {
+ return R.layout.fragment_welcome_page;
+ }
+}
diff --git a/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java b/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java
index 949222a9..55d3cf3a 100644
--- a/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java
+++ b/src/com/android/tv/receiver/AudioCapabilitiesReceiver.java
@@ -22,15 +22,17 @@ import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.media.AudioFormat;
import android.media.AudioManager;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import com.android.tv.TvApplication;
+import com.android.tv.analytics.Analytics;
import com.android.tv.analytics.Tracker;
-import java.util.Arrays;
-
/**
- * Creates HDMI plug broadcast receiver, and reports AC3 passthrough capabilities
- * to Google Analytics. Call {@link #register} to start receiving notifications,
- * and {@link #unregister} to stop.
+ * Creates HDMI plug broadcast receiver, and reports AC3 passthrough capabilities to Google
+ * Analytics and listeners. Call {@link #register} to start receiving notifications, and
+ * {@link #unregister} to stop.
*/
public final class AudioCapabilitiesReceiver {
private static final String PREFS_NAME = "com.android.tv.audio_capabilities";
@@ -45,18 +47,25 @@ public final class AudioCapabilitiesReceiver {
private static final int REPORT_REVISION = 1;
private final Context mContext;
+ private final Analytics mAnalytics;
private final Tracker mTracker;
+ @Nullable
+ private final OnAc3PassthroughCapabilityChangeListener mListener;
private final BroadcastReceiver mReceiver = new HdmiAudioPlugBroadcastReceiver();
/**
* Constructs a new audio capabilities receiver.
*
* @param context context for registering to receive broadcasts
- * @param tracker tracker object used to upload capabilities info to Google Analytics
+ * @param listener listener which receives AC3 passthrough capability change notification
*/
- public AudioCapabilitiesReceiver(Context context, Tracker tracker) {
+ public AudioCapabilitiesReceiver(@NonNull Context context,
+ @Nullable OnAc3PassthroughCapabilityChangeListener listener) {
mContext = context;
- mTracker = tracker;
+ TvApplication tvApplication = (TvApplication) context.getApplicationContext();
+ mAnalytics = tvApplication.getAnalytics();
+ mTracker = tvApplication.getTracker();
+ mListener = listener;
}
public void register() {
@@ -74,23 +83,36 @@ public final class AudioCapabilitiesReceiver {
if (!action.equals(AudioManager.ACTION_HDMI_AUDIO_PLUG)) {
return;
}
- reportAudioCapabilities(intent.getIntArrayExtra(AudioManager.EXTRA_ENCODINGS));
+ boolean supported = false;
+ int[] supportedEncodings = intent.getIntArrayExtra(AudioManager.EXTRA_ENCODINGS);
+ if (supportedEncodings != null) {
+ for (int supportedEncoding : supportedEncodings) {
+ if (supportedEncoding == AudioFormat.ENCODING_AC3) {
+ supported = true;
+ break;
+ }
+ }
+ }
+ if (mListener != null) {
+ mListener.onAc3PassthroughCapabilityChange(supported);
+ }
+ if (!mAnalytics.isAppOptOut()) {
+ reportAudioCapabilities(supported);
+ }
}
}
- private void reportAudioCapabilities(int[] supportedEncodings) {
- boolean newVal = supportedEncodings != null
- && Arrays.binarySearch(supportedEncodings, AudioFormat.ENCODING_AC3) >= 0;
+ private void reportAudioCapabilities(boolean ac3Supported) {
boolean oldVal = getBoolean(SETTINGS_KEY_AC3_PASSTHRU_CAPABILITIES, false);
boolean reported = getBoolean(SETTINGS_KEY_AC3_PASSTHRU_REPORTED, false);
int revision = getInt(SETTINGS_KEY_AC3_REPORT_REVISION, 0);
// Send the value just once. But we send it again if the value changed, to include
// the case where users have switched TV device with different AC3 passthrough capabilities.
- if (!reported || oldVal != newVal || REPORT_REVISION > revision) {
- mTracker.sendAc3PassthroughCapabilities(newVal);
+ if (!reported || oldVal != ac3Supported || REPORT_REVISION > revision) {
+ mTracker.sendAc3PassthroughCapabilities(ac3Supported);
setBoolean(SETTINGS_KEY_AC3_PASSTHRU_REPORTED, true);
- setBoolean(SETTINGS_KEY_AC3_PASSTHRU_CAPABILITIES, newVal);
+ setBoolean(SETTINGS_KEY_AC3_PASSTHRU_CAPABILITIES, ac3Supported);
if (REPORT_REVISION > revision) {
setInt(SETTINGS_KEY_AC3_REPORT_REVISION, REPORT_REVISION);
}
@@ -116,4 +138,14 @@ public final class AudioCapabilitiesReceiver {
private void setInt(String key, int val) {
getSharedPreferences().edit().putInt(key, val).apply();
}
+
+ /**
+ * Listener notified when AC3 passthrough capability changes.
+ */
+ public interface OnAc3PassthroughCapabilityChangeListener {
+ /**
+ * Called when the AC3 passthrough capability changes.
+ */
+ void onAc3PassthroughCapabilityChange(boolean capability);
+ }
}
diff --git a/src/com/android/tv/receiver/BootCompletedReceiver.java b/src/com/android/tv/receiver/BootCompletedReceiver.java
new file mode 100644
index 00000000..61a9baa9
--- /dev/null
+++ b/src/com/android/tv/receiver/BootCompletedReceiver.java
@@ -0,0 +1,62 @@
+/*
+ * 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.receiver;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+
+import com.android.tv.Features;
+import com.android.tv.TvActivity;
+import com.android.tv.recommendation.NotificationService;
+import com.android.tv.util.OnboardingUtils;
+import com.android.tv.util.SetupUtils;
+
+/**
+ * Boot completed receiver. It's used to start the {@code NotificationService} for recommendation,
+ * grant permission to the TIS's and enable {@code TvActivity} if necessary.
+ */
+public class BootCompletedReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // Start {@link NotificationService}.
+ Intent notificationIntent = new Intent(context, NotificationService.class);
+ notificationIntent.setAction(NotificationService.ACTION_SHOW_RECOMMENDATION);
+ context.startService(notificationIntent);
+
+ // Grant permission to already set up packages after the system has finished booting.
+ SetupUtils.grantEpgPermissionToSetUpPackages(context);
+
+ // On-boarding experience.
+ if (Features.ONBOARDING_EXPERIENCE.isEnabled(context)) {
+ if (OnboardingUtils.isFirstBoot(context)) {
+ // Enable the application if this is the first run after the on-boarding experience
+ // is applied just in case when the app is disabled before.
+ PackageManager pm = context.getPackageManager();
+ ComponentName name = new ComponentName(context, TvActivity.class);
+ if (pm.getComponentEnabledSetting(name)
+ != PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
+ pm.setComponentEnabledSetting(name,
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 0);
+ }
+ OnboardingUtils.setFirstBootCompleted(context);
+ }
+ }
+ }
+}
diff --git a/src/com/android/tv/receiver/PackageIntentsReceiver.java b/src/com/android/tv/receiver/PackageIntentsReceiver.java
index fd9d7baf..a6ab858d 100644
--- a/src/com/android/tv/receiver/PackageIntentsReceiver.java
+++ b/src/com/android/tv/receiver/PackageIntentsReceiver.java
@@ -25,6 +25,7 @@ import android.media.tv.TvInputInfo;
import android.media.tv.TvInputManager;
import android.os.Handler;
+import com.android.tv.Features;
import com.android.tv.TvActivity;
import com.android.tv.util.SetupUtils;
@@ -42,7 +43,8 @@ public class PackageIntentsReceiver extends BroadcastReceiver {
private TvInputManager mTvInputManager;
private final Handler mHandler = new Handler();
private Runnable mOnPackageUpdatedRunnable;
- private boolean mPermissionGranted;
+ private PackageManager mPackageManager;
+ private ComponentName mTvActivityComponentName;
private void init(Context context) {
mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE);
@@ -51,27 +53,25 @@ public class PackageIntentsReceiver extends BroadcastReceiver {
mOnPackageUpdatedRunnable = new Runnable() {
@Override
public void run() {
- List<TvInputInfo> inputs = mTvInputManager.getTvInputList();
- // Enable the MainActivity only if there is at least one tuner type input.
- boolean enable = false;
- for (TvInputInfo input : inputs) {
- if (input.getType() == TvInputInfo.TYPE_TUNER) {
- enable = true;
- break;
+ if (!Features.ONBOARDING_EXPERIENCE.isEnabled(applicationContext)) {
+ List<TvInputInfo> inputs = mTvInputManager.getTvInputList();
+ // Enable the MainActivity only if there is at least one tuner type input.
+ boolean enable = false;
+ for (TvInputInfo input : inputs) {
+ if (input.getType() == TvInputInfo.TYPE_TUNER) {
+ enable = true;
+ break;
+ }
}
+ enableTvActivityWithinPackageManager(applicationContext, enable);
}
- enableTvActivityWithinPackageManager(applicationContext, enable);
SetupUtils.getInstance(applicationContext).onInputListUpdated(mTvInputManager);
}
};
- // Grant permission to already set up packages after the system has finished booting. (Note
- // that the PackageIntentsReceiver filters the ACTION_BOOT_COMPLETED action.)
- if (!mPermissionGranted) {
- SetupUtils.grantEpgPermissionToSetUpPackages(applicationContext);
- mPermissionGranted = true;
- }
+ mPackageManager = applicationContext.getPackageManager();
+ mTvActivityComponentName = new ComponentName(applicationContext, TvActivity.class);
}
@Override
@@ -82,8 +82,13 @@ public class PackageIntentsReceiver extends BroadcastReceiver {
mHandler.removeCallbacks(mOnPackageUpdatedRunnable);
mHandler.postDelayed(mOnPackageUpdatedRunnable, TV_INPUT_UPDATE_DELAY_MS);
+
}
+
+ /**
+ * Enables/Disables {@link TvActivity}.
+ */
private void enableTvActivityWithinPackageManager(Context context, boolean enable) {
PackageManager pm = context.getPackageManager();
ComponentName name = new ComponentName(context, TvActivity.class);
diff --git a/src/com/android/tv/recommendation/NotificationService.java b/src/com/android/tv/recommendation/NotificationService.java
index 835a3e53..c00a508e 100644
--- a/src/com/android/tv/recommendation/NotificationService.java
+++ b/src/com/android/tv/recommendation/NotificationService.java
@@ -141,11 +141,16 @@ public class NotificationService extends Service implements Recommender.Listener
getResources().getDimensionPixelOffset(R.dimen.notif_ch_logo_padding_bottom);
mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
- mTvInputManagerHelper = ((TvApplication) getApplicationContext()).getTvInputManagerHelper();
+ TvApplication application = ((TvApplication) getApplicationContext());
+ mTvInputManagerHelper = application.getTvInputManagerHelper();
mHandlerThread = new HandlerThread("tv notification");
mHandlerThread.start();
mHandler = new NotificationHandler(mHandlerThread.getLooper(), this);
mHandler.sendEmptyMessage(MSG_INITIALIZE_RECOMMENDER);
+
+ // Just called for early initialization.
+ application.getChannelDataManager();
+ application.getProgramDataManager();
}
private void handleInitializeRecommender() {
@@ -309,8 +314,10 @@ public class NotificationService extends Service implements Recommender.Listener
return false;
}
final Channel channel = cr.getChannel();
- if (DEBUG) Log.d(TAG, "sendNotification (" + channel.getDisplayName()
- + " notifyId=" + notificationId + ")");
+ if (DEBUG) {
+ Log.d(TAG, "sendNotification (channelName=" + channel.getDisplayName() + " notifyId="
+ + notificationId + ")");
+ }
Intent intent = new Intent(Intent.ACTION_VIEW, channel.getUri());
intent.putExtra(TUNE_PARAMS_RECOMMENDATION_TYPE, mRecommendationType);
final PendingIntent notificationIntent = PendingIntent.getActivity(this, 0, intent, 0);
@@ -359,13 +366,15 @@ public class NotificationService extends Service implements Recommender.Listener
// This callback will run on the main thread.
Bitmap largeIconBitmap = (channelLogo == null) ? posterArtBitmap
: overlayChannelLogo(channelLogo, posterArtBitmap);
+ String channelDisplayName = channel.getDisplayName();
Notification notification =
new Notification.Builder(NotificationService.this)
.setContentIntent(notificationIntent)
.setContentTitle(program.getTitle())
- .setContentText(inputDisplayName + " "
- + channel.getDisplayName())
- .setContentInfo(channel.getDisplayName())
+ .setContentText(inputDisplayName + " " +
+ (TextUtils.isEmpty(channelDisplayName)
+ ? channel.getDisplayNumber() : channelDisplayName))
+ .setContentInfo(channelDisplayName)
.setAutoCancel(true)
.setLargeIcon(largeIconBitmap)
.setSmallIcon(R.drawable.ic_launcher_s)
@@ -375,8 +384,8 @@ public class NotificationService extends Service implements Recommender.Listener
false)
.setSortKey(mRecommender.getChannelSortKey(channelId))
.build();
- notification.color =
- getResources().getColor(R.color.recommendation_card_background);
+ notification.color = Utils.getColor(getResources(),
+ R.color.recommendation_card_background);
if (!TextUtils.isEmpty(program.getThumbnailUri())) {
notification.extras.putString(Notification.EXTRA_BACKGROUND_IMAGE_URI,
program.getThumbnailUri());
diff --git a/src/com/android/tv/recommendation/RecentChannelEvaluator.java b/src/com/android/tv/recommendation/RecentChannelEvaluator.java
index c3482af9..e724f4ce 100644
--- a/src/com/android/tv/recommendation/RecentChannelEvaluator.java
+++ b/src/com/android/tv/recommendation/RecentChannelEvaluator.java
@@ -61,4 +61,4 @@ public class RecentChannelEvaluator extends Recommender.Evaluator {
}
return (maxScore > 0.0) ? maxScore : NOT_RECOMMENDED;
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/recommendation/RecommendationDataManager.java b/src/com/android/tv/recommendation/RecommendationDataManager.java
index 0f59e2bd..693380df 100644
--- a/src/com/android/tv/recommendation/RecommendationDataManager.java
+++ b/src/com/android/tv/recommendation/RecommendationDataManager.java
@@ -31,11 +31,14 @@ import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread;
import com.android.tv.common.WeakHandler;
import com.android.tv.data.Channel;
import com.android.tv.data.Program;
+import com.android.tv.data.WatchedHistoryManager;
+import com.android.tv.util.PermissionUtils;
import java.util.ArrayList;
import java.util.Collection;
@@ -46,7 +49,7 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
-public class RecommendationDataManager {
+public class RecommendationDataManager implements WatchedHistoryManager.Listener {
private static final String TAG = "RecommendationDataManager";
private static final UriMatcher sUriMatcher;
@@ -93,8 +96,9 @@ public class RecommendationDataManager {
private final Set<String> mInputs = new HashSet<>();
private final HandlerThread mHandlerThread;
-
private final Handler mHandler;
+ @Nullable
+ private WatchedHistoryManager mWatchedHistoryManager;
private final List<ListenerRecord> mListeners = new ArrayList<>();
@@ -257,12 +261,19 @@ public class RecommendationDataManager {
mCancelLoadTask = false;
mContext.getContentResolver().registerContentObserver(
TvContract.Channels.CONTENT_URI, true, mContentObserver);
- mContext.getContentResolver().registerContentObserver(
- TvContract.WatchedPrograms.CONTENT_URI, true, mContentObserver);
mHandler.obtainMessage(MSG_UPDATE_CHANNELS, TvContract.Channels.CONTENT_URI)
.sendToTarget();
- mHandler.obtainMessage(MSG_UPDATE_WATCH_HISTORY, TvContract.WatchedPrograms.CONTENT_URI)
- .sendToTarget();
+ if (!PermissionUtils.hasAccessWatchedHistory(mContext)) {
+ mWatchedHistoryManager = new WatchedHistoryManager(mContext);
+ mWatchedHistoryManager.setListener(this);
+ mWatchedHistoryManager.start();
+ } else {
+ mContext.getContentResolver().registerContentObserver(
+ TvContract.WatchedPrograms.CONTENT_URI, true, mContentObserver);
+ mHandler.obtainMessage(MSG_UPDATE_WATCH_HISTORY,
+ TvContract.WatchedPrograms.CONTENT_URI)
+ .sendToTarget();
+ }
mTvInputManager = (TvInputManager) mContext.getSystemService(Context.TV_INPUT_SERVICE);
mTvInputManager.registerCallback(mInternalCallback, mHandler);
for (TvInputInfo input : mTvInputManager.getTvInputList()) {
@@ -374,6 +385,41 @@ public class RecommendationDataManager {
}
}
+ private WatchedProgram convertFromWatchedHistoryManagerRecords(
+ WatchedHistoryManager.WatchedRecord watchedRecord) {
+ long endTime = watchedRecord.watchedStartTime + watchedRecord.duration;
+ Program program = new Program.Builder()
+ .setChannelId(watchedRecord.channelId)
+ .setTitle("")
+ .setStartTimeUtcMillis(watchedRecord.watchedStartTime)
+ .setEndTimeUtcMillis(endTime)
+ .build();
+ return new WatchedProgram(program, watchedRecord.watchedStartTime, endTime);
+ }
+
+ @Override
+ public void onLoadFinished() {
+ for (WatchedHistoryManager.WatchedRecord record
+ : mWatchedHistoryManager.getWatchedHistory()) {
+ updateChannelRecordFromWatchedProgram(
+ convertFromWatchedHistoryManagerRecords(record));
+ }
+ mHandler.sendEmptyMessage(MSG_NOTIFY_CHANNEL_RECORD_MAP_LOADED);
+ }
+
+ @Override
+ public void onNewRecordAdded(WatchedHistoryManager.WatchedRecord watchedRecord) {
+ ChannelRecord channelRecord = updateChannelRecordFromWatchedProgram(
+ convertFromWatchedHistoryManagerRecords(watchedRecord));
+ if (mChannelRecordMapLoaded && channelRecord != null) {
+ synchronized (sListenerLock) {
+ for (ListenerRecord l : mListeners) {
+ l.postNewWatchLog(channelRecord);
+ }
+ }
+ }
+ }
+
private WatchedProgram createWatchedProgramFromWatchedProgramCursor(Cursor cursor) {
// Have to initiate the indexes of WatchedProgram Columns.
if (mIndexWatchChannelId == -1) {
diff --git a/src/com/android/tv/search/DataManagerSearch.java b/src/com/android/tv/search/DataManagerSearch.java
new file mode 100644
index 00000000..4c85af67
--- /dev/null
+++ b/src/com/android/tv/search/DataManagerSearch.java
@@ -0,0 +1,254 @@
+/*
+ * 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.search;
+
+import android.content.Context;
+import android.content.Intent;
+import android.media.tv.TvContentRating;
+import android.media.tv.TvContract;
+import android.media.tv.TvContract.Programs;
+import android.media.tv.TvInputManager;
+import android.support.annotation.UiThread;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.tv.TvApplication;
+import com.android.tv.data.Channel;
+import com.android.tv.data.ChannelDataManager;
+import com.android.tv.data.Program;
+import com.android.tv.data.ProgramDataManager;
+import com.android.tv.search.LocalSearchProvider.SearchResult;
+import com.android.tv.util.MainThreadExecutor;
+import com.android.tv.util.Utils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+/**
+ * An implementation of {@link SearchInterface} to search query from {@link ChannelDataManager}
+ * and {@link ProgramDataManager}.
+ */
+public class DataManagerSearch implements SearchInterface {
+ private static final boolean DEBUG = false;
+ private static final String TAG = "TvProviderSearch";
+
+ private final Context mContext;
+ private final TvInputManager mTvInputManager;
+ private final ChannelDataManager mChannelDataManager;
+ private final ProgramDataManager mProgramDataManager;
+
+ DataManagerSearch(Context context) {
+ mContext = context;
+ mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE);
+ TvApplication application = (TvApplication) context.getApplicationContext();
+ mChannelDataManager = application.getChannelDataManager();
+ mProgramDataManager = application.getProgramDataManager();
+ }
+
+ @Override
+ public List<SearchResult> search(final String query, final int limit, final int action) {
+ Future<List<SearchResult>> future = MainThreadExecutor.getInstance()
+ .submit(new Callable<List<SearchResult>>() {
+ @Override
+ public List<SearchResult> call() throws Exception {
+ return searchFromDataManagers(query, limit, action);
+ }
+ });
+
+ try {
+ return future.get();
+ } catch (InterruptedException e) {
+ Thread.interrupted();
+ return Collections.EMPTY_LIST;
+ } catch (ExecutionException e) {
+ Log.w(TAG, "Error searching for " + query, e);
+ return Collections.EMPTY_LIST;
+ }
+ }
+
+ @UiThread
+ private List<SearchResult> searchFromDataManagers(String query, int limit, int action) {
+ List<SearchResult> results = new ArrayList<>();
+ if (!mChannelDataManager.isDbLoadFinished()) {
+ return results;
+ }
+ if (action == ACTION_TYPE_SWITCH_CHANNEL
+ || action == ACTION_TYPE_SWITCH_INPUT) {
+ // Voice search query should be handled by the a system TV app.
+ return results;
+ }
+ Set<Long> channelsFound = new HashSet<>();
+ List<Channel> channelList = mChannelDataManager.getBrowsableChannelList();
+ query = query.toLowerCase();
+ if (TextUtils.isDigitsOnly(query)) {
+ for (Channel channel : channelList) {
+ if (channelsFound.contains(channel.getId())) {
+ continue;
+ }
+ if (contains(channel.getDisplayNumber(), query)) {
+ addResult(results, channelsFound, channel, null);
+ }
+ if (results.size() >= limit) {
+ return results;
+ }
+ }
+ // TODO: recently watched channels may have higher priority.
+ }
+ for (Channel channel : channelList) {
+ if (channelsFound.contains(channel.getId())) {
+ continue;
+ }
+ if (contains(channel.getDisplayName(), query)
+ || contains(channel.getDescription(), query)) {
+ addResult(results, channelsFound, channel, null);
+ }
+ if (results.size() >= limit) {
+ return results;
+ }
+ }
+ for (Channel channel : channelList) {
+ if (channelsFound.contains(channel.getId())) {
+ continue;
+ }
+ Program program = mProgramDataManager.getCurrentProgram(channel.getId());
+ if (program == null) {
+ continue;
+ }
+ if (contains(program.getTitle(), query)
+ && !isRatingBlocked(program.getContentRatings())) {
+ addResult(results, channelsFound, channel, program);
+ }
+ if (results.size() >= limit) {
+ return results;
+ }
+ }
+ for (Channel channel : channelList) {
+ if (channelsFound.contains(channel.getId())) {
+ continue;
+ }
+ Program program = mProgramDataManager.getCurrentProgram(channel.getId());
+ if (program == null) {
+ continue;
+ }
+ if (contains(program.getDescription(), query)
+ && !isRatingBlocked(program.getContentRatings())) {
+ addResult(results, channelsFound, channel, program);
+ }
+ if (results.size() >= limit) {
+ return results;
+ }
+ }
+ return results;
+ }
+
+ // It assumes that query is already lower cases.
+ private boolean contains(String string, String query) {
+ return string != null && string.toLowerCase().contains(query);
+ }
+
+ /**
+ * If query is matched to channel, {@code program} should be null.
+ */
+ private void addResult(List<SearchResult> results, Set<Long> channelsFound, Channel channel,
+ Program program) {
+ if (program == null) {
+ program = mProgramDataManager.getCurrentProgram(channel.getId());
+ if (program != null && isRatingBlocked(program.getContentRatings())) {
+ program = null;
+ }
+ }
+
+ SearchResult result = new SearchResult();
+
+ long channelId = channel.getId();
+ result.channelId = channelId;
+ result.channelNumber = channel.getDisplayNumber();
+ if (program == null) {
+ result.title = channel.getDisplayName();
+ result.description = channel.getDescription();
+ result.imageUri = TvContract.buildChannelLogoUri(channelId).toString();
+ result.intentAction = Intent.ACTION_VIEW;
+ result.intentData = buildIntentData(channelId);
+ result.contentType = Programs.CONTENT_ITEM_TYPE;
+ result.isLive = true;
+ result.progressPercentage = LocalSearchProvider.PROGRESS_PERCENTAGE_HIDE;
+ } else {
+ result.title = program.getTitle();
+ result.description = buildProgramDescription(channel.getDisplayNumber(),
+ channel.getDisplayName(), program.getStartTimeUtcMillis(),
+ program.getEndTimeUtcMillis());
+ result.imageUri = program.getPosterArtUri();
+ result.intentAction = Intent.ACTION_VIEW;
+ result.intentData = buildIntentData(channelId);
+ result.contentType = Programs.CONTENT_ITEM_TYPE;
+ result.isLive = true;
+ result.videoWidth = program.getVideoWidth();
+ result.videoHeight = program.getVideoHeight();
+ result.duration = program.getDurationMillis();
+ result.progressPercentage = getProgressPercentage(
+ program.getStartTimeUtcMillis(), program.getEndTimeUtcMillis());
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Add a result : channel=" + channel + " program=" + program);
+ }
+ results.add(result);
+ channelsFound.add(channel.getId());
+ }
+
+ private String buildProgramDescription(String channelNumber, String channelName,
+ long programStartUtcMillis, long programEndUtcMillis) {
+ return Utils.getDurationString(mContext, programStartUtcMillis, programEndUtcMillis, false)
+ + System.lineSeparator() + channelNumber + " " + channelName;
+ }
+
+ private int getProgressPercentage(long startUtcMillis, long endUtcMillis) {
+ long current = System.currentTimeMillis();
+ if (startUtcMillis > current || endUtcMillis <= current) {
+ return LocalSearchProvider.PROGRESS_PERCENTAGE_HIDE;
+ }
+ return (int)(100 * (current - startUtcMillis) / (endUtcMillis - startUtcMillis));
+ }
+
+ private String buildIntentData(long channelId) {
+ return TvContract.buildChannelUri(channelId).buildUpon()
+ .appendQueryParameter(Utils.PARAM_SOURCE, SOURCE_TV_SEARCH)
+ .build().toString();
+ }
+
+ private boolean isRatingBlocked(TvContentRating[] ratings) {
+ if (ratings == null || ratings.length == 0
+ || !mTvInputManager.isParentalControlsEnabled()) {
+ return false;
+ }
+ for (TvContentRating rating : ratings) {
+ try {
+ if (mTvInputManager.isRatingBlocked(rating)) {
+ return true;
+ }
+ } catch (IllegalArgumentException e) {
+ // Do nothing.
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/com/android/tv/search/LocalSearchProvider.java b/src/com/android/tv/search/LocalSearchProvider.java
index e9cffa31..3cc21ace 100644
--- a/src/com/android/tv/search/LocalSearchProvider.java
+++ b/src/com/android/tv/search/LocalSearchProvider.java
@@ -25,6 +25,8 @@ import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
+import com.android.tv.util.PermissionUtils;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -60,13 +62,12 @@ public class LocalSearchProvider extends ContentProvider {
private static final String LIVE_CONTENTS = "1";
static final String SUGGEST_PARAMETER_ACTION = "action";
- static final int DEFAULT_SEARCH_ACTION = TvProviderSearch.ACTION_TYPE_AMBIGUOUS;
+ static final int DEFAULT_SEARCH_ACTION = SearchInterface.ACTION_TYPE_AMBIGUOUS;
- private TvProviderSearch mTvProviderSearch;
+ private SearchInterface mSearch;
@Override
public boolean onCreate() {
- mTvProviderSearch = new TvProviderSearch(getContext());
return true;
}
@@ -77,6 +78,11 @@ public class LocalSearchProvider extends ContentProvider {
Log.d(TAG, "query(" + uri + ", " + Arrays.toString(projection) + ", " + selection + ", "
+ Arrays.toString(selectionArgs) + ", " + sortOrder + ")");
}
+ if (PermissionUtils.hasAccessAllEpg(getContext())) {
+ mSearch = new TvProviderSearch(getContext());
+ } else {
+ mSearch = new DataManagerSearch(getContext());
+ }
String query = uri.getLastPathSegment();
int limit = DEFAULT_SEARCH_LIMIT;
int action = DEFAULT_SEARCH_ACTION;
@@ -88,7 +94,7 @@ public class LocalSearchProvider extends ContentProvider {
}
List<SearchResult> results = new ArrayList<>();
if (!TextUtils.isEmpty(query)) {
- results.addAll(mTvProviderSearch.search(query, limit, action));
+ results.addAll(mSearch.search(query, limit, action));
}
return createSuggestionsCursor(results);
}
@@ -165,4 +171,4 @@ public class LocalSearchProvider extends ContentProvider {
", title: " + title;
}
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/search/ProgramGuideSearchFragment.java b/src/com/android/tv/search/ProgramGuideSearchFragment.java
index bb6cdc69..7d6efcb3 100644
--- a/src/com/android/tv/search/ProgramGuideSearchFragment.java
+++ b/src/com/android/tv/search/ProgramGuideSearchFragment.java
@@ -42,6 +42,7 @@ import android.view.ViewGroup;
import com.android.tv.MainActivity;
import com.android.tv.R;
import com.android.tv.util.ImageLoader;
+import com.android.tv.util.PermissionUtils;
import java.util.List;
@@ -129,7 +130,7 @@ public class ProgramGuideSearchFragment extends SearchFragment {
private final ArrayObjectAdapter mResultAdapter =
new ArrayObjectAdapter(new ListRowPresenter());
private MainActivity mMainActivity;
- private TvProviderSearch mTvProviderSearch;
+ private SearchInterface mSearch;
private int mMainCardWidth;
private int mMainCardHeight;
private SearchTask mSearchTask;
@@ -139,7 +140,11 @@ public class ProgramGuideSearchFragment extends SearchFragment {
super.onCreate(savedInstanceState);
mMainActivity = (MainActivity) getActivity();
- mTvProviderSearch = new TvProviderSearch(mMainActivity);
+ if (PermissionUtils.hasAccessAllEpg(mMainActivity)) {
+ mSearch = new TvProviderSearch(mMainActivity);
+ } else {
+ mSearch = new DataManagerSearch(mMainActivity);
+ }
Resources res = getResources();
mMainCardWidth = res.getDimensionPixelSize(R.dimen.card_image_layout_width);
mMainCardHeight = res.getDimensionPixelSize(R.dimen.card_image_layout_height);
@@ -186,7 +191,7 @@ public class ProgramGuideSearchFragment extends SearchFragment {
@Override
protected List<LocalSearchProvider.SearchResult> doInBackground(Void... params) {
- return mTvProviderSearch.search(mQuery, SEARCH_RESULT_MAX,
+ return mSearch.search(mQuery, SEARCH_RESULT_MAX,
TvProviderSearch.ACTION_TYPE_AMBIGUOUS);
}
diff --git a/src/com/android/tv/search/SearchInterface.java b/src/com/android/tv/search/SearchInterface.java
new file mode 100644
index 00000000..7394150e
--- /dev/null
+++ b/src/com/android/tv/search/SearchInterface.java
@@ -0,0 +1,41 @@
+/*
+ * 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.search;
+
+import com.android.tv.search.LocalSearchProvider.SearchResult;
+
+import java.util.List;
+
+/**
+ * Interface for channel and program search.
+ */
+public interface SearchInterface {
+ String SOURCE_TV_SEARCH = "TvSearch";
+
+ int ACTION_TYPE_AMBIGUOUS = 1;
+ int ACTION_TYPE_SWITCH_CHANNEL = 2;
+ int ACTION_TYPE_SWITCH_INPUT = 3;
+
+ /**
+ * Search channels, inputs, or programs.
+ * This assumes that parental control settings will not be change while searching.
+ *
+ * @param action One of {@link #ACTION_TYPE_SWITCH_CHANNEL}, {@link #ACTION_TYPE_SWITCH_INPUT},
+ * or {@link #ACTION_TYPE_AMBIGUOUS},
+ */
+ public List<SearchResult> search(String query, int limit, int action);
+}
diff --git a/src/com/android/tv/search/TvProviderSearch.java b/src/com/android/tv/search/TvProviderSearch.java
index 00eb68bb..a5ad00ff 100644
--- a/src/com/android/tv/search/TvProviderSearch.java
+++ b/src/com/android/tv/search/TvProviderSearch.java
@@ -33,6 +33,7 @@ import android.text.TextUtils;
import android.util.Log;
import com.android.tv.search.LocalSearchProvider.SearchResult;
+import com.android.tv.util.PermissionUtils;
import com.android.tv.util.Utils;
import junit.framework.Assert;
@@ -48,18 +49,15 @@ import java.util.Map;
import java.util.Objects;
import java.util.Set;
-public class TvProviderSearch {
+/**
+ * An implementation of {@link SearchInterface} to search query from TvProvider directly.
+ */
+public class TvProviderSearch implements SearchInterface {
private static final boolean DEBUG = false;
private static final String TAG = "TvProviderSearch";
private static final int NO_LIMIT = 0;
- static final int ACTION_TYPE_AMBIGUOUS = 1;
- static final int ACTION_TYPE_SWITCH_CHANNEL = 2;
- static final int ACTION_TYPE_SWITCH_INPUT = 3;
-
- private static final String SOURCE_TV_SEARCH = "TvSearch";
-
private final Context mContext;
private final ContentResolver mContentResolver;
private final TvInputManager mTvInputManager;
@@ -77,9 +75,14 @@ public class TvProviderSearch {
* @param action One of {@link #ACTION_TYPE_SWITCH_CHANNEL}, {@link #ACTION_TYPE_SWITCH_INPUT},
* or {@link #ACTION_TYPE_AMBIGUOUS},
*/
+ @Override
@WorkerThread
public List<SearchResult> search(String query, int limit, int action) {
List<SearchResult> results = new ArrayList<>();
+ if (!PermissionUtils.hasAccessAllEpg(mContext)) {
+ // TODO: support this feature for non-system LC app. b/23939816
+ return results;
+ }
Set<Long> channelsFound = new HashSet<>();
if (action == ACTION_TYPE_SWITCH_CHANNEL) {
results.addAll(searchChannels(query, channelsFound, limit));
@@ -464,7 +467,6 @@ public class TvProviderSearch {
return result;
}
-
@WorkerThread
private class ChannelComparatorWithSameDisplayNumber implements Comparator<SearchResult> {
private final Map<Long, Long> mMaxWatchStartTimeMap = new HashMap<>();
diff --git a/src/com/android/tv/ui/ChannelBannerView.java b/src/com/android/tv/ui/ChannelBannerView.java
index 683e6c3a..bf8e69c7 100644
--- a/src/com/android/tv/ui/ChannelBannerView.java
+++ b/src/com/android/tv/ui/ChannelBannerView.java
@@ -200,8 +200,9 @@ public class ChannelBannerView extends FrameLayout implements Channel.LoadImageC
R.dimen.channel_banner_channel_logo_margin_start);
mProgramDescriptionTextViewWidth = mResources.getDimensionPixelSize(
R.dimen.channel_banner_program_description_width);
- mChannelBannerTextColor = mResources.getColor(R.color.channel_banner_text_color);
- mChannelBannerDimTextColor = mResources.getColor(R.color.channel_banner_dim_text_color);
+ mChannelBannerTextColor = Utils.getColor(mResources, R.color.channel_banner_text_color);
+ mChannelBannerDimTextColor = Utils.getColor(mResources,
+ R.color.channel_banner_dim_text_color);
mResizeAnimDuration = mResources.getInteger(R.integer.channel_banner_fast_anim_duration);
mResizeInterpolator = AnimationUtils.loadInterpolator(context,
diff --git a/src/com/android/tv/ui/KeypadChannelSwitchView.java b/src/com/android/tv/ui/KeypadChannelSwitchView.java
index 3ba2738e..722a3759 100644
--- a/src/com/android/tv/ui/KeypadChannelSwitchView.java
+++ b/src/com/android/tv/ui/KeypadChannelSwitchView.java
@@ -21,6 +21,7 @@ import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
+import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
@@ -42,8 +43,7 @@ import com.android.tv.analytics.DurationTimer;
import com.android.tv.analytics.Tracker;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelNumber;
-
-import junit.framework.Assert;
+import com.android.tv.util.SoftPreconditions;
import java.util.ArrayList;
import java.util.List;
@@ -62,6 +62,7 @@ public class KeypadChannelSwitchView extends LinearLayout implements
private final Tracker mTracker;
private final DurationTimer mViewDurationTimer = new DurationTimer();
private boolean mNavigated = false;
+ @Nullable //Once mChannels is set to null it should not be used again.
private List<Channel> mChannels;
private TextView mChannelNumberView;
private ListView mChannelItemListView;
@@ -157,7 +158,7 @@ public class KeypadChannelSwitchView extends LinearLayout implements
} else {
mSelectedChannel = (Channel) mAdapter.getItem(position);
}
- if(position!=0 && !mNavigated) {
+ if (position != 0 && !mNavigated) {
mNavigated = true;
mTracker.sendChannelInputNavigated();
}
@@ -178,7 +179,7 @@ public class KeypadChannelSwitchView extends LinearLayout implements
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
- Assert.assertNotNull(mChannels);
+ SoftPreconditions.checkNotNull(mChannels, TAG, "mChannels");
if (isChannelNumberKey(keyCode)) {
onNumberKeyUp(keyCode - KeyEvent.KEYCODE_0);
return true;
@@ -227,7 +228,7 @@ public class KeypadChannelSwitchView extends LinearLayout implements
mAdapter.notifyDataSetChanged();
}
- public void setChannels(List<Channel> channels) {
+ public void setChannels(@Nullable List<Channel> channels) {
mChannels = channels;
}
diff --git a/src/com/android/tv/ui/SelectInputView.java b/src/com/android/tv/ui/SelectInputView.java
index e347fbb1..b5b6ef34 100644
--- a/src/com/android/tv/ui/SelectInputView.java
+++ b/src/com/android/tv/ui/SelectInputView.java
@@ -22,6 +22,7 @@ import android.hardware.hdmi.HdmiDeviceInfo;
import android.media.tv.TvInputInfo;
import android.media.tv.TvInputManager;
import android.media.tv.TvInputManager.TvInputCallback;
+import android.support.annotation.NonNull;
import android.support.v17.leanback.widget.VerticalGridView;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
@@ -33,13 +34,13 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
-import com.android.tv.MainActivity;
import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.analytics.DurationTimer;
import com.android.tv.analytics.Tracker;
import com.android.tv.data.Channel;
import com.android.tv.util.TvInputManagerHelper;
+import com.android.tv.util.Utils;
import java.util.ArrayList;
import java.util.Collections;
@@ -53,36 +54,71 @@ public class SelectInputView extends VerticalGridView implements
private static final String TAG = "SelectInputView";
private static final boolean DEBUG = false;
public static final String SCREEN_NAME = "Input selection";
+ private static final int TUNER_INPUT_POSITION = 0;
- private final MainActivity mMainActivity;
+ private final TvApplication mApplication;
private final TvInputManagerHelper mTvInputManagerHelper;
private final List<TvInputInfo> mInputList = new ArrayList<>();
private final InputsComparator mComparator = new InputsComparator();
private final Tracker mTracker;
private final DurationTimer mViewDurationTimer = new DurationTimer();
+ private final TvInputCallback mTvInputCallback = new TvInputCallback() {
+ @Override
+ public void onInputAdded(String inputId) {
+ buildInputListAndNotify();
+ updateSelectedPositionIfNeeded();
+ }
+
+ @Override
+ public void onInputRemoved(String inputId) {
+ buildInputListAndNotify();
+ updateSelectedPositionIfNeeded();
+ }
+
+ @Override
+ public void onInputUpdated(String inputId) {
+ buildInputListAndNotify();
+ updateSelectedPositionIfNeeded();
+ }
+
+ @Override
+ public void onInputStateChanged(String inputId, int state) {
+ buildInputListAndNotify();
+ updateSelectedPositionIfNeeded();
+ }
+
+ private void updateSelectedPositionIfNeeded() {
+ if (!isFocusable() || mSelectedInput == null) {
+ return;
+ }
+ if (!isInputEnabled(mSelectedInput)) {
+ setSelectedPosition(TUNER_INPUT_POSITION);
+ return;
+ }
+ if (getInputPosition(mSelectedInput.getId()) != getSelectedPosition()) {
+ setSelectedPosition(getInputPosition(mSelectedInput.getId()));
+ }
+ }
+ };
+
+ private Channel mCurrentChannel;
+ private OnInputSelectedCallback mCallback;
private final Runnable mHideRunnable = new Runnable() {
@Override
public void run() {
- // Just dismiss the view when no action is required.
- if (mSelectedInput == null
- || TextUtils.equals(mSelectedInput.getId(), mCurrentInputId)
- || (!mSelectedInput.isPassthroughInput() && mCurrentInputId == null)) {
- mMainActivity.getOverlayManager().hideOverlays(
- TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG
- | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS
- | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE
- | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_MENU);
+ if (mSelectedInput == null) {
return;
}
// TODO: pass english label to tracker http://b/22355024
final String label = mSelectedInput.loadLabel(getContext()).toString();
mTracker.sendInputSelected(label);
- if (mSelectedInput.isPassthroughInput()) {
- mMainActivity.tuneToChannel(
- Channel.createPassthroughChannel(mSelectedInput.getId()));
- } else {
- mMainActivity.tuneToLastWatchedChannelForTunerInput();
+ if (mCallback != null) {
+ if (mSelectedInput.isPassthroughInput()) {
+ mCallback.onPassthroughInputSelected(mSelectedInput);
+ } else {
+ mCallback.onTunerInputSelected();
+ }
}
}
};
@@ -97,9 +133,6 @@ public class SelectInputView extends VerticalGridView implements
private boolean mResetTransitionAlpha;
private TvInputInfo mSelectedInput;
- // The ID of the currently selected pass-through input. The null value means that the currently
- // selected input is a tuner.
- private String mCurrentInputId;
private int mMaxItemWidth;
public SelectInputView(Context context) {
@@ -114,59 +147,21 @@ public class SelectInputView extends VerticalGridView implements
super(context, attrs, defStyleAttr);
setAdapter(new InputListAdapter());
- mMainActivity = (MainActivity) context;
+ mApplication = (TvApplication) context.getApplicationContext();
mTracker = ((TvApplication) context.getApplicationContext()).getTracker();
- mTvInputManagerHelper = mMainActivity.getTvInputManagerHelper();
- mTvInputManagerHelper.addCallback(new TvInputCallback() {
- @Override
- public void onInputAdded(String inputId) {
- buildInputListAndNotify();
- updateSelectedPositionIfNeeded();
- }
-
- @Override
- public void onInputRemoved(String inputId) {
- buildInputListAndNotify();
- updateSelectedPositionIfNeeded();
- }
-
- @Override
- public void onInputUpdated(String inputId) {
- buildInputListAndNotify();
- updateSelectedPositionIfNeeded();
- }
-
- @Override
- public void onInputStateChanged(String inputId, int state) {
- buildInputListAndNotify();
- updateSelectedPositionIfNeeded();
- }
-
- private void updateSelectedPositionIfNeeded() {
- if (!isFocusable() || mSelectedInput == null) {
- return;
- }
- if (!isInputEnabled(mSelectedInput)) {
- setSelectedPosition(0);
- return;
- }
- if (getInputPosition(mSelectedInput.getId()) != getSelectedPosition()) {
- setSelectedPosition(getInputPosition(mSelectedInput.getId()));
- }
- }
- });
+ mTvInputManagerHelper = mApplication.getTvInputManagerHelper();
Resources resources = context.getResources();
mInputItemHeight = resources.getDimensionPixelSize(R.dimen.input_banner_item_height);
mShowDurationMillis = resources.getInteger(R.integer.select_input_show_duration);
mRippleAnimDurationMillis = resources.getInteger(
R.integer.select_input_ripple_anim_duration);
- mTextColorPrimary = resources.getColor(R.color.select_input_text_color_primary);
- mTextColorSecondary = resources.getColor(R.color.select_input_text_color_secondary);
- mTextColorDisabled = resources.getColor(R.color.select_input_text_color_disabled);
+ mTextColorPrimary = Utils.getColor(resources, R.color.select_input_text_color_primary);
+ mTextColorSecondary = Utils.getColor(resources, R.color.select_input_text_color_secondary);
+ mTextColorDisabled = Utils.getColor(resources, R.color.select_input_text_color_disabled);
mItemViewForMeasure = LayoutInflater.from(context).inflate(
- R.layout.select_input_item, null, false);
+ R.layout.select_input_item, this, false);
buildInputListAndNotify();
}
@@ -204,15 +199,15 @@ public class SelectInputView extends VerticalGridView implements
mResetTransitionAlpha = fromEmptyScene;
buildInputListAndNotify();
- Channel channel = mMainActivity.getCurrentChannel();
- mCurrentInputId = channel != null && channel.isPassthrough() ? channel.getInputId() : null;
- if (mCurrentInputId != null
- && !isInputEnabled(mTvInputManagerHelper.getTvInputInfo(mCurrentInputId))) {
- // If current input is disabled, the first item will be focused. The tuner input
- // is usually the first item.
- setSelectedPosition(0);
+ mTvInputManagerHelper.addCallback(mTvInputCallback);
+ String currentInputId = mCurrentChannel != null && mCurrentChannel.isPassthrough() ?
+ mCurrentChannel.getInputId() : null;
+ if (currentInputId != null
+ && !isInputEnabled(mTvInputManagerHelper.getTvInputInfo(currentInputId))) {
+ // If current input is disabled, the tuner input will be focused.
+ setSelectedPosition(TUNER_INPUT_POSITION);
} else {
- setSelectedPosition(getInputPosition(mCurrentInputId));
+ setSelectedPosition(getInputPosition(currentInputId));
}
setFocusable(true);
requestFocus();
@@ -226,12 +221,13 @@ public class SelectInputView extends VerticalGridView implements
}
}
}
- return 0;
+ return TUNER_INPUT_POSITION;
}
@Override
public void onExitAction() {
mTracker.sendHideInputSelection(mViewDurationTimer.reset());
+ mTvInputManagerHelper.removeCallback(mTvInputCallback);
removeCallbacks(mHideRunnable);
}
@@ -302,6 +298,21 @@ public class SelectInputView extends VerticalGridView implements
!= TvInputManager.INPUT_STATE_DISCONNECTED;
}
+ /**
+ * Sets a callback which receives the notifications of input selection.
+ */
+ public void setOnInputSelectedCallback(OnInputSelectedCallback callback) {
+ mCallback = callback;
+ }
+
+ /**
+ * Sets the current channel. The initial selection will be the input which contains the
+ * {@code channel}.
+ */
+ public void setCurrentChannel(Channel channel) {
+ mCurrentChannel = channel;
+ }
+
class InputListAdapter extends RecyclerView.Adapter<InputListAdapter.ViewHolder> {
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
@@ -438,4 +449,19 @@ public class SelectInputView extends VerticalGridView implements
}
}
}
+
+ /**
+ * A callback interface for the input selection.
+ */
+ public static interface OnInputSelectedCallback {
+ /**
+ * Called when the tuner input is selected.
+ */
+ void onTunerInputSelected();
+
+ /**
+ * Called when the passthrough input is selected.
+ */
+ void onPassthroughInputSelected(@NonNull TvInputInfo input);
+ }
}
diff --git a/src/com/android/tv/ui/SetupView.java b/src/com/android/tv/ui/SetupView.java
index 330b7e9f..cb25f6f9 100644
--- a/src/com/android/tv/ui/SetupView.java
+++ b/src/com/android/tv/ui/SetupView.java
@@ -378,7 +378,7 @@ public class SetupView extends FullscreenDialogView {
final TvInputInfo input = mInputList.get(position);
viewHolder.mTitle.setText(input.loadLabel(getContext()));
int channelCount = mChannelDataManager.getChannelCountForInput(input.getId());
- if (mSetupUtils.hasSetupLaunched(input.getId())) {
+ if (mSetupUtils.isSetupDone(input.getId())) {
if (channelCount == 0) {
viewHolder.mDescription.setText(R.string.setup_input_no_channels);
} else {
diff --git a/src/com/android/tv/ui/TunableTvView.java b/src/com/android/tv/ui/TunableTvView.java
index eba43594..f526c33c 100644
--- a/src/com/android/tv/ui/TunableTvView.java
+++ b/src/com/android/tv/ui/TunableTvView.java
@@ -34,6 +34,7 @@ import android.media.tv.TvView.TvInputCallback;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
@@ -43,6 +44,7 @@ import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
+import android.widget.ImageView;
import android.widget.TextView;
import com.android.tv.R;
@@ -52,8 +54,10 @@ import com.android.tv.analytics.Tracker;
import com.android.tv.common.TvCommonConstants;
import com.android.tv.data.Channel;
import com.android.tv.data.StreamInfo;
+import com.android.tv.data.WatchedHistoryManager;
import com.android.tv.parental.ContentRatingsManager;
import com.android.tv.recommendation.NotificationService;
+import com.android.tv.util.PermissionUtils;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
@@ -98,6 +102,8 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
private Channel mCurrentChannel;
private TvInputManagerHelper mInputManagerHelper;
private ContentRatingsManager mContentRatingsManager;
+ @Nullable
+ private WatchedHistoryManager mWatchedHistoryManager;
private boolean mStarted;
private TvInputInfo mInputInfo;
private OnTuneListener mOnTuneListener;
@@ -121,6 +127,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
private boolean mIsPip;
private int mScreenHeight;
private int mShrunkenTvViewHeight;
+ private boolean mCanModifyParentalControls;
@TimeShiftState private int mTimeShiftState = TIME_SHIFT_STATE_NONE;
private TimeShiftListener mTimeShiftListener;
@@ -135,7 +142,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
private final View mBlockScreenView;
private final View mBlockScreenDescriptionView;
- private final View mBlockScreenIconView;
+ private final ImageView mBlockScreenIconView;
private final View mBlockScreenShrunkenIconView;
private final TextView mBlockScreenTextView;
@@ -144,7 +151,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
private final Animator mBlockScreenDescriptionFadeOut;
// A View to hide screen when there's problem in video playback.
- private final View mHideScreenView;
+ private final TextView mHideScreenView;
// A View to block screen until onContentAllowed is received if parental control is on.
private final View mBlockScreenForTuneView;
@@ -164,6 +171,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
@Override
public void onConnectionFailed(String inputId) {
Log.w(TAG, "Failed to bind an input");
+ mTracker.sendInputConnectionFailure(inputId);
Channel channel = mCurrentChannel;
mCurrentChannel = null;
mInputInfo = null;
@@ -182,6 +190,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
@Override
public void onDisconnected(String inputId) {
Log.w(TAG, "Session is released by crash");
+ mTracker.sendInputDisconnected(inputId);
Channel channel = mCurrentChannel;
mCurrentChannel = null;
mInputInfo = null;
@@ -273,6 +282,14 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
if (mOnTuneListener != null) {
mOnTuneListener.onStreamInfoChanged(TunableTvView.this);
}
+ switch (reason) {
+ case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING:
+ case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN:
+ case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL:
+ mTracker.sendChannelVideoUnavailable(mCurrentChannel, reason);
+ default:
+ // do nothing
+ }
}
@Override
@@ -314,12 +331,18 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
super(context, attrs, defStyleAttr, defStyleRes);
inflate(getContext(), R.layout.tunable_tv_view, this);
- mTracker = ((TvApplication) context.getApplicationContext()).getTracker();
+ TvApplication tvApplication = (TvApplication) context.getApplicationContext();
+ mCanModifyParentalControls = PermissionUtils.hasModifyParentalControls(context);
+ mTracker = tvApplication.getTracker();
mBlockScreenType = BLOCK_SCREEN_TYPE_NORMAL;
mBlockScreenView = findViewById(R.id.block_screen);
mBlockScreenDescriptionView = findViewById(R.id.block_screen_description);
- mBlockScreenIconView = mBlockScreenView.findViewById(R.id.block_screen_icon);
+ mBlockScreenIconView = (ImageView) mBlockScreenView.findViewById(R.id.block_screen_icon);
+ if (!mCanModifyParentalControls) {
+ mBlockScreenIconView.setImageResource(R.drawable.ic_message_lock_no_permission);
+ mBlockScreenIconView.setScaleType(ImageView.ScaleType.CENTER);
+ }
mBlockScreenShrunkenIconView = mBlockScreenView.findViewById(
R.id.block_screen_shrunken_icon);
mBlockScreenTextView = (TextView) mBlockScreenView.findViewById(R.id.block_screen_text);
@@ -355,7 +378,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
}
});
- mHideScreenView = findViewById(R.id.hide_screen);
+ mHideScreenView = (TextView) findViewById(R.id.hide_screen);
mBufferingSpinnerView = findViewById(R.id.buffering_spinner);
mBlockScreenForTuneView = findViewById(R.id.block_screen_for_tune);
mDimScreenView = findViewById(R.id.dim);
@@ -401,7 +424,12 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
}
mStarted = false;
if (mCurrentChannel != null) {
- mTracker.sendChannelViewStop(mCurrentChannel, mChannelViewTimer.reset());
+ long duration = mChannelViewTimer.reset();
+ mTracker.sendChannelViewStop(mCurrentChannel, duration);
+ if (mWatchedHistoryManager != null && !mCurrentChannel.isPassthrough()) {
+ mWatchedHistoryManager.logChannelViewStop(mCurrentChannel,
+ System.currentTimeMillis(), duration);
+ }
}
reset();
}
@@ -420,6 +448,10 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
mTvView.setMain();
}
+ public void setWatchedHistoryManager(WatchedHistoryManager watchedHistoryManager) {
+ mWatchedHistoryManager = watchedHistoryManager;
+ }
+
public boolean isPlaying() {
return mStarted;
}
@@ -451,14 +483,17 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
return false;
}
if (mCurrentChannel != null) {
- mTracker.sendChannelViewStop(mCurrentChannel, mChannelViewTimer.reset());
+ long duration = mChannelViewTimer.reset();
+ mTracker.sendChannelViewStop(mCurrentChannel, duration);
+ if (mWatchedHistoryManager != null && !mCurrentChannel.isPassthrough()) {
+ mWatchedHistoryManager.logChannelViewStop(mCurrentChannel,
+ System.currentTimeMillis(), duration);
+ }
}
mOnTuneListener = listener;
mCurrentChannel = channel;
boolean tunedByRecommendation = params != null
&& params.getString(NotificationService.TUNE_PARAMS_RECOMMENDATION_TYPE) != null;
- mTracker.sendChannelViewStart(mCurrentChannel, tunedByRecommendation);
- mChannelViewTimer.start();
boolean needSurfaceSizeUpdate = false;
if (!inputInfo.equals(mInputInfo)) {
mInputInfo = inputInfo;
@@ -471,6 +506,8 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
}
needSurfaceSizeUpdate = true;
}
+ mTracker.sendChannelViewStart(mCurrentChannel, tunedByRecommendation);
+ mChannelViewTimer.start();
mVideoWidth = 0;
mVideoHeight = 0;
mVideoFormat = StreamInfo.VIDEO_DEFINITION_LEVEL_UNKNOWN;
@@ -840,7 +877,11 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
mBlockScreenTextView.setText("");
break;
case BLOCK_SCREEN_TYPE_NORMAL:
- mBlockScreenTextView.setText(R.string.tvview_channel_locked);
+ if (mCanModifyParentalControls) {
+ mBlockScreenTextView.setText(R.string.tvview_channel_locked);
+ } else {
+ mBlockScreenTextView.setText(R.string.tvview_channel_locked_no_permission);
+ }
break;
}
} else if (mBlockedContentRating != null) {
@@ -859,10 +900,20 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
break;
case BLOCK_SCREEN_TYPE_NORMAL:
if (TextUtils.isEmpty(name)) {
- mBlockScreenTextView.setText(R.string.tvview_content_locked);
+ if (mCanModifyParentalControls) {
+ mBlockScreenTextView.setText(R.string.tvview_content_locked);
+ } else {
+ mBlockScreenTextView.setText(
+ R.string.tvview_content_locked_no_permission);
+ }
} else {
- mBlockScreenTextView.setText(getContext().getString(
- R.string.tvview_content_locked_format, name));
+ if (mCanModifyParentalControls) {
+ mBlockScreenTextView.setText(getContext().getString(
+ R.string.tvview_content_locked_format, name));
+ } else {
+ mBlockScreenTextView.setText(getContext().getString(
+ R.string.tvview_content_locked_format_no_permission, name));
+ }
}
break;
}
@@ -903,8 +954,15 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
private void hideScreenByVideoAvailability(int reason) {
switch (reason) {
+ case TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY:
+ mHideScreenView.setVisibility(VISIBLE);
+ mHideScreenView.setText(R.string.tvview_msg_audio_only);
+ mBufferingSpinnerView.setVisibility(GONE);
+ unmuteIfPossible();
+ break;
case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING:
mBufferingSpinnerView.setVisibility(VISIBLE);
+ mute();
break;
case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN:
case TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING:
@@ -912,12 +970,13 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
case VIDEO_UNAVAILABLE_REASON_NOT_TUNED:
default:
mHideScreenView.setVisibility(VISIBLE);
+ mHideScreenView.setText(null);
mBufferingSpinnerView.setVisibility(GONE);
+ mute();
break;
}
mVideoAvailable = false;
mVideoUnavailableReason = reason;
- mute();
}
private void unhideScreenByVideoAvailability() {
diff --git a/src/com/android/tv/ui/TvTransitionManager.java b/src/com/android/tv/ui/TvTransitionManager.java
index 7096893f..444b5c0c 100644
--- a/src/com/android/tv/ui/TvTransitionManager.java
+++ b/src/com/android/tv/ui/TvTransitionManager.java
@@ -123,6 +123,7 @@ public class TvTransitionManager extends TransitionManager {
initIfNeeded();
if (mCurrentScene != mSelectInputScene) {
transitionTo(mSelectInputScene);
+ mSelectInputView.setCurrentChannel(mMainActivity.getCurrentChannel());
}
}
diff --git a/src/com/android/tv/ui/TvViewUiManager.java b/src/com/android/tv/ui/TvViewUiManager.java
index f93cf45c..d767906b 100644
--- a/src/com/android/tv/ui/TvViewUiManager.java
+++ b/src/com/android/tv/ui/TvViewUiManager.java
@@ -47,6 +47,7 @@ import com.android.tv.R;
import com.android.tv.TvOptionsManager;
import com.android.tv.data.DisplayMode;
import com.android.tv.util.TvSettings;
+import com.android.tv.util.Utils;
/**
* The TvViewUiManager is responsible for handling UI layouting and animation of main and PIP
@@ -778,8 +779,9 @@ public class TvViewUiManager {
// Set marginEnd as well because setTvViewPosition uses both start/end margin.
layoutParams.setMarginEnd(mScreenWidth - layoutParams.width - marginStart);
- setBackgroundColor(mResources.getColor(isTvViewFullScreen() ? R.color.tvactivity_background
- : R.color.tvactivity_background_on_shrunken_tvview), layoutParams, animate);
+ setBackgroundColor(Utils.getColor(mResources, isTvViewFullScreen()
+ ? R.color.tvactivity_background : R.color.tvactivity_background_on_shrunken_tvview),
+ layoutParams, animate);
setTvViewPosition(layoutParams, tvViewFrame, animate);
// Update the current display mode.
diff --git a/src/com/android/tv/ui/sidepanel/AboutFragment.java b/src/com/android/tv/ui/sidepanel/AboutFragment.java
index 5b7444b2..e880fe37 100644
--- a/src/com/android/tv/ui/sidepanel/AboutFragment.java
+++ b/src/com/android/tv/ui/sidepanel/AboutFragment.java
@@ -16,12 +16,17 @@
package com.android.tv.ui.sidepanel;
+import android.app.Activity;
+import android.content.Context;
import android.view.View;
+import android.widget.CompoundButton;
import android.widget.TextView;
+import com.android.tv.Features;
import com.android.tv.MainActivity;
import com.android.tv.R;
import com.android.tv.TvApplication;
+import com.android.tv.analytics.OptOutPreferenceHelper;
import com.android.tv.dialog.WebDialogFragment;
import com.android.tv.license.LicenseUtils;
@@ -29,16 +34,15 @@ import java.util.ArrayList;
import java.util.List;
/**
- * Shows version and optional license information.
+ * Shows version, optional license information and Analytics OptOut.
*/
public class AboutFragment extends SideFragment {
- private final static String TAG = "AboutFragment";
private static final String TRACKER_LABEL = "about";
/**
* Shows the application version name.
*/
- public static class VersionItem extends Item {
+ private static final class VersionItem extends Item {
@Override
protected int getResourceId() {
return R.layout.option_item_simple;
@@ -61,7 +65,7 @@ public class AboutFragment extends SideFragment {
/**
* Opens a dialog showing open source licenses.
*/
- public static class LicenseActionItem extends ActionItem {
+ public static final class LicenseActionItem extends ActionItem {
public final static String DIALOG_TAG = LicenseActionItem.class.getSimpleName();
public static final String TRACKER_LABEL = "Open Source Licenses";
private final MainActivity mMainActivity;
@@ -79,6 +83,70 @@ public class AboutFragment extends SideFragment {
}
}
+ /**
+ * Sets the users preference for allowing analytics.
+ */
+ private static final class AllowAnalyticsItem extends SwitchItem {
+ //TODO: change this to use SwitchPreference
+ private final OptOutPreferenceHelper mPreferenceHelper;
+ private TextView mDescriptionView;
+ private int mOriginalMaxDescriptionLine;
+ private MainActivity mMainActivity;
+ private View mBoundView;
+
+ public AllowAnalyticsItem(Context context) {
+ super(context.getResources().getString(R.string.about_menu_improve),
+ context.getResources().getString(R.string.about_menu_improve),
+ context.getResources().getString(R.string.about_menu_improve_summary));
+ mPreferenceHelper = ((TvApplication) context.getApplicationContext())
+ .getOptPreferenceHelper();
+ }
+
+ @Override
+ protected void onBind(View view) {
+ super.onBind(view);
+ mDescriptionView = (TextView) view.findViewById(getDescriptionViewId());
+ mOriginalMaxDescriptionLine = mDescriptionView.getMaxLines();
+ mDescriptionView.setMaxLines(Integer.MAX_VALUE);
+ mMainActivity = (MainActivity) view.getContext();
+ mBoundView = view;
+ }
+
+ @Override
+ protected void onUnbind() {
+ super.onUnbind();
+ mDescriptionView.setMaxLines(mOriginalMaxDescriptionLine);
+ mDescriptionView = null;
+ mMainActivity = null;
+ mBoundView = null;
+ }
+
+ @Override
+ protected void onUpdate() {
+ super.onUpdate();
+ setChecked(!mPreferenceHelper
+ .getOptOutPreference(OptOutPreferenceHelper.ANALYTICS_OPT_OUT_DEFAULT_VALUE));
+ }
+
+ @Override
+ protected void onSelected() {
+ super.onSelected();
+ mPreferenceHelper.setOptOutPreference(!isChecked());
+ }
+
+ @Override
+ public void setChecked(boolean checked) {
+ super.setChecked(checked);
+ if (mMainActivity != null && mBoundView != null && mBoundView.hasFocus()) {
+ // Quick fix for accessibility
+ // TODO: Need to change the resource in the future.
+ mMainActivity.sendAccessiblityText(checked ?
+ mMainActivity.getString(R.string.options_item_pip_on)
+ : mMainActivity.getString(R.string.options_item_pip_off));
+ }
+ }
+ }
+
@Override
protected String getTitle() {
return getResources().getString(R.string.side_panel_title_about);
@@ -93,8 +161,12 @@ public class AboutFragment extends SideFragment {
protected List<Item> getItemList() {
List<Item> items = new ArrayList<>();
items.add(new VersionItem());
- if (LicenseUtils.hasLicenses(getActivity().getAssets())) {
- items.add(new LicenseActionItem((MainActivity) getActivity()));
+ Activity activity = getActivity();
+ if (LicenseUtils.hasLicenses(activity.getAssets())) {
+ items.add(new LicenseActionItem((MainActivity) activity));
+ }
+ if (Features.ANALYTICS_OPT_OUT.isEnabled(activity)) {
+ items.add(new AllowAnalyticsItem(activity));
}
return items;
}
diff --git a/src/com/android/tv/ui/sidepanel/ActionItem.java b/src/com/android/tv/ui/sidepanel/ActionItem.java
index c75eff9b..23aff91c 100644
--- a/src/com/android/tv/ui/sidepanel/ActionItem.java
+++ b/src/com/android/tv/ui/sidepanel/ActionItem.java
@@ -70,4 +70,4 @@ public abstract class ActionItem extends Item {
iconView.setVisibility(View.GONE);
}
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/ui/sidepanel/ChannelSourcesFragment.java b/src/com/android/tv/ui/sidepanel/ChannelSourcesFragment.java
index a95b8149..7289034f 100644
--- a/src/com/android/tv/ui/sidepanel/ChannelSourcesFragment.java
+++ b/src/com/android/tv/ui/sidepanel/ChannelSourcesFragment.java
@@ -101,4 +101,4 @@ public class ChannelSourcesFragment extends SideFragment {
.show();
}
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/ui/sidepanel/CheckBoxItem.java b/src/com/android/tv/ui/sidepanel/CheckBoxItem.java
index 205f1bc8..79c2b0a7 100644
--- a/src/com/android/tv/ui/sidepanel/CheckBoxItem.java
+++ b/src/com/android/tv/ui/sidepanel/CheckBoxItem.java
@@ -77,4 +77,4 @@ public class CheckBoxItem extends CompoundButtonItem {
protected void onSelected() {
setChecked(!isChecked());
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/ui/sidepanel/Item.java b/src/com/android/tv/ui/sidepanel/Item.java
index da9b39b0..4ae6e523 100644
--- a/src/com/android/tv/ui/sidepanel/Item.java
+++ b/src/com/android/tv/ui/sidepanel/Item.java
@@ -86,4 +86,4 @@ public abstract class Item {
}
}
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/ui/sidepanel/RadioButtonItem.java b/src/com/android/tv/ui/sidepanel/RadioButtonItem.java
index b6c36795..e0477493 100644
--- a/src/com/android/tv/ui/sidepanel/RadioButtonItem.java
+++ b/src/com/android/tv/ui/sidepanel/RadioButtonItem.java
@@ -41,4 +41,4 @@ public class RadioButtonItem extends CompoundButtonItem {
protected void onSelected() {
setChecked(true);
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/ui/sidepanel/SwitchItem.java b/src/com/android/tv/ui/sidepanel/SwitchItem.java
index c04a72ac..ef9966a5 100644
--- a/src/com/android/tv/ui/sidepanel/SwitchItem.java
+++ b/src/com/android/tv/ui/sidepanel/SwitchItem.java
@@ -20,11 +20,15 @@ import com.android.tv.R;
public class SwitchItem extends CompoundButtonItem {
public SwitchItem(String title) {
- super(title, null);
+ this(title, null, null);
}
public SwitchItem(String checkedTitle, String uncheckedTitle) {
- super(checkedTitle, uncheckedTitle, null);
+ this(checkedTitle, uncheckedTitle, null);
+ }
+
+ public SwitchItem(String checkedTitle, String uncheckedTitle, String description) {
+ super(checkedTitle, uncheckedTitle, description);
}
@Override
@@ -41,4 +45,4 @@ public class SwitchItem extends CompoundButtonItem {
protected void onSelected() {
setChecked(!isChecked());
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/ui/sidepanel/parentalcontrols/ProgramRestrictionsFragment.java b/src/com/android/tv/ui/sidepanel/parentalcontrols/ProgramRestrictionsFragment.java
index c878900a..1df7fe59 100644
--- a/src/com/android/tv/ui/sidepanel/parentalcontrols/ProgramRestrictionsFragment.java
+++ b/src/com/android/tv/ui/sidepanel/parentalcontrols/ProgramRestrictionsFragment.java
@@ -82,4 +82,4 @@ public class ProgramRestrictionsFragment extends SideFragment {
items.add(ratingsItem);
return items;
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/util/AsyncDbTask.java b/src/com/android/tv/util/AsyncDbTask.java
index 82d42377..fbc93f3a 100644
--- a/src/com/android/tv/util/AsyncDbTask.java
+++ b/src/com/android/tv/util/AsyncDbTask.java
@@ -162,6 +162,9 @@ public abstract class AsyncDbTask<Params, Progress, Result>
}
return null;
}
+ } catch (SecurityException e) {
+ Log.d(TAG, "Security exception during query", e);
+ return null;
}
}
diff --git a/src/com/android/tv/util/BooleanSystemProperty.java b/src/com/android/tv/util/BooleanSystemProperty.java
deleted file mode 100644
index 6786868e..00000000
--- a/src/com/android/tv/util/BooleanSystemProperty.java
+++ /dev/null
@@ -1,95 +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.util.Log;
-
-import java.lang.reflect.Method;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Lazy loaded boolean system property.
- *
- * <p>Set with <code>adb shell setprop <em>key</em> <em>value</em></code> where:
- * Values 'n', 'no', '0', 'false' or 'off' are considered false.
- * Values 'y', 'yes', '1', 'true' or 'on' are considered true.
- * (case sensitive). See <a href=
- * "https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/os/SystemProperties.java"
- * >android.os.SystemProperties.getBoolean</a>.
- */
-public final class BooleanSystemProperty {
- private final static String TAG = "BooleanSystemProperty";
- private final static boolean DEBUG = false;
- private static final List<BooleanSystemProperty> ALL_PROPERTIES = new ArrayList<>();
- private final boolean mDefaultValue;
- private final String mKey;
- private Boolean mValue = null;
-
- public BooleanSystemProperty(String key, boolean defaultValue) {
- mDefaultValue = defaultValue;
- mKey = key;
- ALL_PROPERTIES.add(this);
- }
-
- public static void resetAll() {
- for (BooleanSystemProperty prop : ALL_PROPERTIES) {
- prop.reset();
- }
- }
-
- /**
- * Gets system properties set by <code>adb shell setprop <em>key</em> <em>value</em></code>
- *
- * @param key the property key.
- * @param defaultValue the value to return if the property is undefined or empty.
- * @return the system property value or the default value.
- */
- private static boolean getBoolean(String key, boolean defaultValue) {
- try {
- final Class<?> systemProperties = Class.forName("android.os.SystemProperties");
- final Method get = systemProperties.getMethod("getBoolean", String.class, Boolean.TYPE);
- return (boolean) get.invoke(null, key, defaultValue);
- } catch (Exception e) {
- Log.e(TAG, "Error getting boolean for " + key, e);
- // This should never happen
- return defaultValue;
- }
- }
-
- /**
- * Clears the cached value. The next call to getValue will check {@code
- * android.os.SystemProperties}.
- */
- public void reset() {
- mValue = null;
- }
-
- /**
- * Returns the value of the system property.
- *
- * <p>If the value is cached get the value from {@code android.os.SystemProperties} with the
- * default set in the constructor.
- */
- public boolean getValue() {
- if (mValue == null) {
- mValue = getBoolean(mKey, mDefaultValue);
- if (DEBUG) Log.d(TAG, mKey + "=" + mValue);
- }
- return mValue;
- }
-}
diff --git a/src/com/android/tv/util/Clock.java b/src/com/android/tv/util/Clock.java
index 58653068..f6c3782e 100644
--- a/src/com/android/tv/util/Clock.java
+++ b/src/com/android/tv/util/Clock.java
@@ -31,4 +31,4 @@ public interface Clock {
return System.currentTimeMillis();
}
};
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/util/CollectionUtils.java b/src/com/android/tv/util/CollectionUtils.java
new file mode 100644
index 00000000..e07bac90
--- /dev/null
+++ b/src/com/android/tv/util/CollectionUtils.java
@@ -0,0 +1,41 @@
+/*
+ * 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.Build;
+import android.util.ArraySet;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Static utilities for collections
+ */
+public class CollectionUtils {
+ /**
+ * Returns a new Set suitable for small data sets.
+ *
+ * <p>In M and above this is a ArraySet otherwise it is a HashSet
+ */
+ public static <T> Set<T> createSmallSet() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ return new ArraySet<T>();
+ } else {
+ return new HashSet<T>();
+ }
+ }
+}
diff --git a/src/com/android/tv/receiver/NotificationReceiver.java b/src/com/android/tv/util/EngOnlyFeature.java
index 0bcb44c4..904e2369 100644
--- a/src/com/android/tv/receiver/NotificationReceiver.java
+++ b/src/com/android/tv/util/EngOnlyFeature.java
@@ -11,22 +11,22 @@
* 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.
+ * limitations under the License
*/
-package com.android.tv.receiver;
+package com.android.tv.util;
-import android.content.BroadcastReceiver;
import android.content.Context;
-import android.content.Intent;
-import com.android.tv.recommendation.NotificationService;
+import com.android.tv.BuildConfig;
+import com.android.tv.common.feature.Feature;
-public class NotificationReceiver extends BroadcastReceiver {
+/**
+ * A feature that is only available on {@link BuildConfig#ENG} builds.
+ */
+public final class EngOnlyFeature implements Feature {
@Override
- public void onReceive(Context context, Intent intent) {
- Intent notificationIntent = new Intent(context, NotificationService.class);
- notificationIntent.setAction(NotificationService.ACTION_SHOW_RECOMMENDATION);
- context.startService(notificationIntent);
+ public boolean isEnabled(Context context) {
+ return BuildConfig.ENG;
}
}
diff --git a/src/com/android/tv/util/ImageCache.java b/src/com/android/tv/util/ImageCache.java
index 67a63a59..e849da89 100644
--- a/src/com/android/tv/util/ImageCache.java
+++ b/src/com/android/tv/util/ImageCache.java
@@ -20,13 +20,12 @@ import android.support.annotation.VisibleForTesting;
import android.util.Log;
import android.util.LruCache;
-import com.android.tv.MainActivity;
import com.android.tv.util.BitmapUtils.ScaledBitmapInfo;
/**
* A convenience class for caching bitmap.
*/
-public class ImageCache implements MainActivity.MemoryManageable {
+public class ImageCache implements MemoryManageable {
private static final float MAX_CACHE_SIZE_PERCENT = 0.8f;
private static final float MIN_CACHE_SIZE_PERCENT = 0.05f;
private static final float DEFAULT_CACHE_SIZE_PERCENT = 0.1f;
diff --git a/src/com/android/tv/util/MainThreadExecutor.java b/src/com/android/tv/util/MainThreadExecutor.java
new file mode 100644
index 00000000..817286f7
--- /dev/null
+++ b/src/com/android/tv/util/MainThreadExecutor.java
@@ -0,0 +1,86 @@
+/*
+ * 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.Handler;
+import android.os.Looper;
+
+import java.util.List;
+import java.util.concurrent.AbstractExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * An executor service that executes its tasks on the main thread.
+ *
+ * Shutting down this executor is not supported.
+ */
+public class MainThreadExecutor extends AbstractExecutorService {
+
+ private final static MainThreadExecutor INSTANCE = new MainThreadExecutor();
+
+ public final static MainThreadExecutor getInstance() {
+ return INSTANCE;
+ }
+
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
+
+ @Override
+ public void execute(Runnable runnable) {
+ if (Looper.getMainLooper() == Looper.myLooper()) {
+ runnable.run();
+ } else {
+ mHandler.post(runnable);
+ }
+ }
+
+ /**
+ * Not supported and throws an exception when used.
+ */
+ @Override
+ @Deprecated
+ public void shutdown() {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Not supported and throws an exception when used.
+ */
+ @Override
+ @Deprecated
+ public List<Runnable> shutdownNow() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isShutdown() {
+ return false;
+ }
+
+ @Override
+ public boolean isTerminated() {
+ return false;
+ }
+
+ /**
+ * Not supported and throws an exception when used.
+ */
+ @Override
+ @Deprecated
+ public boolean awaitTermination(long l, TimeUnit timeUnit) throws InterruptedException {
+ throw new UnsupportedOperationException();
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/util/MemoryManageable.java b/src/com/android/tv/util/MemoryManageable.java
new file mode 100644
index 00000000..c5e5d869
--- /dev/null
+++ b/src/com/android/tv/util/MemoryManageable.java
@@ -0,0 +1,29 @@
+/*
+ * 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;
+
+/**
+ * Interface for the fine-grained memory management.
+ * The class which wants to release memory based on the system constraints should inherit
+ * this interface and implement {@link #performTrimMemory}.
+ */
+public interface MemoryManageable {
+ /**
+ * For more information, see {@link android.content.ComponentCallbacks2#onTrimMemory}.
+ */
+ void performTrimMemory(int level);
+}
diff --git a/src/com/android/tv/util/MultiLongSparseArray.java b/src/com/android/tv/util/MultiLongSparseArray.java
new file mode 100644
index 00000000..4c067892
--- /dev/null
+++ b/src/com/android/tv/util/MultiLongSparseArray.java
@@ -0,0 +1,117 @@
+/*
+ * 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.support.annotation.VisibleForTesting;
+import android.util.LongSparseArray;
+
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * Uses a {@link LongSparseArray} to hold sets of {@code T}.
+ *
+ * <p>This has the same memory and performance trade offs listed in {@link LongSparseArray}.
+ */
+public class MultiLongSparseArray<T> {
+ @VisibleForTesting
+ static final int DEFAULT_MAX_EMPTIES_KEPT = 4;
+ private final LongSparseArray<Set<T>> mSparseArray;
+ private final Set<T>[] mEmptySets;
+ private int mEmptyIndex = -1;
+
+ public MultiLongSparseArray() {
+ mSparseArray = new LongSparseArray<>();
+ mEmptySets = new Set[DEFAULT_MAX_EMPTIES_KEPT];
+ }
+
+ public MultiLongSparseArray(int initialCapacity, int emptyCacheSize) {
+ mSparseArray = new LongSparseArray<>(initialCapacity);
+ mEmptySets = new Set[emptyCacheSize];
+ }
+
+ /**
+ * Adds a mapping from the specified key to the specified value,
+ * replacing the previous mapping from the specified key if there
+ * was one.
+ */
+ public void put(long key, T value) {
+ Set<T> values = mSparseArray.get(key);
+ if (values == null) {
+ values = getEmptySet();
+ mSparseArray.put(key, values);
+ }
+ values.add(value);
+ }
+
+ /**
+ * Removes the value at the specified index.
+ */
+ public void remove(long key, T value) {
+ Set<T> values = mSparseArray.get(key);
+ if (values != null) {
+ values.remove(value);
+ if (values.isEmpty()) {
+ mSparseArray.remove(key);
+ cacheEmptySet(values);
+ }
+ }
+ }
+
+ /**
+ * Gets the set of Objects mapped from the specified key, or an empty set
+ * if no such mapping has been made.
+ */
+ public Iterable<T> get(long key) {
+ Set<T> values = mSparseArray.get(key);
+ return values == null ? Collections.EMPTY_SET : values;
+ }
+
+ /**
+ * Clears cached empty sets.
+ */
+ public void clearEmptyCache() {
+ while (mEmptyIndex >= 0) {
+ mEmptySets[mEmptyIndex--] = null;
+ }
+ }
+
+ @VisibleForTesting
+ int getEmptyCacheSize() {
+ return mEmptyIndex + 1;
+ }
+
+ private void cacheEmptySet(Set<T> emptySet) {
+ if (mEmptyIndex < DEFAULT_MAX_EMPTIES_KEPT - 1) {
+ mEmptySets[++mEmptyIndex] = emptySet;
+ }
+ }
+
+ private Set<T> getEmptySet() {
+ if (mEmptyIndex < 0) {
+ return CollectionUtils.createSmallSet();
+ }
+ Set<T> emptySet = mEmptySets[mEmptyIndex];
+ mEmptySets[mEmptyIndex--] = null;
+ return emptySet;
+ }
+
+ @Override
+ public String toString() {
+ return mSparseArray.toString() + "(emptyCacheSize=" + getEmptyCacheSize() + ")";
+ }
+}
diff --git a/src/com/android/tv/util/OnboardingUtils.java b/src/com/android/tv/util/OnboardingUtils.java
new file mode 100644
index 00000000..c693185e
--- /dev/null
+++ b/src/com/android/tv/util/OnboardingUtils.java
@@ -0,0 +1,99 @@
+/*
+ * 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.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.media.tv.TvContract.Channels;
+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.
+ */
+public final class OnboardingUtils {
+ private static final String PREF_KEY_IS_FIRST_BOOT = "pref_onbaording_is_first_boot";
+ private static final String PREF_KEY_IS_FIRST_RUN = "pref_onbaording_is_first_run";
+ private static final String PREF_KEY_ARE_CHANNELS_AVAILABLE =
+ "pref_onbaording_are_channels_available";
+
+ /**
+ * Checks if this is the first boot after the onboarding experience has been applied.
+ */
+ public static boolean isFirstBoot(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context)
+ .getBoolean(PREF_KEY_IS_FIRST_BOOT, true);
+ }
+
+ /**
+ * Marks that the first boot has been completed.
+ */
+ public static void setFirstBootCompleted(Context context) {
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .edit()
+ .putBoolean(PREF_KEY_IS_FIRST_BOOT, false)
+ .apply();
+ }
+
+ /**
+ * Checks if this is the first run of {@link com.android.tv.MainActivity} after the
+ * onboarding experience has been applied.
+ */
+ public static boolean isFirstRun(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context)
+ .getBoolean(PREF_KEY_IS_FIRST_RUN, true);
+ }
+
+ /**
+ * Marks that the first run of {@link com.android.tv.MainActivity} has been completed.
+ */
+ public static void setFirstRunCompleted(Context context) {
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .edit()
+ .putBoolean(PREF_KEY_IS_FIRST_RUN, false)
+ .apply();
+ }
+
+ /**
+ * Checks whether the onboarding screen should be shown or not.
+ */
+ public static boolean needToShowOnboarding(Context context) {
+ return isFirstRun(context) || !areChannelsAvailable(context);
+ }
+
+ /**
+ * Checks if there are any available tuner channels.
+ */
+ @UiThread
+ public static boolean areChannelsAvailable(Context context) {
+ ChannelDataManager manager = ((TvApplication) context.getApplicationContext())
+ .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.getCount() != 0;
+ }
+ }
+}
diff --git a/src/com/android/tv/util/PermissionUtils.java b/src/com/android/tv/util/PermissionUtils.java
new file mode 100644
index 00000000..f39dba81
--- /dev/null
+++ b/src/com/android/tv/util/PermissionUtils.java
@@ -0,0 +1,70 @@
+package com.android.tv.util;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Build;
+
+/**
+ * Util class to handle permissions.
+ */
+public class PermissionUtils {
+ private static Boolean sHasAccessAllEpgPermission;
+ private static Boolean sHasAccessWatchedHistoryPermission;
+ private static Boolean sHasModifyParentalControlsPermission;
+
+ public static boolean hasAccessAllEpg(Context context) {
+ if (sHasAccessAllEpgPermission == null) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ sHasAccessAllEpgPermission = context.checkSelfPermission(
+ "com.android.providers.tv.permission.ACCESS_ALL_EPG_DATA")
+ == PackageManager.PERMISSION_GRANTED;
+ } else {
+ sHasAccessAllEpgPermission = context.getPackageManager().checkPermission(
+ "com.android.providers.tv.permission.ACCESS_ALL_EPG_DATA",
+ context.getPackageName()) == PackageManager.PERMISSION_GRANTED;
+ }
+ }
+ return sHasAccessAllEpgPermission;
+ }
+
+ public static boolean hasAccessWatchedHistory(Context context) {
+ if (sHasAccessWatchedHistoryPermission == null) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ sHasAccessWatchedHistoryPermission = context.checkSelfPermission(
+ "com.android.providers.tv.permission.ACCESS_WATCHED_PROGRAMS")
+ == PackageManager.PERMISSION_GRANTED;
+ } else {
+ sHasAccessWatchedHistoryPermission = context.getPackageManager().checkPermission(
+ "com.android.providers.tv.permission.ACCESS_WATCHED_PROGRAMS",
+ context.getPackageName()) == PackageManager.PERMISSION_GRANTED;
+ }
+ }
+ return sHasAccessWatchedHistoryPermission;
+ }
+
+ public static boolean hasModifyParentalControls(Context context) {
+ if (sHasModifyParentalControlsPermission == null) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ sHasModifyParentalControlsPermission = context.checkSelfPermission(
+ "android.permission.MODIFY_PARENTAL_CONTROLS")
+ == PackageManager.PERMISSION_GRANTED;
+ } else {
+ sHasModifyParentalControlsPermission = context.getPackageManager().checkPermission(
+ "android.permission.MODIFY_PARENTAL_CONTROLS",
+ context.getPackageName()) == PackageManager.PERMISSION_GRANTED;
+ }
+ }
+ return sHasModifyParentalControlsPermission;
+ }
+
+ public static boolean hasReadTvListings(Context context) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ return context.checkSelfPermission("android.permission.READ_TV_LISTINGS")
+ == PackageManager.PERMISSION_GRANTED;
+ } else {
+ return context.getPackageManager().checkPermission(
+ "android.permission.MODIFY_PARENTAL_CONTROLS",
+ context.getPackageName()) == PackageManager.PERMISSION_GRANTED;
+ }
+ }
+}
diff --git a/src/com/android/tv/util/PipInputManager.java b/src/com/android/tv/util/PipInputManager.java
index d5817907..3e4db654 100644
--- a/src/com/android/tv/util/PipInputManager.java
+++ b/src/com/android/tv/util/PipInputManager.java
@@ -30,7 +30,6 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -50,7 +49,7 @@ public class PipInputManager {
private final ChannelTuner mChannelTuner;
private boolean mStarted;
private final Map<String, PipInput> mPipInputMap = new HashMap<>(); // inputId -> PipInput
- private final Set<Listener> mListeners = new HashSet<>();
+ private final Set<Listener> mListeners = CollectionUtils.createSmallSet();
private final TvInputCallback mTvInputCallback = new TvInputCallback() {
@Override
diff --git a/src/com/android/tv/util/RecurringRunner.java b/src/com/android/tv/util/RecurringRunner.java
index f51918e9..dcede666 100644
--- a/src/com/android/tv/util/RecurringRunner.java
+++ b/src/com/android/tv/util/RecurringRunner.java
@@ -52,8 +52,8 @@ public final class RecurringRunner {
}
public void start() {
+ SoftPreconditions.checkState(!mRunning, TAG, "start is called twice.");
if (mRunning) {
- Utils.engThrowElseWarn(TAG, "start is called twice.", new IllegalStateException());
return;
}
mRunning = true;
diff --git a/src/com/android/tv/util/SetupUtils.java b/src/com/android/tv/util/SetupUtils.java
index 683216a5..46299d3b 100644
--- a/src/com/android/tv/util/SetupUtils.java
+++ b/src/com/android/tv/util/SetupUtils.java
@@ -27,6 +27,10 @@ import android.os.Build;
import android.preference.PreferenceManager;
import android.util.Log;
+import com.android.tv.TvApplication;
+import com.android.tv.data.Channel;
+import com.android.tv.data.ChannelDataManager;
+
import java.util.HashSet;
import java.util.Set;
@@ -40,18 +44,20 @@ public class SetupUtils {
// Known inputs are inputs which are shown in SetupView before. When a new input is installed,
// the input will not be included in "PREF_KEY_KNOWN_INPUTS".
private static final String PREF_KEY_KNOWN_INPUTS = "known_inputs";
- // Set up inputs are inputs whose setup activity has been launched from Live channels app.
+ // Set up inputs are inputs whose setup activity has been launched and finished successfully.
private static final String PREF_KEY_SET_UP_INPUTS = "set_up_inputs";
private static final String PREF_KEY_IS_FIRST_TUNE = "is_first_tune";
private static SetupUtils sSetupUtils;
+ private final TvApplication mTvApplication;
private final SharedPreferences mSharedPreferences;
private final Set<String> mKnownInputs;
private final Set<String> mSetUpInputs;
private boolean mIsFirstTune;
- private SetupUtils(Context context) {
- mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+ private SetupUtils(TvApplication tvApplication) {
+ mTvApplication = tvApplication;
+ mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(tvApplication);
mSetUpInputs = new HashSet<>(mSharedPreferences.getStringSet(PREF_KEY_SET_UP_INPUTS,
new HashSet<String>()));
mKnownInputs = new HashSet<>(mSharedPreferences.getStringSet(PREF_KEY_KNOWN_INPUTS,
@@ -66,10 +72,66 @@ public class SetupUtils {
if (sSetupUtils != null) {
return sSetupUtils;
}
- sSetupUtils = new SetupUtils(context.getApplicationContext());
+ sSetupUtils = new SetupUtils((TvApplication) context.getApplicationContext());
return sSetupUtils;
}
+ /**
+ * Additional work after the setup of TV input.
+ */
+ public void onTvInputSetupFinished(final String inputId, final Runnable postRunnable) {
+ // When TIS adds several channels, ChannelDataManager.Listener.onChannelList
+ // Updated() can be called several times. In this case, it is hard to detect
+ // which one is the last callback. To reduce error prune, we update channel
+ // list again and make all channels of {@code inputId} browsable.
+ onSetupDone(inputId);
+ final ChannelDataManager manager = mTvApplication.getChannelDataManager();
+ if (!manager.isDbLoadFinished()) {
+ manager.addListener(new ChannelDataManager.Listener() {
+ @Override
+ public void onLoadFinished() {
+ manager.removeListener(this);
+ updateChannelBrowsable(mTvApplication, inputId, postRunnable);
+ }
+
+ @Override
+ public void onChannelListUpdated() { }
+
+ @Override
+ public void onChannelBrowsableChanged() { }
+ });
+ } else {
+ updateChannelBrowsable(mTvApplication, inputId, postRunnable);
+ }
+ }
+
+ private static void updateChannelBrowsable(Context context, final String inputId,
+ final Runnable postRunnable) {
+ TvApplication tvApplication = (TvApplication) context.getApplicationContext();
+ final ChannelDataManager manager = tvApplication.getChannelDataManager();
+ manager.updateChannels(new Runnable() {
+ @Override
+ public void run() {
+ boolean browsableChanged = false;
+ for (Channel channel : manager.getChannelList()) {
+ if (channel.getInputId().equals(inputId)) {
+ if (!channel.isBrowsable()) {
+ manager.updateBrowsable(channel.getId(), true, true);
+ browsableChanged = true;
+ }
+ }
+ }
+ if (browsableChanged) {
+ manager.notifyChannelBrowsableChanged();
+ manager.applyUpdatedValuesToDb();
+ }
+ if (postRunnable != null) {
+ postRunnable.run();
+ }
+ }
+ });
+ }
+
public boolean isFirstTune() {
return mIsFirstTune;
}
@@ -91,14 +153,14 @@ public class SetupUtils {
}
/**
- * Returns true, if {@code inputId}'s setup activity has been launched.
+ * Returns {@code true}, if {@code inputId}'s setup has been done before.
*/
- public boolean hasSetupLaunched(String inputId) {
- boolean launched = mSetUpInputs.contains(inputId);
+ public boolean isSetupDone(String inputId) {
+ boolean done = mSetUpInputs.contains(inputId);
if (DEBUG) {
- Log.d(TAG, "hasSetupLaunched: (input=" + inputId + ", result= " + launched + ")");
+ Log.d(TAG, "isSetupDone: (input=" + inputId + ", result= " + done + ")");
}
- return launched;
+ return done;
}
/**
@@ -120,7 +182,7 @@ public class SetupUtils {
*/
public static void grantEpgPermissionToSetUpPackages(Context context) {
// TvProvider allows granting of Uri permissions starting from MNC.
- if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
SharedPreferences sharedPreferences =
PreferenceManager.getDefaultSharedPreferences(context);
Set<String> setUpInputs = new HashSet<>(sharedPreferences.getStringSet(
@@ -143,7 +205,7 @@ public class SetupUtils {
*/
public static void grantEpgPermission(Context context, String packageName) {
// TvProvider allows granting of Uri permissions starting from MNC.
- if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (DEBUG) {
Log.d(TAG, "grantEpgPermission(context=" + context + ", packageName=" + packageName
+ ")");
@@ -155,7 +217,7 @@ public class SetupUtils {
context.grantUriPermission(packageName, TvContract.Programs.CONTENT_URI, modeFlags);
} catch (SecurityException e) {
Log.e(TAG, "Either TvProvider does not allow granting of Uri permissions or the app"
- + " does not have permission" + e);
+ + " does not have permission.", e);
}
}
}
@@ -194,11 +256,15 @@ public class SetupUtils {
}
/**
- * Called when an setup activity is launched. Once it is called, {@link #hasSetupLaunched}
- * will return true for {@code inputId}.
+ * Called when an setup is done. Once it is called, {@link #isSetupDone} returns {@code true}
+ * for {@code inputId}.
*/
- public void onSetupLaunched(String inputId) {
- if (DEBUG) Log.d(TAG, "onSetupLaunched: input=" + inputId);
+ public void onSetupDone(String inputId) {
+ if (DEBUG) Log.d(TAG, "onSetupDone: input=" + inputId);
+ if (!mKnownInputs.contains(inputId)) {
+ Log.i(TAG, "An unknown input's setup has been done. inputId=" + inputId);
+ mKnownInputs.add(inputId);
+ }
mSetUpInputs.add(inputId);
mSharedPreferences.edit()
.putStringSet(PREF_KEY_SET_UP_INPUTS, mSetUpInputs).apply();
diff --git a/src/com/android/tv/util/SoftPreconditions.java b/src/com/android/tv/util/SoftPreconditions.java
new file mode 100644
index 00000000..5c2a8170
--- /dev/null
+++ b/src/com/android/tv/util/SoftPreconditions.java
@@ -0,0 +1,146 @@
+/*
+ * 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.text.TextUtils;
+import android.util.Log;
+
+import com.android.tv.BuildConfig;
+
+/**
+ * Simple static methods to be called at the start of your own methods to verify
+ * correct arguments and state.
+ *
+ * <p>{@code checkXXX} methods throw exceptions when {@link BuildConfig#ENG} is true, and
+ * logs a warning when it is false.
+ *
+ * <p>This is based on com.android.internal.util.Preconditions.
+ */
+public final class SoftPreconditions {
+ private static final String TAG = "SoftPreconditions";
+
+ /**
+ * Throws or logs if an expression involving the parameter of the calling
+ * method is not true.
+ *
+ * @param expression a boolean expression
+ * @param tag Used to identify the source of a log message. It usually
+ * identifies the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ * @throws IllegalArgumentException if {@code expression} is true
+ */
+ public static void checkArgument(final boolean expression, String tag, String msg) {
+ if (!expression) {
+ warn(tag, "Illegal argument", msg, new IllegalArgumentException(msg));
+ }
+ }
+
+ /**
+ * Throws or logs if an expression involving the parameter of the calling
+ * method is not true.
+ *
+ * @param expression a boolean expression
+ * @throws IllegalArgumentException if {@code expression} is true
+ */
+ public static void checkArgument(final boolean expression) {
+ checkArgument(expression, null, null);
+ }
+
+ /**
+ * Throws or logs if an and object is null.
+ *
+ * @param reference an object reference
+ * @param tag Used to identify the source of a log message. It usually
+ * identifies the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ * @return true if the object is null
+ * @throws NullPointerException if {@code reference} is null
+ */
+ public static <T> T checkNotNull(final T reference, String tag, String msg) {
+ if (reference == null) {
+ warn(tag, "Null Pointer", msg, new NullPointerException(msg));
+ }
+ return reference;
+ }
+
+ /**
+ * Throws or logs if an and object is null.
+ *
+ * @param reference an object reference
+ * @return true if the object is null
+ * @throws NullPointerException if {@code reference} is null
+ */
+ public static <T> T checkNotNull(final T reference) {
+ return checkNotNull(reference, null, null);
+ }
+
+ /**
+ * Throws or logs if an expression involving the state of the calling
+ * instance, but not involving any parameters to the calling method is not true.
+ *
+ * @param expression a boolean expression
+ * @param tag Used to identify the source of a log message. It usually
+ * identifies the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ * @throws IllegalStateException if {@code expression} is true
+ */
+ public static void checkState(final boolean expression, String tag, String msg) {
+ if (!expression) {
+ warn(tag, "Illegal State", msg, new IllegalStateException(msg));
+ }
+ }
+
+ /**
+ * Throws or logs if an expression involving the state of the calling
+ * instance, but not involving any parameters to the calling method is not true.
+ *
+ * @param expression a boolean expression
+ * @throws IllegalStateException if {@code expression} is true
+ */
+ public static void checkState(final boolean expression) {
+ checkState(expression, null, null);
+ }
+
+ /**
+ * Throws a {@link RuntimeException} if {@link BuildConfig#ENG} is true, else log a warning.
+ *
+ * @param tag Used to identify the source of a log message. It usually
+ * identifies the class or activity where the log call occurs.
+ * @param msg The message you would like logged
+ * @param e The exception to throw
+ */
+ public static void warn(String tag, String prefix, String msg, RuntimeException e)
+ throws RuntimeException{
+ if (BuildConfig.ENG) {
+ throw e;
+ } else {
+ if (TextUtils.isEmpty(tag)) {
+ tag = TAG;
+ }
+ String logMessage;
+ if (msg == null) {
+ logMessage = prefix;
+ } else {
+ logMessage = prefix + ": " + msg;
+ }
+ Log.w(tag, logMessage, e);
+ }
+ }
+
+ private SoftPreconditions() {
+ }
+}
diff --git a/src/com/android/tv/util/SystemProperties.java b/src/com/android/tv/util/SystemProperties.java
index 88266cad..235161b6 100644
--- a/src/com/android/tv/util/SystemProperties.java
+++ b/src/com/android/tv/util/SystemProperties.java
@@ -16,6 +16,8 @@
package com.android.tv.util;
+import com.android.tv.common.BooleanSystemProperty;
+
/**
* A convenience class for getting TV related system properties.
*/
@@ -51,12 +53,6 @@ public final class SystemProperties {
"tv_use_debug_keys", false);
/**
- * When true search is available in the EPG. Defaults to false.
- */
- public static final BooleanSystemProperty USE_EPG_SEARCH = new BooleanSystemProperty(
- "tv_use_epg_search", false); // TODO: remove this flag.
-
- /**
* Send {@link com.android.tv.analytics.Tracker} information. Defaults to {@code true}.
*/
public static final BooleanSystemProperty USE_TRACKER = new BooleanSystemProperty(
diff --git a/src/com/android/tv/util/TvInputManagerHelper.java b/src/com/android/tv/util/TvInputManagerHelper.java
index 66c0ba81..d11ecd82 100644
--- a/src/com/android/tv/util/TvInputManagerHelper.java
+++ b/src/com/android/tv/util/TvInputManagerHelper.java
@@ -29,8 +29,6 @@ import android.util.Log;
import com.android.tv.parental.ContentRatingsManager;
import com.android.tv.parental.ParentalControlSettings;
-import junit.framework.Assert;
-
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@@ -50,8 +48,7 @@ public class TvInputManagerHelper {
static {
BUNDLED_PACKAGE_SET.add("com.android.tv");
- BUNDLED_PACKAGE_SET.add("com.android.tv");
- BUNDLED_PACKAGE_SET.add("com.google.android.usbtuner");
+ BUNDLED_PACKAGE_SET.add("com.android.usbtuner");
}
private final Context mContext;
@@ -123,10 +120,6 @@ public class TvInputManagerHelper {
return;
}
mStarted = true;
- List<TvInputInfo> inputs = mTvInputManager.getTvInputList();
- if (inputs.size() < 1) {
- return;
- }
mTvInputManager.registerCallback(mInternalCallback, mHandler);
mInputMap.clear();
mInputStateMap.clear();
@@ -137,7 +130,8 @@ public class TvInputManagerHelper {
mInputStateMap.put(inputId, state);
mInputIdToPartnerInputMap.put(inputId, isPartnerInput(input));
}
- Assert.assertEquals(mInputStateMap.size(), mInputMap.size());
+ SoftPreconditions.checkState(mInputStateMap.size() == mInputMap.size(), TAG,
+ "mInputStateMap not the same size as mInputMap");
mContentRatingsManager.update();
}
@@ -215,7 +209,7 @@ public class TvInputManagerHelper {
}
/**
- * Loads label of {@param info}.
+ * Loads label of {@code info}.
*
* It's visible for comparator test to mock TvInputInfo.
* Package private is enough for this method, but public is necessary to workaround mockito
@@ -230,18 +224,18 @@ public class TvInputManagerHelper {
* Returns if TV input exists with the input id.
*/
public boolean hasTvInputInfo(String inputId) {
+ SoftPreconditions.checkState(mStarted, TAG,
+ "hasTvInputInfo() called before TvInputManagerHelper was started.");
if (!mStarted) {
- Utils.engThrowElseWarn(TAG,
- "hasTvInputInfo() called before TvInputManagerHelper was started.");
return false;
}
return !TextUtils.isEmpty(inputId) && mInputMap.get(inputId) != null;
}
public TvInputInfo getTvInputInfo(String inputId) {
+ SoftPreconditions.checkState(mStarted, TAG,
+ "getTvInputInfo() called before TvInputManagerHelper was started.");
if (!mStarted) {
- Utils.engThrowElseWarn(TAG,
- "getTvInputInfo() called before TvInputManagerHelper was started.");
return null;
}
if (inputId == null) {
@@ -270,8 +264,10 @@ public class TvInputManagerHelper {
}
public int getInputState(String inputId) {
+ SoftPreconditions.checkState(mStarted, TAG, "AvailabilityManager not started");
if (!mStarted) {
- throw new IllegalStateException("AvailabilityManager doesn't started");
+ return TvInputManager.INPUT_STATE_DISCONNECTED;
+
}
Integer state = mInputStateMap.get(inputId);
if (state == null) {
diff --git a/src/com/android/tv/util/Utils.java b/src/com/android/tv/util/Utils.java
index a0ed0924..81b469fe 100644
--- a/src/com/android/tv/util/Utils.java
+++ b/src/com/android/tv/util/Utils.java
@@ -17,11 +17,15 @@
package com.android.tv.util;
import android.annotation.SuppressLint;
+import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
import android.database.Cursor;
import android.media.tv.TvContentRating;
import android.media.tv.TvContract;
@@ -29,6 +33,7 @@ import android.media.tv.TvContract.Channels;
import android.media.tv.TvInputInfo;
import android.media.tv.TvTrackInfo;
import android.net.Uri;
+import android.os.Build;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
@@ -38,14 +43,15 @@ import android.text.format.DateUtils;
import android.util.Log;
import android.view.View;
-import com.android.tv.BuildConfig;
+import com.android.tv.Features;
import com.android.tv.R;
+import com.android.tv.TvApplication;
import com.android.tv.data.Channel;
import com.android.tv.data.Program;
import com.android.tv.data.StreamInfo;
-
import java.util.ArrayList;
import java.util.Calendar;
+import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
@@ -386,6 +392,8 @@ public class Utils {
String language = context.getString(R.string.default_language);
if (track.getLanguage() != null) {
language = new Locale(track.getLanguage()).getDisplayName();
+ } else {
+ Log.d(TAG, "No language information found for the audio track: " + track);
}
StringBuilder metadata = new StringBuilder();
@@ -405,8 +413,13 @@ public class Utils {
metadata.append(context.getString(R.string.multi_audio_channel_surround_8));
break;
default:
- metadata.append(context.getString(R.string.multi_audio_channel_suffix,
- track.getAudioChannelCount()));
+ if (track.getAudioChannelCount() > 0) {
+ metadata.append(context.getString(R.string.multi_audio_channel_suffix,
+ track.getAudioChannelCount()));
+ } else {
+ Log.d(TAG, "Invalid audio channel count (" + track.getAudioChannelCount()
+ + ") found for the audio track: " + track);
+ }
break;
}
if (showSampleRate) {
@@ -568,30 +581,103 @@ public class Utils {
}
/**
- * Throws a {@link RuntimeException} if {@link BuildConfig#ENG} is true, else log a warning.
+ * Check if the index is valid for the collection,
+ * @param collection the collection
+ * @param index the index position to test
+ * @return index >= 0 && index < collection.size().
+ */
+ public static boolean isIndexValid(@Nullable Collection<?> collection, int index) {
+ return collection == null ? false : index >= 0 && index < collection.size();
+ }
+
+ /**
+ * Returns a color integer associated with a particular resource ID.
*
- * @param tag Used to log message.
- * @param msg The message
+ * @see #getColor(android.content.res.Resources,int,Theme)
*/
- public static void engThrowElseWarn(String tag, String msg) {
- if (BuildConfig.ENG) {
- throw new RuntimeException(msg);
+ public static int getColor(Resources res, int id) {
+ return getColor(res, id, null);
+ }
+
+ /**
+ * Returns a color integer associated with a particular resource ID.
+ *
+ * <p>In M version, {@link android.content.res.Resources#getColor(int)} was deprecated and
+ * {@link android.content.res.Resources#getColor(int,Theme)} was newly added.
+ *
+ * @see android.content.res.Resources#getColor(int)
+ */
+ public static int getColor(Resources res, int id, @Nullable Theme theme) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ return res.getColor(id, theme);
} else {
- Log.w(tag, msg);
+ return res.getColor(id);
}
}
/**
- * Throws a {@link RuntimeException} if {@link BuildConfig#ENG} is true, else log a warning.
+ * Returns a color state list associated with a particular resource ID.
+ *
+ * @see #getColorStateList(android.content.res.Resources,int,Theme)
+ */
+ public static ColorStateList getColorStateList(Resources res, int id) {
+ return getColorStateList(res, id, null);
+ }
+
+ /**
+ * Returns a color state list associated with a particular resource ID.
+ *
+ * <p>In M version, {@link android.content.res.Resources#getColorStateList(int)} was deprecated
+ * and {@link android.content.res.Resources#getColorStateList(int,Theme)} was newly added.
*
- * @param tag Used to log message.
- * @param msg The message
+ * @see android.content.res.Resources#getColorStateList(int)
*/
- public static void engThrowElseWarn(String tag, String msg, RuntimeException e) {
- if (BuildConfig.ENG) {
- throw e;
+ public static ColorStateList getColorStateList(Resources res, int id, @Nullable Theme theme) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ return res.getColorStateList(id, theme);
} else {
- Log.w(tag, msg);
+ return res.getColorStateList(id);
+ }
+ }
+
+
+ private static final class SyncRunnable implements Runnable {
+ private final Runnable mTarget;
+ private boolean mComplete;
+
+ public SyncRunnable(Runnable target) {
+ mTarget = target;
+ }
+
+ @Override
+ public void run() {
+ try {
+ mTarget.run();
+ } finally {
+ synchronized (this) {
+ mComplete = true;
+ notifyAll();
+ }
+ }
+ }
+
+ public void waitForComplete() {
+ boolean interrupted = false;
+ synchronized (this) {
+ try {
+ while (!mComplete) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ interrupted = true;
+ }
+ }
+ } finally {
+ if (interrupted) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
}
}
}